0) { self::loadConfig($config); } } /** * Convert whole HTML-block contains image urls */ public static function convertHtml($content, $detectBaseUrl = true) { if (empty(self::$url)) { self::$url = $_SERVER['REQUEST_URI']; } if (empty(self::$host)) { self::$host = $_SERVER['HTTP_HOST']; } $timeStart = microtime(true); //ini_set('pcre.backtrack_limit', 100000000); $content = self::removeBomFromUtf($content); $contentOrig = $content; // try auto load config from __DIR__.'config.php' if (empty(self::$siteId)) { self::loadConfig(); } if (empty(self::$siteId)) { return $contentOrig; } if (!self::isEnabled()) { return $contentOrig; } $gziped = false; if (self::isGz($content)) { if ($contentUngzip = gzdecode($content)) { $gziped = true; $content = $contentUngzip; } } $needToConvert = true; if ($needToConvert && self::htmlHasAmpMarkup($content)) { $needToConvert = false; } if ($needToConvert && self::htmlHasXmlMarkup($content)) { $needToConvert = false; } if (!$needToConvert) { return $contentOrig; } if ($detectBaseUrl) { self::$baseUrl = self::getBaseUrlFromHtml($content); if (self::$baseUrl) { self::$baseUrl = parse_url(self::$baseUrl, PHP_URL_PATH); } } //if (self::isBinary($content)) { // return $content; //} /*$domains = self::$domains; if (!is_array($domains)) { $domains = array(); } $domains = array_merge(array(''), $domains); $hostsForRegexp = array(); foreach ($domains as $domain) { //$domain = str_replace(".", "\.", $domain); if ($domain && stripos($domain, 'http://')!==0 && stripos($domain, 'https://')!==0) { $hostsForRegexp[] = 'http://'.$domain; $hostsForRegexp[] = 'https://'.$domain; } else { $hostsForRegexp[] = $domain; } }*/ //foreach ($hostsForRegexp as $host) { /*$firstPartsOfUrl = array(); foreach (self::$whitelistImgUrls as $whiteImgUrl) { if (self::substr($whiteImgUrl, -1, 1)=='/') { $whiteImgUrl = self::substr($whiteImgUrl, 0, -1); } $firstPartsOfUrl[] = preg_quote($host.$whiteImgUrl, '#'); } if (count($firstPartsOfUrl)==0) { $firstPartsOfUrl[] = preg_quote($host, '#'); } //var_dump($firstPartsOfUrl); //$host = preg_quote($host, '#'); //var_dump(self::$whitelistImgUrls); $host = implode('|', $firstPartsOfUrl); var_dump($host);*/ /*$firstPartsOfUrl = array(); foreach (self::$whitelistImgUrls as $whiteImgUrl) { $firstPartsOfUrl[] = preg_quote($whiteImgUrl, '#'); } if (empty($firstPartsOfUrl)) { $firstPartsOfUrl = array('/'); } $firstPartOfUrl = implode('|', $firstPartsOfUrl); */ //$host = preg_quote($host, '#'); $host = ''; //$firstPartOfUrl = '/'; $firstPartOfUrl = ''; // -------------------------------------------- // // @see https://developer.mozilla.org/ru/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images if (!empty(self::$srcsetAttrs)) { // srcset|data-srcset|data-wpfc-original-srcset $srcSetAttrsRegexp = array(); foreach (self::$srcsetAttrs as $attr) { $srcSetAttrsRegexp[] = preg_quote($attr, '#'); } $srcSetAttrsRegexp = implode('|', $srcSetAttrsRegexp); //$content = preg_replace_callback('#<(?P[^\s]+)(?P.*?)\s+(?P'.$srcSetAttrsRegexp.')=(?P"|\')(?P[^"]+?)(?P"|\')(?P[^>]*?)>#siS', array(__NAMESPACE__ .'\ImgUrlConverter', 'callbackForPregReplaceSrcset'), $content); $contentAfterReplace = preg_replace_callback('#<(?Psource|img|picture)(?P[^>]*)\s+(?P'.$srcSetAttrsRegexp.')=(?P"|\'|\\\"|\\\')(?P[^"\']+?)(?P"|\'|\\\"|\\\')(?P[^>]*)>#siS', array(__NAMESPACE__ .'\ImgUrlConverter', 'callbackForPregReplaceSrcset'), $content); if (!empty($contentAfterReplace)) { $content = $contentAfterReplace; } } // -------------------------------------------- //$regexp = '#("|\'|\()'.$host.'('.$firstPartOfUrl.'[^/"\'\s]{1}[^"\']*\.(png|jpg|jpeg){1}(\?.*?)?)("|\'|\))#siS'; // from 1.10 version //$regexp = '#("|\'|\()'.$host.'('.$firstPartOfUrl.'[^"|\'|\)\(]+\.(png|jpg|jpeg){1}(\?.*?)?)("|\'|\))#siS'; $urlBorders = array( array('"', '"', '"'), // "" array('\'', '\'', '\''), // '' array('\(', '\)', '\)\('), // () array('\\\"', '\\\"', '"'), // "" in JSON array("\\\'", "\\\'", "'"), // '' in JSON ); $cdnDomains = array( 'cdn.optipic.io', ); $cdnDomains[] = self::$cdnDomain; $cdnDomains = array_unique($cdnDomains); $cdnDomainsForRegexp = array(); foreach ($cdnDomains as $cdnDomain) { $cdnDomainsForRegexp[] = '\/\/'.preg_quote($cdnDomain, '#'); // plain html $cdnDomainsForRegexp[] = '\\/\\/'.preg_quote($cdnDomain, '#'); // html in json } $cdnDomainsForRegexp = implode("|", $cdnDomainsForRegexp); $regexp = array(); foreach ($urlBorders as $border) { $regexp[] = '#('.$border[0].')\s*'.$host.'('.$firstPartOfUrl.'(?!'.$cdnDomainsForRegexp.')[^'.$border[2].']+\.(png|jpg|jpeg){1}(\?[^"\'\s]*?)?)\s*('.$border[1].')#siS'; } //var_dump($regexp);exit; //$regexp = str_replace('//', '/'); //$content = preg_replace($regexp, '${1}//cdn.optipic.io/site-'.self::$siteId.'${2}${5}', $content); $contentAfterReplace = preg_replace_callback($regexp, array(__NAMESPACE__ .'\ImgUrlConverter', 'callbackForPregReplace'), $content); if (!empty($contentAfterReplace)) { $content = $contentAfterReplace; } //} //self::$baseUrl = false; // ? $content = str_replace('', '' . PHP_EOL . self::getPreloadTags(), $content); if ($gziped) { $content = gzencode($content); // modify Content-Length if it's already sent $headersList = self::getResponseHeadersList(); if (is_array($headersList) && !empty($headersList['Content-Length'])) { header('Content-Length: ' . self::strlen($content)); } } $timeEnd = microtime(true); self::log(($timeEnd-$timeStart), 'Conversion finished in (sec.):'); return $content; } public static function getPreloadTags() { return '' . PHP_EOL . ''; } public static function trimList($list) { $trimmed = array(); foreach ($list as $item) { $item = trim(str_replace(array("\r\n", "\n", "\r"), '', $item)); if (!empty($item)) { $trimmed[] = $item; } } return $trimmed; } public static function textToArray($data) { if (is_array($data)) { $array = $data; } else { $array = explode("\n", $data); } if (!is_array($array)) { $array = array(); } $array = self::trimList($array); $array = array_unique($array); return $array; } /** * Load config from file or array */ public static function loadConfig($source = false) { if ($source===false) { $source = dirname(__FILE__) . '/config.php'; } if (is_array($source)) { self::$siteId = $source['site_id']; self::$domains = self::textToArray($source['domains']); foreach (self::$domains as $dKey => $domain) { $domainExploded = preg_split("/[\s,]+/siS", $domain); if (!empty($domainExploded[1]) && !empty($domainExploded[0])) { $domain = trim($domainExploded[0]); $domainSiteId = (int) trim($domainExploded[1]); self::$additionalDomains[$domain] = $domainSiteId; self::$domains[$dKey] = $domain; } } self::$exclusionsUrl = self::textToArray($source['exclusions_url']); self::$whitelistImgUrls = self::textToArray($source['whitelist_img_urls']); self::$srcsetAttrs = self::textToArray($source['srcset_attrs']); if (isset($source['admin_key'])) { self::$adminKey = $source['admin_key']; } if (isset($source['log'])) { if ($source['log']) { self::$enableLog = true; } } if (!empty($source['cdn_domain'])) { self::$cdnDomain = $source['cdn_domain']; } } elseif (file_exists($source)) { $config = require($source); if (is_array($config)) { self::$configFullPath = $source; self::loadConfig($config); } } } /** * Check if convertation enabled on current URL */ public static function isEnabled() { //$url = $_SERVER['REQUEST_URI']; $url = self::$url; if (in_array($url, self::$exclusionsUrl)) { return false; } // check rules with mask foreach (self::$exclusionsUrl as $exclUrl) { if (self::substr($exclUrl, -1)=='*') { $regexp = "#^".self::substr($exclUrl, 0, -1)."#i"; if (preg_match($regexp, $url)) { return false; } } } return true; } /** * Callback-function for preg_replace() to replace image URLs */ public static function callbackForPregReplace($matches) { self::log($matches, 'callbackForPregReplace -> $matches'); $replaceWithoutOptiPic = $matches[0]; $urlOriginal = $matches[2]; $slash = '/'; // skip images from json (json-encoded) if (stripos($replaceWithoutOptiPic, "\\/")!==false) { //return $replaceWithoutOptiPic; $slash = '\\/'; //var_dump($urlOriginal); $parseUrl = parse_url(json_decode('"'.$urlOriginal.'"')); //var_dump($parseUrl); $parseUrl['path'] = trim(json_encode($parseUrl['path']), "'\""); if (!empty($parseUrl['query'])) { $parseUrl['query'] = trim(json_encode($parseUrl['query']), "'\""); } //var_dump($parseUrl);exit; } else { $parseUrl = parse_url($urlOriginal); } if (!empty($parseUrl['host'])) { if (!in_array($parseUrl['host'], self::$domains) && !in_array($parseUrl['host'], array_keys(self::$additionalDomains))) { self::log($urlOriginal, 'callbackForPregReplace -> url original:'); self::log($replaceWithoutOptiPic, 'callbackForPregReplace -> url with optipic:'); return $replaceWithoutOptiPic; } } $ext = self::strtolower(pathinfo($parseUrl['path'], PATHINFO_EXTENSION)); if (!in_array($ext, array('png', 'jpg', 'jpeg'))) { return $replaceWithoutOptiPic; } if (self::urlHasPhpScript($urlOriginal)) { return $replaceWithoutOptiPic; } $urlOriginal = $parseUrl['path']; if (!empty($parseUrl['query'])) { $urlOriginal .= '?'.$parseUrl['query']; } $urlOriginal = self::getUrlFromRelative($urlOriginal, self::$baseUrl, $slash); $sid = self::$siteId; if (in_array($parseUrl['host'], array_keys(self::$additionalDomains))) { $sid = self::$additionalDomains[$parseUrl['host']]; } $replaceWithOptiPic = $matches[1].$slash.$slash.self::$cdnDomain.$slash.'site-'.$sid.$urlOriginal.$matches[5]; self::log($urlOriginal, 'callbackForPregReplace -> url original:'); self::log($replaceWithOptiPic, 'callbackForPregReplace -> url with optipic:'); if (self::substr($urlOriginal, 0, 7)=='http://') { return $replaceWithoutOptiPic; } if (self::substr($urlOriginal, 0, 8)=='https://') { return $replaceWithoutOptiPic; } if (self::substr($urlOriginal, 0, 2)=='//') { return $replaceWithoutOptiPic; } if (empty(self::$whitelistImgUrls)) { return $replaceWithOptiPic; } if (in_array($urlOriginal, self::$whitelistImgUrls)) { return $replaceWithOptiPic; } foreach (self::$whitelistImgUrls as $whiteUrl) { if (strpos($urlOriginal, $whiteUrl)===0) { return $replaceWithOptiPic; } } return $replaceWithoutOptiPic; } /** * Callback-function for preg_replace() to replace "srcset" attributes */ public static function callbackForPregReplaceSrcset($matches) { $isConverted = false; $originalContent = $matches[0]; $listConverted = array(); $list = explode(",", $matches['set']); foreach ($list as $item) { $source = preg_split("/[\s,]+/siS", trim($item)); $url = trim($source[0]); $size = (isset($source[1]))? trim($source[1]): ''; $toConvertUrl = "'".$url."'"; $convertedUrl = self::convertHtml($toConvertUrl, false); if ($toConvertUrl!=$convertedUrl) { $isConverted = true; $listConverted[] = trim(self::substr($convertedUrl, 1, -1).' '.$size); } } if ($isConverted) { return '<'.$matches['tag'].$matches['prefix'].' '.$matches['attr'].'='.$matches['quote1'].implode(", ", $listConverted).$matches['quote2'].$matches['suffix'].'>'; } else { return $originalContent; } } /*public static function isBinary($str) { return preg_match('~[^\x20-\x7E\t\r\n]~', $str) > 0; }*/ /** * Remove UTF-8 BOM-symbol from text */ public static function removeBomFromUtf($text) { $bom = pack('H*', 'EFBBBF'); $text = preg_replace("/^$bom/", '', $text); return $text; } /** * Check if gziped data */ public static function isGz($str) { if (self::strlen($str) < 2) { return false; } return (ord(self::substr($str, 0, 1)) == 0x1f && ord(self::substr($str, 1, 1)) == 0x8b); } public static function getUrlFromRelative($relativeUrl, $baseUrl = false, $slash = '/') { self::log($relativeUrl, 'getUrlFromRelative -> param orig'); if (stripos($relativeUrl, '../')!==false) { $relativeUrl = self::resolveFilename($relativeUrl, $slash); } if (self::substr($relativeUrl, 0, self::strlen($slash))==$slash) { return $relativeUrl; } /*if (self::substr($relativeUrl, 0, 2)=='\/') { // for json-encoded urls when / --> \/ return $relativeUrl; }*/ if (!$baseUrl) { //$baseUrl = pathinfo(self::$url, PATHINFO_DIRNAME); $baseUrl = self::getBaseDirOfUrl(self::$url); } //$baseUrl .= '/'; // CASE filepath ".img.png" (remove first dot) if (self::substr($relativeUrl, 0, 1) == '.' && self::substr($relativeUrl, 1, 1) != '.') { $relativeUrl = self::substr($relativeUrl, 1); } // CASE baseUrl "." (remove first dot) if (self::strlen($baseUrl)>0 && self::substr($baseUrl, 0, 1) == '.' && self::substr($baseUrl, 1, 1) != '.') { $baseUrl = (self::strlen($baseUrl)>1)? "".self::substr($baseUrl, 1): ""; } // CASE /catalog + img.png (/catalogimg.png is wrong) if (self::substr($baseUrl, -1)!='/' && self::substr($relativeUrl, 0, 1) != '/') { $tryUrl = str_replace($slash.$slash, $slash, $baseUrl.$slash.$relativeUrl); // Try to /catalog/img.png if (file_exists(self::getDocumentDoot().$slash.$tryUrl)) { return $tryUrl; } else { // Try to /img.png $tryUrl = str_replace($slash.$slash, $slash, '/'.$relativeUrl); if (file_exists(self::getDocumentDoot().$slash.$tryUrl)) { return $tryUrl; } } } // double slash to one slash $url = str_replace($slash.$slash, $slash, $baseUrl.$relativeUrl); return $url; } public static function resolveFilename($filename, $slash = '/') { $filename = str_replace($slash.$slash, $slash, $filename); $parts = explode($slash, $filename); $out = array(); foreach ($parts as $part) { if ($part == '.') { continue; } if ($part == '..') { array_pop($out); continue; } $out[] = $part; } return implode($slash, $out); } /** * Get main base path (dir) from full URL * * https://domain.com/catalog/catalog.php --> https://domain.com/catalog/ */ public static function getBaseDirOfUrl($url) { $urlParsed = parse_url($url); if (empty($urlParsed['path'])) { return '/'; } $urlPath = $urlParsed['path']; $baseUrl = $urlPath; $pathinfo = pathinfo($urlPath); if (!empty($pathinfo['extension'])) { $baseUrl = $pathinfo['dirname']; } if (stripos($url, $baseUrl.'/')!==false) { $baseUrl .= '/'; } if (self::substr($baseUrl, -1)!='/') { $pathinfo = pathinfo($baseUrl); if (!empty($pathinfo['dirname'])) { $baseUrl = $pathinfo['dirname']; } } return $baseUrl; } public static function getBaseUrlFromHtml($html) { preg_match('#(?P[^>]*)\s+href=(?P[^>\s]+)#isS', $html, $matches); $baseUrl = false; if (!empty($matches['base_url'])) { $baseUrl = trim($matches['base_url'], '"/'); $baseUrl = trim($baseUrl, "'"); $baseUrl = self::getBaseDirOfUrl($baseUrl); /*if (self::strlen($baseUrl)>0 && self::substr($baseUrl, -1, 1)!='/') { $baseUrl .= '/'; }*/ } return $baseUrl; } public static function getResponseHeadersList() { $list = array(); $headersList = headers_list(); if (is_array($headersList)) { foreach ($headersList as $row) { list($headerKey, $headerValue) = explode(":", $row); $headerKey = trim($headerKey); $headerValue = trim($headerValue); $list[$headerKey] = $headerValue; } } return $list; } /** * Log debug info into file */ public static function log($data, $comment = '') { if (!self::$enableLog) { return; } $date = \DateTime::createFromFormat('U.u', microtime(true)); if (!$date) { $date = new \DateTime(); } $dateFormatted = $date->format("Y-m-d H:i:s u"); $line = "[$dateFormatted] ".self::$url."\n"; if ($comment) { $line .= "# ".$comment."\n"; } $line .= var_export($data, true)."\n"; file_put_contents(dirname(__FILE__) . '/log.txt', $line, FILE_APPEND); } public static function getDefaultSettings($settingKey = false) { $settings = array( 'srcset_attrs' => array( 'srcset', 'data-srcset', ), 'domains' => array(), ); if ($currentDomain = self::getCurrentDomain(true)) { $settings['domains'] = array( $currentDomain, 'www.'.$currentDomain, ); } if ($settingKey) { return (!empty($settings[$settingKey]))? $settings[$settingKey]: ''; } return $settings; } public static function getCurrentDomain($trimWww = false) { if (is_null(self::$host) && !empty($_SERVER['HTTP_HOST'])) { self::$host = $_SERVER['HTTP_HOST']; } if (empty(self::$host)) { return false; } $currentHost = explode(":", self::$host); $currentHost = trim($currentHost[0]); if ($trimWww) { if (stripos($currentHost, 'www.')===0) { $currentHost = self::substr($currentHost, 4); } } return $currentHost; } public static function strlen($str) { //return strlen($str); return \Tools::strlen($str); } public static function substr($string, $offset, $length = null) { //return substr($string, $offset, $length); if (is_null($length)) { $length = false; } return \Tools::substr($string, $offset, $length); } public static function strtolower($str) { //return strtolower($str); return \Tools::strtolower($str); } public static function file_get_contents($filepath) { //return file_get_contents($filepath); return \Tools::file_get_contents($filepath); } public static function htmlHasAmpMarkup($html) { return (stripos($html, "