<?php
 class vB_BbCode_Url extends vB_BbCode { public const TYPE = 'Url'; public const USES_DATA_STORAGE = true; private bool $urlWasLoaded = false; public static function generateFreshFromDataAndOption(vB_BbCodeHelper $bbCodeHelper, string $data, ?string $option) : static { $record = static::getDefaultRecord(); $record['hash'] = static::getHash($bbCodeHelper, $data, $option); $cacheData = []; ['link' => $link, ] = static::getLinkAndText($bbCodeHelper, $data, $option); $cacheData['url'] = $link; $cacheData['vbmeta'] = 'pending just-in-time fetch'; $record['expires'] = 0; $record['data'] = $cacheData; $modified = true; return new static($bbCodeHelper, $record, $modified); } protected function refreshExpiredDataAtConstruction() : void { } public static function getHash(vB_BbCodeHelper $bbCodeHelper, string $data, ?string $option) : string { ['link' => $link, 'text' => $text, 'defaultTitle' => $defaultTitle,] = static::getLinkAndText($bbCodeHelper, $data, $option); $unique = [ 'url' => $link, ]; return md5(json_encode($unique)); } private static function getLinkAndText(vB_BbCodeHelper $bbCodeHelper, $data, $option) : array { $text = $data; $link = trim($option); if (!$link) { $link = trim($data); $link = static::cleanAndCanonicalizeUrl($bbCodeHelper, $link); $defaultTitle = true; } else { $link = static::cleanAndCanonicalizeUrl($bbCodeHelper, $link); $maybelink = static::cleanAndCanonicalizeUrl($bbCodeHelper, $data); $defaultTitle = ($link == $maybelink); } return ['link' => $link, 'text' => $text, 'defaultTitle' => $defaultTitle,]; } private $doLoadUrls = true; private $doShowPreview = false; private $doTruncate = true; public function setRenderOptionsAndContext(array $renderOptions, array $renderContext) : void { $doPreview = $renderOptions['url_preview'] ?? false; $doPreview = ($doPreview AND !empty($renderContext['tagIsOnOwnLine'])); $this->doShowPreview = $doPreview; $this->doLoadUrls = $renderOptions['url_load_allowed'] ?? true; $doTruncate = $renderOptions['url_truncate'] ?? true; $this->doTruncate = $doTruncate; } private function linkChanged(string $link) : bool { $linkChanged = ($link != $this->record['data']['url'] ?? ''); if ($linkChanged) { $this->record['data'] = [ 'url' => $link, ]; return true; } return false; } private function hasCache() : bool { $expired = $this->isDataExpired(); $dataIsMissing = (empty($this->record['data']['lastfetch'])); if (!$expired AND !$dataIsMissing) { return true; } return false; } public function canHandleBbCode(string $data, ?string $option) : bool { return true; } public function renderBbCode($data, $option) : string { ['link' => $link, 'text' => $text, 'defaultTitle' => $defaultTitle,] = static::getLinkAndText($this->bbcodeHelper, $data, $option); $ellipsizedText = static::trimAnchorContent($text, $link, $defaultTitle); $datastore = vB::getDatastore(); $isInternal = vB::getUrlLoader()->isSiteUrl($link); $rel = ($this->shouldDoUrlFollow($datastore, $link, $isInternal) ? '' : ' rel="nofollow"' ); $target = ($this->shouldUseSameWindow($datastore, $link, $isInternal) ? '' : ' target="_blank"' ); $previewBlock = ($this->doShowPreview AND $defaultTitle); $pagedata = null; if ($previewBlock) { $linkHasChanged = $this->linkChanged($link); if (!$linkHasChanged AND $this->hasCache()) { $pagedata = $this->record['data']; } else { $url = $this->record['data']['url']; $urlUnescaped = vB_String::unHtmlSpecialChars($url); $urlCacheLib = vB_Library::instance('urlCache'); $cachedUrl = $urlCacheLib->fetchUrl($urlUnescaped, !$this->doLoadUrls, $this->urlWasLoaded); if ($cachedUrl AND empty($cachedUrl->data['errors'])) { $this->record['expires'] = $cachedUrl->expires; $this->record['data'] = $cachedUrl->data; $this->record['data']['url'] = $url; $this->modified = true; $pagedata = $this->record['data']; } else { } } } if ($previewBlock AND !empty($pagedata) AND empty($pagedata['errors'])) { $defaults = $this->fetchDefaults(); $htmlsafeimglink = htmlentities($pagedata['images'][0] ?? $defaults['image']); $metasafe = htmlentities($pagedata['meta'], ENT_NOQUOTES); $titlesafe = htmlentities($pagedata['title'], ENT_NOQUOTES); $html = <<<EOHTML
<div class="b-bbcode b-bbcode__url b-bbcode__url--preview">
	<a href="$link"{$target}{$rel} class="urlimg_container">
		<img class="js-img-fallback" src="$htmlsafeimglink">
	</a>
	<div class="urlinfo">
		<div class="title ellipsis">
			<a href="$link"{$target}{$rel}>
				{$titlesafe}
			</a>
		</div>
		<div class="url ellipsis">
			<a href="$link"{$target}{$rel}>
				$link
			</a>
		</div>
		<div class="meta">
			{$metasafe}
		</div>
	</div>
</div>
EOHTML;
} else { $htmlsafelink = str_replace(['`', '"', "'", '['], ['&#96;', '&quot;', '&#39;', '&#91;'], $link); if ($this->doTruncate) { $text = $ellipsizedText; } $html = '<a href="' . $htmlsafelink . '"' . $target . $rel . '>' . $text . '</a>'; } return $html; } public function getRenderMetaData() : array { return [ self::RENDER_META_URL_LOADED => $this->urlWasLoaded, ]; } private function fetchDefaults() { $options = vB::getDatastore()->getValue('options'); $image = $options['frontendurl'] . '/core/images/default/google-material/image_placeholder.svg'; return [ 'image' => $image, ]; } private function shouldUseSameWindow($datastore, $link, $isLinkInternal) : bool { return $isLinkInternal AND $datastore->getOption('url_samewindow'); } private function shouldDoUrlFollow($datastore, $link, $isLinkInternal) : bool { $options = $datastore->getValue('options'); if (!$options['url_nofollow']) { return true; } if ($isLinkInternal) { return true; } $current_url = vB::getString()->parseUrl($options['bburl']); $vBHttpHost = vB::getRequest()->getVbHttpHost(); $current_host = preg_replace('#:(\d)+$#', '', $vBHttpHost); $allowed = preg_split('#\s+#', $options['url_nofollow_whitelist'], -1, PREG_SPLIT_NO_EMPTY); $allowed[] = preg_replace('#^www\.#i', '', $current_host); $allowed[] = preg_replace('#^www\.#i', '', $current_url['host']); $target_url = preg_replace('#^([a-z0-9]+:(//)?)#', '', $link); foreach ($allowed AS $host) { if (vB_String::stripos($target_url, $host) !== false) { return true; } } return false; } private static function trimAnchorContent($text, $link, $defaultTitle) : string { if ($defaultTitle OR str_replace('  ', '', $text) == $link) { $tmp = vB_String::unHtmlSpecialChars($link); if (vB_String::vbStrlen($tmp) > 55) { $text = vB_String::htmlSpecialCharsUni(vB_String::vbChop($tmp, 36) . '...' . substr($tmp, -14)); } else { $text = str_replace('  ', '', $text); } } return $text; } private static function cleanAndCanonicalizeUrl(vB_BbCodeHelper $bbCodeHelper, $url) { $rightlink = trim($url); $rightlink = $bbCodeHelper->stripSmilies($rightlink); $rightlink = str_replace('  ', '', $rightlink); if (!preg_match('#^[a-z0-9]+(?<!about|javascript|vbscript|data):#si', $rightlink)) { $rightlink = "https://$rightlink"; } return $rightlink; } } 