diff --git a/optipic/ImgUrlConverter.php b/optipic/ImgUrlConverter.php new file mode 100644 index 0000000..4c9ac57 --- /dev/null +++ b/optipic/ImgUrlConverter.php @@ -0,0 +1,551 @@ +0) { + self::loadConfig($config); + } + } + + /** + * Convert whole HTML-block contains image urls + */ + public static function convertHtml($content) { + + $timeStart = microtime(true); + + //ini_set('pcre.backtrack_limit', 100000000); + + $content = self::removeBomFromUtf($content); + + // try auto load config from __DIR__.'config.php' + if(empty(self::$siteId)) { + self::loadConfig(); + } + + if(!self::isEnabled()) { + return $content; + } + + $gziped = false; + if(self::isGz($content)) { + if($contentUngzip = gzdecode($content)) { + $gziped = true; + $content = $contentUngzip; + } + } + + 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(substr($whiteImgUrl, -1, 1)=='/') { + $whiteImgUrl = 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('\(', '\)'), // () + ); + $regexp = array(); + foreach($urlBorders as $border) { + $regexp[] = '#('.$border[0].')'.$host.'('.$firstPartOfUrl.'(?!\/\/cdn\.optipic\.io)[^'.$border[1].']+\.(png|jpg|jpeg){1}(\?[^"\'\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; + + 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: ' . strlen($content)); + } + } + + $timeEnd = microtime(true); + self::log(($timeEnd-$timeStart), 'Conversion finished in (sec.):'); + + return $content; + } + + public static function trimList($list) { + $trimmed = array(); + foreach ($list as $ind => $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 = __DIR__ . '/config.php'; + } + + if(is_array($source)) { + self::$siteId = $source['site_id']; + + self::$domains = self::textToArray($source['domains']); + + 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; + } + } + } + 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']; + if(in_array($url, self::$exclusionsUrl)) { + return false; + } + // check rules with mask + foreach(self::$exclusionsUrl as $exclUrl) { + if(substr($exclUrl, -1)=='*') { + $regexp = "#^".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]; + + // skip images from json (json-encoded) + if(stripos($replaceWithoutOptiPic, "\\/")!==false) { + return $replaceWithoutOptiPic; + } + + $urlOriginal = $matches[2]; + + $parseUrl = parse_url($urlOriginal); + + if(!empty($parseUrl['host'])) { + if(!in_array($parseUrl['host'], self::$domains)) { + self::log($urlOriginal, 'callbackForPregReplace -> url original:'); + self::log($replaceWithoutOptiPic, 'callbackForPregReplace -> url with optipic:'); + return $replaceWithoutOptiPic; + } + } + + $ext = pathinfo($parseUrl['path'], PATHINFO_EXTENSION); + if(!in_array($ext, array('png', 'jpg', 'jpeg'))) { + return $replaceWithoutOptiPic; + } + + $urlOriginal = $parseUrl['path']; + if(!empty($parseUrl['query'])) { + $urlOriginal .= '?'.$parseUrl['query']; + } + $urlOriginal = self::getUrlFromRelative($urlOriginal, self::$baseUrl); + + + $replaceWithOptiPic = $matches[1].'//cdn.optipic.io/site-'.self::$siteId.$urlOriginal.$matches[5]; + + self::log($urlOriginal, 'callbackForPregReplace -> url original:'); + self::log($replaceWithOptiPic, 'callbackForPregReplace -> url with optipic:'); + + if(substr($urlOriginal, 0, 7)=='http://') { + return $replaceWithoutOptiPic; + } + if(substr($urlOriginal, 0, 8)=='https://') { + return $replaceWithoutOptiPic; + } + if(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); + if($toConvertUrl!=$convertedUrl) { + $isConverted = true; + $listConverted[] = trim(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 (strlen($str) < 2) return false; + return (ord(substr($str, 0, 1)) == 0x1f && ord(substr($str, 1, 1)) == 0x8b); + } + + public static function getUrlFromRelative($relativeUrl, $baseUrl=false) { + if(substr($relativeUrl, 0, 1)=='/') { + return $relativeUrl; + } + if(substr($relativeUrl, 0, 2)=='\/') { // for json-encoded urls when / --> \/ + return $relativeUrl; + } + + if(!$baseUrl) { + $baseUrl = pathinfo($_SERVER['REQUEST_URI'], PATHINFO_DIRNAME); + } + $baseUrl .= '/'; + $url = str_replace('//', '/', $baseUrl.$relativeUrl); + return $url; + } + + + + /** + * Get main base path (dir) from full URL + * + * https://domain.com/catalog/catalog.php --> https://domain.com/catalog/ + */ + public static function getBaseDirOfUrl($url) { + $pathinfo = pathinfo($url); + if(!empty($pathinfo['extension'])) { + return $pathinfo['dirname']; + } + return $url; + } + + public static function getBaseUrlFromHtml($html) { + preg_match('#(?Pbase)(?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(strlen($baseUrl)>0 && 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] {$_SERVER['REQUEST_URI']}\n"; + if($comment) { + $line .= "# ".$comment."\n"; + } + $line .= var_export($data, true)."\n"; + file_put_contents(__DIR__ . '/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(empty($_SERVER['HTTP_HOST'])) { + return false; + } + + $currentHost = explode(":", $_SERVER['HTTP_HOST']); + $currentHost = trim($currentHost[0]); + if($trimWww) { + if(stripos($currentHost, 'www.')===0) { + $currentHost = substr($currentHost, 4); + } + } + + return $currentHost; + } +} +?> \ No newline at end of file diff --git a/optipic/optipic.php b/optipic/optipic.php new file mode 100644 index 0000000..7a4742e --- /dev/null +++ b/optipic/optipic.php @@ -0,0 +1,84 @@ +getName() == 'site'){ + $settings = $this->getSettings(); + if($settings['autoreplace_active'] && $settings['site_id']){ + require_once 'ImgUrlConverter.php'; + optipic\cdn\ImgUrlConverter::loadConfig($settings); + $content = $app->getBody(); + $content = optipic\cdn\ImgUrlConverter::convertHtml($content); + $app->setBody($content); + } + } + elseif($app->isAdmin()) { + $bodyHtml = $app->getBody(); + + $isOptipicSettingsPage = stripos($bodyHtml, 'optipic-plugin-page-detecor-mark')!==false; + + if($isOptipicSettingsPage) { + + $settings = $this->getSettings(); + + if($sid = $settings['site_id']) { + $uri = JUri::getInstance(); + $host = $uri->getHost(); + + $js = ''; + + $bodyHtml = str_replace ("", $js." ", $bodyHtml); + $app->setBody($bodyHtml); + } + + + } + } + + return true; + } + + function getSettings(){ + $autoreplaceActive = $this->params->get('autoreplace_active', ''); + $siteId = $this->params->get('site_id', ''); + $domains = $this->params->get('domains', '')!=''? explode("\n", $this->params->get('domains', '')):array(); + $exclusionsUrl = $this->params->get('exclusions_url', '')!=''?explode("\n", $this->params->get('exclusions_url', '')):array(); + $whitelistImgUrls = $this->params->get('whitelist_img_urls', '')!=''?explode("\n", $this->params->get('whitelist_img_urls', '')):array(); + $srcsetAttrs = $this->params->get('srcset_attrs', '')!=''?explode("\n", $this->params->get('srcset_attrs', '')):array(); + + return array( + 'autoreplace_active' => $autoreplaceActive, + 'site_id' => $siteId, // your SITE ID from CDN OptiPic controll panel + 'domains' => $domains, // list of domains should replace to cdn.optipic.io + 'exclusions_url' => $exclusionsUrl, // list of URL exclusions - where is URL should not converted + 'whitelist_img_urls' => $whitelistImgUrls, // whitelist of images URL - what should to be converted (parts or full urls start from '/') + 'srcset_attrs' => $srcsetAttrs, // tag's srcset attributes // @see https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images + ); + } +} +?> \ No newline at end of file diff --git a/optipic/optipic.xml b/optipic/optipic.xml new file mode 100644 index 0000000..9f6d113 --- /dev/null +++ b/optipic/optipic.xml @@ -0,0 +1,64 @@ + + + PLG_SYSTEM_OPTIPIC + OptiPic.io + 2020-02-05 + + GNU General Public License version 2 or later + info@optipic.io + https://optipic.io/cdn/ + 1.14.0 + PLG_SYSTEM_OPTIPIC_DESC + + optipic.php + ImgUrlConverter.php + + + language/en-GB/en-GB.plg_system_optipic.ini + language/en-GB/en-GB.plg_system_optipic.sys.ini + language/ru-RU/ru-RU.plg_system_optipic.ini + language/ru-RU/ru-RU.plg_system_optipic.sys.ini + + + +
+ + + + + + + + + + + + +
+
+
+ + + https://optipic.io/plugins/joomla/manifest.xml + +
\ No newline at end of file