diff --git a/app/addons/optipic/ImgUrlConverter.php b/app/addons/optipic/ImgUrlConverter.php new file mode 100644 index 0000000..9f76a2f --- /dev/null +++ b/app/addons/optipic/ImgUrlConverter.php @@ -0,0 +1,556 @@ +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(empty(self::$siteId)) { + return $content; + } + + 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 = self::getBaseDirOfUrl($_SERVER['REQUEST_URI']); + } + $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) { + $urlParsed = parse_url($url); + $urlPath = $urlParsed['path']; + $pathinfo = pathinfo($urlPath); + if(!empty($pathinfo['extension'])) { + return $pathinfo['dirname']; + } + return $urlPath; + } + + 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/app/addons/optipic/Optipic.php b/app/addons/optipic/Optipic.php new file mode 100644 index 0000000..c14a5c4 --- /dev/null +++ b/app/addons/optipic/Optipic.php @@ -0,0 +1,30 @@ + Registry::get('addons.optipic.autoreplace_active'), + 'site_id' => Registry::get('addons.optipic.site_id'), + 'domains' => Registry::get('addons.optipic.domains')!='' ? explode("\n", Registry::get('addons.optipic.domains')) : array(), + 'exclusions_url' => Registry::get('addons.optipic.exclusions_url')!='' ? explode("\n", Registry::get('addons.optipic.exclusions_url')) : array(), + 'whitelist_img_urls' => Registry::get('addons.optipic.whitelist_img_urls')!='' ? explode("\n", Registry::get('addons.optipic.whitelist_img_urls')) : array(), + 'srcset_attrs' => Registry::get('addons.optipic.srcset_attrs')!='' ? explode("\n", Registry::get('addons.optipic.srcset_attrs')) : array(), + ); + } + + public function changeContent($content) + { + $settings = $this->getSettings(); + + if ($settings['autoreplace_active']=='Y' && $settings['site_id']!=''){ + ImgUrlConverter::loadConfig($settings); + $content = ImgUrlConverter::convertHtml($content); + } + + return $content; + } +} \ No newline at end of file diff --git a/app/addons/optipic/addon.xml b/app/addons/optipic/addon.xml index b1c45ed..d3e5cf5 100644 --- a/app/addons/optipic/addon.xml +++ b/app/addons/optipic/addon.xml @@ -1,97 +1,90 @@ - - - - - optipic - - 1.0 - - en - - OptiPic - - OptiPic.io - image optimization via smart CDN. The module automates the process of optimizing and compressing all images on the site according to the recommendations of Google PageSpeed Insights. - - 100 - - active - - - OptiPic.io - оптимизация изображений через умный CDN. Модуль автоматизирует процесс оптимизации и сжатия всех изображений на сайте согласно рекомендациям Google PageSpeed Insights. - - - - - -
- - Common settings - - Общие настройки - - - - - - - header - - OptiPic CDN Integration Settings - - - Настройки интеграции OptiPic CDN - - - - - input - Site ID in your CDN OptiPic account - - OptiPic.io and add your site to your CDN account]]> - - ID сайта в личном кабинете CDN OptiPic - - OptiPic.io и добавьте свой сайт в личный кабинет CDN]]> - - - - - checkbox - Auto-replace image URLs - - Автоматическая подмена URL изображений на сайте - - - - - input - In what attributes of the <img> tag will auto-replace URL occur (comma-separated list) - - src, data-lazy, data-full-img, data-original - - В каких атрибутах тега <img> будет происходить автоподмена URL (список через запятую) - - - - -
-
-
+ + + + +optipic + +OptiPic + +OptiPic.io - image optimization via smart CDN. The module automates the process of optimizing and compressing all images on the site according to the recommendations of Google PageSpeed Insights. + +1.14.0 + +en + +100 + +active + + + + OptiPic.io + info@optipic.io + https://optipic.io + + + +MULTIVENDOR,ULTIMATE + + + + + + + + + + + + + +
+ + + + + checkbox + + + + input + + + + textarea + + + + textarea + + + + textarea + + + + textarea + + + + info + fn_optipic_settings_field_info + + + +
+
+
\ No newline at end of file diff --git a/app/addons/optipic/func.php b/app/addons/optipic/func.php index 1c8628b..4c57173 100644 --- a/app/addons/optipic/func.php +++ b/app/addons/optipic/func.php @@ -1,185 +1,70 @@ - '/]+?)('.$attr.'?)="([^"]+\.(png|jpg|jpeg)?)"([^>]*?)>/simS', - 'quote' => '"', - ), - array( - 'pattern' => "/]+?)(".$attr."?)='([^']+\.(png|jpg|jpeg)?)'([^>]*?)>/simS", - 'quote' => "'", - ), - ); - - foreach($patterns as $pattern) { - //var_dump($pattern); - $qoute = $pattern['quote']; - $content = preg_replace_callback( - $pattern['pattern'], - function($matches) use ($dir, $settings, $qoute) { - //var_dump($matches);exit; - $quoteSymbol = '"'; - if($qoute) { - $quoteSymbol = $qoute; - } - - $url = $matches[3]; - //var_dump($matches); - - $optipicUrl = optipic_build_img_url($url, array('site_id'=>$settings['site_id'])); - //var_dump($optipicUrl);exit; - - if($optipicUrl==$url) { - return $matches[0]; - } - else { - return ''; - } - }, - $content - ); - } - } - //exit; - } - } - - echo $content; - } -} - -function optipic_get_settings() { - - $optipicSiteID = Registry::get('addons.optipic.cdn_site_id'); - $autoreplaceActive = Registry::get('addons.optipic.cdn_autoreplace_active'); - $imgAttrs = Registry::get('addons.optipic.cdn_autoreplace_img_attrs'); - - $attrs = []; - foreach(explode(",", $imgAttrs) as $attr) { - $attr = trim($attr); - if($attr) { - $attrs[] = preg_quote($attr, '/'); - } - } - - return array( - 'site_id' => $optipicSiteID, - 'autoreplace_active' => ($autoreplaceActive=='Y'), - 'img_attrs' => $attrs, - ); -} - -function optipic_build_img_url($localUrl, $params=array()) { - $dir = optipic_get_current_url_dir(); - - $schema = "//"; - - if(!isset($params['site_id'])) { - $settings = optipic_get_settings(); - $params['site_id'] = $settings['site_id']; - } - - if(isset($params['url_schema'])) { - if($params['url_schema']=='http') { - $schema = "http://"; - } - elseif($params['url_schema']=='https') { - $schema = "https://"; - } - } - - - if($params['site_id']) { - if(!fn_strlen(trim($localUrl)) || stripos($localUrl, 'cdn.optipic.io')!==false) { - return $localUrl; - } - /*elseif(stripos($localUrl, 'http://')===0) { - return $localUrl; - } - elseif(stripos($localUrl, 'https://')===0) { - return $localUrl; - } - elseif(stripos($localUrl, '//')===0) { - return $localUrl; - }*/ - else { - - // убираем адрес сайта из начала URL (для http) - if(stripos($localUrl, Registry::get('config.http_location'))===0) { - //$protocol = defined('HTTPS') ? 'https' : 'http'; - $localUrl = fn_substr($localUrl, fn_strlen(Registry::get('config.http_location'))); - } - // убираем адрес сайта из начала URL (для https) - elseif(stripos($localUrl, Registry::get('config.https_location'))===0) { - //$protocol = defined('HTTPS') ? 'https' : 'http'; - $localUrl = fn_substr($localUrl, fn_strlen(Registry::get('config.http_location'))); - } - - // если URL не абсолютный - приводим его к абсолютному - if(substr($localUrl, 0, 1)!='/') { - $localUrl = $dir.$localUrl; - } - - $url = $schema.'cdn.optipic.io/site-'.$params['site_id']; - if(isset($params['q'])) { - $url .= '/optipic-q='.$params['q']; - } - if(isset($params['maxw'])) { - $url .= '/optipic-maxw='.$params['maxw']; - } - if(isset($params['maxh'])) { - $url .= '/optipic-maxh='.$params['maxh']; - } - - $url .= $localUrl; - - return $url; - - //return ''; - } - } - // Если URL - else { - return $localUrl; - } - - -} - -function optipic_get_current_url_dir() { - return fn_substr($_SERVER['REQUEST_URI'], 0, strripos($_SERVER['REQUEST_URI'], '/')+1); +changeContent($content); + } + + echo $content; +} + +function fn_optipic_settings_field_info() { + + require_once dirname(__FILE__) . '/ImgUrlConverter.php'; + require_once dirname(__FILE__) . '/Optipic.php'; + + $optipic = new Optipic(); + $currentHost = \optipic\cdn\ImgUrlConverter::getCurrentDomain(); + + $settings = $optipic->getSettings(); + + $srcJs = 'https://optipic.io/api/cp/stat?domain='.$currentHost.'&sid='.$settings['site_id'].'&cms=cscart&stype=cdn&append_to=%23content_optipic&version=1.14.0'; + + $html = ''; + + //$html .= var_export(\optipic\cdn\ImgUrlConverter::getDefaultSettings(), true); + $defaultSettings = \optipic\cdn\ImgUrlConverter::getDefaultSettings(); + $defaultDomains = implode("\\n", $defaultSettings['domains']); + $defaultSrcsetAttrs = implode("\\n", $defaultSettings['srcset_attrs']); + + if($currentHost) { + $html .= ''; + } + + $html .= << +window.optipicAfterInitExternalCallback = function($) { + var optipicDomainsTextarea = $(".control-group.optipic textarea[id^='addon_option_optipic_domains']"); + if(optipicDomainsTextarea.length==1 && optipicDomainsTextarea.val().length==0) { + optipicDomainsTextarea.val("$defaultDomains"); + console.log("auto set Domains"); + } + + var optipicSrcsetAttrsTextarea = $(".control-group.optipic textarea[id^='addon_option_optipic_srcset_attrs']"); + if(optipicSrcsetAttrsTextarea.length==1 && optipicSrcsetAttrsTextarea.val().length==0) { + optipicSrcsetAttrsTextarea.val("$defaultSrcsetAttrs"); + console.log("auto set Srcset"); + } +} + + +JS +; + + return $html; + //return ''; } \ No newline at end of file diff --git a/app/addons/optipic/init.php b/app/addons/optipic/init.php index f5f23d0..0e0180b 100644 --- a/app/addons/optipic/init.php +++ b/app/addons/optipic/init.php @@ -1,8 +1,10 @@ -