%PDF- %PDF-
Direktori : /home/bitrix/www/bitrix/modules/main/lib/urlpreview/ |
Current File : /home/bitrix/www/bitrix/modules/main/lib/urlpreview/urlpreview.php |
<?php namespace Bitrix\Main\UrlPreview; use Bitrix\Main\Application; use Bitrix\Main\ArgumentException; use Bitrix\Main\Config\Option; use Bitrix\Main\DB\SqlQueryException; use Bitrix\Main\Loader; use Bitrix\Main\Security\Sign\Signer; use Bitrix\Main\Web\HttpClient; use Bitrix\Main\Web\HttpHeaders; use Bitrix\Main\Web\Uri; class UrlPreview { const SIGN_SALT = 'url_preview'; const USER_AGENT = 'Bitrix link preview'; /** @var int Maximum allowed length of the description. */ const MAX_DESCRIPTION = 500; const IFRAME_MAX_WIDTH = 640; const IFRAME_MAX_HEIGHT = 340; protected static $trustedHosts = [ 'youtube.com' => 'youtube.com', 'youtu.be' => 'youtu.be', 'vimeo.com' => 'vimeo.com', 'rutube.ru' => 'rutube.ru', 'facebook.com' => 'facebook.com', 'vk.com' => 'vk.com', 'instagram.com' => 'instagram.com', ]; /** * Returns associated metadata for the specified URL * * @param string $url URL. * @param bool $addIfNew Should metadata be fetched and saved, if not found in database. * @param bool $reuseExistingMetadata Allow reading of the cached metadata. * @return array|false Metadata for the URL if found, or false otherwise. */ public static function getMetadataByUrl($url, $addIfNew = true, $reuseExistingMetadata = true) { if(!static::isEnabled()) return false; $url = static::normalizeUrl($url); if($url == '') return false; if($reuseExistingMetadata) { if($metadata = UrlMetadataTable::getByUrl($url)) { if($metadata['TYPE'] == UrlMetadataTable::TYPE_TEMPORARY && $addIfNew) { $metadata = static::resolveTemporaryMetadata($metadata['ID']); } return $metadata; } } if(!$addIfNew) return false; $metadataId = static::reserveIdForUrl($url); $metadata = static::fetchUrlMetadata($url); if(is_array($metadata) && count($metadata) > 0) { $result = UrlMetadataTable::update($metadataId, $metadata); $metadata['ID'] = $result->getId(); return $metadata; } return false; } /** * Returns html code for url preview * * @param array $userField Userfield's value. * @param array $userFieldParams Userfield's parameters. * @param string $cacheTag Cache tag for returned preview (out param). * @param bool $edit Show method build preview for editing the userfield. * @return string HTML code for the preview. */ public static function showView($userField, $userFieldParams, &$cacheTag, $edit = false) { global $APPLICATION; $edit = !!$edit; $cacheTag = ''; if(!static::isEnabled()) return null; $metadataId = (int)$userField['VALUE'][0]; $metadata = false; if($metadataId > 0) { $metadata = UrlMetadataTable::getById($metadataId)->fetch(); if(isset($metadata['TYPE']) && $metadata['TYPE'] == UrlMetadataTable::TYPE_TEMPORARY) $metadata = static::resolveTemporaryMetadata($metadata['ID']); } if(is_array($metadata)) { if($metadata['TYPE'] == UrlMetadataTable::TYPE_DYNAMIC) { $routeRecord = Router::dispatch(new Uri(static::unfoldShortLink($metadata['URL']))); if(isset($routeRecord['MODULE']) && Loader::includeModule($routeRecord['MODULE'])) { $className = $routeRecord['CLASS']; $routeRecord['PARAMETERS']['URL'] = $metadata['URL']; $parameters = $routeRecord['PARAMETERS']; if($edit && (!method_exists($className, 'checkUserReadAccess') || !$className::checkUserReadAccess($parameters, static::getCurrentUserId()))) return null; if(method_exists($className, 'buildPreview')) { $metadata['HANDLER'] = $routeRecord; $metadata['HANDLER']['BUILD_METHOD'] = 'buildPreview'; } if(method_exists($className, 'getCacheTag')) $cacheTag = $className::getCacheTag(); } else if(!$edit) { return null; } } } else if(!$edit) { return null; } ob_start(); $APPLICATION->IncludeComponent( 'bitrix:main.urlpreview', '', array( 'USER_FIELD' => $userField, 'METADATA' => $metadata, 'PARAMS' => $userFieldParams, 'EDIT' => ($edit ? 'Y' : 'N'), 'CHECK_ACCESS' => ($edit ? 'Y' : 'N'), ) ); return ob_get_clean(); } /** * Returns html code for url preview edit form * * @param array $userField Userfield's value. * @param array $userFieldParams Userfield's parameters. * @return string HTML code for the preview. */ public static function showEdit($userField, $userFieldParams) { return static::showView($userField, $userFieldParams, $cacheTag, true); } /** * Checks if metadata for the provided url is already fetched and cached. * * @param string $url Document's URL. * @return bool True if metadata for the url is located in database, false otherwise. */ public static function isUrlCached($url) { $url = static::normalizeUrl($url); if($url == '') return false; return (static::isUrlLocal(new Uri($url)) || !!UrlMetadataTable::getByUrl($url)); } /** * If url is remote - returns metadata for this url. If url is local - checks current user access to the entity * behind the url, and returns html preview for this entity. * * @param string $url Document's URL. * @param bool $addIfNew Should method fetch and store metadata for the document, if it is not found in database. * @params bool $reuseExistingMetadata Allow reading of the cached metadata. * @return array|false Metadata for the document, or false if metadata could not be fetched/parsed. */ public static function getMetadataAndHtmlByUrl($url, $addIfNew = true, $reuseExistingMetadata = true) { $metadata = static::getMetadataByUrl($url, $addIfNew, $reuseExistingMetadata); if($metadata === false) return false; if($metadata['TYPE'] == UrlMetadataTable::TYPE_STATIC || $metadata['TYPE'] == UrlMetadataTable::TYPE_FILE) { return $metadata; } else if($metadata['TYPE'] == UrlMetadataTable::TYPE_DYNAMIC) { if($preview = static::getDynamicPreview($url)) { $metadata['HTML'] = $preview; return $metadata; } } return false; } /** * Returns stored metadata for array of IDs * * @param array $ids Array of record's IDs. * @param bool $checkAccess Should method check current user's access to the internal entities, or not. * @params int $userId. Id of the users to check access. If == 0, will check access for current user. * @return array|false Array with provided IDs as the keys. */ public static function getMetadataAndHtmlByIds(array $ids, $checkAccess = true, $userId = 0) { if(!static::isEnabled()) return false; $result = array(); $queryResult = UrlMetadataTable::getList(array( 'filter' => array( 'ID' => $ids, '!=TYPE' => UrlMetadataTable::TYPE_TEMPORARY ) )); while($metadata = $queryResult->fetch()) { if($metadata['TYPE'] == UrlMetadataTable::TYPE_DYNAMIC) { $metadata['HTML'] = static::getDynamicPreview($metadata['URL'], $checkAccess, $userId); if($metadata['HTML'] === false) continue; } $result[$metadata['ID']] = $metadata; } return $result; } /** * Creates temporary record for url * * @param string $url URL for which temporary record should be created. * @return int Temporary record's id. */ public static function reserveIdForUrl($url) { if($metadata = UrlMetadataTable::getByUrl($url)) { $id = $metadata['ID']; } else { $result = UrlMetadataTable::add(array( 'URL' => $url, 'TYPE' => UrlMetadataTable::TYPE_TEMPORARY )); $id = $result->getId(); } return $id; } /** * Fetches and stores metadata for temporary record, created by UrlPreview::reserveIdForUrl. If metadata could * not be fetched, deletes record. * @param int $id Metadata record's id. * @param bool $checkAccess Should method check current user's access to the entity, or not. * @params int $userId. Id of the users to check access. If == 0, will check access for current user. * @return array|false Metadata if fetched, false otherwise. */ public static function resolveTemporaryMetadata($id, $checkAccess = true, $userId = 0) { $metadata = UrlMetadataTable::getById($id)->fetch(); if(!is_array($metadata)) return false; if($metadata['TYPE'] == UrlMetadataTable::TYPE_TEMPORARY) { $metadata['URL'] = static::normalizeUrl($metadata['URL']); $metadata = static::fetchUrlMetadata($metadata['URL']); if($metadata === false) { UrlMetadataTable::delete($id); return false; } UrlMetadataTable::update($id, $metadata); return $metadata; } else if($metadata['TYPE'] == UrlMetadataTable::TYPE_STATIC || $metadata['TYPE'] == UrlMetadataTable::TYPE_FILE) { return $metadata; } else if($metadata['TYPE'] == UrlMetadataTable::TYPE_DYNAMIC) { if($preview = static::getDynamicPreview($metadata['URL'], $checkAccess, $userId)) { $metadata['HTML'] = $preview; return $metadata; } } return false; } /** * Returns HTML code for the dynamic (internal url) preview. * @param string $url URL of the internal document. * @param bool $checkAccess Should method check current user's access to the entity, or not. * @params int $userId. Id of the users to check access. If userId == 0, will check access for current user. * @return string|false HTML code of the preview, or false if case of any errors (including access denied)/ */ public static function getDynamicPreview($url, $checkAccess = true, $userId = 0) { $routeRecord = Router::dispatch(new Uri(static::unfoldShortLink($url))); if($routeRecord === false) return false; if(isset($routeRecord['MODULE']) && Loader::includeModule($routeRecord['MODULE'])) { $className = $routeRecord['CLASS']; $parameters = $routeRecord['PARAMETERS']; $parameters['URL'] = $url; if($userId == 0) $userId = static::getCurrentUserId(); if ($checkAccess && (!method_exists($className, 'checkUserReadAccess') || $userId == 0 || !$className::checkUserReadAccess($parameters, $userId))) return false; if (method_exists($className, 'buildPreview')) { $preview = $className::buildPreview($parameters); return (strlen($preview) > 0 ? $preview : false); } } return false; } /** * Returns attach for the IM message with the requested internal entity content. * @param string $url URL of the internal document. * @param bool $checkAccess Should method check current user's access to the entity, or not. * @params int $userId. Id of the users to check access. If userId == 0, will check access for current user. * @return \CIMMessageParamAttach | false */ public static function getImAttach($url, $checkAccess = true, $userId = 0) { //todo: caching $routeRecord = Router::dispatch(new Uri(static::unfoldShortLink($url))); if($routeRecord === false) return false; if($userId == 0) $userId = static::getCurrentUserId(); if(isset($routeRecord['MODULE']) && Loader::includeModule($routeRecord['MODULE'])) { $className = $routeRecord['CLASS']; $parameters = $routeRecord['PARAMETERS']; $parameters['URL'] = $url; if ($checkAccess && (!method_exists($className, 'checkUserReadAccess') || $userId == 0 || !$className::checkUserReadAccess($parameters, $userId))) return false; if (method_exists($className, 'getImAttach')) { return $className::getImAttach($parameters); } } return false; } /** * Returns true if current user has read access to the content behind internal url. * @param string $url URL of the internal document. * @params int $userId. Id of the users to check access. If userId == 0, will check access for current user. * @return bool True if current user has read access to the main entity of the document, or false otherwise. */ public static function checkDynamicPreviewAccess($url, $userId = 0) { $routeRecord = Router::dispatch(new Uri(static::unfoldShortLink($url))); if($routeRecord === false) return false; if(isset($routeRecord['MODULE']) && Loader::includeModule($routeRecord['MODULE'])) { $className = $routeRecord['CLASS']; $parameters = $routeRecord['PARAMETERS']; if($userId == 0) $userId = static::getCurrentUserId(); return (method_exists($className, 'checkUserReadAccess') && $userId > 0 && $className::checkUserReadAccess($parameters, $userId)); } return false; } /** * Sets main image url for the metadata with given id. * @param int $id Id of the metadata to set image url. * @param string $imageUrl Url of the image. * @return bool Returns true in case of successful update, or false otherwise. * @throws ArgumentException */ public static function setMetadataImage($id, $imageUrl) { if(!is_int($id)) throw new ArgumentException("Id of the metadata must be an integer", "id"); if(!is_string($imageUrl) && !is_null($imageUrl)) throw new ArgumentException("Url of the image must be a string", "imageUrl"); $metadata = UrlMetadataTable::getList(array( 'select' => array('IMAGE', 'IMAGE_ID', 'EXTRA'), 'filter' => array('=ID' => $id) ))->fetch(); if(isset($metadata['EXTRA']['IMAGES'])) { $imageIndex = array_search($imageUrl, $metadata['EXTRA']['IMAGES']); if($imageIndex === false) unset($metadata['EXTRA']['SELECTED_IMAGE']); else $metadata['EXTRA']['SELECTED_IMAGE'] = $imageIndex; } if(static::getOptionSaveImages()) { $metadata['IMAGE_ID'] = static::saveImage($imageUrl); $metadata['IMAGE'] = null; } else { $metadata['IMAGE'] = $imageUrl; $metadata['IMAGE_ID'] = null; } return UrlMetadataTable::update($id, $metadata)->isSuccess(); } /** * Checks if UrlPreview is enabled in module option * @return bool True if UrlPreview is enabled in module options. */ public static function isEnabled() { static $result = null; if(is_null($result)) { $result = Option::get('main', 'url_preview_enable', 'N') === 'Y'; } return $result; } /** * Signs value using UrlPreview salt * @param string $id Unsigned value. * @return string Signed value. * @throws \Bitrix\Main\ArgumentTypeException */ public static function sign($id) { $signer = new Signer(); return $signer->sign((string)$id, static::SIGN_SALT); } /** * @param string $url URL of the document. * @return array|false Fetched metadata or false if metadata was not found, or was invalid. */ protected static function fetchUrlMetadata($url) { $uriParser = new Uri($url); if(static::isUrlLocal($uriParser)) { if($routeRecord = Router::dispatch(new Uri(static::unfoldShortLink($url)))) { $metadata = array( 'URL' => $url, 'TYPE' => UrlMetadataTable::TYPE_DYNAMIC, ); } } else { $metadataRemote = static::getRemoteUrlMetadata($uriParser); if(is_array($metadataRemote) && count($metadataRemote) > 0) { $metadata = array( 'URL' => $url, 'TYPE' => $metadataRemote['TYPE'] ?: UrlMetadataTable::TYPE_STATIC, 'TITLE' => $metadataRemote['TITLE'], 'DESCRIPTION' => $metadataRemote['DESCRIPTION'], 'IMAGE_ID' => $metadataRemote['IMAGE_ID'], 'IMAGE' => $metadataRemote['IMAGE'], 'EMBED' => $metadataRemote['EMBED'], 'EXTRA' => $metadataRemote['EXTRA'] ); } } if(isset($metadata['TYPE'])) { return $metadata; } return false; } /** * Returns true if given URL is local * * @param Uri $uri Absolute URL to be checked. * @return bool */ protected static function isUrlLocal(Uri $uri) { if($uri->getHost() == '') return true; $host = \Bitrix\Main\Context::getCurrent()->getRequest()->getHttpHost(); return $uri->getHost() === $host; } /** * @param Uri $uri Absolute URL to get metadata for. * @return array|false */ protected static function getRemoteUrlMetadata(Uri $uri) { $httpClient = new HttpClient(); $httpClient->setTimeout(5); $httpClient->setStreamTimeout(5); $httpClient->setHeader('User-Agent', self::USER_AGENT, true); if(!$httpClient->query('GET', $uri->getUri())) return false; if($httpClient->getStatus() !== 200) return false; $htmlContentType = strtolower($httpClient->getHeaders()->getContentType()); $peerIpAddress = $httpClient->getPeerAddress(); if($htmlContentType !== 'text/html') { $metadata = static::getFileMetadata($httpClient->getEffectiveUrl(), $httpClient->getHeaders()); $metadata['EXTRA']['PEER_IP_ADDRESS'] = $peerIpAddress; $metadata['EXTRA']['PEER_IP_PRIVATE'] = static::isIpAddressPrivate($peerIpAddress); return $metadata; } $html = $httpClient->getResult(); $htmlDocument = new HtmlDocument($html, $uri); $htmlDocument->setEncoding($httpClient->getCharset()); ParserChain::extractMetadata($htmlDocument); $metadata = $htmlDocument->getMetadata(); if(is_array($metadata) && static::validateRemoteMetadata($metadata)) { if(isset($metadata['IMAGE']) && static::getOptionSaveImages()) { $metadata['IMAGE_ID'] = static::saveImage($metadata['IMAGE']); unset($metadata['IMAGE']); } if(isset($metadata['DESCRIPTION']) && strlen($metadata['DESCRIPTION']) > static::MAX_DESCRIPTION) { $metadata['DESCRIPTION'] = substr( $metadata['DESCRIPTION'], 0, static::MAX_DESCRIPTION ); } if(!is_array($metadata['EXTRA'])) { $metadata['EXTRA'] = array(); } $metadata['EXTRA'] = array_merge($metadata['EXTRA'], array( 'PEER_IP_ADDRESS' => $peerIpAddress, 'PEER_IP_PRIVATE' => static::isIpAddressPrivate($peerIpAddress) )); return $metadata; } return false; } /** * @param string $url Image's URL. * @return integer Saved file identifier */ protected static function saveImage($url) { $fileId = false; $file = new \CFile(); $httpClient = new HttpClient(); $httpClient->setTimeout(5); $httpClient->setStreamTimeout(5); $urlComponents = parse_url($url); if ($urlComponents && strlen($urlComponents["path"]) > 0) $tempPath = $file->GetTempName('', bx_basename($urlComponents["path"])); else $tempPath = $file->GetTempName('', bx_basename($url)); $httpClient->download($url, $tempPath); $fileName = $httpClient->getHeaders()->getFilename(); $localFile = \CFile::MakeFileArray($tempPath); $localFile['MODULE_ID'] = 'main'; if(is_array($localFile)) { if(strlen($fileName) > 0) { $localFile['name'] = $fileName; } if(\CFile::CheckImageFile($localFile, 0, 0, 0, array("IMAGE")) === null) { $fileId = $file->SaveFile($localFile, 'urlpreview', true); } } return ($fileId === false ? null : $fileId); } /** * If provided url does not contain scheme part, tries to add it * * @param string $url URL to be fixed. * @return string Fixed URL. */ protected static function normalizeUrl($url) { if(strpos($url, 'https://') === 0 || strpos($url, 'http://') === 0) { //nop } else if(strpos($url, '//') === 0) { $url = 'http:'.$url; } else if(strpos($url, '/') === 0) { //nop } else { $url = 'http://'.$url; } $parsedUrl = new Uri($url); $parsedUrl->setHost(ToLower($parsedUrl->getHost())); return $parsedUrl->getUri(); } /** * Returns value of the option for saving images locally. * @return bool True if images should be saved locally. */ protected static function getOptionSaveImages() { static $result = null; if(is_null($result)) { $result = Option::get('main', 'url_preview_save_images', 'N') === 'Y'; } return $result; } /** * Checks if metadata is complete. * @param array $metadata HTML document metadata. * @return bool True if metadata is complete, false otherwise. */ protected static function validateRemoteMetadata(array $metadata) { $result = ((isset($metadata['TITLE']) && isset($metadata['IMAGE'])) || (isset($metadata['TITLE']) && isset($metadata['DESCRIPTION'])) || isset($metadata['EMBED'])); return $result; } /** * Returns id of currently logged user. * @return int User's id. */ public static function getCurrentUserId() { return ($GLOBALS['USER'] instanceof \CUser) ? (int)$GLOBALS['USER']->getId() : 0; } /** * Unfolds internal short url. If url is not classified as a short link, returns input $url. * @param string $shortUrl Short URL. * @return string Full URL. */ protected static function unfoldShortLink($shortUrl) { $result = $shortUrl; if($shortUri = \CBXShortUri::GetUri($shortUrl)) { $result = $shortUri['URI']; } return $result; } /** * Returns metadata for downloadable file. * @param string $path Path part of the URL. * @param HttpHeaders $httpHeaders Server's response headers. * @return array|bool Metadata record if mime type and filename were detected, or false otherwise. */ protected static function getFileMetadata($path, HttpHeaders $httpHeaders) { $mimeType = $httpHeaders->getContentType(); $filename = $httpHeaders->getFilename() ?: bx_basename($path); $result = false; if($mimeType && $filename) { $result = array( 'TYPE' => UrlMetadataTable::TYPE_FILE, 'EXTRA' => array( 'ATTACHMENT' => strtolower($httpHeaders->getContentDisposition()) === 'attachment' ? 'Y' : 'N', 'MIME_TYPE' => $mimeType, 'FILENAME' => $filename, 'SIZE' => $httpHeaders->get('Content-Length') ) ); } return $result; } /** * @param string $ipAddress * @return bool */ public static function isIpAddressPrivate($ipAddress) { return filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false; } /** * Returns true if host of $uri is in $trustedHosts list. * * @param Uri $uri * @return bool */ public static function isHostTrusted(Uri $uri) { $result = false; $domainNameParts = explode('.', $uri->getHost()); if(is_array($domainNameParts) && ($partsCount = count($domainNameParts)) >= 2) { $domainName = $domainNameParts[$partsCount-2] . '.' . $domainNameParts[$partsCount-1]; $result = isset(static::$trustedHosts[$domainName]); } return $result; } /** * Returns video metaData for $url if its host is trusted. * * @param string $url * @return array|false */ public static function fetchVideoMetaData($url) { $uri = new Uri($url); if(static::isHostTrusted($uri) || static::isEnabled()) { $url = static::normalizeUrl($url); $metadataId = static::reserveIdForUrl($url); $metadata = static::fetchUrlMetadata($url); if(is_array($metadata) && count($metadata) > 0) { $result = UrlMetadataTable::update($metadataId, $metadata); $metadata['ID'] = $result->getId(); } else { return false; } if(isset($metadata['EMBED']) && !empty($metadata['EMBED']) && strpos($metadata['EMBED'], '<iframe') === false) { $url = static::getInnerFrameUrl($metadata['ID'], $metadata['EXTRA']['PROVIDER_NAME']); if(intval($metadata['EXTRA']['VIDEO_WIDTH']) <= 0) { $metadata['EXTRA']['VIDEO_WIDTH'] = self::IFRAME_MAX_WIDTH; } if(intval($metadata['EXTRA']['VIDEO_HEIGHT']) <= 0) { $metadata['EXTRA']['VIDEO_HEIGHT'] = self::IFRAME_MAX_HEIGHT; } $metadata['EMBED'] = '<iframe src="'.$url.'" allowfullscreen="" width="'.$metadata['EXTRA']['VIDEO_WIDTH'].'" height="'.$metadata['EXTRA']['VIDEO_HEIGHT'].'" frameborder="0"></iframe>'; } if($metadata['EMBED'] || $metadata['EXTRA']['VIDEO']) { return $metadata; } } return false; } /** * Returns inner frame url to embed third parties html video players. * * @param int $id * @param string $provider * @return bool|string */ public static function getInnerFrameUrl($id, $provider = '') { $result = false; $componentPath = \CComponentEngine::makeComponentPath('bitrix:main.urlpreview'); if(!empty($componentPath)) { $componentPath = getLocalPath('components'.$componentPath.'/frame.php'); $uri = new Uri($componentPath); $uri->addParams(array('id' => $id, 'provider' => $provider)); $result = static::normalizeUrl($uri->getLocator()); } return $result; } }