code; } /** * @param int $code */ public function setCode($code) { $this->code = $code; } /** * @return array */ public function getHeaders() { return $this->headers; } /** * @param string $name * @return string|null */ public function getHeader($name) { foreach ($this->headers as $k => $v) { if (strcasecmp($name, $k) === 0) { return $v; } } return null; } public function setHeaders(array $headers) { $this->headers = $headers; } /** * @param $name * @param $value */ public function setHeader($name, $value) { $this->headers[$name] = $value; } /** * @return string|iterable */ public function getContent() { return $this->content; } /** * @param string|iterable $content */ public function setContent($content) { $this->content = $content; } public function isCompressible() { return strpos($this->getHeader('Content-Type'), 'image/') === false; } } namespace Kibo\Phast\HTTP; interface Client { /** * Retrieve a URL using the GET HTTP method * * @param URL $url * @param array $headers - headers to send in headerName => headerValue format * @return Response * @throws \Exception */ public function get(\Kibo\Phast\ValueObjects\URL $url, array $headers = array()); /** * Send data to a URL using the POST HTTP method * * @param URL $url * @param array|string $data - if array, it will be encoded as form data, if string - will be sent as is * @param array $headers - headers to send in headerName => headerValue format * @return Response * @throws \Exception */ public function post(\Kibo\Phast\ValueObjects\URL $url, $data, array $headers = array()); } namespace Kibo\Phast\HTTP; class CURLClient implements \Kibo\Phast\HTTP\Client { public function get(\Kibo\Phast\ValueObjects\URL $url, array $headers = array()) { $this->checkCURL(); return $this->request($url, $headers); } public function post(\Kibo\Phast\ValueObjects\URL $url, $data, array $headers = array()) { $this->checkCURL(); return $this->request($url, $headers, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $data]); } private function checkCURL() { if (!function_exists('curl_init')) { throw new \Kibo\Phast\HTTP\Exceptions\NetworkError('cURL is not installed'); } } private function request(\Kibo\Phast\ValueObjects\URL $url, array $headers = array(), array $opts = array()) { $response = new \Kibo\Phast\HTTP\Response(); $readHeader = function ($_, $headerLine) use($response) { if (strpos($headerLine, 'HTTP/') === 0) { $response->setHeaders([]); } else { list($name, $value) = explode(':', $headerLine, 2); if (trim($name) !== '') { $response->setHeader($name, trim($value)); } } return strlen($headerLine); }; $ch = curl_init((string) $url); curl_setopt_array($ch, $opts + [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $this->makeHeaders($headers), CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 5, CURLOPT_HEADERFUNCTION => $readHeader, CURLOPT_CAINFO => __DIR__ . '/cacert.pem', CURLOPT_ENCODING => '']); $responseText = @curl_exec($ch); if ($responseText === false) { throw new \Kibo\Phast\HTTP\Exceptions\NetworkError(curl_error($ch), curl_errno($ch)); } $info = curl_getinfo($ch); if (!preg_match('/^2/', $info['http_code'])) { throw new \Kibo\Phast\HTTP\Exceptions\HTTPError($info['http_code']); } $response->setCode($info['http_code']); $response->setContent($responseText); return $response; } private function makeHeaders(array $headers) { $result = []; foreach ($headers as $k => $v) { $result[] = "{$k}: {$v}"; } return $result; } } namespace Kibo\Phast\HTTP; class Request { /** * @var array */ private $env; /** * @var array */ private $cookie; /** * @var string */ private $query; private function __construct() { } public static function fromGlobals() { $instance = new self(); $instance->env = $_SERVER; $instance->cookie = $_COOKIE; return $instance; } public static function fromArray(array $get = array(), array $env = array(), array $cookie = array()) { if ($get) { $url = isset($env['REQUEST_URI']) ? $env['REQUEST_URI'] : ''; $env['REQUEST_URI'] = \Kibo\Phast\ValueObjects\URL::fromString($url)->withQuery(http_build_query($get))->toString(); } $instance = new self(); $instance->env = $env; $instance->cookie = $cookie; return $instance; } /** * @return array */ public function getGet() { return $this->getQuery()->toAssoc(); } /** * @return Query */ public function getQuery() { return \Kibo\Phast\ValueObjects\Query::fromString($this->getQueryString()); } /** * @param $name string * @return string|null */ public function getHeader($name) { $key = 'HTTP_' . strtoupper(str_replace('-', '_', $name)); return $this->getEnvValue($key); } public function getPathInfo() { $pathInfo = $this->getEnvValue('PATH_INFO'); if ($pathInfo) { return $pathInfo; } $script = $this->getEnvValue('PHP_SELF'); $uri = $this->getEnvValue('DOCUMENT_URI'); if ($script !== null && $uri !== null && strpos($uri, $script . '/') === 0) { return substr($uri, strlen($script)); } } public function getCookie($name) { if (isset($this->cookie[$name])) { return $this->cookie[$name]; } } public function getQueryString() { $parsed = parse_url($this->getEnvValue('REQUEST_URI')); if (isset($parsed['query'])) { return $parsed['query']; } } public function getAbsoluteURI() { return ($this->getEnvValue('HTTPS') ? 'https' : 'http') . '://' . $this->getHost() . $this->getURI(); } public function getHost() { return $this->getHeader('Host'); } public function getURI() { return $this->getEnvValue('REQUEST_URI'); } private function getEnvValue($key) { if (isset($this->env[$key])) { return $this->env[$key]; } } public function getDocumentRoot() { $scriptName = (string) $this->getEnvValue('SCRIPT_NAME'); $scriptFilename = $this->normalizePath((string) $this->getEnvValue('SCRIPT_FILENAME')); if (strpos($scriptName, '/') === 0 && $this->isAbsolutePath($scriptFilename) && $this->isSuffix($scriptName, $scriptFilename)) { return substr($scriptFilename, 0, strlen($scriptFilename) - strlen($scriptName)); } return $this->getEnvValue('DOCUMENT_ROOT'); } private function normalizePath($path) { return str_replace('\\', '/', $path); } private function isAbsolutePath($path) { return preg_match('~^/|^[a-z]:/~i', $path); } private function isSuffix($suffix, $string) { return substr($string, -strlen($suffix)) === $suffix; } public function isCloudflare() { return !!$this->getHeader('CF-Ray'); } } namespace Kibo\Phast\HTTP; class ClientFactory { const CONFIG_KEY = 'httpClient'; /** * @param array $config * @return Client */ public function make(array $config) { $spec = $config[self::CONFIG_KEY]; if (is_callable($spec)) { $client = $spec(); } elseif (class_exists($spec)) { $client = new $spec(); } else { throw new \Kibo\Phast\Exceptions\RuntimeException(self::CONFIG_KEY . ' config value must be either callable or a class name'); } return $client; } } namespace Kibo\Phast\HTTP\Exceptions; class HTTPError extends \RuntimeException { } namespace Kibo\Phast\HTTP\Exceptions; class NetworkError extends \RuntimeException { } namespace Kibo\Phast\Environment; class DefaultConfiguration { public static function get() { $request = \Kibo\Phast\HTTP\Request::fromGlobals(); return ['securityToken' => null, 'retrieverMap' => [$request->getHost() => $request->getDocumentRoot()], 'httpClient' => \Kibo\Phast\HTTP\CURLClient::class, 'cache' => ['cacheRoot' => sys_get_temp_dir() . '/phast-cache-' . (new \Kibo\Phast\Common\System())->getUserId(), 'shardingDepth' => 1, 'garbageCollection' => ['maxItems' => 100, 'probability' => 0.1, 'maxAge' => 86400 * 365], 'diskCleanup' => ['maxSize' => 500 * pow(1024, 2), 'probability' => 0.02, 'portionToFree' => 0.5]], 'servicesUrl' => '/phast.php', 'serviceRequestFormat' => \Kibo\Phast\Services\ServiceRequest::FORMAT_PATH, 'compressServiceResponse' => true, 'optimizeHTMLDocumentsOnly' => true, 'outputServerSideStats' => true, 'documents' => ['maxBufferSizeToApply' => pow(1024, 2), 'baseUrl' => $request->getAbsoluteURI(), 'filters' => [\Kibo\Phast\Filters\HTML\CommentsRemoval\Filter::class => [], \Kibo\Phast\Filters\HTML\Minify\Filter::class => [], \Kibo\Phast\Filters\HTML\MinifyScripts\Filter::class => [], \Kibo\Phast\Filters\HTML\BaseURLSetter\Filter::class => [], \Kibo\Phast\Filters\HTML\ImagesOptimizationService\Tags\Filter::class => [], \Kibo\Phast\Filters\HTML\LazyImageLoading\Filter::class => [], \Kibo\Phast\Filters\HTML\CSSInlining\Filter::class => ['optimizerSizeDiffThreshold' => 1024, 'whitelist' => ['~^https?://fonts\\.googleapis\\.com/~' => ['ieCompatible' => false], '~^https?://ajax\\.googleapis\\.com/ajax/libs/jqueryui/~', '~^https?://maxcdn\\.bootstrapcdn\\.com/[^?#]*\\.css~', '~^https?://idangero\\.us/~', '~^https?://[^/]*\\.github\\.io/~', '~^https?://\\w+\\.typekit\\.net/~' => ['ieCompatible' => false], '~^https?://stackpath\\.bootstrapcdn\\.com/~', '~^https?://cdnjs\\.cloudflare\\.com/~']], \Kibo\Phast\Filters\HTML\ImagesOptimizationService\CSS\Filter::class => [], \Kibo\Phast\Filters\HTML\DelayedIFrameLoading\Filter::class => [], \Kibo\Phast\Filters\HTML\ScriptsProxyService\Filter::class => ['urlRefreshTime' => 7200], \Kibo\Phast\Filters\HTML\Diagnostics\Filter::class => ['enabled' => 'diagnostics'], \Kibo\Phast\Filters\HTML\ScriptsDeferring\Filter::class => [], \Kibo\Phast\Filters\HTML\PhastScriptsCompiler\Filter::class => []]], 'images' => ['enable-cache' => 'imgcache', 'api-mode' => false, 'factory' => \Kibo\Phast\Filters\Image\ImageFactory::class, 'maxImageInliningSize' => 512, 'whitelist' => ['~^https?://ajax\\.googleapis\\.com/ajax/libs/jqueryui/~'], 'filters' => [\Kibo\Phast\Filters\Image\ImageAPIClient\Filter::class => ['api-url' => 'https://optimize.phast.io/?service=images', 'host-name' => $request->getHost(), 'request-uri' => $request->getURI(), 'plugin-version' => 'phast-core-1.0']]], 'styles' => ['filters' => [\Kibo\Phast\Filters\Text\Decode\Filter::class => [], \Kibo\Phast\Filters\CSS\ImportsStripper\Filter::class => [], \Kibo\Phast\Filters\CSS\CSSMinifier\Filter::class => [], \Kibo\Phast\Filters\CSS\CSSURLRewriter\Filter::class => [], \Kibo\Phast\Filters\CSS\ImageURLRewriter\Filter::class => ['maxImageInliningSize' => 512], \Kibo\Phast\Filters\CSS\FontSwap\Filter::class => []]], 'logging' => ['logWriters' => [['class' => \Kibo\Phast\Logging\LogWriters\PHPError\Writer::class, 'levelMask' => \Kibo\Phast\Logging\LogLevel::EMERGENCY | \Kibo\Phast\Logging\LogLevel::ALERT | \Kibo\Phast\Logging\LogLevel::CRITICAL | \Kibo\Phast\Logging\LogLevel::ERROR | \Kibo\Phast\Logging\LogLevel::WARNING], ['enabled' => 'diagnostics', 'class' => \Kibo\Phast\Logging\LogWriters\JSONLFile\Writer::class, 'logRoot' => sys_get_temp_dir() . '/phast-logs']]], 'switches' => ['phast' => true, 'diagnostics' => false], 'scripts' => ['removeLicenseHeaders' => false, 'whitelist' => ['~^https?://' . preg_quote($request->getHost(), '~') . '/~', '~^https?://(ssl|www)\\.google-analytics\\.com/(analytics\\.js|ga\\.js|gtm/js)($|\\?)~', '~^https?://www\\.googletagmanager\\.com/~', '~^https?://www\\.googleadservices\\.com/~', '~^https?://pixel\\.adcrowd\\.com/~', '~^https?://connect\\.facebook\\.net/~', '~^https?://static\\.hotjar\\.com/~', '~^https?://v2\\.zopim\\.com/~', '~^https?://stats\\.g\\.doubleclick\\.net/dc\\.js$~', '~^https?://s\\.pinimg\\.com/~']]]; } } namespace Kibo\Phast\Environment; class Package { /** * @var string */ protected $type; /** * @var string */ protected $namespace; /** * @param $className * @param string|null $type * @return Package */ public static function fromPackageClass($className, $type = null) { $instance = new self(); $lastSeparatorPosition = strrpos($className, '\\'); $instance->type = empty($type) ? substr($className, $lastSeparatorPosition + 1) : $type; $instance->namespace = substr($className, 0, $lastSeparatorPosition); return $instance; } /** * @return string */ public function getType() { return $this->type; } /** * @return string */ public function getNamespace() { return $this->namespace; } /** * @return bool */ public function hasFactory() { return $this->classExists($this->getFactoryClassName()); } /** * @return mixed */ public function getFactory() { if ($this->hasFactory()) { $class = $this->getFactoryClassName(); return new $class(); } throw new \Kibo\Phast\Environment\Exceptions\PackageHasNoFactoryException("Package {$this->namespace} has no factory"); } /** * @return bool */ public function hasDiagnostics() { return $this->classExists($this->getDiagnosticsClassName()); } /** * @return Diagnostics */ public function getDiagnostics() { if ($this->hasDiagnostics()) { $class = $this->getDiagnosticsClassName(); return new $class(); } throw new \Kibo\Phast\Environment\Exceptions\PackageHasNoDiagnosticsException("Package {$this->namespace} has no diagnostics"); } private function getFactoryClassName() { return $this->getClassName('Factory'); } private function getDiagnosticsClassName() { return $this->getClassName('Diagnostics'); } private function getClassName($class) { return $this->namespace . '\\' . $class; } private function classExists($class) { // Don't trigger any autoloaders if Phast has been compiled into a // single file, and avoid triggering Magento code generation. $useAutoloader = basename(__FILE__) == 'Package.php'; return class_exists($class, $useAutoloader); } } namespace Kibo\Phast\Environment; class Switches { const SWITCH_PHAST = 'phast'; const SWITCH_DIAGNOSTICS = 'diagnostics'; private static $defaults = array(self::SWITCH_PHAST => true, self::SWITCH_DIAGNOSTICS => false); private $switches = array(); public static function fromArray(array $switches) { $instance = new self(); $instance->switches = array_merge($instance->switches, $switches); return $instance; } public static function fromString($switches) { $instance = new self(); if (empty($switches)) { return $instance; } foreach (explode(',', $switches) as $switch) { if ($switch[0] == '-') { $instance->switches[substr($switch, 1)] = false; } else { $instance->switches[$switch] = true; } } return $instance; } public function merge(\Kibo\Phast\Environment\Switches $switches) { $instance = new self(); $instance->switches = array_merge($this->switches, $switches->switches); return $instance; } public function isOn($switch) { if (isset($this->switches[$switch])) { return (bool) $this->switches[$switch]; } if (isset(self::$defaults[$switch])) { return (bool) self::$defaults[$switch]; } return true; } public function toArray() { return array_merge(self::$defaults, $this->switches); } } namespace Kibo\Phast\Environment; class Configuration { /** * @var array */ private $sourceConfig; /** * @var Switches */ private $switches; /** * @return Configuration */ public static function fromDefaults() { return new self(\Kibo\Phast\Environment\DefaultConfiguration::get()); } /** * Configuration constructor. * @param array $sourceConfig */ public function __construct(array $sourceConfig) { $this->sourceConfig = $sourceConfig; if (!isset($this->sourceConfig['switches'])) { $this->switches = new \Kibo\Phast\Environment\Switches(); } else { $this->switches = \Kibo\Phast\Environment\Switches::fromArray($this->sourceConfig['switches']); } } /** * @param Configuration $config * @return $this */ public function withUserConfiguration(\Kibo\Phast\Environment\Configuration $config) { $result = $this->recursiveMerge($this->sourceConfig, $config->sourceConfig); return new self($result); } public function withServiceRequest(\Kibo\Phast\Services\ServiceRequest $request) { $clone = clone $this; $clone->switches = $this->switches->merge($request->getSwitches()); return $clone; } public function getRuntimeConfig() { $config = $this->sourceConfig; $switchables = [&$config['documents']['filters'], &$config['images']['filters'], &$config['logging']['logWriters'], &$config['styles']['filters']]; foreach ($switchables as &$switchable) { if (!is_array($switchable)) { continue; } $switchable = array_filter($switchable, function ($item) { if (!isset($item['enabled'])) { return true; } if ($item['enabled'] === false) { return false; } return $this->switches->isOn($item['enabled']); }); } if (isset($config['images']['enable-cache']) && is_string($config['images']['enable-cache'])) { $config['images']['enable-cache'] = $this->switches->isOn($config['images']['enable-cache']); } $config['switches'] = $this->switches->toArray(); return new \Kibo\Phast\Environment\Configuration($config); } public function toArray() { return $this->sourceConfig; } private function recursiveMerge(array $a1, array $a2) { foreach ($a2 as $key => $value) { if (isset($a1[$key]) && is_array($a1[$key]) && is_array($value)) { $a1[$key] = $this->recursiveMerge($a1[$key], $value); } elseif (is_string($key)) { $a1[$key] = $value; } else { $a1[] = $value; } } return $a1; } } namespace Kibo\Phast\Cache; interface Cache { /** * @param string $key * @param callable|null $cached * @param int $expiresIn * @return mixed */ public function get($key, callable $cached = null, $expiresIn = 0); /** * @param string $key * @param mixed $value * @param int $expiresIn * @return mixed */ public function set($key, $value, $expiresIn = 0); } namespace Kibo\Phast\Cache\File; abstract class ProbabilisticExecutor { /** * @var string */ protected $cacheRoot; /** * @var float */ protected $probability = 0; /** * @var ObjectifiedFunctions */ protected $functions; protected abstract function execute(); protected function __construct(array $config, \Kibo\Phast\Common\ObjectifiedFunctions $functions = null) { $this->cacheRoot = $config['cacheRoot']; $this->functions = is_null($functions) ? new \Kibo\Phast\Common\ObjectifiedFunctions() : $functions; } public function __destruct() { if ($this->shouldExecute()) { $this->execute(); } } private function shouldExecute() { if (!$this->functions->file_exists($this->cacheRoot)) { return false; } if ($this->probability <= 0) { return false; } if ($this->probability >= 1) { return true; } return $this->functions->mt_rand(1, round(1 / $this->probability)) == 1; } protected function getCacheFiles($path) { /** @var \SplFileInfo $item */ foreach ($this->makeFileSystemIterator($path) as $item) { if ($this->isShard($item)) { foreach ($this->getCacheFiles($item->getRealPath()) as $item) { (yield $item); } } elseif ($this->isCacheEntry($item)) { (yield $item); } } } /** * @return \Iterator */ protected function makeFileSystemIterator($path) { try { $items = iterator_to_array(new \FilesystemIterator($path)); shuffle($items); return new \ArrayIterator($items); } catch (\Exception $e) { return new \ArrayIterator([]); } } protected function isShard(\SplFileInfo $item) { return $item->isDir() && !$item->isLink() && preg_match('/^[a-f\\d]{2}$/', $item->getFilename()); } protected function isCacheEntry(\SplFileInfo $item) { return $item->isFile() && preg_match('/^[a-f\\d]{32}-/', $item->getFilename()); } } namespace Kibo\Phast\Cache\File; class GarbageCollector extends \Kibo\Phast\Cache\File\ProbabilisticExecutor { /** * @var integer */ private $shardingDepth; /** * @var integer */ private $gcMaxAge; /** * @var integer */ private $gcMaxItems; public function __construct(array $config, \Kibo\Phast\Common\ObjectifiedFunctions $functions = null) { $this->shardingDepth = $config['shardingDepth']; $this->gcMaxAge = $config['garbageCollection']['maxAge']; $this->gcMaxItems = $config['garbageCollection']['maxItems']; $this->probability = $config['garbageCollection']['probability']; parent::__construct($config, $functions); } protected function execute() { $files = $this->getCacheFiles($this->cacheRoot); $deleted = 0; /** @var \SplFileInfo $file */ foreach ($this->filterOldFiles($files) as $file) { @$this->functions->unlink($file->getRealPath()); $deleted++; if ($deleted == $this->gcMaxItems) { break; } } } /** * @param \Iterator $files * @return \Generator */ private function filterOldFiles(\Iterator $files) { $maxTimeModified = time() - $this->gcMaxAge; /** @var \SplFileInfo $file */ foreach ($files as $file) { if ($file->getMTime() < $maxTimeModified) { (yield $file); } } } } namespace Kibo\Phast\Cache\File; class Cache implements \Kibo\Phast\Cache\Cache { use \Kibo\Phast\Logging\LoggingTrait; const VERSION = '3'; /** * @var GarbageCollector */ private static $garbageCollector; /** * @var DiskCleanup */ private static $diskCleanup; /** * @var string */ private $cacheRoot; /** * @var string */ private $cacheNS; /** * @var integer */ private $shardingDepth; /** * @var integer */ private $gcMaxAge; /** * @var ObjectifiedFunctions */ private $functions; /** * @var System */ private $system; public function __construct(array $config, $cacheNamespace, \Kibo\Phast\Common\ObjectifiedFunctions $functions = null) { $this->cacheRoot = $config['cacheRoot']; $this->shardingDepth = $config['shardingDepth']; $this->gcMaxAge = $config['garbageCollection']['maxAge']; $this->cacheNS = $cacheNamespace; if ($functions) { $this->functions = $functions; } else { $this->functions = new \Kibo\Phast\Common\ObjectifiedFunctions(); } $this->system = new \Kibo\Phast\Common\System($this->functions); if (!isset(self::$garbageCollector)) { self::$garbageCollector = new \Kibo\Phast\Cache\File\GarbageCollector($config, $this->functions); self::$diskCleanup = new \Kibo\Phast\Cache\File\DiskCleanup($config, $this->functions); } } public function get($key, callable $cached = null, $expiresIn = 0) { $contents = $this->getFromCache($key); if (!is_null($contents)) { return $contents; } if (is_null($cached)) { return null; } $contents = $cached(); $this->storeCache($key, $contents, $expiresIn); return $contents; } public function set($key, $value, $expiresIn = 0) { $this->storeCache($key, $value, $expiresIn); } /** * @return GarbageCollector */ public function getGarbageCollector() { return self::$garbageCollector; } /** * @return DiskCleanup */ public function getDiskCleanup() { return self::$diskCleanup; } private function getCacheDir($key) { $hashedKey = $this->getHashedKey($key); $parts = [$this->cacheRoot]; for ($i = 0; $i < $this->shardingDepth * 2; $i += 2) { $parts[] = substr($hashedKey, $i, 2); } return join('/', $parts); } private function getCacheFilename($key) { return $this->getCacheDir($key) . '/' . $this->getHashedKey($key) . '-' . ltrim($this->cacheNS, '/'); } private function getHashedKey($key) { return md5($key); } private function storeCache($key, $contents, $expiresIn) { $dir = $this->getCacheDir($key); if (!file_exists($dir)) { @mkdir($dir, 0700, true); } if (($uid = $this->system->getUserId()) && $uid !== $this->functions->fileowner($this->cacheRoot)) { $this->logger()->critical('Phast: FileCache: Cache root {cacheRoot} owned by {fileOwner}, but process user is {userId}!', ['cacheRoot' => $this->cacheRoot, 'fileOwner' => fileowner($this->cacheRoot), 'userId' => $uid]); return; } $file = $this->getCacheFilename($key); $expirationTime = $expiresIn > 0 ? $this->functions->time() + $expiresIn : 0; $serialized = serialize($contents); $serialized = implode(' ', [$expirationTime, self::VERSION, md5($serialized), $serialized]); $result = @$this->functions->file_put_contents($file, $serialized); if ($result === false) { @chmod($file, 0600); @unlink($file); $result = @$this->functions->file_put_contents($file, $serialized); } if ($result !== strlen($serialized)) { $this->logger()->critical('Phast: FileCache: Error writing to file {filename}. {written} of {total} bytes written!', ['filename' => $file, 'written' => json_encode($result), 'total' => strlen($serialized)]); } } private function getFromCache($key) { $file = $this->getCacheFilename($key); $contents = @$this->functions->file_get_contents($file); if ($contents === false) { return null; } @(list($expirationTime, $version, $data) = explode(' ', $contents, 3)); if ($version === '2') { $data = unserialize($data); } elseif ($version === self::VERSION) { @(list($hash, $data) = explode(' ', $data, 2)); if (md5($data) != $hash) { $this->logger()->error('Phast: FileCache: Cache file was corrupted: {file}', ['file' => $file]); return null; } $data = unserialize($data); } else { $this->logger()->debug('Phast: FileCache: Refusing to read old cache file {file}', ['file' => $file]); return null; } if ($expirationTime > $this->functions->time() || $expirationTime == 0) { if ($this->functions->time() - @$this->functions->filectime($file) >= round($this->gcMaxAge / 10)) { @$this->functions->touch($file); } return $data; } return null; } } namespace Kibo\Phast\Cache\File; class DiskCleanup extends \Kibo\Phast\Cache\File\ProbabilisticExecutor { /** * @var integer */ private $maxSize; /** * @var float */ private $portionToFree; public function __construct(array $config, \Kibo\Phast\Common\ObjectifiedFunctions $functions = null) { $this->maxSize = $config['diskCleanup']['maxSize']; $this->probability = $config['diskCleanup']['probability']; $this->portionToFree = $config['diskCleanup']['portionToFree']; parent::__construct($config, $functions); } protected function execute() { $usedSpace = $this->calculateUsedSpace(); $neededSpace = round($this->portionToFree * $this->maxSize); $bytesToDelete = $usedSpace - $this->maxSize + $neededSpace; $deletedBytes = 0; /** @var \SplFileInfo $file */ foreach ($this->getCacheFiles($this->cacheRoot) as $file) { if ($deletedBytes >= $bytesToDelete) { break; } $deletedBytes += $file->getSize(); @unlink($file->getRealPath()); } } private function calculateUsedSpace() { $size = 0; /** @var \SplFileInfo $file */ foreach ($this->getCacheFiles($this->cacheRoot) as $file) { $size += $file->getSize(); } return $size; } } namespace Kibo\Phast; class PhastDocumentFilters { const DOCUMENT_PATTERN = "~\n \\s* (<\\?xml[^>]*>)?\n (\\s* )*\n \\s* (]*>)?\n (\\s* )*\n \\s* [^>]* \\s ( amp | \342\232\241 ) [\\s=>] )?\n .*\n ( | )\n ~xsiA"; /** * @return ?OutputBufferHandler */ public static function deploy(array $userConfig = array()) { $runtimeConfig = self::configure($userConfig); if (!$runtimeConfig) { return null; } $handler = new \Kibo\Phast\Common\OutputBufferHandler($runtimeConfig['documents']['maxBufferSizeToApply'], function ($html, $applyCheckBuffer) use($runtimeConfig) { return self::applyWithRuntimeConfig($html, $runtimeConfig, $applyCheckBuffer); }); $handler->install(); \Kibo\Phast\Logging\Log::info('Phast deployed!'); return $handler; } public static function apply($html, array $userConfig) { $runtimeConfig = self::configure($userConfig); if (!$runtimeConfig) { return $html; } return self::applyWithRuntimeConfig($html, $runtimeConfig); } private static function configure(array $userConfig) { $request = \Kibo\Phast\Services\ServiceRequest::fromHTTPRequest(\Kibo\Phast\HTTP\Request::fromGlobals()); $runtimeConfig = \Kibo\Phast\Environment\Configuration::fromDefaults()->withUserConfiguration(new \Kibo\Phast\Environment\Configuration($userConfig))->withServiceRequest($request)->getRuntimeConfig()->toArray(); \Kibo\Phast\Logging\Log::init($runtimeConfig['logging'], $request, 'dom-filters'); \Kibo\Phast\Services\ServiceRequest::setDefaultSerializationMode($runtimeConfig['serviceRequestFormat']); if ($request->hasRequestSwitchesSet()) { \Kibo\Phast\Logging\Log::info('Request has switches set! Sending "noindex" header!'); header('X-Robots-Tag: noindex'); } if (!$runtimeConfig['switches']['phast']) { \Kibo\Phast\Logging\Log::info('Phast is off. Skipping document filter deployment!'); return; } return $runtimeConfig; } private static function applyWithRuntimeConfig($buffer, $runtimeConfig, $applyCheckBuffer = null) { if (is_null($applyCheckBuffer)) { $applyCheckBuffer = $buffer; } if (!self::shouldApply($applyCheckBuffer, $runtimeConfig)) { \Kibo\Phast\Logging\Log::info("Buffer ({bufferSize} bytes) doesn't look like html! Not applying filters", ['bufferSize' => strlen($applyCheckBuffer)]); return $buffer; } $compositeFilter = (new \Kibo\Phast\Filters\HTML\Composite\Factory())->make($runtimeConfig); if (self::isAMP($applyCheckBuffer)) { $compositeFilter->selectFilters(function ($filter) { return $filter instanceof \Kibo\Phast\Filters\HTML\AMPCompatibleFilter; }); } return $compositeFilter->apply($buffer); } private static function shouldApply($buffer, $runtimeConfig) { if ($runtimeConfig['optimizeHTMLDocumentsOnly']) { return preg_match(self::DOCUMENT_PATTERN, $buffer); } return strpos($buffer, '<') !== false; } private static function isAMP($buffer) { return preg_match(self::DOCUMENT_PATTERN, $buffer, $match) && !empty($match['amp']); } } namespace Kibo\Phast\Diagnostics; interface Diagnostics { /** * @param array $config */ public function diagnose(array $config); } namespace Kibo\Phast\Diagnostics; class SystemDiagnostics { /** * @param array $userConfigArr * @return Status[] */ public function run(array $userConfigArr) { $results = []; $userConfig = new \Kibo\Phast\Environment\Configuration($userConfigArr); $config = \Kibo\Phast\Environment\Configuration::fromDefaults()->withUserConfiguration($userConfig); foreach ($this->getExaminedItems($config) as $type => $group) { foreach ($group['items'] as $name) { $enabled = call_user_func($group['enabled'], $name); $package = \Kibo\Phast\Environment\Package::fromPackageClass($name, $type); try { $diagnostic = $package->getDiagnostics(); $diagnostic->diagnose($config->toArray()); $results[] = new \Kibo\Phast\Diagnostics\Status($package, true, '', $enabled); } catch (\Kibo\Phast\Environment\Exceptions\PackageHasNoDiagnosticsException $e) { $results[] = new \Kibo\Phast\Diagnostics\Status($package, true, '', $enabled); } catch (\Kibo\Phast\Exceptions\RuntimeException $e) { $results[] = new \Kibo\Phast\Diagnostics\Status($package, false, $e->getMessage(), $enabled); } catch (\Exception $e) { $results[] = new \Kibo\Phast\Diagnostics\Status($package, false, sprintf('Unknown error: Exception: %s, Message: %s, Code: %s', get_class($e), $e->getMessage(), $e->getCode()), $enabled); } } } return $results; } private function getExaminedItems(\Kibo\Phast\Environment\Configuration $config) { $runtimeConfig = $config->getRuntimeConfig()->toArray(); $configArr = $config->toArray(); return ['HTMLFilter' => ['items' => array_keys($configArr['documents']['filters']), 'enabled' => function ($filter) use($runtimeConfig) { return isset($runtimeConfig['documents']['filters'][$filter]); }], 'ImageFilter' => ['items' => array_keys($configArr['images']['filters']), 'enabled' => function ($filter) use($runtimeConfig) { return isset($runtimeConfig['images']['filters'][$filter]); }], 'Cache' => ['items' => [\Kibo\Phast\Cache\File\Cache::class], 'enabled' => function () { return true; }]]; } } namespace Kibo\Phast\Diagnostics; class Status implements \JsonSerializable { /** * @var Package */ private $package; /** * @var bool */ private $available; /** * @var string */ private $reason; /** * @var bool */ private $enabled; /** * Status constructor. * @param Package $package * @param bool $available * @param string $reason * @param bool $enabled */ public function __construct(\Kibo\Phast\Environment\Package $package, $available, $reason, $enabled) { $this->package = $package; $this->available = $available; $this->reason = $reason; $this->enabled = $enabled; } /** * @return Package */ public function getPackage() { return $this->package; } /** * @return bool */ public function isAvailable() { return $this->available; } /** * @return string */ public function getReason() { return $this->reason; } /** * @return bool */ public function isEnabled() { return $this->enabled; } /** * @return array */ public function toArray() { return ['package' => ['type' => $this->package->getType(), 'name' => $this->package->getNamespace()], 'available' => $this->available, 'reason' => $this->reason, 'enabled' => $this->enabled]; } public function jsonSerialize() { return $this->toArray(); } } namespace Kibo\Phast\Retrievers; interface Retriever { /** * @param URL $url * @return string|bool */ public function retrieve(\Kibo\Phast\ValueObjects\URL $url); /** * @param URL $url * @return integer|bool */ public function getCacheSalt(\Kibo\Phast\ValueObjects\URL $url); } namespace Kibo\Phast\Retrievers; trait DynamicCacheSaltTrait { public function getCacheSalt(\Kibo\Phast\ValueObjects\URL $url) { return md5($url->toString()) . '-' . floor(time() / 7200); } } namespace Kibo\Phast\Retrievers; class RemoteRetriever implements \Kibo\Phast\Retrievers\Retriever { use \Kibo\Phast\Retrievers\DynamicCacheSaltTrait; use \Kibo\Phast\Logging\LoggingTrait; private $client; public function __construct(\Kibo\Phast\HTTP\Client $client) { $this->client = $client; } public function retrieve(\Kibo\Phast\ValueObjects\URL $url) { try { $response = $this->client->get($url, ['User-Agent' => 'Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:56.0) Gecko/20100101 Firefox/56.0']); } catch (\Exception $e) { $this->logger()->warning('Caught {cls} while fetching {url}: ({code}) {message}', ['cls' => get_class($e), 'url' => (string) $url, 'code' => $e->getCode(), 'message' => $e->getMessage()]); return false; } return $response->getContent(); } } namespace Kibo\Phast\Retrievers; class RemoteRetrieverFactory { public function make(array $config) { return new \Kibo\Phast\Retrievers\RemoteRetriever((new \Kibo\Phast\HTTP\ClientFactory())->make($config)); } } namespace Kibo\Phast\Retrievers; class UniversalRetriever implements \Kibo\Phast\Retrievers\Retriever { /** * @var Retriever[] */ private $retrievers = array(); public function retrieve(\Kibo\Phast\ValueObjects\URL $url) { return $this->iterateRetrievers(function (\Kibo\Phast\Retrievers\Retriever $retriever) use($url) { return $retriever->retrieve($url); }); } public function getCacheSalt(\Kibo\Phast\ValueObjects\URL $url) { return $this->iterateRetrievers(function (\Kibo\Phast\Retrievers\Retriever $retriever) use($url) { return $retriever->getCacheSalt($url); }); } private function iterateRetrievers(callable $callback) { foreach ($this->retrievers as $retriever) { $result = $callback($retriever); if ($result !== false) { return $result; } } return false; } public function addRetriever(\Kibo\Phast\Retrievers\Retriever $retriever) { $this->retrievers[] = $retriever; } } namespace Kibo\Phast\Retrievers; class LocalRetriever implements \Kibo\Phast\Retrievers\Retriever { /** * @var array */ private $map; /** * @var ObjectifiedFunctions */ private $funcs; /** * LocalRetriever constructor. * * @param array $map * @param ObjectifiedFunctions|null $functions */ public function __construct(array $map, \Kibo\Phast\Common\ObjectifiedFunctions $functions = null) { $this->map = $map; if ($functions) { $this->funcs = $functions; } else { $this->funcs = new \Kibo\Phast\Common\ObjectifiedFunctions(); } } public static function getAllowedExtensions() { return ['css', 'js', 'bmp', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'ico', 'svg', 'txt']; } public function retrieve(\Kibo\Phast\ValueObjects\URL $url) { return $this->guard($url, function ($file) { return @$this->funcs->file_get_contents($file); }); } public function getCacheSalt(\Kibo\Phast\ValueObjects\URL $url) { return $this->guard($url, function ($file) { $size = @$this->funcs->filesize($file); $mtime = @$this->funcs->filectime($file); if ($size === false && $mtime === false) { return ''; } return "{$mtime}-{$size}"; }); } public function getSize(\Kibo\Phast\ValueObjects\URL $url) { return $this->guard($url, function ($file) { return @$this->funcs->filesize($file); }); } private function guard(\Kibo\Phast\ValueObjects\URL $url, callable $cb) { if (!in_array($this->getExtensionForURL($url), self::getAllowedExtensions())) { return false; } $file = $this->getFileForURL($url); if ($file === false) { return false; } return $cb($file); } private function getExtensionForURL(\Kibo\Phast\ValueObjects\URL $url) { $dotPosition = strrpos($url->getPath(), '.'); if ($dotPosition === false) { return ''; } return strtolower(substr($url->getPath(), $dotPosition + 1)); } private function getFileForURL(\Kibo\Phast\ValueObjects\URL $url) { if (!isset($this->map[$url->getHost()])) { return false; } $submap = $this->map[$url->getHost()]; if (!is_array($submap)) { return $this->appendNormalized($submap, $url->getPath()); } $selectedPath = null; $selectedRoot = null; foreach ($submap as $prefix => $root) { $pattern = '~^(?=/)/*?(?:' . str_replace('~', '\\~', $prefix) . ')(?/*(?<=/).*)~'; if (preg_match($pattern, $url->getPath(), $match) && ($selectedPath === null || strlen($match['path']) < strlen($selectedPath))) { $selectedRoot = $root; $selectedPath = $match['path']; } } if ($selectedPath === null) { return false; } return $this->appendNormalized($selectedRoot, $selectedPath); } private function appendNormalized($target, $appended) { $appended = explode("\0", $appended)[0]; $appended = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $appended); $absolutes = []; foreach (explode(DIRECTORY_SEPARATOR, $appended) as $part) { if ($part == '' || $part == '.') { } elseif ($part == '..') { if (array_pop($absolutes) === null) { return false; } } else { $absolutes[] = $part; } } return $target . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $absolutes); } } namespace Kibo\Phast\Retrievers; class CachingRetriever implements \Kibo\Phast\Retrievers\Retriever { use \Kibo\Phast\Retrievers\DynamicCacheSaltTrait { getCacheSalt as getDynamicCacheSalt; } /** * @var Cache */ private $cache; /** * @var Retriever */ private $retriever; /** * CachingRetriever constructor. * * @param Retriever $retriever * @param Cache $cache * @param int $defaultCacheTime */ public function __construct(\Kibo\Phast\Cache\Cache $cache, \Kibo\Phast\Retrievers\Retriever $retriever = null, $defaultCacheTime = 0) { $this->cache = $cache; $this->retriever = $retriever; } public function retrieve(\Kibo\Phast\ValueObjects\URL $url) { if ($this->retriever) { return $this->getCachedWithRetriever($url); } return $this->getFromCacheOnly($url); } public function getCacheSalt(\Kibo\Phast\ValueObjects\URL $url) { if ($this->retriever) { return $this->retriever->getCacheSalt($url); } return $this->getDynamicCacheSalt($url); } private function getCachedWithRetriever(\Kibo\Phast\ValueObjects\URL $url) { return $this->cache->get($this->getCacheKey($url), function () use($url) { return $this->retriever->retrieve($url); }); } private function getFromCacheOnly(\Kibo\Phast\ValueObjects\URL $url) { $cached = $this->cache->get($this->getCacheKey($url)); if (!$cached) { return false; } return $cached; } private function getCacheKey(\Kibo\Phast\ValueObjects\URL $url) { return $url . '-' . $this->getCacheSalt($url); } } namespace Kibo\Phast\Retrievers; class PostDataRetriever implements \Kibo\Phast\Retrievers\Retriever { /** * @var ObjectifiedFunctions */ private $funcs; private $content; /** * PostDataRetriever constructor. * @param ObjectifiedFunctions $funcs */ public function __construct(\Kibo\Phast\Common\ObjectifiedFunctions $funcs = null) { $this->funcs = is_null($funcs) ? new \Kibo\Phast\Common\ObjectifiedFunctions() : $funcs; } public function retrieve(\Kibo\Phast\ValueObjects\URL $url) { if (!isset($this->content)) { $this->content = $this->funcs->file_get_contents('php://input'); } return $this->content; } public function getCacheSalt(\Kibo\Phast\ValueObjects\URL $url) { return md5($this->retrieve($url)); } } namespace Kibo\Phast\Filters\HTML\Composite; class Factory { use \Kibo\Phast\Logging\LoggingTrait; public function make(array $config) { $composite = new \Kibo\Phast\Filters\HTML\Composite\Filter(\Kibo\Phast\ValueObjects\URL::fromString($config['documents']['baseUrl']), $config['outputServerSideStats']); foreach (array_keys($config['documents']['filters']) as $class) { $package = \Kibo\Phast\Environment\Package::fromPackageClass($class); if ($package->hasFactory()) { $filter = $package->getFactory()->make($config); } elseif (!class_exists($class)) { $this->logger(__METHOD__, __LINE__)->error("Skipping non-existent filter class: {$class}"); continue; } else { $filter = new $class(); } $composite->addHTMLFilter($filter); } return $composite; } } namespace Kibo\Phast\Filters\HTML\Composite; class Filter { use \Kibo\Phast\Logging\LoggingTrait; /** * @var URL */ private $baseUrl; private $outputStats; /** * @var HTMLStreamFilter[] */ private $filters = array(); private $timings = array(); /** * Filter constructor. * @param URL $baseUrl * @param $outputStats */ public function __construct(\Kibo\Phast\ValueObjects\URL $baseUrl, $outputStats) { $this->baseUrl = $baseUrl; $this->outputStats = $outputStats; } /** * @param string $buffer * @return string */ public function apply($buffer) { $timeStart = microtime(true); try { return $this->tryToApply($buffer, $timeStart); } catch (\Exception $e) { $this->logger()->critical('Phast: CompositeHTMLFilter: {exception} Msg: {message}, Code: {code}, File: {file}, Line: {line}', ['exception' => get_class($e), 'message' => $e->getMessage(), 'code' => $e->getCode(), 'file' => $e->getFile(), 'line' => $e->getLine()]); return $buffer; } } public function addHTMLFilter(\Kibo\Phast\Filters\HTML\HTMLStreamFilter $filter) { $this->filters[] = $filter; } private function tryToApply($buffer, $timeStart) { $context = new \Kibo\Phast\Filters\HTML\HTMLPageContext($this->baseUrl); $elements = (new \Kibo\Phast\Parsing\HTML\PCRETokenizer())->tokenize($buffer); foreach ($this->filters as $filter) { $this->logger()->info('Starting {filter}', ['filter' => get_class($filter)]); $elements = $filter->transformElements($elements, $context); } $output = ''; foreach ($elements as $element) { $output .= $element; } $timeDelta = microtime(true) - $timeStart; if ($this->outputStats) { $output .= sprintf("\n\n", $timeDelta * 1000); } return $output; } public function selectFilters($callback) { $this->filters = array_filter($this->filters, $callback); } } namespace Kibo\Phast\Filters\HTML; interface AMPCompatibleFilter { } namespace Kibo\Phast\Filters\HTML; interface HTMLStreamFilter { /** * @param \Traversable $elements * @param HTMLPageContext $context * @return \Traversable */ public function transformElements(\Traversable $elements, \Kibo\Phast\Filters\HTML\HTMLPageContext $context); } namespace Kibo\Phast\Filters\HTML\MinifyScripts; class Filter implements \Kibo\Phast\Filters\HTML\HTMLStreamFilter { use \Kibo\Phast\Filters\HTML\Helpers\JSDetectorTrait; private $cache; public function __construct(\Kibo\Phast\Cache\File\Cache $cache) { $this->cache = $cache; } public function transformElements(\Traversable $elements, \Kibo\Phast\Filters\HTML\HTMLPageContext $context) { $inTags = ['pre' => 0, 'textarea' => 0]; foreach ($elements as $element) { if ($element instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag && $element->getTagName() === 'script' && ($content = $element->getTextContent()) !== '') { $content = trim($content); if ($this->isJSElement($element) && preg_match('~[()[\\]{};]\\s~', $content)) { $content = preg_replace('~^\\s*\\s*$~s', '$1', $content); $content = $this->cache->get(md5($content), function () use($content) { return (new \Kibo\Phast\Common\JSMinifier($content, true))->min(); }); } elseif (($data = @json_decode($content)) !== null && ($newContent = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) !== false) { $content = str_replace('setTextContent($content); } (yield $element); } } } namespace Kibo\Phast\Filters\HTML\MinifyScripts; class Factory { public function make(array $config) { return new \Kibo\Phast\Filters\HTML\MinifyScripts\Filter(new \Kibo\Phast\Cache\File\Cache($config['cache'], 'minified-inline-scripts')); } } namespace Kibo\Phast\Filters\HTML\ImagesOptimizationService; class ImageURLRewriterFactory { public function make(array $config, $filterClass = '') { $signature = (new \Kibo\Phast\Security\ServiceSignatureFactory())->make($config); if (isset($config['documents']['filters'][$filterClass])) { $classConfig = $config['documents']['filters'][$filterClass]; } elseif (isset($config['styles']['filters'][$filterClass])) { $classConfig = $config['styles']['filters'][$filterClass]; } else { $classConfig = []; } if (isset($classConfig['serviceUrl'])) { $serviceUrl = $classConfig['serviceUrl']; } else { $serviceUrl = $config['servicesUrl'] . '?service=images'; } return new \Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageURLRewriter($signature, new \Kibo\Phast\Retrievers\LocalRetriever($config['retrieverMap']), (new \Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageInliningManagerFactory())->make($config), \Kibo\Phast\ValueObjects\URL::fromString($config['documents']['baseUrl']), \Kibo\Phast\ValueObjects\URL::fromString($serviceUrl), $config['images']['whitelist']); } } namespace Kibo\Phast\Filters\HTML\ImagesOptimizationService; class ImageInliningManager { use \Kibo\Phast\Logging\LoggingTrait; /** * @var Cache */ private $cache; /** * @var int */ private $maxImageInliningSize; /** * ImageInliningManager constructor. * @param Cache $cache * @param int $maxImageInliningSize */ public function __construct(\Kibo\Phast\Cache\Cache $cache, $maxImageInliningSize) { $this->cache = $cache; $this->maxImageInliningSize = $maxImageInliningSize; } /** * @return int */ public function getMaxImageInliningSize() { return $this->maxImageInliningSize; } /** * @param Resource $resource * @return string|null */ public function getUrlForInlining(\Kibo\Phast\ValueObjects\Resource $resource) { if ($resource->getMimeType() !== 'image/svg+xml') { return $this->cache->get($this->getCacheKey($resource)); } try { if ($this->hasSizeForInlining($resource)) { return $resource->toDataURL(); } } catch (\Kibo\Phast\Exceptions\ItemNotFoundException $e) { $this->logger()->warning('Could not fetch contents for {url}. Message is {message}', ['url' => $resource->getUrl()->toString(), 'message' => $e->getMessage()]); } return null; } public function maybeStoreForInlining(\Kibo\Phast\ValueObjects\Resource $resource) { if ($this->shouldStoreForInlining($resource)) { $this->logger()->info('Storing {url} for inlining', ['url' => $resource->getUrl()->toString()]); $this->cache->set($this->getCacheKey($resource), $resource->toDataURL()); } else { $this->logger()->info('Not storing {url} for inlining', ['url' => $resource->getUrl()->toString()]); } } private function shouldStoreForInlining(\Kibo\Phast\ValueObjects\Resource $resource) { return $this->hasSizeForInlining($resource) && strpos($resource->getMimeType(), 'image/') === 0 && $resource->getMimeType() !== 'image/webp'; } private function hasSizeForInlining(\Kibo\Phast\ValueObjects\Resource $resource) { $size = $resource->getSize(); return $size !== false && $size <= $this->maxImageInliningSize; } private function getCacheKey(\Kibo\Phast\ValueObjects\Resource $resource) { return $resource->getUrl()->toString() . '|' . $resource->getCacheSalt() . '|' . $this->maxImageInliningSize; } } namespace Kibo\Phast\Filters\HTML\ImagesOptimizationService; class ImageURLRewriter { use \Kibo\Phast\Logging\LoggingTrait; /** * @var ServiceSignature */ protected $signature; /** * @var Retriever */ protected $retriever; /** * @var ImageInliningManager */ protected $inliningManager; /** * @var URL */ protected $baseUrl; /** * @var URL */ protected $serviceUrl; /** * @var string[] */ protected $whitelist; /** * @var Resource[] */ protected $inlinedResources; /** * ImageURLRewriter constructor. * @param ServiceSignature $signature * @param LocalRetriever $retriever * @param ImageInliningManager $inliningManager * @param URL $baseUrl * @param URL $serviceUrl * @param array $whitelist */ public function __construct(\Kibo\Phast\Security\ServiceSignature $signature, \Kibo\Phast\Retrievers\LocalRetriever $retriever, \Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageInliningManager $inliningManager, \Kibo\Phast\ValueObjects\URL $baseUrl, \Kibo\Phast\ValueObjects\URL $serviceUrl, array $whitelist) { $this->signature = $signature; $this->retriever = $retriever; $this->inliningManager = $inliningManager; $this->baseUrl = $baseUrl; $this->serviceUrl = $serviceUrl; $this->whitelist = $whitelist; } /** * @param string $url * @param URL|null $baseUrl * @param array $params * @param bool $mustExist * @return string */ public function rewriteUrl($url, \Kibo\Phast\ValueObjects\URL $baseUrl = null, array $params = array(), $mustExist = false) { if (strpos($url, '#') === 0) { return $url; } $this->inlinedResources = []; $absolute = $this->makeURLAbsoluteToBase($url, $baseUrl); if (!$this->shouldRewriteUrl($absolute)) { return $url; } $resource = \Kibo\Phast\ValueObjects\Resource::makeWithRetriever($absolute, $this->retriever); if ($mustExist && $resource->getSize() === false) { return $url; } $dataUrl = $this->inliningManager->getUrlForInlining($resource); if ($dataUrl) { $this->inlinedResources = [$resource]; return $dataUrl; } $params['src'] = $absolute->toString(); return $this->makeSignedUrl($params); } /** * @param $styleContent * @return string */ public function rewriteStyle($styleContent) { $allInlined = []; $result = preg_replace_callback('~ (\\b (?: image | background ):) ([^;}]*) ~xiS', function ($match) use(&$allInlined) { return $match[1] . $this->rewriteStyleRule($match[2], $allInlined); }, $styleContent); $this->inlinedResources = array_values($allInlined); return $result; } private function rewriteStyleRule($ruleContent, &$allInlined) { return preg_replace_callback('~ ( \\b url \\( [\'"]? ) ( [^\'")] ++ ) ~xiS', function ($match) use(&$allInlined) { $url = $match[1] . $this->rewriteUrl($match[2]); if (!empty($this->inlinedResources)) { $inlined = $this->inlinedResources[0]; $allInlined[$inlined->getUrl()->toString()] = $inlined; } return $url; }, $ruleContent); } /** * @return Resource[] */ public function getInlinedResources() { return $this->inlinedResources; } /** * @return string */ public function getCacheSalt() { $parts = array_merge([$this->signature->getCacheSalt(), $this->baseUrl->toString(), $this->serviceUrl->toString(), $this->inliningManager->getMaxImageInliningSize(), '20180413'], array_keys($this->whitelist), array_values($this->whitelist)); return join('-', $parts); } /** * @param string $url * @param URL|null $baseUrl * @return URL */ private function makeURLAbsoluteToBase($url, \Kibo\Phast\ValueObjects\URL $baseUrl = null) { $url = trim($url); if (!$url || substr($url, 0, 5) === 'data:') { return null; } $this->logger()->info('Rewriting img {url}', ['url' => $url]); $baseUrl = is_null($baseUrl) ? $this->baseUrl : $baseUrl; return \Kibo\Phast\ValueObjects\URL::fromString($url)->withBase($baseUrl); } /** * @param string $url * @return bool */ private function shouldRewriteUrl($url) { if (!$url) { return false; } foreach ($this->whitelist as $pattern) { if (preg_match($pattern, $url)) { return true; } } $urlObject = \Kibo\Phast\ValueObjects\URL::fromString($url); if (preg_match('~\\.(jpe?g|gif|png)$~i', $urlObject->getPath()) && $this->retriever->getCacheSalt($urlObject)) { return true; } return false; } /** * @param array $params * @return string */ private function makeSignedUrl(array $params) { $params['cacheMarker'] = $this->retriever->getCacheSalt(\Kibo\Phast\ValueObjects\URL::fromString($params['src'])); return (new \Kibo\Phast\Services\ServiceRequest())->withParams($params)->withUrl($this->serviceUrl)->sign($this->signature)->serialize(); } } namespace Kibo\Phast\Filters\HTML\ImagesOptimizationService\Tags; class Filter implements \Kibo\Phast\Filters\HTML\HTMLStreamFilter, \Kibo\Phast\Filters\HTML\AMPCompatibleFilter { const IMG_SRC_ATTR_PATTERN = '~^(|data-(|lazy-|wood-))src$~i'; const IMG_SRCSET_ATTR_PATTERN = '~^(|data-(|lazy-|wood-))srcset$~i'; /** * @var ImageURLRewriter */ private $rewriter; private $inPictureTag = false; private $inBody = false; private $imagePathPattern; /** * Filter constructor. * @param ImageURLRewriter $rewriter */ public function __construct(\Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageURLRewriter $rewriter) { $this->rewriter = $rewriter; $this->imagePathPattern = $this->makeImagePathPattern(); } public function transformElements(\Traversable $elements, \Kibo\Phast\Filters\HTML\HTMLPageContext $context) { foreach ($elements as $element) { if ($element instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag) { $this->handleTag($element, $context); } elseif ($element instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\ClosingTag) { $this->handleClosingTag($element); } (yield $element); } } private function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag, \Kibo\Phast\Filters\HTML\HTMLPageContext $context) { $isImage = false; if ($tag->getTagName() == 'img' || $this->inPictureTag && $tag->getTagName() == 'source' || $tag->getTagName() == 'amp-img') { $isImage = true; } elseif ($tag->getTagName() == 'picture') { $this->inPictureTag = true; } elseif ($tag->getTagName() == 'video' || $tag->getTagName() == 'audio') { $this->inPictureTag = false; } elseif ($tag->getTagName() == 'body') { $this->inBody = true; } elseif ($tag->getTagName() == 'meta') { return; } foreach ($tag->getAttributes() as $k => $v) { if (!$v) { continue; } if ($isImage && preg_match(self::IMG_SRC_ATTR_PATTERN, $k)) { $this->rewriteSrc($tag, $context, $k); } elseif ($isImage && preg_match(self::IMG_SRCSET_ATTR_PATTERN, $k)) { $this->rewriteSrcset($tag, $context, $k); } elseif ($this->inBody && preg_match($this->imagePathPattern, parse_url($v, PHP_URL_PATH))) { $this->rewriteArbitraryAttribute($tag, $context, $k); } } } private function makeImagePathPattern() { $pieces = []; foreach (\Kibo\Phast\ValueObjects\Resource::EXTENSION_TO_MIME_TYPE as $ext => $mime) { if (strpos($mime, 'image/') === 0) { $pieces[] = preg_quote($ext, '~'); } } return '~\\.(?:' . implode('|', $pieces) . ')$~'; } private function handleClosingTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\ClosingTag $closingTag) { if ($closingTag->getTagName() == 'picture') { $this->inPictureTag = false; } } private function rewriteSrc(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $img, \Kibo\Phast\Filters\HTML\HTMLPageContext $context, $attribute) { $url = $img->getAttribute($attribute); $params = []; foreach (['width', 'height'] as $attr) { $value = $img->getAttribute($attr); if (preg_match('/^[1-9][0-9]*$/', $value)) { $params[$attr] = $value; } } $newURL = $this->rewriter->rewriteUrl($url, $context->getBaseUrl(), $params); $img->setAttribute($attribute, $newURL); } private function rewriteSrcset(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $img, \Kibo\Phast\Filters\HTML\HTMLPageContext $context, $attribute) { $srcset = $img->getAttribute($attribute); $rewritten = preg_replace_callback('/([^,\\s]+)(\\s+(?:[^,]+))?/', function ($match) use($context) { $url = $this->rewriter->rewriteUrl($match[1], $context->getBaseUrl()); if (isset($match[2])) { return $url . $match[2]; } return $url; }, $srcset); $img->setAttribute($attribute, $rewritten); } private function rewriteArbitraryAttribute(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $element, \Kibo\Phast\Filters\HTML\HTMLPageContext $context, $attribute) { $url = $element->getAttribute($attribute); $newUrl = $this->rewriter->rewriteUrl($url, $context->getBaseUrl(), [], true); $element->setAttribute($attribute, $newUrl); } } namespace Kibo\Phast\Filters\HTML\ImagesOptimizationService; class ImageInliningManagerFactory { public function make(array $config) { $cache = new \Kibo\Phast\Cache\File\Cache($config['cache'], 'inline-images-1'); return new \Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageInliningManager($cache, $config['images']['maxImageInliningSize']); } } namespace Kibo\Phast\Filters\HTML; class HTMLPageContext { /** * @var URL */ private $baseUrl; /** * @var PhastJavaScript[] */ private $phastJavaScripts = array(); /** * HTMLPageContext constructor. * @param URL $baseUrl */ public function __construct(\Kibo\Phast\ValueObjects\URL $baseUrl) { $this->baseUrl = $baseUrl; } /** * @param URL $baseUrl */ public function setBaseUrl(\Kibo\Phast\ValueObjects\URL $baseUrl) { $this->baseUrl = $baseUrl; } /** * @return URL */ public function getBaseUrl() { return $this->baseUrl; } /** * @param PhastJavaScript $script */ public function addPhastJavascript(\Kibo\Phast\ValueObjects\PhastJavaScript $script) { $this->phastJavaScripts[] = $script; } /** * @return PhastJavaScript[] */ public function getPhastJavaScripts() { return $this->phastJavaScripts; } } namespace Kibo\Phast\Filters\HTML\Helpers; trait JSDetectorTrait { /** * @param Tag $element * @return bool */ private function isJSElement(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $element) { if (!$element->hasAttribute('type')) { return true; } return (bool) preg_match('~^(text|application)/javascript(;|$)~i', $element->getAttribute('type')); } } namespace Kibo\Phast\Filters\HTML; abstract class BaseHTMLStreamFilter implements \Kibo\Phast\Filters\HTML\HTMLStreamFilter { /** * @var HTMLPageContext */ protected $context; /** * @var \Traversable */ protected $elements; /** * @param Tag $tag * @return Element[]|\Generator */ protected abstract function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag); public function transformElements(\Traversable $elements, \Kibo\Phast\Filters\HTML\HTMLPageContext $context) { $this->context = $context; $this->elements = $elements; $this->beforeLoop(); foreach ($this->elements as $element) { if ($element instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag && $this->isTagOfInterest($element)) { foreach ($this->handleTag($element) as $item) { (yield $item); } } else { (yield $element); } } $this->afterLoop(); } /** * @param Tag $tag * @return bool */ protected function isTagOfInterest(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag) { return true; } protected function beforeLoop() { } protected function afterLoop() { } } namespace Kibo\Phast\Filters\HTML\PhastScriptsCompiler; class Filter implements \Kibo\Phast\Filters\HTML\HTMLStreamFilter { /** * @var PhastJavaScriptCompiler */ private $compiler; /** * Filter constructor. * @param PhastJavaScriptCompiler $compiler */ public function __construct(\Kibo\Phast\Filters\HTML\PhastScriptsCompiler\PhastJavaScriptCompiler $compiler) { $this->compiler = $compiler; } public function transformElements(\Traversable $elements, \Kibo\Phast\Filters\HTML\HTMLPageContext $context) { $buffered = []; $buffering = false; foreach ($elements as $element) { if ($this->isClosingBodyTag($element)) { if ($buffering) { foreach ($buffered as $bufElement) { (yield $bufElement); } $buffered = []; } $buffering = true; } if ($buffering) { $buffered[] = $element; } else { (yield $element); } } $scripts = $context->getPhastJavaScripts(); if (!empty($scripts)) { (yield $this->compileScript($scripts)); } foreach ($buffered as $element) { (yield $element); } } /** * @param PhastJavaScript[] $scripts * @return Tag */ private function compileScript(array $scripts) { $names = array_map(function (\Kibo\Phast\ValueObjects\PhastJavaScript $script) { $matches = []; preg_match('~[^/]*?\\/?[^/]+$~', $script->getFilename(), $matches); return $matches[0]; }, $scripts); $script = new \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag('script'); $script->setAttribute('data-phast-compiled-js-names', join(',', $names)); $compiled = $this->compiler->compileScriptsWithConfig($scripts); $script->setTextContent($compiled); return $script; } private function isClosingBodyTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Element $element) { return $element instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\ClosingTag && $element->getTagName() == 'body'; } } namespace Kibo\Phast\Filters\HTML\PhastScriptsCompiler; class PhastJavaScriptCompiler { /** * @var Cache */ private $cache; /** * @var string */ private $serviceUrl; private $serviceRequestFormat; /** * @var \stdClass */ private $lastCompiledConfig; /** * PhastJavaScriptCompiler constructor. * @param Cache $cache * @param string $serviceUrl */ public function __construct(\Kibo\Phast\Cache\Cache $cache, $serviceUrl, $serviceRequestFormat) { $this->cache = $cache; $this->serviceUrl = (new \Kibo\Phast\Services\ServiceRequest())->withUrl(\Kibo\Phast\ValueObjects\URL::fromString((string) $serviceUrl))->serialize(\Kibo\Phast\Services\ServiceRequest::FORMAT_QUERY); $this->serviceRequestFormat = $serviceRequestFormat; } /** * @return \stdClass|null */ public function getLastCompiledConfig() { return $this->lastCompiledConfig; } /** * @param PhastJavaScript[] $scripts * @return string */ public function compileScripts(array $scripts) { return $this->cache->get($this->getCacheKey($scripts), function () use($scripts) { return $this->performCompilation($scripts); }); } /** * @param PhastJavaScript[] $scripts * @return string */ public function compileScriptsWithConfig(array $scripts) { $bundlerMappings = \Kibo\Phast\Services\Bundler\ShortBundlerParamsParser::getParamsMappings(); $jsMappings = array_combine(array_values($bundlerMappings), array_keys($bundlerMappings)); $resourcesLoader = \Kibo\Phast\ValueObjects\PhastJavaScript::fromString('/home/albert/code/phast/src/Build/../../src/Filters/HTML/PhastScriptsCompiler/resources-loader.js', "var Promise=phast.ES6Promise.Promise;phast.ResourceLoader=function(a,b){this.get=function(c){return b.get(c).then(function(d){if(typeof d!==\"string\"){throw new Error(\"response should be string\")}return d}).catch(function(){var e=a.get(c);e.then(function(f){b.set(c,f)});return e})}};phast.ResourceLoader.RequestParams={};phast.ResourceLoader.RequestParams.FaultyParams={};phast.ResourceLoader.RequestParams.fromString=function(g){try{return JSON.parse(g)}catch(h){return phast.ResourceLoader.RequestParams.FaultyParams}};phast.ResourceLoader.BundlerServiceClient=function(i,j,k){var l=phast.ResourceLoader.BundlerServiceClient.RequestsPack;var m=l.PackItem;var n;this.get=function(q){if(q===phast.ResourceLoader.RequestParams.FaultyParams){return Promise.reject(new Error(\"Parameters did not parse as JSON\"))}return new Promise(function(r,s){if(n===undefined){n=new l(j)}n.add(new m({success:r,error:s},q));setTimeout(o);if(n.toQuery().length>4500){console.log(\"[Phast] Resource loader: Pack got too big; flushing early...\");o()}})};function o(){if(n===undefined){return}var t=n;n=undefined;p(t)}function p(u){var v=phast.buildServiceUrl({serviceUrl:i,pathInfo:k},\"service=bundler&\"+u.toQuery());var w=function(){console.error(\"[Phast] Request to bundler failed with status\",y.status);console.log(\"URL:\",v);u.handleError()};var x=function(){if(y.status>=200&&y.status<300){u.handleResponse(y.responseText)}else{u.handleError()}};var y=new XMLHttpRequest;y.open(\"GET\",v);y.addEventListener(\"error\",w);y.addEventListener(\"abort\",w);y.addEventListener(\"load\",x);y.send()}};phast.ResourceLoader.BundlerServiceClient.RequestsPack=function(z){var A={};this.getLength=function(){var F=0;for(var G in A){F++}return F};this.add=function(H){var I;if(H.params.token){I=\"token=\"+H.params.token}else if(H.params.ref){I=\"ref=\"+H.params.ref}else{I=\"\"}if(!A[I]){A[I]={params:H.params,requests:[H.request]}}else{A[I].requests.push(H.request)}};this.toQuery=function(){var J=[],K=[],L=\"\";B().forEach(function(M){var N,O;for(var P in A[M].params){if(P===\"cacheMarker\"){K.push(A[M].params.cacheMarker);continue}N=z[P]?z[P]:P;if(P===\"strip-imports\"){O=encodeURIComponent(N)}else if(P===\"src\"){O=encodeURIComponent(N)+\"=\"+encodeURIComponent(C(A[M].params.src,L));L=A[M].params.src}else{O=encodeURIComponent(N)+\"=\"+encodeURIComponent(A[M].params[P])}J.push(O)}});if(K.length>0){J.unshift(\"c=\"+phast.hash(K.join(\"|\"),23045))}return E(J.join(\"&\"))};function B(){return Object.keys(A).sort(function(R,S){return Q(R,S)?1:Q(S,R)?-1:0});function Q(T,U){if(typeof A[T].params.src!==\"undefined\"&&typeof A[U].params.src!==\"undefined\"){return A[T].params.src>A[U].params.src}return T>U}}function C(V,W){var X=0,Y=Math.pow(36,2)-1;while(Xnb.maxStorageSize){return Fb||Eb.length>nb.maxStorageSize?Promise.reject(new Error(\"Storage quota will be exceeded\")):ub(Db,Eb)}qb=Hb;var Ib=new pb(Db,Eb);return ob.store(Ib)})}function ub(Jb,Kb){return vb().then(function(){return tb(Jb,Kb,true)})}function vb(){return ob.clear().then(function(){qb=0})}function wb(){if(qb!==null){return Promise.resolve(qb)}var Lb=0;return ob.iterateOnAll(function(Mb){Lb+=Mb.content.length}).then(function(){qb=Lb;return Promise.resolve(qb)})}};phast.ResourceLoader.StorageCache.StorageCacheParams=function(){this.maxStorageSize=4.5*1024*1024};phast.ResourceLoader.BlackholeCache=function(){this.get=function(){return Promise.reject()};this.set=function(){return Promise.reject()}};phast.ResourceLoader.make=function(Nb,Ob,Pb){var Qb=Sb();var Rb=new phast.ResourceLoader.BundlerServiceClient(Nb,Ob,Pb);return new phast.ResourceLoader(Rb,Qb);function Sb(){var Tb=window.navigator.userAgent;if(/safari/i.test(Tb)&&!/chrome|android/i.test(Tb)){console.log(\"[Phast] Not using IndexedDB cache on Safari\");return new phast.ResourceLoader.BlackholeCache}else{var Ub=new phast.ResourceLoader.IndexedDBStorage.ConnectionParams;var Vb=new phast.ResourceLoader.IndexedDBStorage(Ub);var Wb=new phast.ResourceLoader.StorageCache.StorageCacheParams;return new phast.ResourceLoader.StorageCache(Wb,Vb)}}};\n"); $resourcesLoader->setConfig('resourcesLoader', ['serviceUrl' => (string) $this->serviceUrl, 'shortParamsMappings' => $jsMappings, 'pathInfo' => $this->serviceRequestFormat === \Kibo\Phast\Services\ServiceRequest::FORMAT_PATH]); $scripts = array_merge([\Kibo\Phast\ValueObjects\PhastJavaScript::fromString('/home/albert/code/phast/src/Build/../../src/Filters/HTML/PhastScriptsCompiler/runner.js', "phast.config=JSON.parse(atob(phast.config));while(phast.scripts.length){phast.scripts.shift()()}\n"), \Kibo\Phast\ValueObjects\PhastJavaScript::fromString('/home/albert/code/phast/src/Build/../../src/Filters/HTML/PhastScriptsCompiler/es6-promise.js', "(function(a,b){typeof exports===\"object\"&&typeof module!==\"undefined\"?module.exports=b():typeof define===\"function\"&&define.amd?define(b):a.ES6Promise=b()})(phast,function(){\"use strict\";function c(ia){var ja=typeof ia;return ia!==null&&(ja===\"object\"||ja===\"function\")}function d(ka){return typeof ka===\"function\"}var e=void 0;if(Array.isArray){e=Array.isArray}else{e=function(la){return Object.prototype.toString.call(la)===\"[object Array]\"}}var f=e;var g=0;var h=void 0;var i=void 0;var j=function ma(na,oa){w[g]=na;w[g+1]=oa;g+=2;if(g===2){if(i){i(x)}else{z()}}};function k(pa){i=pa}function l(qa){j=qa}var m=typeof window!==\"undefined\"?window:undefined;var n=m||{};var o=n.MutationObserver||n.WebKitMutationObserver;var p=typeof self===\"undefined\"&&typeof process!==\"undefined\"&&{}.toString.call(process)===\"[object process]\";var q=typeof Uint8ClampedArray!==\"undefined\"&&typeof importScripts!==\"undefined\"&&typeof MessageChannel!==\"undefined\";function r(){return function(){return process.nextTick(x)}}function s(){if(typeof h!==\"undefined\"){return function(){h(x)}}return v()}function t(){var ra=0;var sa=new o(x);var ta=document.createTextNode(\"\");sa.observe(ta,{characterData:true});return function(){ta.data=ra=++ra%2}}function u(){var ua=new MessageChannel;ua.port1.onmessage=x;return function(){return ua.port2.postMessage(0)}}function v(){var va=setTimeout;return function(){return va(x,1)}}var w=new Array(1e3);function x(){for(var wa=0;wa>>16)*g&65535)<<16)&4294967295;k=k<<15|k>>>17;k=(k&65535)*i+(((k>>>16)*i&65535)<<16)&4294967295;e^=k;e=e<<13|e>>>19;f=(e&65535)*5+(((e>>>16)*5&65535)<<16)&4294967295;e=(f&65535)+27492+(((f>>>16)+58964&65535)<<16)}k=0;switch(c){case 3:k^=(a.charCodeAt(l+2)&255)<<16;case 2:k^=(a.charCodeAt(l+1)&255)<<8;case 1:k^=a.charCodeAt(l)&255;k=(k&65535)*g+(((k>>>16)*g&65535)<<16)&4294967295;k=k<<15|k>>>17;k=(k&65535)*i+(((k>>>16)*i&65535)<<16)&4294967295;e^=k}e^=a.length;e^=e>>>16;e=(e&65535)*2246822507+(((e>>>16)*2246822507&65535)<<16)&4294967295;e^=e>>>13;e=(e&65535)*3266489909+(((e>>>16)*3266489909&65535)<<16)&4294967295;e^=e>>>16;return e>>>0}phast.hash=murmurhash3_32_gc;\n"), \Kibo\Phast\ValueObjects\PhastJavaScript::fromString('/home/albert/code/phast/src/Build/../../src/Filters/HTML/PhastScriptsCompiler/service-url.js', "phast.buildServiceUrl=function(a,b){if(a.pathInfo){return appendPathInfo(a.serviceUrl,buildQuery(b))}else{return appendQueryString(a.serviceUrl,buildQuery(b))}};function buildQuery(c){if(typeof c===\"string\"){return c}var d=[];for(var e in c){if(c.hasOwnProperty(e)){d.push(encodeURIComponent(e)+\"=\"+encodeURIComponent(c[e]))}}return d.join(\"&\")}function appendPathInfo(f,g){var h=btoa(g).replace(/=/g,\"\").replace(/\\//g,\"_\").replace(/\\+/g,\"-\");return f.replace(/\\?.*\$/,\"\").replace(/\\/__p__\\.js\$/,\"\")+\"/\"+h+\".q.js\"}function appendQueryString(i,j){var k=i.indexOf(\"?\")>-1?\"&\":\"?\";return i+k+j}\n"), $resourcesLoader, \Kibo\Phast\ValueObjects\PhastJavaScript::fromString('/home/albert/code/phast/src/Build/../../src/Filters/HTML/PhastScriptsCompiler/phast.js', "var Promise=phast.ES6Promise;phast.ResourceLoader.instance=phast.ResourceLoader.make(phast.config.resourcesLoader.serviceUrl,phast.config.resourcesLoader.shortParamsMappings,phast.config.resourcesLoader.pathInfo);phast.forEachSelectedElement=function(a,b){Array.prototype.forEach.call(window.document.querySelectorAll(a),b)};phast.once=function(c){var d=false;return function(){if(!d){d=true;c.apply(this,Array.prototype.slice(arguments))}}};phast.on=function(e,f){return new Promise(function(g){e.addEventListener(f,g)})};phast.wait=function(h){return new Promise(function(i){setTimeout(i,h)})};phast.on(document,\"DOMContentLoaded\").then(function(){var j,k;function l(n){return n&&n.nodeType===8&&/^\\s*\\[Phast\\]/.test(n.textContent)}function m(o){while(o){if(l(o)){return o}o=o.nextSibling}return false}k=m(document.documentElement.nextSibling);if(k===false){k=m(document.body.firstChild)}if(k){j=k.textContent.replace(/^\\s+|\\s+\$/g,\"\").split(\"\\n\");console.groupCollapsed(j.shift());console.log(j.join(\"\\n\"));console.groupEnd()}});phast.on(document,\"DOMContentLoaded\").then(function(){var p=performance.timing;var q=[];q.push([\"Downloading phases:\"]);q.push([\" Look up hostname in DNS + %s ms\",t(p.domainLookupEnd-p.fetchStart)]);q.push([\" Establish connection + %s ms\",t(p.connectEnd-p.domainLookupEnd)]);q.push([\" Send request + %s ms\",t(p.requestStart-p.connectEnd)]);q.push([\" Receive first byte + %s ms\",t(p.responseStart-p.requestStart)]);q.push([\" Download page + %s ms\",t(p.responseEnd-p.responseStart)]);q.push([\"\"]);q.push([\"Totals:\"]);q.push([\" Time to first byte %s ms\",t(p.responseStart-p.fetchStart)]);q.push([\" (since request start) %s ms\",t(p.responseStart-p.requestStart)]);q.push([\" Total request time %s ms\",t(p.responseEnd-p.fetchStart)]);q.push([\" (since request start) %s ms\",t(p.responseEnd-p.requestStart)]);q.push([\" \"]);var r=[];var s=[];q.forEach(function(u){r.push(u.shift());s=s.concat(u)});console.groupCollapsed(\"[Phast] Client-side performance metrics\");console.log.apply(console,[r.join(\"\\n\")].concat(s));console.groupEnd();function t(v){v=\"\"+v;while(v.length<4){v=\" \"+v}return v}});\n")], $scripts); $compiled = $this->compileScripts($scripts); return '(' . $compiled . ')(' . $this->compileConfig($scripts) . ');'; } /** * @param PhastJavaScript[] $scripts * @return string */ private function performCompilation(array $scripts) { $compiled = implode(',', array_map(function (\Kibo\Phast\ValueObjects\PhastJavaScript $script) { return $this->interpolate($script->getContents()); }, $scripts)); return 'function phastScripts(phast){phast.scripts=[' . $compiled . '];(phast.scripts.shift())();}'; } /** * @param PhastJavaScript[] $scripts * @return string */ private function compileConfig(array $scripts) { $config = new \stdClass(); foreach ($scripts as $script) { if ($script->hasConfig()) { $config->{$script->getConfigKey()} = $script->getConfig(); } } $this->lastCompiledConfig = $config; return \Kibo\Phast\Common\JSON::encode(['config' => base64_encode(\Kibo\Phast\Common\JSON::encode($config))]); } /** * @param string $script * @return string */ private function interpolate($script) { return sprintf('(function(){%s})', $script); } /** * @param PhastJavaScript[] $scripts * @return string */ private function getCacheKey(array $scripts) { return array_reduce($scripts, function ($carry, \Kibo\Phast\ValueObjects\PhastJavaScript $script) { $carry .= $script->getFilename() . '-' . $script->getCacheSalt() . "\n"; return $carry; }, ''); } } namespace Kibo\Phast\Filters\HTML\LazyImageLoading; class Filter extends \Kibo\Phast\Filters\HTML\BaseHTMLStreamFilter { protected function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag) { if (!$tag->hasAttribute('loading')) { $tag->setAttribute('loading', 'lazy'); } (yield $tag); } protected function isTagOfInterest(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag) { return $tag->getTagName() == 'img'; } } namespace Kibo\Phast\Filters\HTML\ScriptsProxyService; class Filter extends \Kibo\Phast\Filters\HTML\BaseHTMLStreamFilter { use \Kibo\Phast\Filters\HTML\Helpers\JSDetectorTrait, \Kibo\Phast\Logging\LoggingTrait; /** * @var array */ private $config; /** * @var ServiceSignature */ private $signature; /** * @var LocalRetriever */ private $retriever; private $tokenRefMaker; /** * @var ObjectifiedFunctions */ private $functions; /** * @var bool */ private $didInject = false; public function __construct(array $config, \Kibo\Phast\Security\ServiceSignature $signature, \Kibo\Phast\Retrievers\LocalRetriever $retriever, \Kibo\Phast\Services\Bundler\TokenRefMaker $tokenRefMaker, \Kibo\Phast\Common\ObjectifiedFunctions $functions = null) { $this->config = $config; $this->signature = $signature; $this->retriever = $retriever; $this->tokenRefMaker = $tokenRefMaker; $this->functions = is_null($functions) ? new \Kibo\Phast\Common\ObjectifiedFunctions() : $functions; } protected function isTagOfInterest(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag) { return $tag->getTagName() == 'script' && $this->isJSElement($tag); } protected function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $script) { $this->rewriteScriptSource($script); if (!$this->didInject) { $this->addScript(); $this->didInject = true; } (yield $script); } private function rewriteScriptSource(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $element) { if (!$element->hasAttribute('src')) { return; } $src = trim($element->getAttribute('src')); $url = $this->getAbsoluteURL($src); $cacheMarker = $this->retriever->getCacheSalt($url); if (!$cacheMarker) { return; } $cacheMarker .= '-' . \Kibo\Phast\Filters\JavaScript\Minification\JSMinifierFilter::VERSION; $element->setAttribute('src', $this->makeProxiedURL($url, $cacheMarker)); $element->setAttribute('data-phast-original-src', (string) $url); $element->setAttribute('data-phast-params', $this->makeServiceParams($url, $cacheMarker)); } private function makeProxiedURL(\Kibo\Phast\ValueObjects\URL $url, $cacheMarker) { $params = ['service' => 'scripts', 'src' => (string) $url->withoutQuery(), 'cacheMarker' => $cacheMarker]; return (new \Kibo\Phast\Services\ServiceRequest())->withUrl(\Kibo\Phast\ValueObjects\URL::fromString($this->config['serviceUrl']))->withParams($params)->serialize(); } private function makeServiceParams(\Kibo\Phast\ValueObjects\URL $url, $cacheMarker) { return \Kibo\Phast\Services\Bundler\ServiceParams::fromArray(['src' => (string) $url->withoutQuery(), 'cacheMarker' => $cacheMarker, 'isScript' => '1'])->sign($this->signature)->replaceByTokenRef($this->tokenRefMaker)->serialize(); } private function addScript() { $config = ['serviceUrl' => $this->config['serviceUrl'], 'pathInfo' => \Kibo\Phast\Services\ServiceRequest::getDefaultSerializationMode() === \Kibo\Phast\Services\ServiceRequest::FORMAT_PATH, 'urlRefreshTime' => $this->config['urlRefreshTime'], 'whitelist' => $this->config['match']]; $script = \Kibo\Phast\ValueObjects\PhastJavaScript::fromString('/home/albert/code/phast/src/Build/../../src/Filters/HTML/ScriptsProxyService/rewrite-function.js', "var config=phast.config[\"script-proxy-service\"];var urlPattern=/^(https?:)?\\/\\//;var cacheMarker=Math.floor((new Date).getTime()/1e3/config.urlRefreshTime);var whitelist=compileWhitelistPatterns(config.whitelist);phast.scripts.push(function(){overrideDOMMethod(\"appendChild\");overrideDOMMethod(\"insertBefore\")});function compileWhitelistPatterns(a){var b=/^(.)(.*)\\1([a-z]*)\$/i;var c=[];a.forEach(function(d){var e=b.exec(d);if(!e){window.console&&window.console.log(\"Phast: Not a pattern:\",d);return}try{c.push(new RegExp(e[2],e[3]))}catch(f){window.console&&window.console.log(\"Phast: Failed to compile pattern:\",d)}});return c}function checkWhitelist(g){for(var h=0;hsetConfig('script-proxy-service', $config); $this->context->addPhastJavaScript($script); } private function getAbsoluteURL($url) { return \Kibo\Phast\ValueObjects\URL::fromString($url)->withBase($this->context->getBaseUrl()); } } namespace Kibo\Phast\Filters\HTML\BaseURLSetter; class Filter extends \Kibo\Phast\Filters\HTML\BaseHTMLStreamFilter { protected function isTagOfInterest(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag) { return $tag->getTagName() == 'base' && $tag->hasAttribute('href'); } protected function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag) { $base = \Kibo\Phast\ValueObjects\URL::fromString($tag->getAttribute('href')); $current = $this->context->getBaseUrl(); $this->context->setBaseUrl($base->withBase($current)); (yield $tag); } } namespace Kibo\Phast\Filters\HTML\CSSInlining; class OptimizerFactory { /** * @var Cache */ private $cache; public function __construct(array $config) { $this->cache = new \Kibo\Phast\Cache\File\Cache($config['cache'], 'css-optimizitor'); } /** * @param \Traversable $elements * @return Optimizer */ public function makeForElements(\Traversable $elements) { return new \Kibo\Phast\Filters\HTML\CSSInlining\Optimizer($elements, $this->cache); } } namespace Kibo\Phast\Filters\HTML\CSSInlining; class Filter extends \Kibo\Phast\Filters\HTML\BaseHTMLStreamFilter { use \Kibo\Phast\Logging\LoggingTrait; const CSS_IMPORTS_REGEXP = '~ @import \\s++ ( url \\( )?+ # url() is optional ( (?(1) ["\']?+ | ["\'] ) ) # without url() a quote is necessary \\s*+ (?[A-Za-z0-9_/.:?&=+%,-]++) \\s*+ \\2 # match ending quote (?(1)\\)) # match closing paren if url( was used \\s*+ ; ~xi'; /** * @var ServiceSignature */ private $signature; /** * @var int */ private $maxInlineDepth = 2; /** * @var URL */ private $baseURL; /** * @var string[] */ private $whitelist = array(); /** * @var string */ private $serviceUrl; /** * @var int */ private $optimizerSizeDiffThreshold; /** * @var Retriever */ private $localRetriever; /** * @var Retriever */ private $retriever; /** * @var OptimizerFactory */ private $optimizerFactory; /** * @var ServiceFilter */ private $cssFilter; /** * @var Optimizer */ private $optimizer; /** * @var TokenRefMaker */ private $tokenRefMaker; /** * @var string[] */ private $cacheMarkers = array(); public function __construct(\Kibo\Phast\Security\ServiceSignature $signature, \Kibo\Phast\ValueObjects\URL $baseURL, array $config, \Kibo\Phast\Retrievers\Retriever $localRetriever, \Kibo\Phast\Retrievers\Retriever $retriever, \Kibo\Phast\Filters\HTML\CSSInlining\OptimizerFactory $optimizerFactory, \Kibo\Phast\Services\ServiceFilter $cssFilter, \Kibo\Phast\Services\Bundler\TokenRefMaker $tokenRefMaker) { $this->signature = $signature; $this->baseURL = $baseURL; $this->serviceUrl = \Kibo\Phast\ValueObjects\URL::fromString((string) $config['serviceUrl']); $this->optimizerSizeDiffThreshold = (int) $config['optimizerSizeDiffThreshold']; $this->localRetriever = $localRetriever; $this->retriever = $retriever; $this->optimizerFactory = $optimizerFactory; $this->cssFilter = $cssFilter; $this->tokenRefMaker = $tokenRefMaker; foreach ($config['whitelist'] as $key => $value) { if (!is_array($value)) { $this->whitelist[$value] = ['ieCompatible' => true]; $key = $value; } else { $this->whitelist[$key] = $value; } } } protected function beforeLoop() { $this->elements = iterator_to_array($this->elements); $this->optimizer = $this->optimizerFactory->makeForElements(new \ArrayIterator($this->elements)); } protected function isTagOfInterest(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag) { return $tag->getTagName() == 'style' || $tag->getTagName() == 'link' && $tag->getAttribute('rel') == 'stylesheet' && $tag->hasAttribute('href'); } protected function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag) { if ($tag->getTagName() == 'link') { return $this->inlineLink($tag, $this->context->getBaseUrl()); } return $this->inlineStyle($tag); } protected function afterLoop() { $this->addIEFallbackScript(); $this->addInlinedRetrieverScript(); } private function inlineLink(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $link, \Kibo\Phast\ValueObjects\URL $baseUrl) { $href = trim($link->getAttribute('href')); if (trim($href, '/') == '') { return [$link]; } $location = \Kibo\Phast\ValueObjects\URL::fromString($href)->withBase($baseUrl); if (!$this->findInWhitelist($location) && !$this->localRetriever->getCacheSalt($location)) { return [$link]; } $media = $link->getAttribute('media'); if (preg_match('~^\\s*(this\\.)?media\\s*=\\s*(?[\'"])(?((?!\\k).)+?)\\k\\s*(;|$)~', $link->getAttribute('onload'), $match)) { $media = $match['m']; } $elements = $this->inlineURL($location, $media); return is_null($elements) ? [$link] : $elements; } private function inlineStyle(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $style) { $processed = $this->cssFilter->apply(\Kibo\Phast\ValueObjects\Resource::makeWithContent($this->baseURL, $style->textContent), [])->getContent(); $elements = $this->inlineCSS($this->baseURL, $processed, $style->getAttribute('media'), false); if (($id = $style->getAttribute('id')) != '') { if (sizeof($elements) == 1) { $elements[0]->setAttribute('id', $id); } else { foreach ($elements as $element) { $element->setAttribute('data-phast-original-id', $id); } } } return $elements; } private function findInWhitelist(\Kibo\Phast\ValueObjects\URL $url) { $stringUrl = (string) $url; foreach ($this->whitelist as $pattern => $settings) { if (preg_match($pattern, $stringUrl)) { return $settings; } } return false; } /** * @param URL $url * @param string $media * @param boolean $ieCompatible * @param int $currentLevel * @param string[] $seen * @return Tag[]|null * @throws \Kibo\Phast\Exceptions\ItemNotFoundException */ private function inlineURL(\Kibo\Phast\ValueObjects\URL $url, $media, $ieCompatible = true, $currentLevel = 0, $seen = array()) { $whitelistEntry = $this->findInWhitelist($url); if (!$whitelistEntry) { $whitelistEntry = !!$this->localRetriever->getCacheSalt($url); } if (!$whitelistEntry) { $this->logger()->info('Not inlining {url}. Not in whitelist', ['url' => $url]); return [$this->makeLink($url, $media)]; } if (isset($whitelistEntry['ieCompatible']) && !$whitelistEntry['ieCompatible']) { $ieFallbackUrl = $ieCompatible ? $url : null; $ieCompatible = false; } else { $ieFallbackUrl = null; } if (in_array($url, $seen)) { return []; } if ($currentLevel > $this->maxInlineDepth) { return $this->addIEFallback($ieFallbackUrl, [$this->makeLink($url, $media)]); } $seen[] = $url; $this->logger()->info('Inlining {url}.', ['url' => (string) $url]); $content = $this->retriever->retrieve($url); if ($content === false) { return $this->addIEFallback($ieFallbackUrl, [$this->makeServiceLink($url, $media)]); } $content = $this->cssFilter->apply(\Kibo\Phast\ValueObjects\Resource::makeWithContent($url, $content), [])->getContent(); $this->cacheMarkers[$url->toString()] = \Kibo\Phast\Common\Base64url::shortHash(implode("\0", [$this->retriever->getCacheSalt($url), $content])); $optimized = $this->optimizer->optimizeCSS($content); if ($optimized === null) { $this->logger()->error('CSS optimizer failed for {url}', ['url' => (string) $url]); return null; } $isOptimized = false; if (strlen($content) - strlen($optimized) > $this->optimizerSizeDiffThreshold) { $content = $optimized; $isOptimized = true; } $elements = $this->inlineCSS($url, $content, $media, $isOptimized, $ieCompatible, $currentLevel, $seen); $this->addIEFallback($ieFallbackUrl, $elements); return $elements; } private function inlineCSS(\Kibo\Phast\ValueObjects\URL $url, $content, $media, $optimized, $ieCompatible = true, $currentLevel = 0, $seen = array()) { $urlMatches = $this->getImportedURLs($content); $elements = []; foreach ($urlMatches as $match) { $matchedUrl = \Kibo\Phast\ValueObjects\URL::fromString($match['url'])->withBase($url); $replacement = $this->inlineURL($matchedUrl, $media, $ieCompatible, $currentLevel + 1, $seen); if ($replacement !== null) { $content = str_replace($match[0], '', $content); $elements = array_merge($elements, $replacement); } } $elements[] = $this->makeStyle($url, $content, $media, $optimized); return $elements; } private function addIEFallback(\Kibo\Phast\ValueObjects\URL $fallbackUrl = null, array $elements = null) { if ($fallbackUrl === null || !$elements) { return $elements; } foreach ($elements as $element) { $element->setAttribute('data-phast-nested-inlined', ''); } $element->setAttribute('data-phast-ie-fallback-url', (string) $fallbackUrl); $element->removeAttribute('data-phast-nested-inlined'); $this->logger()->info('Set {url} as IE fallback URL', ['url' => (string) $fallbackUrl]); return $elements; } private function addIEFallbackScript() { $this->logger()->info('Adding IE fallback script'); $this->context->addPhastJavaScript(\Kibo\Phast\ValueObjects\PhastJavaScript::fromString('/home/albert/code/phast/src/Build/../../src/Filters/HTML/CSSInlining/ie-fallback.js', "(function(){var a=window.navigator.userAgent;if(a.indexOf(\"MSIE \")===-1&&a.indexOf(\"Trident/\")===-1){return}Array.prototype.forEach.call(document.querySelectorAll(\"style[data-phast-ie-fallback-url]\"),function(b){var c=document.createElement(\"link\");if(b.hasAttribute(\"media\")){c.setAttribute(\"media\",b.getAttribute(\"media\"))}c.setAttribute(\"rel\",\"stylesheet\");c.setAttribute(\"href\",b.getAttribute(\"data-phast-ie-fallback-url\"));b.parentNode.insertBefore(c,b);b.parentNode.removeChild(b)});Array.prototype.forEach.call(document.querySelectorAll(\"style[data-phast-nested-inlined]\"),function(d){d.parentNode.removeChild(d)})})();\n")); } private function addInlinedRetrieverScript() { $this->logger()->info('Adding inlined retriever script'); $this->context->addPhastJavaScript(\Kibo\Phast\ValueObjects\PhastJavaScript::fromString('/home/albert/code/phast/src/Build/../../src/Filters/HTML/CSSInlining/inlined-css-retriever.js', "phast.stylesLoading=0;var resourceLoader=phast.ResourceLoader.instance;phast.forEachSelectedElement(\"style[data-phast-params]\",function(a){var b=a.getAttribute(\"data-phast-params\");var c=phast.ResourceLoader.RequestParams.fromString(b);phast.stylesLoading++;resourceLoader.get(c).then(function(d){a.textContent=d;a.removeAttribute(\"data-phast-params\")}).catch(function(e){console.warn(\"[Phast] Failed to load CSS\",c,e);var f=a.getAttribute(\"data-phast-original-src\");if(!f){console.error(\"[Phast] No data-phast-original-src on ]*+)?+> )\n ", 'TAG' => "\n < @@tag_name \\s*+ @@attrs? @tag_end\n ", 'tag_name' => "\n [^\\s>]++\n ", 'attrs' => ' (?: @attr )*+ ', 'attr' => "\n \\s*+\n @@attr_name\n (?: \\s*+ = \\s*+ @attr_value )?\n ", 'attr_name' => "\n [^\\s>][^\\s>=]*+\n ", 'attr_value' => "\n (?|\n \"(?'attr_value'[^\"]*+)\" |\n ' (?'attr_value' [^']*+) ' |\n (?'attr_value' [^\\s>]*+)\n )\n ", 'tag_end' => "\n \\s*+ >\n ", 'CLOSING_TAG' => ' ]*+ > '); public function __construct() { $this->mainPattern = $this->compilePattern($this->mainPattern, $this->subroutines); $this->attributePattern = $this->compilePattern($this->attributePattern, $this->subroutines); } public function tokenize($data) { $offset = 0; while (preg_match($this->mainPattern, $data, $match, PREG_OFFSET_CAPTURE, $offset)) { if ($match[0][1] > $offset) { $element = new \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Junk(); $element->originalString = substr($data, $offset, $match[0][1] - $offset); (yield $element); } if (!empty($match['COMMENT'][0])) { $element = new \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Comment(); $element->originalString = $match[0][0]; } elseif (!empty($match['TAG'][0]) || !empty($match['SCRIPT'][0]) || !empty($match['STYLE'][0])) { $attributes = $match['attrs'][0] === '' ? [] : $this->parseAttributes($match['attrs'][0]); $element = new \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag($match['tag_name'][0], $attributes); $element->originalString = $match['TAG'][0]; if (isset($match['body'][1]) && $match['body'][1] != -1) { $element->setTextContent($match['body'][0]); $element = $element->withClosingTag($match['closing_tag'][0]); } } elseif (!empty($match['CLOSING_TAG'][0])) { $element = new \Kibo\Phast\Parsing\HTML\HTMLStreamElements\ClosingTag($match['tag_name'][0]); $element->originalString = $match[0][0]; } else { throw new \Kibo\Phast\Exceptions\RuntimeException("Unhandled match:\n" . \Kibo\Phast\Common\JSON::prettyEncode($match)); } (yield $element); $offset = $match[0][1] + strlen($match[0][0]); } if ($offset < strlen($data) - 1) { $element = new \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Junk(); $element->originalString = substr($data, $offset); (yield $element); } } private function parseAttributes($str) { $matches = $this->repeatMatch($this->attributePattern, $str); foreach ($matches as $match) { (yield $match['attr_name'][0] => isset($match['attr_value'][0]) ? html_entity_decode($match['attr_value'][0], ENT_QUOTES, 'UTF-8') : ''); } } private function repeatMatch($pattern, $subject) { $offset = 0; while (preg_match($pattern, $subject, $match, PREG_OFFSET_CAPTURE, $offset)) { (yield $match); $offset = $match[0][1] + strlen($match[0][0]); } if ($offset < strlen($subject) - 1) { throw new \Kibo\Phast\Exceptions\RuntimeException('Unmatched part of subject: ' . substr($subject, $offset)); } } /** * Replace subroutines in patterns */ private function compilePattern($pattern, array $subroutines) { return preg_replace_callback('/@(@?)(\\w+)/', function ($match) use($subroutines) { $capture = !empty($match[1]); $ref = $match[2]; if (!isset($subroutines[$ref])) { throw new \Kibo\Phast\Exceptions\RuntimeException("Unknown pattern '{$ref}' used, or circular reference"); } $subroutine = $subroutines[$ref]; unset($subroutines[$ref]); $replace = $this->compilePattern($subroutine, $subroutines); if ($capture) { $replace = "(?'{$ref}'{$replace})"; } else { $replace = "(?:{$replace})"; } return $replace; }, $pattern); } } namespace Kibo\Phast\Parsing\HTML\HTMLStreamElements; class Element { /** * @var string */ public $originalString; /** * @param string $originalString */ public function setOriginalString($originalString) { $this->originalString = $originalString; } public function __get($name) { $method = 'get' . ucfirst($name); if (method_exists($this, $method)) { return call_user_func([$this, $method]); } } public function __set($name, $value) { $method = 'set' . ucfirst($name); if (method_exists($this, $method)) { return call_user_func([$this, $method], $value); } } public function toString() { return $this->__toString(); } public function __toString() { return isset($this->originalString) ? $this->originalString : ''; } public function dump() { return '<' . preg_replace('~^.*\\\\~', '', get_class($this)) . ' ' . $this->dumpValue() . '>'; } public function dumpValue() { return \Kibo\Phast\Common\JSON::encode($this->originalString); } } namespace Kibo\Phast\Parsing\HTML\HTMLStreamElements; class Junk extends \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Element { } namespace Kibo\Phast\Parsing\HTML\HTMLStreamElements; class Comment extends \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Element { public function isIEConditional() { return (bool) preg_match('/^ )++ ~xsiA'; private $filterCb; /** * @var ?string */ private $buffer = ''; private $offset = 0; /** * @var integer */ private $maxBufferSizeToApply; private $canceled = false; public function __construct($maxBufferSizeToApply, callable $filterCb) { $this->maxBufferSizeToApply = $maxBufferSizeToApply; $this->filterCb = $filterCb; } public function install() { $ignoreHandlers = ['default output handler', 'ob_gzhandler']; if (!array_diff(ob_list_handlers(), $ignoreHandlers)) { while (@ob_end_clean()) { } } ob_start([$this, 'handleChunk'], 2); ob_implicit_flush(1); } public function handleChunk($chunk, $phase) { if ($this->buffer === null) { return $chunk; } $this->buffer .= $chunk; if ($this->canceled) { return $this->stop(); } if (strlen($this->buffer) > $this->maxBufferSizeToApply) { $this->logger()->info('Buffer exceeds max. size ({buffersize} bytes). Not applying', ['buffersize' => $this->maxBufferSizeToApply]); return $this->stop(); } $output = ''; if (preg_match(self::START_PATTERN, $this->buffer, $match, 0, $this->offset)) { $this->offset += strlen($match[0]); $output .= $match[0]; } if ($phase & PHP_OUTPUT_HANDLER_FINAL) { $output .= $this->finalize(); } if ($output !== '') { @header_remove('Content-Length'); } return $output; } private function finalize() { $input = substr($this->buffer, $this->offset); $result = call_user_func($this->filterCb, $input, $this->buffer); $this->buffer = null; return $result; } private function stop() { $output = $this->buffer; $this->buffer = null; return $output; } public function cancel() { $this->canceled = true; } } namespace Kibo\Phast\ValueObjects; class PhastJavaScript { /** * @var string */ private $filename; /** * @var string */ private $contents; /** * @var string */ private $configKey; /** * @var mixed */ private $config; /** * @var ObjectifiedFunctions */ private $funcs; /** * @param string $filename * @param string $contents */ private function __construct($filename, $contents) { $this->filename = $filename; $this->contents = $contents; } /** * @param string $filename * @param ObjectifiedFunctions|null $funcs */ public static function fromFile($filename, \Kibo\Phast\Common\ObjectifiedFunctions $funcs = null) { $funcs = $funcs ? $funcs : new \Kibo\Phast\Common\ObjectifiedFunctions(); $contents = $funcs->file_get_contents($filename); if ($contents === false) { throw new \RuntimeException("Failed to read script: {$filename}"); } $contents = (new \Kibo\Phast\Common\JSMinifier($contents))->min(); return new self($filename, $contents); } /** * @param string $filename * @param string $contents */ public static function fromString($filename, $contents) { return new self($filename, $contents); } /** * @return string */ public function getFilename() { return $this->filename; } /** * @return bool|string */ public function getContents() { return $this->contents; } /** * @return string */ public function getCacheSalt() { $hash = md5($this->getContents(), true); return substr(preg_replace('/^[a-z0-9]/i', '', base64_encode($hash)), 0, 16); } /** * @param string $configKey * @param mixed $config */ public function setConfig($configKey, $config) { $this->configKey = $configKey; $this->config = $config; } /** * @return bool */ public function hasConfig() { return isset($this->configKey); } /** * @return string */ public function getConfigKey() { return $this->configKey; } /** * @return mixed */ public function getConfig() { return $this->config; } } namespace Kibo\Phast\ValueObjects; class Resource { const EXTENSION_TO_MIME_TYPE = array('gif' => 'image/gif', 'png' => 'image/png', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'bmp' => 'image/bmp', 'webp' => 'image/webp', 'svg' => 'image/svg+xml', 'css' => 'text/css', 'js' => 'application/javascript', 'json' => 'application/json'); /** * @var URL */ private $url; /** * @var Retriever */ private $retriever; /** * @var string */ private $content; /** * @var string */ private $mimeType; /** * @var Resource[] */ private $dependencies = array(); private function __construct() { } public static function makeWithContent(\Kibo\Phast\ValueObjects\URL $url, $content, $mimeType = null) { $instance = new self(); $instance->url = $url; $instance->mimeType = $mimeType; $instance->content = $content; return $instance; } public static function makeWithRetriever(\Kibo\Phast\ValueObjects\URL $url, \Kibo\Phast\Retrievers\Retriever $retriever, $mimeType = null) { $instance = new self(); $instance->url = $url; $instance->mimeType = $mimeType; $instance->retriever = $retriever; return $instance; } /** * @return URL */ public function getUrl() { return $this->url; } /** * @return string * @throws ItemNotFoundException */ public function getContent() { if (!isset($this->content)) { $this->content = $this->retriever->retrieve($this->url); if ($this->content === false) { throw new \Kibo\Phast\Exceptions\ItemNotFoundException("Could not get {$this->url}"); } } return $this->content; } /** * @return string|null */ public function getMimeType() { if (!isset($this->mimeType)) { $ext = strtolower($this->url->getExtension()); $ext2mime = self::EXTENSION_TO_MIME_TYPE; if (isset($ext2mime[$ext])) { $this->mimeType = self::EXTENSION_TO_MIME_TYPE[$ext]; } } return $this->mimeType; } /** * @return bool|int */ public function getSize() { if (isset($this->retriever) && method_exists($this->retriever, 'getSize')) { return $this->retriever->getSize($this->url); } if (isset($this->content)) { return strlen($this->content); } return false; } public function toDataURL() { $mime = $this->getMimeType(); $content = $this->getContent(); return "data:{$mime};base64," . base64_encode($content); } /** * @return Resource[] */ public function getDependencies() { return $this->dependencies; } /** * @return bool|int */ public function getCacheSalt() { return isset($this->retriever) ? $this->retriever->getCacheSalt($this->url) : 0; } /** * @param string $content * @param string|null $mimeType * @return Resource */ public function withContent($content, $mimeType = null) { $new = clone $this; $new->content = $content; if (!is_null($mimeType)) { $new->mimeType = $mimeType; } return $new; } /** * @param Resource[] $dependencies * @return Resource */ public function withDependencies(array $dependencies) { $new = clone $this; $new->dependencies = $dependencies; return $new; } } namespace Kibo\Phast\ValueObjects; class Query implements \IteratorAggregate { private $tuples = array(); /** * @param array $assoc * @return Query */ public static function fromAssoc($assoc) { $result = new static(); foreach ($assoc as $k => $v) { $result->add($k, $v); } return $result; } /** * @param string $string * @return Query */ public static function fromString($string) { $result = new static(); foreach (explode('&', $string) as $piece) { if ($piece === '') { continue; } $parts = array_map('urldecode', explode('=', $piece, 2)); $result->add($parts[0], isset($parts[1]) ? $parts[1] : ''); } return $result; } public function add($key, $value) { $this->tuples[] = [(string) $key, (string) $value]; } public function get($key, $default = null) { foreach ($this->tuples as $tuple) { if ($tuple[0] === (string) $key) { return $tuple[1]; } } return $default; } public function delete($key) { $this->tuples = array_filter($this->tuples, function ($tuple) use($key) { return $tuple[0] !== (string) $key; }); } public function set($key, $value) { $this->delete($key); $this->add($key, $value); } public function has($key) { foreach ($this->tuples as $tuple) { if ($tuple[0] === (string) $key) { return true; } } return false; } public function update(\Kibo\Phast\ValueObjects\Query $source) { foreach ($source as $key => $value) { $this->delete($key); } foreach ($source as $key => $value) { $this->add($key, $value); } } public function toAssoc() { $assoc = []; foreach ($this->tuples as $tuple) { if (!array_key_exists($tuple[0], $assoc)) { $assoc[$tuple[0]] = $tuple[1]; } } return $assoc; } public function getIterator() { foreach ($this->tuples as $tuple) { (yield $tuple[0] => $tuple[1]); } } public function pop($key) { $value = $this->get($key); $this->delete($key); return $value; } public function getAll($key) { $result = []; foreach ($this->tuples as $tuple) { if ($tuple[0] === (string) $key) { $result[] = $tuple[1]; } } return $result; } } namespace Kibo\Phast\ValueObjects; class URL { /** * @var string */ private $scheme; /** * @var string */ private $host; /** * @var string */ private $port; /** * @var string */ private $user; /** * @var string */ private $pass; /** * @var string */ private $path; /** * @var string */ private $query; /** * @var string */ private $fragment; /** * @param $string * @return URL */ public static function fromString($string) { $components = parse_url($string); if (!$components) { return new self(); } return self::fromArray($components); } /** * @param array $arr Should follow the format produced by parse_url() * @return URL * @see parse_url() */ public static function fromArray(array $arr) { $url = new self(); foreach ($arr as $key => $value) { $url->{$key} = $key == 'path' ? $url->normalizePath($value) : $value; } return $url; } /** * If $this can be interpreted as relative to $base, * will produce URL that is $base/$this. * Otherwise the returned URL will point to the same place as $this * * @param URL $base * @return URL * * @example this: www/htdocs + base: /var -> /var/www/htdocs * @example this: /var + base: http://example.com -> http://example.com/var * @example this: /var + base: /www -> /var */ public function withBase(\Kibo\Phast\ValueObjects\URL $base) { $new = clone $this; foreach (['scheme', 'host', 'port', 'user', 'pass', 'path'] as $key) { if ($key == 'path') { $new->path = $this->resolvePath($base->path, $this->path); } elseif (!isset($this->{$key}) && isset($base->{$key})) { $new->{$key} = $base->{$key}; } elseif (isset($this->{$key})) { break; } } return $new; } /** * Tells whether $this can be interpreted as at the same host as $url * * @param URL $url * @return bool */ public function isLocalTo(\Kibo\Phast\ValueObjects\URL $url) { return empty($this->host) || $this->host === $url->host; } /** * @return string */ public function toString() { $scheme = isset($this->scheme) ? $this->scheme . '://' : ''; $host = isset($this->host) ? $this->host : ''; $port = isset($this->port) ? ':' . $this->port : ''; $user = isset($this->user) ? $this->user : ''; $pass = isset($this->pass) ? ':' . $this->pass : ''; $pass = $user || $pass ? "{$pass}@" : ''; $path = isset($this->path) ? $this->getPath() : ''; $query = isset($this->query) ? '?' . $this->query : ''; $fragment = isset($this->fragment) ? '#' . $this->fragment : ''; return "{$scheme}{$user}{$pass}{$host}{$port}{$path}{$query}{$fragment}"; } private function normalizePath($path) { $stack = []; $head = null; foreach (explode('/', $path) as $part) { if ($part == '.' || $part == '') { continue; } if (!is_null($head) && $part == '..' && $head != '..') { array_pop($stack); $head = empty($stack) ? null : $stack[count($stack) - 1]; } else { $stack[] = $head = $part; } } $normalized = substr($path, 0, 1) == '/' ? '/' : ''; if (!empty($stack)) { $normalized .= join('/', $stack); $normalized .= substr($path, -1) == '/' ? '/' : ''; } return $normalized; } private function resolvePath($base, $requested) { if (!$requested) { return $base; } if ($requested[0] == '/') { return $requested; } if (substr($base, -1, 1) == '/') { $usedBase = $base; } else { $usedBase = dirname($base); } return rtrim($usedBase, '/') . '/' . $requested; } /** * @return string */ public function getScheme() { return $this->scheme; } /** * @return string */ public function getHost() { return $this->host; } /** * @return string */ public function getPort() { return $this->port; } /** * @return string */ public function getUser() { return $this->user; } /** * @return string */ public function getPass() { return $this->pass; } /** * @return string */ public function getPath() { return $this->path; } /** * @return string */ public function getQuery() { return $this->query; } /** * @return string */ public function getExtension() { $matches = []; if (preg_match('/\\.([^.]*)$/', $this->path, $matches)) { return $matches[1]; } return ''; } /** * @return string */ public function getFragment() { return $this->fragment; } /** * @param string $path * @return self */ public function withPath($path) { $url = clone $this; $url->path = (string) $path; return $url; } /** * @param string|null $query * @return self */ public function withQuery($query) { $url = clone $this; if ($query === null) { $url->query = null; } else { $url->query = (string) $query; } return $url; } /** * @return self */ public function withoutQuery() { $url = clone $this; $url->query = null; return $url; } public function __toString() { return $this->toString(); } public function rewrite(\Kibo\Phast\ValueObjects\URL $from, \Kibo\Phast\ValueObjects\URL $to) { $str_from = rtrim($from->toString(), '/'); $str_to = rtrim($to->toString(), '/'); return \Kibo\Phast\ValueObjects\URL::fromString(preg_replace('~^' . preg_quote($str_from, '~') . '(?=$|/)~', $str_to, $this->toString())); } } namespace Kibo\Phast\Logging; class LogLevel { const EMERGENCY = 128; const ALERT = 64; const CRITICAL = 32; const ERROR = 16; const WARNING = 8; const NOTICE = 4; const INFO = 2; const DEBUG = 1; public static function toString($level) { switch ($level) { case self::EMERGENCY: return 'EMERGENCY'; case self::ALERT: return 'ALERT'; case self::CRITICAL: return 'CRITICAL'; case self::ERROR: return 'ERROR'; case self::WARNING: return 'WARNING'; case self::NOTICE: return 'NOTICE'; case self::INFO: return 'INFO'; case self::DEBUG: return 'DEBUG'; default: return 'UNKNOWN'; } } } namespace Kibo\Phast\Logging; class Log { /** * @var Logger */ private static $logger; public static function setLogger(\Kibo\Phast\Logging\Logger $logger) { self::$logger = $logger; } public static function initWithDummy() { self::$logger = new \Kibo\Phast\Logging\Logger(new \Kibo\Phast\Logging\LogWriters\Dummy\Writer()); } public static function init(array $config, \Kibo\Phast\Services\ServiceRequest $request, $service) { $writer = (new \Kibo\Phast\Logging\LogWriters\Factory())->make($config, $request); $logger = new \Kibo\Phast\Logging\Logger($writer); self::$logger = $logger->withContext(['documentRequestId' => $request->getDocumentRequestId(), 'requestId' => mt_rand(0, 99999999), 'service' => $service]); } /** * @return Logger */ public static function get() { if (!isset(self::$logger)) { self::initWithDummy(); } return self::$logger; } /** * @param array $context * @return Logger */ public static function context(array $context) { return self::get()->withContext($context); } /** * System is unusable. * * @param string $message * @param array $context * * @return void */ public static function emergency($message, array $context = array()) { self::get()->emergency($message, $context); } /** * Action must be taken immediately. * * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * * @param string $message * @param array $context * * @return void */ public static function alert($message, array $context = array()) { self::get()->alert($message, $context); } /** * Critical conditions. * * Example: Application component unavailable, unexpected exception. * * @param string $message * @param array $context * * @return void */ public static function critical($message, array $context = array()) { self::get()->critical($message, $context); } /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. * * @param string $message * @param array $context * * @return void */ public static function error($message, array $context = array()) { self::get()->error($message, $context); } /** * Exceptional occurrences that are not errors. * * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * * @param string $message * @param array $context * * @return void */ public static function warning($message, array $context = array()) { self::get()->warning($message, $context); } /** * Normal but significant events. * * @param string $message * @param array $context * * @return void */ public static function notice($message, array $context = array()) { self::get()->notice($message, $context); } /** * Interesting events. * * Example: User logs in, SQL logs. * * @param string $message * @param array $context * * @return void */ public static function info($message, array $context = array()) { self::get()->info($message, $context); } /** * Detailed debug information. * * @param string $message * @param array $context * * @return void */ public static function debug($message, array $context = array()) { self::get()->debug($message, $context); } } namespace Kibo\Phast\Logging\LogWriters; class Factory { public function make(array $config, \Kibo\Phast\Services\ServiceRequest $request) { if (isset($config['logWriters']) && count($config['logWriters']) > 1) { $class = \Kibo\Phast\Logging\LogWriters\Composite\Writer::class; } elseif (isset($config['logWriters'])) { $config = array_pop($config['logWriters']); $class = $config['class']; } else { $class = $config['class']; } $package = \Kibo\Phast\Environment\Package::fromPackageClass($class); $writer = $package->getFactory()->make($config, $request); if (isset($config['levelMask'])) { $writer->setLevelMask($config['levelMask']); } return $writer; } } namespace Kibo\Phast\Logging\LogWriters\JSONLFile; class Factory { public function make(array $config, \Kibo\Phast\Services\ServiceRequest $request) { return new \Kibo\Phast\Logging\LogWriters\JSONLFile\Writer($config['logRoot'], $request->getDocumentRequestId()); } } namespace Kibo\Phast\Logging\LogWriters\Composite; class Factory { public function make(array $config, \Kibo\Phast\Services\ServiceRequest $request) { $writer = new \Kibo\Phast\Logging\LogWriters\Composite\Writer(); $factory = new \Kibo\Phast\Logging\LogWriters\Factory(); foreach ($config['logWriters'] as $writerConfig) { $writer->addWriter($factory->make($writerConfig, $request)); } return $writer; } } namespace Kibo\Phast\Logging\LogWriters\RotatingTextFile; class Factory { public function make(array $config, \Kibo\Phast\Services\ServiceRequest $request) { return new \Kibo\Phast\Logging\LogWriters\RotatingTextFile\Writer($config); } } namespace Kibo\Phast\Logging\LogWriters\PHPError; class Factory { public function make(array $config, \Kibo\Phast\Services\ServiceRequest $request) { return new \Kibo\Phast\Logging\LogWriters\PHPError\Writer($config); } } namespace Kibo\Phast\Logging\Common; trait JSONLFileLogTrait { /** * @var string */ private $dir; /** * @var string */ private $filename; /** * JSONLFileLogWriter constructor. * @param string $dir * @param string $suffix */ public function __construct($dir, $suffix) { $this->dir = $dir; $suffix = preg_replace('/[^0-9A-Za-z_-]/', '', (string) $suffix); if (!empty($suffix)) { $suffix = '-' . $suffix; } $this->filename = $this->dir . '/log' . $suffix . '.jsonl'; } } namespace Kibo\Phast\Logging; class LogEntry implements \JsonSerializable { /** * @var int */ private $level; /** * @var string */ private $message; /** * @var array */ private $context; /** * LogEntry constructor. * @param int $level * @param string $message * @param array $context */ public function __construct($level, $message, array $context) { $this->level = (int) $level; $this->message = $message; $this->context = $context; } /** * @return int */ public function getLevel() { return $this->level; } /** * @return string */ public function getMessage() { return $this->message; } /** * @return array */ public function getContext() { return $this->context; } public function toArray() { return ['level' => $this->level, 'message' => $this->message, 'context' => $this->context]; } public function jsonSerialize() { return $this->toArray(); } } namespace Kibo\Phast\Logging; class Logger { /** * @var LogWriter */ private $writer; /** * @var array */ private $context = array(); /** * @var ObjectifiedFunctions */ private $functions; /** * Logger constructor. * @param LogWriter $writer */ public function __construct(\Kibo\Phast\Logging\LogWriter $writer, \Kibo\Phast\Common\ObjectifiedFunctions $functions = null) { $this->writer = $writer; $this->functions = is_null($functions) ? new \Kibo\Phast\Common\ObjectifiedFunctions() : $functions; } /** * Returns a new logger with default context * merged from the current logger and the passed array * * @param array $context * @return Logger */ public function withContext(array $context) { $logger = clone $this; $logger->context = array_merge($this->context, $context); return $logger; } /** * System is unusable. * * @param string $message * @param array $context * * @return void */ public function emergency($message, array $context = array()) { $this->log(\Kibo\Phast\Logging\LogLevel::EMERGENCY, $message, $context); } /** * Action must be taken immediately. * * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * * @param string $message * @param array $context * * @return void */ public function alert($message, array $context = array()) { $this->log(\Kibo\Phast\Logging\LogLevel::ALERT, $message, $context); } /** * Critical conditions. * * Example: Application component unavailable, unexpected exception. * * @param string $message * @param array $context * * @return void */ public function critical($message, array $context = array()) { $this->log(\Kibo\Phast\Logging\LogLevel::CRITICAL, $message, $context); } /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. * * @param string $message * @param array $context * * @return void */ public function error($message, array $context = array()) { $this->log(\Kibo\Phast\Logging\LogLevel::ERROR, $message, $context); } /** * Exceptional occurrences that are not errors. * * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * * @param string $message * @param array $context * * @return void */ public function warning($message, array $context = array()) { $this->log(\Kibo\Phast\Logging\LogLevel::WARNING, $message, $context); } /** * Normal but significant events. * * @param string $message * @param array $context * * @return void */ public function notice($message, array $context = array()) { $this->log(\Kibo\Phast\Logging\LogLevel::NOTICE, $message, $context); } /** * Interesting events. * * Example: User logs in, SQL logs. * * @param string $message * @param array $context * * @return void */ public function info($message, array $context = array()) { $this->log(\Kibo\Phast\Logging\LogLevel::INFO, $message, $context); } /** * Detailed debug information. * * @param string $message * @param array $context * * @return void */ public function debug($message, array $context = array()) { $this->log(\Kibo\Phast\Logging\LogLevel::DEBUG, $message, $context); } protected function log($level, $message, array $context = array()) { $context = array_merge(['timestamp' => $this->functions->microtime(true)], $context); $this->writer->writeEntry(new \Kibo\Phast\Logging\LogEntry($level, $message, array_merge($this->context, $context))); } } namespace Kibo\Phast\Logging; interface LogReader { /** * Reads LogMessage objects * * @return \Generator */ public function readEntries(); } namespace Kibo\Phast\Logging; trait LoggingTrait { protected function logger($method = null, $line = null) { $context = ['class' => get_class($this)]; if (!is_null($method)) { $context['method'] = $method; } if (!is_null($line)) { $context['line'] = $line; } return \Kibo\Phast\Logging\Log::context($context); } } namespace Kibo\Phast\Logging\LogReaders\JSONLFile; class Reader implements \Kibo\Phast\Logging\LogReader { use \Kibo\Phast\Logging\Common\JSONLFileLogTrait; public function readEntries() { $fp = @fopen($this->filename, 'r'); while ($fp && ($row = @fgets($fp))) { $decoded = @json_decode($row, true); if (!$decoded) { continue; } (yield new \Kibo\Phast\Logging\LogEntry(@$decoded['level'], @$decoded['message'], @$decoded['context'])); } @fclose($fp); @unlink($this->filename); } public function __destruct() { if (!($dir = @opendir($this->dir))) { return; } $tenMinutesAgo = time() - 600; while ($file = @readdir($dir)) { $filename = $this->dir . "/{$file}"; if (preg_match('/\\.jsonl$/', $file) && @filectime($filename) < $tenMinutesAgo) { @unlink($filename); } } } } namespace Kibo\Phast\Logging; interface LogWriter { /** * Set a bit-mask to filter entries that are actually written * * @param int $mask * @return void */ public function setLevelMask($mask); /** * Write an entry to the log * * @param LogEntry $entry * @return void */ public function writeEntry(\Kibo\Phast\Logging\LogEntry $entry); } namespace Kibo\Phast\Services; interface ServiceFilter { /** * @param Resource $resource * @param array $request * @return Resource */ public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request); } namespace Kibo\Phast\Services; class Factory { /** * @param string $service * @param array $config * @return BaseService * @throws ItemNotFoundException */ public function make($service, array $config) { if (!preg_match('/^[a-z]+$/', $service)) { throw new \Kibo\Phast\Exceptions\ItemNotFoundException('Bad service'); } $class = __NAMESPACE__ . '\\' . ucfirst($service) . '\\Factory'; if (class_exists($class)) { return (new $class())->make($config); } throw new \Kibo\Phast\Exceptions\ItemNotFoundException('Unknown service'); } } namespace Kibo\Phast\Services; trait ServiceFactoryTrait { /** * @param array $config * @param $cacheNamespace * @return UniversalRetriever */ public function makeUniversalCachingRetriever(array $config, $cacheNamespace) { $retriever = new \Kibo\Phast\Retrievers\UniversalRetriever(); $retriever->addRetriever(new \Kibo\Phast\Retrievers\LocalRetriever($config['retrieverMap'])); $retriever->addRetriever(new \Kibo\Phast\Retrievers\CachingRetriever(new \Kibo\Phast\Cache\File\Cache($config['cache'], $cacheNamespace), (new \Kibo\Phast\Retrievers\RemoteRetrieverFactory())->make($config))); return $retriever; } public function makeCachingServiceFilter(array $config, \Kibo\Phast\Filters\Service\CompositeFilter $compositeFilter, $cacheNamespace) { return new \Kibo\Phast\Filters\Service\CachingServiceFilter(new \Kibo\Phast\Cache\File\Cache($config['cache'], $cacheNamespace), $compositeFilter, new \Kibo\Phast\Retrievers\LocalRetriever($config['retrieverMap'])); } } namespace Kibo\Phast\Services\Bundler; class Service { use \Kibo\Phast\Logging\LoggingTrait; /** * @var ServiceSignature */ private $signature; /** * @var Retriever */ private $cssRetriever; /** * @var ServiceFilter */ private $cssFilter; /** * @var Retriever */ private $jsRetriever; /** * @var ServiceFilter */ private $jsFilter; private $tokenRefMaker; public function __construct(\Kibo\Phast\Security\ServiceSignature $signature, \Kibo\Phast\Retrievers\Retriever $cssRetriever, \Kibo\Phast\Services\ServiceFilter $cssFilter, \Kibo\Phast\Retrievers\Retriever $jsRetriever, \Kibo\Phast\Services\ServiceFilter $jsFilter, \Kibo\Phast\Services\Bundler\TokenRefMaker $tokenRefMaker) { $this->signature = $signature; $this->cssRetriever = $cssRetriever; $this->cssFilter = $cssFilter; $this->jsRetriever = $jsRetriever; $this->jsFilter = $jsFilter; $this->tokenRefMaker = $tokenRefMaker; } /** * @param ServiceRequest $request * @return Response */ public function serve(\Kibo\Phast\Services\ServiceRequest $request) { $response = new \Kibo\Phast\HTTP\Response(); $response->setHeader('Content-Type', 'application/json'); $response->setContent($this->streamResponse($request)); return $response; } private function streamResponse(\Kibo\Phast\Services\ServiceRequest $request) { (yield '['); $firstRow = true; foreach ($this->getParams($request) as $key => $params) { if (isset($params['ref'])) { $ref = $params['ref']; $params = $this->tokenRefMaker->getParams($ref); if (!$params) { $this->logger()->error('Could not resolve ref {ref}', ['ref' => $ref]); (yield $this->generateJSONRow(['status' => 404], $firstRow)); continue; } } if (!isset($params['src'])) { $this->logger()->error('No src found for set {key}', ['key' => $key]); (yield $this->generateJSONRow(['status' => 404], $firstRow)); continue; } if (!$this->verifyParams($params)) { $this->logger()->error('Params verification failed for set {key}', ['key' => $key]); (yield $this->generateJSONRow(['status' => 401], $firstRow)); continue; } list($retriever, $filter) = $this->getRetrieverAndFilter($params); $resource = \Kibo\Phast\ValueObjects\Resource::makeWithRetriever(\Kibo\Phast\ValueObjects\URL::fromString($params['src']), $retriever); try { $this->logger()->info('Applying for set {key}', ['key' => $key]); $filtered = $filter->apply($resource, $params); (yield $this->generateJSONRow(['status' => 200, 'content' => $filtered->getContent()], $firstRow)); } catch (\Kibo\Phast\Exceptions\ItemNotFoundException $e) { $this->logger()->error('Could not find {url} for set {key}', ['url' => $params['src'], 'key' => $key]); (yield $this->generateJSONRow(['status' => 404], $firstRow)); } catch (\Exception $e) { $this->logger()->critical('Unhandled exception for set {key}: {type} Message: {message} File: {file} Line: {line}', ['key' => $key, 'type' => get_class($e), 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine()]); (yield $this->generateJSONRow(['status' => 500], $firstRow)); } } (yield ']'); } private function getParams(\Kibo\Phast\Services\ServiceRequest $request) { $params = $request->getParams(); if (isset($params['src_0'])) { return (new \Kibo\Phast\Services\Bundler\BundlerParamsParser())->parse($request); } return (new \Kibo\Phast\Services\Bundler\ShortBundlerParamsParser())->parse($request); } private function verifyParams(array $params) { return \Kibo\Phast\Services\Bundler\ServiceParams::fromArray($params)->verify($this->signature); } private function getRetrieverAndFilter(array $params) { if (isset($params['isScript'])) { return [$this->jsRetriever, $this->jsFilter]; } return [$this->cssRetriever, $this->cssFilter]; } private function generateJSONRow(array $content, &$firstRow) { if (!$firstRow) { $prepend = ','; } else { $prepend = ''; $firstRow = false; } return $prepend . \Kibo\Phast\Common\JSON::encode($content); } } namespace Kibo\Phast\Services\Bundler; class BundlerParamsParser { public function parse(\Kibo\Phast\Services\ServiceRequest $request) { $result = []; foreach ($request->getParams() as $name => $value) { if (strpos($name, '_') !== false) { list($name, $key) = explode('_', $name, 2); $result[$key][$name] = $value; } } return $result; } } namespace Kibo\Phast\Services\Bundler; class ShortBundlerParamsParser { public static function getParamsMappings() { return ['s' => 'src', 'i' => 'strip-imports', 'c' => 'cacheMarker', 't' => 'token', 'j' => 'isScript', 'r' => 'ref']; } public function parse(\Kibo\Phast\Services\ServiceRequest $request) { $query_string = $request->getHTTPRequest()->getQueryString(); if (preg_match('/(^|&)f=/', $query_string)) { $query = \Kibo\Phast\ValueObjects\Query::fromString($this->unobfuscateQuery($query_string)); } else { $query = $request->getQuery(); } $query = $this->unshortenParams($query->getIterator()); $query = $this->uncompressSrcs($query); $result = []; $current = null; foreach ($query as $key => $value) { if (in_array($key, ['src', 'ref'])) { if ($current) { $result[] = $current; } $current = []; } if ($current !== null) { $current[$key] = $value; } } if ($current) { $result[] = $current; } return $result; } private function unobfuscateQuery($query) { $query = str_rot13($query); if (strpos($query, '%2S') !== false) { $query = preg_replace_callback('/%../', function ($match) { return str_rot13($match[0]); }, $query); } return $query; } private function unshortenParams(\Generator $query) { $mappings = self::getParamsMappings(); foreach ($query as $key => $value) { if (isset($mappings[$key])) { (yield $mappings[$key] => $value === '' ? '1' : $value); } else { (yield $key => $value); } } } private function uncompressSrcs(\Generator $query) { $lastUrl = ''; foreach ($query as $key => $value) { if ($key === 'src') { $prefixLength = (int) base_convert(substr($value, 0, 2), 36, 10); $suffix = substr($value, 2); $value = substr($lastUrl, 0, $prefixLength) . $suffix; $lastUrl = $value; } (yield $key => $value); } } } namespace Kibo\Phast\Services\Bundler; class TokenRefMaker { private $cache; public function __construct(\Kibo\Phast\Cache\Cache $cache) { $this->cache = $cache; } public function getRef($token, array $params) { $ref = \Kibo\Phast\Common\Base64url::shortHash(\Kibo\Phast\Common\JSON::encode($params)); $cachedParams = $this->cache->get($ref); if (!$cachedParams) { $this->cache->set($ref, $params); $cachedParams = $this->cache->get($ref); } if ($cachedParams === $params) { return $ref; } } public function getParams($ref) { return $this->cache->get($ref); } } namespace Kibo\Phast\Services\Bundler; class TokenRefMakerFactory { public function make(array $config) { $cache = new \Kibo\Phast\Cache\File\Cache($config['cache'], 'token-refs'); return new \Kibo\Phast\Services\Bundler\TokenRefMaker($cache); } } namespace Kibo\Phast\Services\Bundler; class ServiceParams { /** * @var string */ private $token; /** * @var array */ private $params; private function __construct() { } /** * @param array $params * @return ServiceParams */ public static function fromArray(array $params) { $instance = new self(); if (isset($params['token'])) { $instance->token = $params['token']; unset($params['token']); } $instance->params = $params; return $instance; } /** * @param ServiceSignature $signature * @return ServiceParams */ public function sign(\Kibo\Phast\Security\ServiceSignature $signature) { $new = new self(); $new->token = $this->makeToken($signature); $new->params = $this->params; return $new; } /** * @param ServiceSignature $signature * @return bool */ public function verify(\Kibo\Phast\Security\ServiceSignature $signature) { if (!isset($this->token)) { return false; } return $this->token == $this->makeToken($signature); } /** * @return mixed */ public function toArray() { $params = $this->params; if ($this->token) { $params['token'] = $this->token; } return $params; } public function serialize() { return \Kibo\Phast\Common\JSON::encode($this->toArray()); } private function makeToken(\Kibo\Phast\Security\ServiceSignature $signature) { $params = $this->params; if (isset($params['cacheMarker'])) { unset($params['cacheMarker']); } ksort($params); array_walk($params, function (&$item) { $item = (string) $item; }); return $signature->sign(json_encode($params)); } public function replaceByTokenRef(\Kibo\Phast\Services\Bundler\TokenRefMaker $maker) { if (!isset($this->token)) { return $this; } $ref = $maker->getRef($this->token, $this->toArray()); return $ref ? \Kibo\Phast\Services\Bundler\ServiceParams::fromArray(['ref' => $ref]) : $this; } } namespace Kibo\Phast\Services\Bundler; class Factory { use \Kibo\Phast\Services\ServiceFactoryTrait; public function make(array $config) { $cssServiceFactory = new \Kibo\Phast\Services\Css\Factory(); $jsServiceFactory = new \Kibo\Phast\Services\Scripts\Factory(); $cssFilter = $this->makeCachingServiceFilter($config, $cssServiceFactory->makeFilter($config), 'bundler-css'); $jsFilter = $this->makeCachingServiceFilter($config, $jsServiceFactory->makeFilter($config), 'bundler-js'); return new \Kibo\Phast\Services\Bundler\Service((new \Kibo\Phast\Security\ServiceSignatureFactory())->make($config), $cssServiceFactory->makeRetriever($config), $cssFilter, $jsServiceFactory->makeRetriever($config), $jsFilter, (new \Kibo\Phast\Services\Bundler\TokenRefMakerFactory())->make($config)); } } namespace Kibo\Phast\Services\Css; class Factory { use \Kibo\Phast\Services\ServiceFactoryTrait; public function make(array $config) { $cssComposite = $this->makeFilter($config); $composite = $this->makeCachingServiceFilter($config, $cssComposite, 'css-processing-2'); return new \Kibo\Phast\Services\Css\Service((new \Kibo\Phast\Security\ServiceSignatureFactory())->make($config), [], $this->makeRetriever($config), $composite, $config); } public function makeRetriever(array $config) { return $this->makeUniversalCachingRetriever($config, 'css'); } public function makeFilter(array $config) { return (new \Kibo\Phast\Filters\CSS\Composite\Factory())->make($config); } } namespace Kibo\Phast\Services\Diagnostics; class Factory { public function make(array $config) { $logRoot = null; foreach ($config['logging']['logWriters'] as $writerConfig) { if ($writerConfig['class'] == \Kibo\Phast\Logging\LogWriters\JSONLFile\Writer::class) { $logRoot = $writerConfig['logRoot']; break; } } return new \Kibo\Phast\Services\Diagnostics\Service($logRoot); } } namespace Kibo\Phast\Services\Diagnostics; class Service { private $logRoot; public function __construct($logRoot) { $this->logRoot = $logRoot; } public function serve(\Kibo\Phast\Services\ServiceRequest $request) { $params = $request->getParams(); if (isset($params['documentRequestId'])) { $items = $this->getRequestLog($params['documentRequestId']); } else { $items = $this->getSystemDiagnostics(); } $response = new \Kibo\Phast\HTTP\Response(); $response->setContent(\Kibo\Phast\Common\JSON::prettyEncode($items)); $response->setHeader('Content-Type', 'application/json'); return $response; } private function getRequestLog($requestId) { return iterator_to_array((new \Kibo\Phast\Logging\LogReaders\JSONLFile\Reader($this->logRoot, $requestId))->readEntries()); } private function getSystemDiagnostics() { return (new \Kibo\Phast\Diagnostics\SystemDiagnostics())->run(require PHAST_CONFIG_FILE); } } namespace Kibo\Phast\Services\Scripts; class Factory { use \Kibo\Phast\Services\ServiceFactoryTrait; public function make(array $config) { $cachedComposite = $this->makeFilter($config); return new \Kibo\Phast\Services\Scripts\Service((new \Kibo\Phast\Security\ServiceSignatureFactory())->make($config), $config['scripts']['whitelist'], $this->makeRetriever($config), $this->makeCachingServiceFilter($config, $cachedComposite, 'scripts-minified'), $config); } public function makeRetriever(array $config) { return $this->makeUniversalCachingRetriever($config, 'scripts'); } public function makeFilter(array $config) { $filter = new \Kibo\Phast\Filters\Service\CompositeFilter(); $filter->addFilter(new \Kibo\Phast\Filters\Text\Decode\Filter()); $filter->addFilter(new \Kibo\Phast\Filters\JavaScript\Minification\JSMinifierFilter(@$config['scripts']['removeLicenseHeaders'])); return $filter; } } namespace Kibo\Phast\Services\Images; class Factory { public function make(array $config) { if ($config['images']['api-mode']) { $retriever = new \Kibo\Phast\Retrievers\PostDataRetriever(); } else { $retriever = new \Kibo\Phast\Retrievers\UniversalRetriever(); $retriever->addRetriever(new \Kibo\Phast\Retrievers\LocalRetriever($config['retrieverMap'])); $retriever->addRetriever((new \Kibo\Phast\Retrievers\RemoteRetrieverFactory())->make($config)); } return new \Kibo\Phast\Services\Images\Service((new \Kibo\Phast\Security\ServiceSignatureFactory())->make($config), $config['images']['whitelist'], $retriever, (new \Kibo\Phast\Filters\Image\Composite\Factory($config))->make(), $config); } } namespace Kibo\Phast\Services; abstract class BaseService { use \Kibo\Phast\Logging\LoggingTrait; /** * @var ServiceSignature */ protected $signature; /** * @var string[] */ protected $whitelist = array(); /** * @var Retriever */ protected $retriever; /** * @var ServiceFilter */ protected $filter; /** * @var array */ protected $config; /** * BaseService constructor. * @param ServiceSignature $signature * @param array $whitelist * @param Retriever $retriever * @param ServiceFilter $filter * @param array $config */ public function __construct(\Kibo\Phast\Security\ServiceSignature $signature, array $whitelist, \Kibo\Phast\Retrievers\Retriever $retriever, \Kibo\Phast\Services\ServiceFilter $filter, array $config) { $this->signature = $signature; $this->whitelist = $whitelist; $this->retriever = $retriever; $this->filter = $filter; $this->config = $config; } /** * @param ServiceRequest $request * @return Response */ public function serve(\Kibo\Phast\Services\ServiceRequest $request) { $this->validateRequest($request); $request = $this->getParams($request); $resource = \Kibo\Phast\ValueObjects\Resource::makeWithRetriever(\Kibo\Phast\ValueObjects\URL::fromString(isset($request['src']) ? $request['src'] : ''), $this->retriever); $filtered = $this->filter->apply($resource, $request); return $this->makeResponse($filtered, $request); } /** * @param ServiceRequest $request * @return array */ protected function getParams(\Kibo\Phast\Services\ServiceRequest $request) { return $request->getParams(); } /** * @param Resource $resource * @param array $request * @return Response */ protected function makeResponse(\Kibo\Phast\ValueObjects\Resource $resource, array $request) { $response = new \Kibo\Phast\HTTP\Response(); $response->setContent($resource->getContent()); return $response; } protected function validateRequest(\Kibo\Phast\Services\ServiceRequest $request) { $this->validateIntegrity($request); try { $this->validateToken($request); } catch (\Kibo\Phast\Exceptions\UnauthorizedException $e) { $this->validateWhitelisted($request); } } protected function validateIntegrity(\Kibo\Phast\Services\ServiceRequest $request) { $params = $request->getParams(); if (!isset($params['src'])) { throw new \Kibo\Phast\Exceptions\ItemNotFoundException('No source is set!'); } } protected function validateToken(\Kibo\Phast\Services\ServiceRequest $request) { if (!$request->verify($this->signature)) { throw new \Kibo\Phast\Exceptions\UnauthorizedException('Invalid token in request: ' . $request->serialize(\Kibo\Phast\Services\ServiceRequest::FORMAT_QUERY)); } } protected function validateWhitelisted(\Kibo\Phast\Services\ServiceRequest $request) { $params = $request->getParams(); foreach ($this->whitelist as $pattern) { if (preg_match($pattern, $params['src'])) { return; } } throw new \Kibo\Phast\Exceptions\UnauthorizedException('Not allowed url: ' . $params['src']); } } namespace Kibo\Phast\Services; class ServiceRequest { const FORMAT_QUERY = 1; const FORMAT_PATH = 2; private static $defaultSerializationMode = self::FORMAT_PATH; /** * @var string */ private static $propagatedSwitches = ''; /** * @var Switches */ private static $switches; /** * @var string */ private static $documentRequestId; /** * @var Request */ private $httpRequest; /** * @var URL */ private $url; /** * @var Query */ private $query; /** * @var string */ private $token; public function __construct() { if (!isset(self::$switches)) { self::$switches = new \Kibo\Phast\Environment\Switches(); } $this->query = new \Kibo\Phast\ValueObjects\Query(); } public static function resetRequestState() { self::$defaultSerializationMode = self::FORMAT_PATH; self::$propagatedSwitches = ''; self::$switches = null; self::$documentRequestId = null; } public static function setDefaultSerializationMode($mode) { self::$defaultSerializationMode = $mode; } public static function getDefaultSerializationMode() { return self::$defaultSerializationMode; } public static function fromHTTPRequest(\Kibo\Phast\HTTP\Request $request) { $query = $request->getQuery(); if ($query->get('src')) { $query->set('src', preg_replace('~^hxxp(?=s?://)~', 'http', $query->get('src'))); } $pathInfo = $request->getPathInfo(); if ($pathParams = self::parseBase64PathInfo($pathInfo)) { $query->update($pathParams); } elseif ($pathInfo) { $query->update(self::parsePathInfo($pathInfo)); } $instance = new self(); self::$switches = new \Kibo\Phast\Environment\Switches(); $instance->httpRequest = $request; if ($request->getCookie('phast')) { self::$switches = \Kibo\Phast\Environment\Switches::fromString($request->getCookie('phast')); } if ($token = $query->pop('token')) { $instance->token = $token; } $instance->query = $query; if ($query->get('phast')) { self::$propagatedSwitches = $query->get('phast'); $paramsSwitches = \Kibo\Phast\Environment\Switches::fromString($query->get('phast')); self::$switches = self::$switches->merge($paramsSwitches); } if ($query->get('documentRequestId')) { self::$documentRequestId = $query->get('documentRequestId'); } else { self::$documentRequestId = (string) mt_rand(0, 999999999); } return $instance; } public function hasRequestSwitchesSet() { return !empty(self::$propagatedSwitches); } /** * @return Switches */ public function getSwitches() { return self::$switches; } /** * @return array */ public function getParams() { return $this->query->toAssoc(); } /** * @return Query */ public function getQuery() { return $this->query; } /** * @return Request */ public function getHTTPRequest() { return $this->httpRequest; } /** * @return string */ public function getDocumentRequestId() { return self::$documentRequestId; } /** * @param array $params * @return ServiceRequest */ public function withParams(array $params) { $result = clone $this; $result->query = \Kibo\Phast\ValueObjects\Query::fromAssoc($params); return $result; } /** * @param URL $url * @return ServiceRequest */ public function withUrl(\Kibo\Phast\ValueObjects\URL $url) { $result = clone $this; $result->url = $url; return $result; } /** * @param ServiceSignature $signature * @return ServiceRequest */ public function sign(\Kibo\Phast\Security\ServiceSignature $signature) { $token = $signature->sign($this->getVerificationString()); $result = clone $this; $result->token = $token; return $result; } /** * @param ServiceSignature $signature * @return bool */ public function verify(\Kibo\Phast\Security\ServiceSignature $signature) { return $signature->verify($this->token, $this->getVerificationString()) || $signature->verify($this->token, $this->getVerificationStringWithoutStemSuffix()); } private static function parsePathInfo($string) { $values = new \Kibo\Phast\ValueObjects\Query(); $parts = explode('/', $string); foreach ($parts as $part) { if ($part === '') { continue; } $pair = explode('=', $part); if (isset($pair[1])) { $values->set($pair[0], self::decodeSingleValue($pair[1])); } elseif (preg_match('/^__p__(@[1-9][0-9]*x)?\\./', $pair[0], $match)) { if (!empty($match[1]) && $values->has('src')) { $values->set('src', self::appendStemSuffix($values->get('src'), $match[1])); } break; } else { $values->set('src', self::decodeSingleValue($pair[0])); } } return $values; } private static function decodeSingleValue($value) { return urldecode(str_replace('-', '%', $value)); } private static function appendStemSuffix($src, $suffix) { $url = \Kibo\Phast\ValueObjects\URL::fromString($src); $path = preg_replace_callback('/\\.\\w+$/', function ($match) use($suffix) { return $suffix . $match[0]; }, $url->getPath()); return $url->withPath($path)->toString(); } private static function parseBase64PathInfo($string) { if (!preg_match('~^/([a-z0-9_-]+)\\.q\\.js$~i', $string, $match)) { return null; } return \Kibo\Phast\ValueObjects\Query::fromString(\Kibo\Phast\Common\Base64url::decode($match[1])); } /** * @param callable $paramsFilter * @return string */ private function getVerificationString($paramsFilter = null) { $params = $this->getAllParams(); if ($paramsFilter) { $params = $paramsFilter($params); } ksort($params); return http_build_query($params); } private function getVerificationStringWithoutStemSuffix() { return $this->getVerificationString(function ($params) { if (isset($params['src'])) { $params['src'] = $this->stripStemSuffix($params['src']); } return $params; }); } private function stripStemSuffix($src) { $url = \Kibo\Phast\ValueObjects\URL::fromString($src); $path = preg_replace('/@[1-9][0-9]*x(?=\\.\\w+$)/', '', $url->getPath()); return $url->withPath($path)->toString(); } public function serialize($format = null) { $params = $this->getAllParams(); if ($this->token) { $params['token'] = $this->token; } if (is_null($format)) { $format = self::$defaultSerializationMode; } if ($format == self::FORMAT_PATH) { return $this->serializeToPathFormat($params); } return $this->serializeToQueryFormat($params); } private function getAllParams() { $urlParams = []; if ($this->url) { parse_str($this->url->getQuery(), $urlParams); } $params = array_merge($urlParams, $this->query->toAssoc()); if (!empty(self::$propagatedSwitches)) { $params['phast'] = self::$propagatedSwitches; } if (self::$switches->isOn(\Kibo\Phast\Environment\Switches::SWITCH_DIAGNOSTICS)) { $params['documentRequestId'] = self::$documentRequestId; } return $params; } private function serializeToQueryFormat(array $params) { $encoded = http_build_query($params); if (!isset($this->url)) { return $encoded; } $serialized = preg_replace('~\\?.*~', '', (string) $this->url); if (self::$defaultSerializationMode === self::FORMAT_PATH && !preg_match('~/$~', $serialized)) { $serialized .= '/' . $this->getDummyFilename($params); } return $serialized . '?' . $encoded; } /** @return string */ private function serializeToPathFormat(array $params) { $encodedSrc = null; $values = []; foreach (explode('&', http_build_query($params)) as $element) { list($key, $value) = explode('=', $element, 2); $encodedValue = str_replace(['-', '%'], ['%2D', '-'], $value); if ($key == 'src') { $encodedSrc = $encodedValue; } else { $values[] = $key . '=' . $encodedValue; } } if ($encodedSrc) { array_unshift($values, $encodedSrc); } $params = '/' . join('/', $values) . '/' . $this->getDummyFilename($params); if (isset($this->url)) { return preg_replace(['~\\?.*~', '~/$~'], '', $this->url) . $params; } return $params; } private function getDummyFilename(array $params) { return '__p__.' . $this->getDummyExtension($params); } private function getDummyExtension(array $params) { $default = 'js'; if (empty($params['src'])) { return $default; } $url = \Kibo\Phast\ValueObjects\URL::fromString($params['src']); $ext = strtolower($url->getExtension()); if (preg_match('/^(jpe?g|gif|png|js|css)$/', $ext)) { return $ext; } return $default; } } namespace Kibo\Phast\Security; class ServiceSignature { const AUTO_TOKEN_SIZE = 128; const SIGNATURE_LENGTH = 16; /** * @var Cache */ private $cache; /** * @var array */ private $identities; /** * ServiceSignature constructor. * * @param Cache $cache */ public function __construct(\Kibo\Phast\Cache\Cache $cache) { $this->cache = $cache; } /** * @param string|array $identities */ public function setIdentities($identities) { if (is_string($identities)) { $this->identities = ['' => $identities]; } else { $this->identities = $identities; } } /** * @return string */ public function getCacheSalt() { $identities = $this->getIdentities(); return md5(join('=>', array_merge(array_keys($identities), array_values($identities)))); } public function sign($value) { $identities = $this->getIdentities(); $users = array_keys($identities); list($user, $token) = [array_shift($users), array_shift($identities)]; return $user . substr(md5($token . $value), 0, self::SIGNATURE_LENGTH); } public function verify($signature, $value) { $user = substr($signature, 0, -self::SIGNATURE_LENGTH); $identities = $this->getIdentities(); if (!isset($identities[$user])) { return false; } $token = $identities[$user]; $signer = new self($this->cache); $signer->setIdentities([$user => $token]); return $signature === $signer->sign($value); } public static function generateToken() { $token = ''; for ($i = 0; $i < self::AUTO_TOKEN_SIZE; $i++) { $token .= chr(mt_rand(33, 126)); } return $token; } private function getIdentities() { if (!isset($this->identities)) { $token = $this->cache->get('security-token', function () { return self::generateToken(); }); $this->identities = ['' => $token]; } return $this->identities; } } namespace Kibo\Phast\Security; class ServiceSignatureFactory { public function make(array $config) { $cache = new \Kibo\Phast\Cache\File\Cache($config['cache'], 'signature'); $signature = new \Kibo\Phast\Security\ServiceSignature($cache); if (isset($config['securityToken'])) { $signature->setIdentities($config['securityToken']); } return $signature; } } namespace Kibo\Phast\Exceptions; class LogicException extends \LogicException { } namespace Kibo\Phast\Exceptions; class RuntimeException extends \RuntimeException { } namespace Kibo\Phast\Exceptions; class CachedExceptionException extends \Exception { } namespace Kibo\Phast\Exceptions; class ItemNotFoundException extends \Exception { /** * @var URL */ private $url; public function __construct($message = '', $code = 0, \Throwable $previous = null, \Kibo\Phast\ValueObjects\URL $failed = null) { parent::__construct($message, $code, $previous); $this->url = $failed; } /** * @return URL */ public function getUrl() { return $this->url; } } namespace Kibo\Phast\Exceptions; class UnauthorizedException extends \Exception { } namespace Kibo\Phast\Exceptions; class UndefinedObjectifiedFunction extends \RuntimeException { } namespace JSMin; class UnterminatedCommentException extends \Exception { } namespace JSMin; class UnterminatedRegExpException extends \Exception { } namespace JSMin; /** * JSMin.php - modified PHP implementation of Douglas Crockford's JSMin. * * * $minifiedJs = JSMin::minify($js); * * * This is a modified port of jsmin.c. Improvements: * * Does not choke on some regexp literals containing quote characters. E.g. /'/ * * Spaces are preserved after some add/sub operators, so they are not mistakenly * converted to post-inc/dec. E.g. a + ++b -> a+ ++b * * Preserves multi-line comments that begin with /*! * * PHP 5 or higher is required. * * Permission is hereby granted to use this version of the library under the * same terms as jsmin.c, which has the following license: * * -- * Copyright (c) 2002 Douglas Crockford (www.crockford.com) * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies * of the Software, and to permit persons to whom the Software is furnished to do * so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * The Software shall be used for Good, not Evil. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * -- * * @package JSMin * @author Ryan Grove (PHP port) * @author Steve Clay (modifications + cleanup) * @author Andrea Giammarchi (spaceBeforeRegExp) * @copyright 2002 Douglas Crockford (jsmin.c) * @copyright 2008 Ryan Grove (PHP port) * @license http://opensource.org/licenses/mit-license.php MIT License * @link http://code.google.com/p/jsmin-php/ */ class JSMin { const ACTION_KEEP_A = 1; const ACTION_DELETE_A = 2; const ACTION_DELETE_A_B = 3; protected $a = "\n"; protected $b = ''; protected $input = ''; protected $inputIndex = 0; protected $inputLength = 0; protected $lookAhead = null; protected $output = ''; protected $lastByteOut = ''; protected $keptComment = ''; /** * Minify Javascript. * * @param string $js Javascript to be minified * * @return string */ public static function minify($js) { $jsmin = new \JSMin\JSMin($js); return $jsmin->min(); } /** * @param string $input */ public function __construct($input) { $this->input = $input; } /** * Perform minification, return result * * @return string */ public function min() { if ($this->output !== '') { // min already run return $this->output; } $mbIntEnc = null; if (function_exists('mb_strlen') && (int) ini_get('mbstring.func_overload') & 2) { $mbIntEnc = mb_internal_encoding(); mb_internal_encoding('8bit'); } if (isset($this->input[0]) && $this->input[0] === "\357") { $this->input = substr($this->input, 3); } $this->input = str_replace("\r\n", "\n", $this->input); $this->inputLength = strlen($this->input); $this->action(self::ACTION_DELETE_A_B); while ($this->a !== null) { // determine next command $command = self::ACTION_KEEP_A; // default if ($this->isWhiteSpace($this->a)) { if (($this->lastByteOut === '+' || $this->lastByteOut === '-') && $this->b === $this->lastByteOut) { // Don't delete this space. If we do, the addition/subtraction // could be parsed as a post-increment } elseif (!$this->isAlphaNum($this->b)) { $command = self::ACTION_DELETE_A; } } elseif ($this->isLineTerminator($this->a)) { if ($this->isWhiteSpace($this->b)) { $command = self::ACTION_DELETE_A_B; // in case of mbstring.func_overload & 2, must check for null b, // otherwise mb_strpos will give WARNING } elseif ($this->b === null || false === strpos('{[(+-!~', $this->b) && !$this->isAlphaNum($this->b)) { $command = self::ACTION_DELETE_A; } } elseif (!$this->isAlphaNum($this->a)) { if ($this->isWhiteSpace($this->b) || $this->isLineTerminator($this->b) && false === strpos('}])+-"\'', $this->a)) { $command = self::ACTION_DELETE_A_B; } } $this->action($command); } $this->output = trim($this->output); if ($mbIntEnc !== null) { mb_internal_encoding($mbIntEnc); } return $this->output; } /** * ACTION_KEEP_A = Output A. Copy B to A. Get the next B. * ACTION_DELETE_A = Copy B to A. Get the next B. * ACTION_DELETE_A_B = Get the next B. * * @param int $command * @throws UnterminatedRegExpException|UnterminatedStringException */ protected function action($command) { // make sure we don't compress "a + ++b" to "a+++b", etc. if ($command === self::ACTION_DELETE_A_B && $this->b === ' ' && ($this->a === '+' || $this->a === '-')) { // Note: we're at an addition/substraction operator; the inputIndex // will certainly be a valid index if ($this->input[$this->inputIndex] === $this->a) { // This is "+ +" or "- -". Don't delete the space. $command = self::ACTION_KEEP_A; } } switch ($command) { case self::ACTION_KEEP_A: // 1 $this->output .= $this->a; if ($this->keptComment) { $this->output = rtrim($this->output, "\n"); $this->output .= $this->keptComment; $this->keptComment = ''; } $this->lastByteOut = $this->a; // fallthrough intentional case self::ACTION_DELETE_A: // 2 $this->a = $this->b; if ($this->a === "'" || $this->a === '"' || $this->a === '`') { // string/template literal $delimiter = $this->a; $str = $this->a; // in case needed for exception for (;;) { $this->output .= $this->a; $this->lastByteOut = $this->a; $this->a = $this->get(); if ($this->a === $this->b) { // end quote break; } if ($delimiter === '`' && $this->isLineTerminator($this->a)) { // leave the newline } elseif ($this->isEOF($this->a)) { $byte = $this->inputIndex - 1; throw new \JSMin\UnterminatedStringException("JSMin: Unterminated String at byte {$byte}: {$str}"); } $str .= $this->a; if ($this->a === '\\') { $this->output .= $this->a; $this->lastByteOut = $this->a; $this->a = $this->get(); $str .= $this->a; } } } // fallthrough intentional case self::ACTION_DELETE_A_B: // 3 $this->b = $this->next(); if ($this->b === '/' && $this->isRegexpLiteral()) { $this->output .= $this->a . $this->b; $pattern = '/'; // keep entire pattern in case we need to report it in the exception for (;;) { $this->a = $this->get(); $pattern .= $this->a; if ($this->a === '[') { for (;;) { $this->output .= $this->a; $this->a = $this->get(); $pattern .= $this->a; if ($this->a === ']') { break; } if ($this->a === '\\') { $this->output .= $this->a; $this->a = $this->get(); $pattern .= $this->a; } if ($this->isEOF($this->a)) { throw new \JSMin\UnterminatedRegExpException("JSMin: Unterminated set in RegExp at byte " . $this->inputIndex . ": {$pattern}"); } } } if ($this->a === '/') { // end pattern break; // while (true) } elseif ($this->a === '\\') { $this->output .= $this->a; $this->a = $this->get(); $pattern .= $this->a; } elseif ($this->isEOF($this->a)) { $byte = $this->inputIndex - 1; throw new \JSMin\UnterminatedRegExpException("JSMin: Unterminated RegExp at byte {$byte}: {$pattern}"); } $this->output .= $this->a; $this->lastByteOut = $this->a; } $this->b = $this->next(); } } } /** * @return bool */ protected function isRegexpLiteral() { if (false !== strpos("(,=:[!&|?+-~*{;", $this->a)) { // we can't divide after these tokens return true; } // check if first non-ws token is "/" (see starts-regex.js) $length = strlen($this->output); if ($this->isWhiteSpace($this->a) || $this->isLineTerminator($this->a)) { if ($length < 2) { // weird edge case return true; } } // if the "/" follows a keyword, it must be a regexp, otherwise it's best to assume division $subject = $this->output . trim($this->a); if (!preg_match('/(?:case|else|in|return|typeof)$/', $subject, $m)) { // not a keyword return false; } // can't be sure it's a keyword yet (see not-regexp.js) $charBeforeKeyword = substr($subject, 0 - strlen($m[0]) - 1, 1); if ($this->isAlphaNum($charBeforeKeyword)) { // this is really an identifier ending in a keyword, e.g. "xreturn" return false; } // it's a regexp. Remove unneeded whitespace after keyword if ($this->isWhiteSpace($this->a) || $this->isLineTerminator($this->a)) { $this->a = ''; } return true; } /** * Return the next character from stdin. Watch out for lookahead. If the character is a control character, * translate it to a space or linefeed. * * @return string */ protected function get() { $c = $this->lookAhead; $this->lookAhead = null; if ($c === null) { // getc(stdin) if ($this->inputIndex < $this->inputLength) { $c = $this->input[$this->inputIndex]; $this->inputIndex += 1; } else { $c = null; } } if ($c === "\r") { return "\n"; } return $c; } /** * Does $a indicate end of input? * * @param string $a * @return bool */ protected function isEOF($a) { return $a === null || $this->isLineTerminator($a); } /** * Get next char (without getting it). If is ctrl character, translate to a space or newline. * * @return string */ protected function peek() { $this->lookAhead = $this->get(); return $this->lookAhead; } /** * Return true if the character is a letter, digit, underscore, dollar sign, or non-ASCII character. * * @param string $c * * @return bool */ protected function isAlphaNum($c) { return preg_match('/^[a-z0-9A-Z_\\$\\\\]$/', $c) || ord($c) > 126; } /** * Consume a single line comment from input (possibly retaining it) */ protected function consumeSingleLineComment() { $comment = ''; while (true) { $get = $this->get(); $comment .= $get; if ($this->isEOF($get)) { // if IE conditional comment if (preg_match('/^\\/@(?:cc_on|if|elif|else|end)\\b/', $comment)) { $this->keptComment .= "/{$comment}"; } return; } } } /** * Consume a multiple line comment from input (possibly retaining it) * * @throws UnterminatedCommentException */ protected function consumeMultipleLineComment() { $this->get(); $comment = ''; for (;;) { $get = $this->get(); if ($get === '*') { if ($this->peek() === '/') { // end of comment reached $this->get(); if (0 === strpos($comment, '!')) { // preserved by YUI Compressor if (!$this->keptComment) { // don't prepend a newline if two comments right after one another $this->keptComment = "\n"; } $this->keptComment .= "/*!" . substr($comment, 1) . "*/\n"; } else { if (preg_match('/^@(?:cc_on|if|elif|else|end)\\b/', $comment)) { // IE conditional $this->keptComment .= "/*{$comment}*/"; } } return; } } elseif ($get === null) { throw new \JSMin\UnterminatedCommentException("JSMin: Unterminated comment at byte {$this->inputIndex}: /*{$comment}"); } $comment .= $get; } } /** * Get the next character, skipping over comments. Some comments may be preserved. * * @return string */ protected function next() { $get = $this->get(); if ($get === '/') { switch ($this->peek()) { case '/': $this->consumeSingleLineComment(); $get = "\n"; break; case '*': $this->consumeMultipleLineComment(); $get = ' '; break; } } return $get; } protected function isWhiteSpace($s) { // https://www.ecma-international.org/ecma-262/#sec-white-space return $s !== null && strpos(" \t\v\f", $s) !== false; } protected function isLineTerminator($s) { // https://www.ecma-international.org/ecma-262/#sec-line-terminators return $s !== null && strpos("\n\r", $s) !== false; } } namespace JSMin; class UnterminatedStringException extends \Exception { } namespace Kibo\PhastPlugins\SDK; /** * Provides commonly needed URLs * * Interface HostURLs * @see URL */ interface HostURLs { /** * The URL at which static resource (JS, CSS, IMG) * optimizations reside * * @return URL */ public function getServicesURL(); /** * The full URL of the root of the current site * * @return URL */ public function getSiteURL(); /** * The CDN equivalent of a specified URL * * @return URL */ public function getCDNURL(\Kibo\Phast\ValueObjects\URL $url); /** * URL of the admin page at which * the plugin's settings are located * * @return URL */ public function getSettingsURL(); /** * URL for admin panel AJAX communication * * @return URL */ public function getAJAXEndPoint(); /** * A URL to a publicly available image. * * @return URL */ public function getTestImageURL(); } namespace Kibo\PhastPlugins\SDK; /** * Services container for the Phast Plugins Services SDK * * Class SDK */ class ServiceSDK { /** * @var ServiceHost */ protected $host; /** * @var EnvironmentIdentifier */ private $environmentIdentifier; public function __construct(\Kibo\PhastPlugins\SDK\ServiceHost $host) { $this->host = $host; $this->environmentIdentifier = new \Kibo\PhastPlugins\SDK\Configuration\EnvironmentIdentifier(); } public function getServiceAPI() { return new \Kibo\PhastPlugins\SDK\APIs\Service($this->getServiceConfiguration()); } public function getServiceConfiguration() { return new \Kibo\PhastPlugins\SDK\Configuration\ServiceConfiguration($this->getPHPFilesServiceConfigurationRepository(), $this->getEnvironmentIdentifier(), $this->getCacheRootManager(), [$this->host, 'onServiceConfigurationLoad']); } public function getCacheRootManager() { return new \Kibo\PhastPlugins\SDK\Caching\CacheRootManager($this->host->getCacheRootCandidatesProvider()); } /** * Returns a default implementation of the * ServiceConfigurationRepository interface * * @see ServiceConfigurationRepository * @return PHPFilesServiceConfigurationRepository */ public function getPHPFilesServiceConfigurationRepository() { return new \Kibo\PhastPlugins\SDK\Configuration\PHPFilesServiceConfigurationRepository($this->getCacheRootManager()); } public function getEnvironmentIdentifier() { return $this->environmentIdentifier; } } namespace Kibo\PhastPlugins\SDK; /** * Services container for the Phast Plugins SDK * * Class SDK */ class SDK extends \Kibo\PhastPlugins\SDK\ServiceSDK { /** * @var PluginHost */ protected $host; /** * SDK constructor. * @param PluginHost $host */ public function __construct(\Kibo\PhastPlugins\SDK\PluginHost $host) { parent::__construct($host); } /** * The current SDK version * * @return string */ public function getSDKVersion() { return '8'; } /** * The current plugin version. * Composed from the host plugin name, * the host plugin version * and the SDK version * * @return string */ public function getPluginVersion() { return join('-', [$this->host->getPluginHostName(), $this->host->getPluginHostVersion(), $this->getSDKVersion()]); } /** * @return Phast */ public function getPhastAPI() { return new \Kibo\PhastPlugins\SDK\APIs\Phast($this->getPhastConfiguration()); } public function getAdminPanel() { return new \Kibo\PhastPlugins\SDK\AdminPanel\AdminPanel($this->host->getPhastUser(), $this->host->getHostURLs()->getAJAXEndPoint(), $this->getAdminPanelData(), $this->getTranslationsManager(), $this->host->isDev()); } public function getAJAXRequestsDispatcher() { return new \Kibo\PhastPlugins\SDK\AJAX\RequestsDispatcher($this->host->getPhastUser(), $this); } public function getAdminPanelData() { return new \Kibo\PhastPlugins\SDK\AdminPanel\AdminPanelData($this->getPluginConfiguration(), $this->getServiceConfigurationGenerator(), $this->getPhastConfiguration(), $this->getCacheRootManager(), $this->host); } public function getInstallNotice() { return new \Kibo\PhastPlugins\SDK\AdminPanel\InstallNotice($this->getPluginConfiguration(), $this->host->getInstallNoticeRenderer(), $this->getTranslationsManager(), $this->host->getHostURLs()->getSettingsURL(), $this->host->getHostURLs()->getAJAXEndPoint()); } public function getPluginConfiguration() { return new \Kibo\PhastPlugins\SDK\Configuration\PluginConfiguration($this->getPluginConfigurationRepository(), $this->getServiceConfigurationGenerator(), $this->getCacheRootManager(), $this->host->getPhastUser(), $this->host->getNonceChecker()); } public function getPhastConfiguration() { return new \Kibo\PhastPlugins\SDK\Configuration\PhastConfiguration($this->getServiceConfigurationGenerator(), $this->getServiceConfiguration(), $this->getPluginConfiguration(), [$this->host, 'onPhastConfigurationLoad']); } public function getAutoConfiguration() { return new \Kibo\PhastPlugins\SDK\Configuration\AutoConfiguration($this->getPluginConfiguration(), $this->getPhastConfiguration(), $this->host->getHostURLs()->getServicesURL(), $this->host->getHostURLs()->getTestImageURL(), $this->host->getNonce(), $this->host->getHostURLs()->getAJAXEndPoint()); } public function getTranslationsManager() { return new \Kibo\PhastPlugins\SDK\AdminPanel\TranslationsManager($this->host->getLocale(), $this->host->getPluginName()); } private function getServiceConfigurationGenerator() { return new \Kibo\PhastPlugins\SDK\Configuration\ServiceConfigurationGenerator($this->getPHPFilesServiceConfigurationRepository(), $this->getEnvironmentIdentifier(), $this->getPluginVersion(), $this->host->getHostURLs()); } public function updatePreviewCookie($enable = true) { if (headers_sent()) { return false; } $enabled = isset($_COOKIE['PHAST_PREVIEW']) && (bool) $_COOKIE['PHAST_PREVIEW']; if (!$enabled && $enable) { return setcookie('PHAST_PREVIEW', '1', 0, '/'); } if ($enabled && !$enable) { return setcookie('PHAST_PREVIEW', '0', 0, '/'); } return true; } private function getPluginConfigurationRepository() { return new \Kibo\PhastPlugins\SDK\Configuration\PluginConfigurationRepository($this->host->getKeyValueStore()); } } namespace Kibo\PhastPlugins\SDK\AdminPanel; /** * Represents the data that needs to be send * to the plugin's admin panel * * Class AdminPanelData */ class AdminPanelData { /** * @var PluginConfiguration */ private $pluginConfig; /** * @var ServiceConfigurationGenerator */ private $serviceConfigGenerator; /** * @var PhastConfiguration */ private $phastConfig; /** * @var CacheRootManager */ private $cacheRootManager; /** * @var PluginHost */ private $host; public function __construct(\Kibo\PhastPlugins\SDK\Configuration\PluginConfiguration $pluginConfig, \Kibo\PhastPlugins\SDK\Configuration\ServiceConfigurationGenerator $serviceConfigGenerator, \Kibo\PhastPlugins\SDK\Configuration\PhastConfiguration $phastConfig, \Kibo\PhastPlugins\SDK\Caching\CacheRootManager $cacheRootManager, \Kibo\PhastPlugins\SDK\PluginHost $host) { $this->pluginConfig = $pluginConfig; $this->serviceConfigGenerator = $serviceConfigGenerator; $this->phastConfig = $phastConfig; $this->cacheRootManager = $cacheRootManager; $this->host = $host; } public function get() { $siteUrl = $this->host->getHostURLs()->getSiteURL(); $urlWithPhast = $this->addQueryParam($siteUrl, 'phast', 'phast'); $urlWithoutPhast = $this->addQueryParam($siteUrl, 'phast', '-phast'); $pageSpeedToolUrl = 'https://developers.google.com/speed/pagespeed/insights/?url='; $errors = []; if (!$this->cacheRootManager->hasCacheRoot()) { $errors[] = ['type' => 'no-cache-root', 'params' => $this->cacheRootManager->getCacheRootCandidates()]; } if (!$this->serviceConfigGenerator->generateIfNotExists($this->pluginConfig)) { $errors[] = ['type' => 'no-service-config', 'params' => $this->cacheRootManager->getCacheRootCandidates()]; } $warnings = []; $api_client_warning = []; $phast_config = $this->phastConfig->get(); $diagnostics = new \Kibo\Phast\Diagnostics\SystemDiagnostics(); foreach ($diagnostics->run($phast_config) as $status) { if ($status->isAvailable()) { continue; } $package = $status->getPackage(); $type = $package->getType(); if ($type == 'Cache') { $errors[] = ['type' => 'cache', 'params' => [$status->getReason()]]; } elseif ($type == 'ImageFilter') { $name = substr($package->getNamespace(), strrpos($package->getNamespace(), '\\') + 1); if ($name === 'ImageAPIClient') { $api_client_warning[] = 'Image optimization API error: ' . $status->getReason(); } else { $warnings[] = $status->getReason(); } } } $phastpress_config = $this->pluginConfig->get(); if ($phastpress_config['img-optimization-api']) { $warnings = $api_client_warning; } $nonce = $this->host->getNonce(); return ['config' => $phastpress_config, 'settingsStrings' => ['urlWithPhast' => $pageSpeedToolUrl . rawurlencode($urlWithPhast), 'urlWithoutPhast' => $pageSpeedToolUrl . rawurlencode($urlWithoutPhast), 'maxImageWidth' => 1920 * 2, 'maxImageHeight' => 1080 * 2], 'errors' => $errors, 'warnings' => $warnings, 'nonce' => $nonce->getValue(), 'nonceName' => $nonce->getFieldName(), 'pluginName' => $this->host->getPluginName(), 'pluginVersion' => $this->host->getPluginHostVersion()]; } private function addQueryParam(\Kibo\Phast\ValueObjects\URL $url, $key, $value) { // TODO: Move this functionality to URL class $urlStr = (string) $url; $glue = strpos($urlStr, '?') === false ? '?' : '&'; return $urlStr . $glue . $key . '=' . $value; } } namespace Kibo\PhastPlugins\SDK\AdminPanel; /** * Represents an installation notice * displayed everywhere in the host system's admin panel * upon plugin activation. * * Class InstallNotice */ class InstallNotice { /** * @var PluginConfiguration */ private $config; /** * @var InstallNoticeRenderer */ private $renderer; /** * @var TranslationsManager */ private $translations; /** * @var URL */ private $settingsUrl; /** * @var URL */ private $ajaxEntryPoint; /** * InstallNotice constructor. * @param PluginConfiguration $config * @param InstallNoticeRenderer $renderer * @param TranslationsManager $translations * @param URL $settingsUrl * @param URL $ajaxEndPoint */ public function __construct(\Kibo\PhastPlugins\SDK\Configuration\PluginConfiguration $config, \Kibo\PhastPlugins\SDK\AdminPanel\InstallNoticeRenderer $renderer, \Kibo\PhastPlugins\SDK\AdminPanel\TranslationsManager $translations, \Kibo\Phast\ValueObjects\URL $settingsUrl, \Kibo\Phast\ValueObjects\URL $ajaxEndPoint) { $this->config = $config; $this->renderer = $renderer; $this->translations = $translations; $this->settingsUrl = $settingsUrl; $this->ajaxEntryPoint = $ajaxEndPoint; } /** * @return string The HTML to render the notice */ public function render() { $display_message = $this->config->shouldShowActivationNotification(); if (!$display_message) { return ''; } $config = $this->config->get(); if ($config['enabled'] && $config['admin-only']) { $status = 'Backend.status.admin'; } elseif ($config['enabled']) { $status = 'Backend.status.on'; } else { $status = 'Backend.status.off'; } $message = $this->translations->get('Backend.install-notice', ['pluginState' => $this->translations->get($status), 'settingsUrl' => (string) $this->settingsUrl]); $onCloseFunction = "\n function () {\n var data = new FormData();\n data.append('phast-plugins-action', 'dismiss-notice')\n var xhr = new XMLHttpRequest();\n xhr.open('POST', '{$this->ajaxEntryPoint}')\n xhr.send(data)\n }\n "; return $this->renderer->render($message, $onCloseFunction); } } namespace Kibo\PhastPlugins\SDK\AdminPanel; interface InstallNoticeRenderer { /** * Renders a system notice. * * @param string $notice The message to show in the notice * @param string $onCloseJSFunction JavaScript function to call on the client when * an event that closes the notice occurs. * @return string HTML for the notice * @see InstallNotice */ public function render($notice, $onCloseJSFunction); } namespace Kibo\PhastPlugins\SDK\AdminPanel; /** * Represents a nonce form field used XSS defense * * Class Nonce */ class Nonce implements \JsonSerializable { /** * @var string */ private $fieldName; /** * @var string */ private $value; private function __construct() { } /** * @param string $fieldName The name of the field in the form * @param string $value The value of the field * @return Nonce */ public static function make($fieldName, $value) { $instance = new self(); $instance->fieldName = $fieldName; $instance->value = $value; return $instance; } /** * @return string */ public function getFieldName() { return $this->fieldName; } /** * @return string */ public function getValue() { return $this->value; } public function jsonSerialize() { return ['fieldName' => $this->fieldName, 'value' => $this->value]; } } namespace Kibo\PhastPlugins\SDK\AdminPanel; /** * Represents the admin panel used for plugin configuration. * Use this class for rendering the admin panel of the plugin. * * Class AdminPanel */ class AdminPanel { /** * @var PhastUser */ private $user; /** * @var URL */ private $ajaxEndPoint; /** * @var AdminPanelData */ private $data; /** * @var TranslationsManager */ private $translations; private $isDev = 'prod'; private $styles = array('prod' => array('app.css'), 'dev' => array()); private $scripts = array('prod' => array('manifest.js', 'vendor.js', 'babel-polyfill.js', 'app.js'), 'dev' => array('http://localhost:25903/babel-polyfill.js', 'http://localhost:25903/app.js')); /** * AdminPanel constructor. * @param PhastUser $user * @param URL $ajaxEndPoint * @param AdminPanelData $data * @param TranslationsManager $translations * @param bool $isDev */ public function __construct(\Kibo\PhastPlugins\SDK\Security\PhastUser $user, \Kibo\Phast\ValueObjects\URL $ajaxEndPoint, \Kibo\PhastPlugins\SDK\AdminPanel\AdminPanelData $data, \Kibo\PhastPlugins\SDK\AdminPanel\TranslationsManager $translations, $isDev) { $this->user = $user; $this->ajaxEndPoint = $ajaxEndPoint; $this->data = $data; $this->translations = $translations; $this->isDev = (bool) $isDev; } /** * Returns the HTML needed to display the admin panel * * @return string */ public function render() { if (!$this->user->mayModifySettings()) { return ''; } $id = 'phast-plugins-sdk-admin-panel'; $template = $this->getResourcesString(); $template .= sprintf('
', $id, json_encode([$id, $this->ajaxEndPoint->toString(), $this->data->get(), $this->translations->getAll()])); return $template; } private function getResourcesString() { return $this->isDev ? $this->getDevResourcesString() : $this->getProdResources(); } private function getProdResources() { $resources = ''; $base = __DIR__ . '/static/'; $cssBase = $base . 'css/'; foreach ($this->styles['prod'] as $style) { $resources .= ''; } $jsBase = $base . 'js/'; foreach ($this->scripts['prod'] as $script) { $resources .= ''; } return $resources; } private function getDevResourcesString() { $resources = ''; foreach ($this->styles['dev'] as $href) { $resources .= ""; } foreach ($this->scripts['dev'] as $src) { $resources .= ""; } return $resources; } } namespace Kibo\PhastPlugins\SDK\AdminPanel; class TranslationsManager { const DEFAULT_LOCALE = 'en'; /** * @var string */ private $locale; /** * @var string */ private $pluginName; /** * @var string */ private $languagesDir; /** * @var array */ private $modules = array(); public function __construct($locale, $pluginName) { $data = \Kibo\PhastPlugins\SDK\Generated\Translations::DATA; if (isset($data[$this->locale])) { $this->locale = $locale; } else { $this->locale = self::DEFAULT_LOCALE; } $this->pluginName = $pluginName; } public function getAll() { return array_merge(['plugin-name' => $this->pluginName], \Kibo\PhastPlugins\SDK\Generated\Translations::DATA[$this->locale]); } public function get($key, $interpolationArguments = array()) { $keyParts = explode('.', $key); $transArr = \Kibo\PhastPlugins\SDK\Generated\Translations::DATA[$this->locale]; while (count($keyParts) > 0) { $part = array_shift($keyParts); if (!isset($transArr[$part])) { return $key; } $transArr = $transArr[$part]; } if (is_string($transArr)) { return $this->interpolate($transArr, $interpolationArguments); } return $key; } private function interpolate($string, $arguments) { $keys = array_map(function ($str) { return '{' . $str . '}'; }, array_keys($arguments)); $params = array_combine($keys, array_values($arguments)); $params['@:plugin-name'] = $this->pluginName; return strtr($string, $params); } } namespace Kibo\PhastPlugins\SDK\Configuration; /** * Represents the javascript used * for auto-configuration done * immediately after activation of the plugin. * * Class AutoConfiguration */ class AutoConfiguration { /** * @var PluginConfiguration */ private $pluginConfig; /** * @var PhastConfiguration */ private $phastConfig; /** * @var URL */ private $servicesUrl; /** * @var URL */ private $testImageUrl; /** * @var Nonce */ private $nonce; /** * @var URL */ private $ajaxEndPoint; /** * AutoConfiguration constructor. * @param PluginConfiguration $pluginConfig * @param PhastConfiguration $phastConfig * @param URL $servicesUrl * @param URL $testImageUrl * @param Nonce $nonce * @param URL $ajaxEndPoint */ public function __construct(\Kibo\PhastPlugins\SDK\Configuration\PluginConfiguration $pluginConfig, \Kibo\PhastPlugins\SDK\Configuration\PhastConfiguration $phastConfig, \Kibo\Phast\ValueObjects\URL $servicesUrl, \Kibo\Phast\ValueObjects\URL $testImageUrl, \Kibo\PhastPlugins\SDK\AdminPanel\Nonce $nonce, \Kibo\Phast\ValueObjects\URL $ajaxEndPoint) { $this->pluginConfig = $pluginConfig; $this->phastConfig = $phastConfig; $this->servicesUrl = $servicesUrl; $this->testImageUrl = $testImageUrl; $this->nonce = $nonce; $this->ajaxEndPoint = $ajaxEndPoint; } /** * Returns the script that needs to rendered * in order for the script to get executed. * * @return string */ public function renderScript() { if (!$this->pluginConfig->shouldAutoConfigure()) { return ''; } $config = \Kibo\Phast\Environment\Configuration::fromDefaults()->withUserConfiguration(new \Kibo\Phast\Environment\Configuration($this->phastConfig->get()))->getRuntimeConfig()->toArray(); $signature = (new \Kibo\Phast\Security\ServiceSignatureFactory())->make($config); $service_image_url = (new \Kibo\Phast\Services\ServiceRequest())->withUrl($this->servicesUrl)->withParams(['service' => 'images', 'src' => (string) $this->testImageUrl])->sign($signature)->serialize(\Kibo\Phast\Services\ServiceRequest::FORMAT_PATH); $nonce = json_encode($this->nonce); return '"; } } namespace Kibo\PhastPlugins\SDK\Configuration; class ServiceConfigurationGenerator { /** * @var ServiceConfigurationRepository */ private $repository; /** * @var EnvironmentIdentifier */ private $environmentIdentifier; /** * @var URL */ private $servicesUrl; /** * @var URL */ private $cdnServicesUrl; /** * @var string */ private $pluginVersion; /** * @var string */ private $cdnHost; public function __construct(\Kibo\PhastPlugins\SDK\Configuration\ServiceConfigurationRepository $repository, \Kibo\PhastPlugins\SDK\Configuration\EnvironmentIdentifier $environmentIdentifier, $pluginVersion, \Kibo\PhastPlugins\SDK\HostURLs $hostUrls) { $this->repository = $repository; $this->environmentIdentifier = $environmentIdentifier; $this->pluginVersion = $pluginVersion; $this->servicesUrl = $hostUrls->getServicesURL(); $this->cdnServicesUrl = $hostUrls->getCDNURL($hostUrls->getServicesURL()); $this->cdnHost = $hostUrls->getCDNURL($hostUrls->getSiteURL())->getHost(); } public function generateIfNotExists(\Kibo\PhastPlugins\SDK\Configuration\PluginConfiguration $pluginConfig) { if (!$this->repository->has()) { return $this->generate($pluginConfig); } $config = $this->repository->get(); $envId = $this->environmentIdentifier->getValue(); if (empty($config['plugin_version']) || $config['plugin_version'] != $this->pluginVersion || empty($config['alternativeServicesUrls'][$envId]) || $config['alternativeServicesUrls'][$envId] != $this->getServicesURLString($pluginConfig)) { return $this->generate($pluginConfig); } return true; } public function generate(\Kibo\PhastPlugins\SDK\Configuration\PluginConfiguration $pluginConfig) { $previousConfig = $this->repository->get(); $plugin_config = $pluginConfig->get(); $plugin_version = $this->pluginVersion; $config = ['plugin_version' => $plugin_version, 'servicesUrl' => $this->getServicesURLString($pluginConfig), 'securityToken' => empty($previousConfig['securityToken']) ? \Kibo\Phast\Security\ServiceSignature::generateToken() : $previousConfig['securityToken'], 'images' => ['filters' => [\Kibo\Phast\Filters\Image\ImageAPIClient\Filter::class => ['enabled' => $plugin_config['img-optimization-api'], 'plugin-version' => $plugin_version]]], 'styles' => ['filters' => [\Kibo\Phast\Filters\CSS\ImageURLRewriter\Filter::class => ['enabled' => $plugin_config['img-optimization-css']]]], 'serviceRequestFormat' => $plugin_config['pathinfo-query-format'] ? \Kibo\Phast\Services\ServiceRequest::FORMAT_PATH : \Kibo\Phast\Services\ServiceRequest::FORMAT_QUERY, 'compressServiceResponse' => isset($plugin_config['compress-service-response']) ? !!$plugin_config['compress-service-response'] : true, 'cdnHost' => $this->cdnHost]; if (isset($previousConfig['alternativeServicesUrls'])) { $config['alternativeServicesUrls'] = $previousConfig['alternativeServicesUrls']; } else { $config['alternativeServicesUrls'] = []; } $id = $this->environmentIdentifier->getValue(); unset($config['alternativeServicesUrls'][$id]); $config['alternativeServicesUrls'][$id] = $this->getServicesURLString($pluginConfig); $config['alternativeServicesUrls'] = array_slice($config['alternativeServicesUrls'], -1000); return $this->repository->store($config); } private function getServicesURLString(\Kibo\PhastPlugins\SDK\Configuration\PluginConfiguration $pluginConfig) { $plugin_config = $pluginConfig->get(); if ($plugin_config['pathinfo-query-format']) { return (string) $this->cdnServicesUrl; } return (string) $this->servicesUrl; } } namespace Kibo\PhastPlugins\SDK\Configuration; /** * Represents the configuration * that needs to be passed to be returned * by the callback passed to * \Kibo\Phast\PhastServices::serve() * * @see \Kibo\Phast\PhastServices::serve() * Class ServiceConfiguration */ class ServiceConfiguration { /** * @var ServiceConfigurationRepository */ private $repository; /** * @var EnvironmentIdentifier */ private $environmentIdentifier; /** * @var CacheRootManager */ private $cacheRootManager; /** * @var callable */ private $onLoadCb; /** * ServiceConfiguration constructor. * @param ServiceConfigurationRepository $repository * @param CacheRootManager $cacheRootManager * @param callable $onLoadCb */ public function __construct(\Kibo\PhastPlugins\SDK\Configuration\ServiceConfigurationRepository $repository, \Kibo\PhastPlugins\SDK\Configuration\EnvironmentIdentifier $environmentIdentifier, \Kibo\PhastPlugins\SDK\Caching\CacheRootManager $cacheRootManager, callable $onLoadCb) { $this->repository = $repository; $this->environmentIdentifier = $environmentIdentifier; $this->cacheRootManager = $cacheRootManager; $this->onLoadCb = $onLoadCb; } /** * Returns the configuration as config * * @return array|bool|mixed */ public function get() { $config = $this->repository->get(); $envId = $this->environmentIdentifier->getValue(); if (isset($config['alternativeServicesUrls'][$envId])) { $config['servicesUrl'] = $config['alternativeServicesUrls'][$envId]; } if (!empty($config['cdnHost'])) { $config['retrieverMap'][$config['cdnHost']] = \Kibo\Phast\HTTP\Request::fromGlobals()->getDocumentRoot(); } $config['cache'] = ['cacheRoot' => $this->cacheRootManager->getCacheRoot()]; $apiFilterName = \Kibo\Phast\Filters\Image\ImageAPIClient\Filter::class; $api_enabled = $config['images']['filters'][$apiFilterName]['enabled']; if (!$api_enabled) { unset($config['images']['filters'][$apiFilterName]); return call_user_func($this->onLoadCb, $config); } $config['images']['filters'][$apiFilterName]['host-name'] = $_SERVER['HTTP_HOST']; $config['images']['filters'][$apiFilterName]['request-uri'] = $_SERVER['REQUEST_URI']; $config['images']['filters'][$apiFilterName]['api-url'] = 'https://optimize.phast.io/?service=images'; return call_user_func($this->onLoadCb, $config); } } namespace Kibo\PhastPlugins\SDK\Configuration; class EnvironmentIdentifier { private $value; public function __construct() { $this->value = sprintf('%s://%s%s', empty($_SERVER['HTTPS']) ? 'http' : 'https', empty($_SERVER['HTTP_HOST']) ? '' : $_SERVER['HTTP_HOST'], empty($_SERVER['SERVER_PORT']) ? '' : ":{$_SERVER['SERVER_PORT']}"); } public function getValue() { return $this->value; } } namespace Kibo\PhastPlugins\SDK\Configuration; /** * Represents the configuration * that needs to be passed to * \Kibo\Phast\PhastDocumentFilters::deploy() * and * \Kibo\Phast\PhastDocumentFilters::apply() * * @see \Kibo\Phast\PhastDocumentFilters * Class PhastConfiguration */ class PhastConfiguration { const SETTINGS_2_FILTERS = array('img-optimization-tags' => array(\Kibo\Phast\Filters\HTML\ImagesOptimizationService\Tags\Filter::class), 'img-optimization-css' => array(\Kibo\Phast\Filters\HTML\ImagesOptimizationService\CSS\Filter::class, \Kibo\Phast\Filters\CSS\ImageURLRewriter\Filter::class), 'img-lazy' => array(\Kibo\Phast\Filters\HTML\LazyImageLoading\Filter::class), 'css-optimization' => array(\Kibo\Phast\Filters\HTML\CSSInlining\Filter::class), 'scripts-defer' => array(\Kibo\Phast\Filters\HTML\ScriptsDeferring\Filter::class), 'scripts-proxy' => array(\Kibo\Phast\Filters\HTML\ScriptsProxyService\Filter::class), 'iframe-defer' => array(\Kibo\Phast\Filters\HTML\DelayedIFrameLoading\Filter::class), 'minify-html' => array(\Kibo\Phast\Filters\HTML\Minify\Filter::class), 'minify-inline-scripts' => array(\Kibo\Phast\Filters\HTML\MinifyScripts\Filter::class)); /** * @var ServiceConfigurationGenerator */ private $serviceConfigGenerator; /** * @var ServiceConfiguration */ private $serviceConfig; /** * @var PluginConfiguration */ private $pluginConfig; /** * @var callable */ private $onLoadCb; /** * PhastConfiguration constructor. * @param ServiceConfigurationGenerator $serviceConfigGenerator * @param ServiceConfiguration $serviceConfig * @param PluginConfiguration $pluginConfig * @param callable $onLoadCb */ public function __construct(\Kibo\PhastPlugins\SDK\Configuration\ServiceConfigurationGenerator $serviceConfigGenerator, \Kibo\PhastPlugins\SDK\Configuration\ServiceConfiguration $serviceConfig, \Kibo\PhastPlugins\SDK\Configuration\PluginConfiguration $pluginConfig, callable $onLoadCb) { $this->serviceConfigGenerator = $serviceConfigGenerator; $this->serviceConfig = $serviceConfig; $this->pluginConfig = $pluginConfig; $this->onLoadCb = $onLoadCb; } /** * Returns the configuration to use on full html documents as an array * * @return array|bool|mixed */ public function getForDocuments() { list($phastConfig, $pluginConfig) = $this->getPhastAndPluginConfigs(); foreach (array_keys(self::SETTINGS_2_FILTERS) as $setting) { $this->setSettingInPhastConfig($setting, $pluginConfig, $phastConfig); } return call_user_func($this->onLoadCb, $phastConfig); } public function getForHTMLSnippets() { list($phastConfig, $pluginConfig) = $this->getPhastAndPluginConfigs(); $defaultConfig = \Kibo\Phast\Environment\Configuration::fromDefaults()->toArray(); $allFilters = array_keys($defaultConfig['documents']['filters']); foreach ($allFilters as $filter) { $phastConfig['documents']['filters'][$filter]['enabled'] = false; } $this->setSettingInPhastConfig('img-optimization-tags', $pluginConfig, $phastConfig); $this->setSettingInPhastConfig('img-optimization-css', $pluginConfig, $phastConfig); $this->setSettingInPhastConfig('img-lazy', $pluginConfig, $phastConfig); $phastConfig['optimizeHTMLDocumentsOnly'] = false; $phastConfig['outputServerSideStats'] = false; return call_user_func($this->onLoadCb, $phastConfig); } private function getPhastAndPluginConfigs() { // TODO: Optimize so we do not read from the service config file a bunch of times $this->serviceConfigGenerator->generateIfNotExists($this->pluginConfig); $pluginConfig = $this->pluginConfig->get(); $phastConfig = $this->serviceConfig->get(); $phastConfig['documents']['filters'] = []; $phastConfig['switches']['phast'] = $phastConfig && $this->pluginConfig->shouldDeployFilters(); return [$phastConfig, $pluginConfig]; } private function setSettingInPhastConfig($settingName, $pluginConfig, &$phastConfig) { foreach (self::SETTINGS_2_FILTERS[$settingName] as $filterClass) { if (!class_exists($filterClass)) { throw new \LogicException("No such filter: {$filterClass}"); } if (strpos($filterClass, \Kibo\Phast\Filters\HTML::class . '\\') === 0) { $object = 'documents'; } elseif (strpos($filterClass, \Kibo\Phast\Filters\CSS::class . '\\') === 0) { $object = 'styles'; } else { throw new \LogicException("Invalid filter namespace: {$filterClass}"); } $phastConfig[$object]['filters'][$filterClass] = ['enabled' => $settingName]; $phastConfig['switches'][$settingName] = $pluginConfig[$settingName]; } } /** * Returns the configuration as an array * * @return array|bool|mixed * @deprecated use PhastConfiguration::getForDocuments() */ public function get() { return $this->getForDocuments(); } } namespace Kibo\PhastPlugins\SDK\Configuration; /** * Manages serialization of the phast service configuration. * Must be as fast as possible, having as little * dependency on the host system as possible (ideally - none). * * Interface ServiceConfigurationRepository */ interface ServiceConfigurationRepository { /** * Store the given config * * @param array $config * @return bool TRUE on success, FALSE on failure */ public function store(array $config); /** * Returns the previously stored config * * @return array|bool - The config on success or * FALSE on failure or if no config has been stored */ public function get(); /** * Tells whether a config has been previously stored * * @return bool */ public function has(); } namespace Kibo\PhastPlugins\SDK\Configuration; class PluginConfigurationRepository { /** * @var KeyValueStore */ private $store; /** * JSONKeyValueStore constructor. * @param KeyValueStore $store */ public function __construct(\Kibo\PhastPlugins\SDK\Configuration\KeyValueStore $store) { $this->store = $store; } /** * @param mixed $key * @param null $default * @return mixed|null */ public function get($key, $default = null) { $value = $this->store->get($key); if (!is_string($value) || $value === 'null') { return $default; } $deserialised = @json_decode($value, true); if (is_null($deserialised)) { return $default; } return $deserialised; } /** * @param mixed $key * @param mixed $value */ public function set($key, $value) { $this->store->set($key, json_encode($value)); } } namespace Kibo\PhastPlugins\SDK\Configuration; /** * A key-value store for use within the plugin's admin panel * * Interface KeyValueStore */ interface KeyValueStore { /** * @param string $key * @return string|null The previously stored value or * null if there was no value stored for this key */ public function get($key); /** * @param string $key * @param string $value * @return void */ public function set($key, $value); } namespace Kibo\PhastPlugins\SDK\Configuration; /** * Represents the configuration of the plugin * * Class PluginConfiguration */ class PluginConfiguration { const KEY_SETTINGS = 'settings'; const KEY_ACTIVATION_NOTIFICATION = 'activation-notification'; /** * @var PluginConfigurationRepository */ private $repo; /** * @var ServiceConfigurationGenerator */ private $serviceConfigGenerator; /** * @var CacheRootManager */ private $cacheRootManager; /** * @var PhastUser */ private $user; /** * @var NonceChecker */ private $nonceChecker; /** * PluginConfiguration constructor. * @param PluginConfigurationRepository $repo * @param ServiceConfigurationGenerator $serviceConfigGenerator * @param CacheRootManager $cacheRootManager * @param PhastUser $user * @param NonceChecker $nonceChecker */ public function __construct(\Kibo\PhastPlugins\SDK\Configuration\PluginConfigurationRepository $repo, \Kibo\PhastPlugins\SDK\Configuration\ServiceConfigurationGenerator $serviceConfigGenerator, \Kibo\PhastPlugins\SDK\Caching\CacheRootManager $cacheRootManager, \Kibo\PhastPlugins\SDK\Security\PhastUser $user, \Kibo\PhastPlugins\SDK\Security\NonceChecker $nonceChecker) { $this->repo = $repo; $this->serviceConfigGenerator = $serviceConfigGenerator; $this->cacheRootManager = $cacheRootManager; $this->user = $user; $this->nonceChecker = $nonceChecker; } public function get() { $userSettings = $this->repo->get(self::KEY_SETTINGS, []); return array_merge($this->getDefaultAdminPanelSettings(), $userSettings); } public function save(array $newConfig) { if (!$this->nonceChecker->checkNonce($newConfig)) { return; } $keys = array_keys($this->getDefaultAdminPanelSettings()); $settings = []; foreach ($keys as $key) { $newConfigKey = "phastpress-{$key}"; if (!isset($newConfig[$newConfigKey])) { continue; } if ($newConfig[$newConfigKey] == 'on') { $settings[$key] = true; } elseif ($newConfig[$newConfigKey] == 'off') { $settings[$key] = false; } } $this->update($settings); } public function update(array $settings) { $this->repo->set(self::KEY_SETTINGS, array_merge($this->get(), $settings)); $this->serviceConfigGenerator->generate($this); } public function shouldShowActivationNotification() { return $this->repo->get(self::KEY_ACTIVATION_NOTIFICATION, true); } public function hideActivationNotification() { $this->repo->set(self::KEY_ACTIVATION_NOTIFICATION, false); } public function shouldAutoConfigure() { return !$this->repo->get(self::KEY_SETTINGS); } public function shouldDeployFilters() { $plugin_config = $this->get(); if (!$plugin_config['enabled']) { return false; } if (!$plugin_config['admin-only']) { return true; } return $this->user->seesPreviewMode(); } public function shouldDisplayFooter() { return $this->get()['footer-link'] && $this->shouldDeployFilters(); } private function getDefaultAdminPanelSettings() { return ['enabled' => true, 'admin-only' => false, 'pathinfo-query-format' => false, 'footer-link' => false, 'compress-service-response' => true, 'img-optimization-tags' => true, 'img-optimization-css' => true, 'img-optimization-api' => true, 'img-lazy' => true, 'css-optimization' => true, 'scripts-rearrangement' => false, 'scripts-defer' => true, 'scripts-proxy' => true, 'iframe-defer' => true, 'minify-html' => true, 'minify-inline-scripts' => true]; } } namespace Kibo\PhastPlugins\SDK\Configuration; /** * A default implementation of the ServiceConfigurationRepository interface * * Class PHPFilesServiceConfigurationRepository */ class PHPFilesServiceConfigurationRepository implements \Kibo\PhastPlugins\SDK\Configuration\ServiceConfigurationRepository { /** * @var CacheRootManager */ private $cacheRootManager; /** * PHPFilesServiceConfigurationRepository constructor. * @param CacheRootManager $cacheRootManager */ public function __construct(\Kibo\PhastPlugins\SDK\Caching\CacheRootManager $cacheRootManager) { $this->cacheRootManager = $cacheRootManager; } public function store(array $config) { return $this->storeInPHPFile($this->getServiceConfigurationFilePath(), $config) !== false; } public function get() { $json = $this->readFromPHPFile($this->getServiceConfigurationFilePath()); if (!$json) { return false; } if (strpos($json, 'a:') === 0) { $config = unserialize($json); } else { $config = json_decode($json, true); } if ($config === null) { return false; } return $config; } public function has() { return !!$this->get(); } private function getServiceConfigurationFilePath() { return $this->getCacheStoredFilePath('service-config'); } private function getCacheStoredFilePath($filename) { $dir = $this->cacheRootManager->getCacheRoot(); if (!$dir) { return false; } $legacyName = "{$dir}/{$filename}.php"; if (@file_exists($legacyName)) { return $legacyName; } foreach (@scandir($dir) as $file) { if (!preg_match('~^' . preg_quote($filename, '~') . '-[a-zA-Z0-9]{16}$~', $file)) { continue; } $path = "{$dir}/{$file}"; if (@is_file($path)) { return $path; } } return "{$dir}/service-config-{$this->generateRandomName()}"; } private function generateRandomName() { $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; $o = ''; for ($i = 0; $i < 16; $i++) { $o .= $chars[mt_rand(0, strlen($chars) - 1)]; } return $o; } private function readFromPHPFile($filename) { $content = @file_get_contents($filename); if (!$content) { return false; } if (!preg_match('/^[^>]*>\\n([a-f0-9]{40})\\n(.*)$/s', $content, $match)) { return false; } if (sha1($match[2]) != $match[1]) { return false; } return $match[2]; } private function storeInPHPFile($filename, $value) { $value = json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR | JSON_UNESCAPED_SLASHES) . "\n"; $content = "\n" . sha1($value) . "\n" . $value; return @file_put_contents($filename, $content, LOCK_EX); } } namespace Kibo\PhastPlugins\SDK\APIs; /** * Presents convenient methods for common tasks. * * Class Service */ class Service { /** * @var ServiceConfiguration */ private $config; /** * Service constructor. * @param ServiceConfiguration $config */ public function __construct(\Kibo\PhastPlugins\SDK\Configuration\ServiceConfiguration $config) { $this->config = $config; } /** * Configures the services and serves the request */ public function serve() { \Kibo\Phast\PhastServices::serve(function () { return $this->config->get(); }); } } namespace Kibo\PhastPlugins\SDK\APIs; /** * Presents convenient methods for common tasks. * * Class Phast */ class Phast { /** * @var PhastConfiguration */ private $config; /** * PhastAPI constructor. * @param PhastConfiguration $config */ public function __construct(\Kibo\PhastPlugins\SDK\Configuration\PhastConfiguration $config) { $this->config = $config; } /** * Applies phast filters to $html * with a configuration suited for full documents * * @param $html * @return string */ public function applyFiltersForDocument($html) { return \Kibo\Phast\PhastDocumentFilters::apply($html, $this->config->getForDocuments()); } /** * Applies phast filters to $html * with a configuration suited for html snippets * * @param $html * @return string */ public function applyFiltersForSnippets($html) { return \Kibo\Phast\PhastDocumentFilters::apply($html, $this->config->getForHTMLSnippets()); } /** * Deploys phast output buffer filters * with a configuration suited for full documents */ public function deployOutputBufferForDocument() { return \Kibo\Phast\PhastDocumentFilters::deploy($this->config->getForDocuments()); } /** * Deploys phast output buffer filters * with a configuration suited for html snippets */ public function deployOutputBufferForSnippets() { return \Kibo\Phast\PhastDocumentFilters::deploy($this->config->getForHTMLSnippets()); } } namespace Kibo\PhastPlugins\SDK\Caching; interface CacheRootCandidatesProvider { /** * Return a list of folders that will potentially be used * for storing cache and service configuration files. * The directories will be checked for write access * in the order they were provided. The first one writable * will be used. * * @return string[] */ public function getCacheRootCandidates(); } namespace Kibo\PhastPlugins\SDK\Caching; class CacheRootManager { /** * @var CacheRootCandidatesProvider */ private $rootsProvider; /** * CacheRootManager constructor. * @param CacheRootCandidatesProvider $rootsProvider */ public function __construct(\Kibo\PhastPlugins\SDK\Caching\CacheRootCandidatesProvider $rootsProvider) { $this->rootsProvider = $rootsProvider; } public function getCacheRootCandidates() { return $this->rootsProvider->getCacheRootCandidates(); } public function getCacheRoot() { $key = $this->getKey(); $candidates = $this->getCacheRootCandidates(); if ($result = $this->findExistingCacheRoot($key, $candidates)) { return $result; } if ($this->createNewCacheRoot($key, $candidates)) { return $this->findExistingCacheRoot($key, $candidates); } return false; } public function hasCacheRoot() { return (bool) $this->getCacheRoot(); } public function getAllCacheRoots() { return $this->findAllExistingCacheRoots($this->getKey(), $this->getCacheRootCandidates()); } private function getKey() { return md5(@$_SERVER['DOCUMENT_ROOT']) . '.' . (new \Kibo\Phast\Common\System())->getUserId(); } private function findExistingCacheRoot($key, $candidates) { foreach ($this->findAllExistingCacheRoots($key, $candidates) as $checkDir) { if (!is_writable($checkDir)) { continue; } if (function_exists('posix_geteuid') && fileowner($checkDir) !== posix_geteuid()) { continue; } $this->createIndexFile($checkDir); return $checkDir; } return false; } private function findAllExistingCacheRoots($key, $candidates) { foreach ($this->getCacheRootCandidates() as $dir) { $checkDirs = ["{$dir}/{$key}", "{$dir}/phastpress.{$key}", "{$dir}/phast.{$key}"]; foreach ($checkDirs as $checkDir) { if (!is_dir($checkDir)) { continue; } (yield $checkDir); } } } private function createNewCacheRoot($key, $candidates) { foreach ($this->getCacheRootCandidates() as $dir) { if (@mkdir("{$dir}/phast.{$key}", 0777, true)) { return true; } } return false; } private function createIndexFile($dir) { $path = "{$dir}/index.html"; if (!@file_exists($path)) { @touch($path); } } } namespace Kibo\PhastPlugins\SDK; class Autoloader { private static $instance; private $psr4 = array(); public static function getInstance() { if (!isset(self::$instance)) { self::$instance = new self(); self::$instance->install(); } return self::$instance; } public function install() { spl_autoload_register(function ($class) { $this->autoload($class); }); } public function addPSR4($namespace, $dir) { $this->psr4[] = [$namespace, $dir]; return $this; } private function autoload($class) { foreach ($this->psr4 as $psr4) { list($namespace, $dir) = $psr4; if (strcasecmp($namespace . '\\', substr($class, 0, strlen($namespace) + 1))) { continue; } $relativeName = substr($class, strlen($namespace) + 1); $relativePath = str_replace('\\', '/', $relativeName) . '.php'; $fullPath = $dir . '/' . $relativePath; if (file_exists($fullPath)) { include $fullPath; return; } } } } namespace Kibo\PhastPlugins\SDK; interface ServiceHost { /** * @return CacheRootCandidatesProvider */ public function getCacheRootCandidatesProvider(); /** * Called right after the service configuration * has been loaded. Use it to modify the config * and take any other needed action before * the service is started. * * @param array $config - The configuration that has been loaded * @return array - The configuration to use for the services */ public function onServiceConfigurationLoad(array $config); } namespace Kibo\PhastPlugins\SDK\Generated; class Translations { const DATA = array('en' => array('AdminPanel' => array('errors' => array('no-cache-root' => '@:plugin-name can not write to any cache directory! Please, make one of the following directories writable: {params}', 'no-service-config' => '@:plugin-name failed to create a service configuration in any of the following directories: {params}', 'network-error' => 'Failed to connect to plugin server! Please, try again later! {params}', 'cache' => '{params}'), 'warnings' => array('disabled' => '@:plugin-name optimizations are off!', 'admin-only' => '@:plugin-name optimizations will be applied only for logged-in users with the "Administrator" privilege. This is for previewing purposes. Select the "On" setting for "@:plugin-name optimizations" below to activate for all users! ')), 'Backend' => array('install-notice' => 'Thank you for using @:plugin-name. Optimizations are {pluginState}. Go to Settings to configure @:plugin-name. ', 'status' => array('on' => 'on', 'off' => 'off', 'admin' => 'on for administrators')), 'Information' => array('additional' => 'Additional information'), 'Notification' => array('error' => 'error', 'warning' => 'warning', 'information' => 'information', 'success' => 'success'), 'OnOffSwitch' => array('on' => 'On', 'off' => 'Off'), 'SavingStatus' => array('saving' => 'Saving', 'saved' => 'Saved'), 'Settings' => array('common' => array('tip' => 'Tip:', 'on' => 'On:', 'off' => 'Off:'), 'sections' => array('plugin' => array('title' => 'Plugin', 'enabled' => array('name' => '@:plugin-name optimizations', 'description' => array('main' => 'Test your site {without} and {with}', 'without' => 'without @:plugin-name', 'with' => 'with @:plugin-name')), 'admin-only' => array('name' => 'Only optimize for administrators', 'description' => array('on' => 'Only privileged users will be served with optimized version', 'off' => 'All users will be served with optimized version', 'tip' => 'Use this to preview your site before launching the optimizations')), 'pathinfo' => array('name' => 'Remove query string from processed resources', 'description' => array('start' => 'Make sure that processed resources don\'t have query strings, for a higher score in GTmetrix.', 'on' => 'Use the path for requests for processed resources. This requires a server that supports "PATH_INFO".', 'off' => 'Use the GET parameters for requests for processed resources.')), 'footer-link' => array('name' => 'Let the world know about @:plugin-name', 'description' => 'Add a "Optimized by @:plugin-name" notice to the footer of your site and help spread the word.'), 'compress-service-response' => array('name' => 'Enable gzip compression on processed resources', 'description' => 'This compresses the optimized and bundled JavaScript and CSS generated by PhastPress. Disable this if your server already compresses PhastPress responses.')), 'images' => array('title' => 'Images', 'tags' => array('name' => 'Optimize images in tags', 'description' => 'Compress images with optimal settings. {newline} Resize images to fit {width}x{height} pixels or to the appropriate size for {imgTag} tags with {widthAttr} or {heightAttr}. {newline} Reload changed images while still leveraging browser caching.'), 'css' => array('name' => 'Optimize images in CSS', 'description' => array(0 => 'Compress images in stylesheets with optional settings and resizes the to fit {width}x{height} pixels.', 1 => 'Reload changed images while still leveraging browser caching.')), 'api' => array('name' => 'Use the Phast Image Optimization API', 'description' => array(0 => 'Optimize your images on our servers free of charge.', 1 => 'This will give you the best possible results without installing any software and will reduce the load on your hosting.')), 'lazy' => array('name' => 'Lazy load images', 'description' => array(0 => 'This adds the loading=lazy attribute to img tags so that images are only load once they are visible on the page.', 1 => 'This helps pass the "Defer offscreen images" audit in PageSpeed Insights.'))), 'html-filters' => array('title' => 'HTML, CSS & JS', 'css' => array('name' => 'Optimize CSS', 'description' => array(0 => 'Incline critical styles first and prevent unused styles from blocking the page load.', 1 => 'Minify stylesheets and leverage browser caching.', 2 => 'Inline Google Fonts CSS to speed up font loading.')), 'async-js' => array('name' => 'Load scripts asynchronously', 'description' => 'Allow the page to finish loading before all scripts have been executed.'), 'minify-js' => array('name' => 'Minify scripts and improve caching', 'description' => array(0 => 'Minify scripts and fix caching for Google Analytics and Hotjar.', 1 => 'Reload changed scripts while still leveraging browser caching.')), 'iframe' => array('name' => 'Defer IFrame loading', 'description' => 'Start loading IFrames after the page has finished loading.'), 'minify-html' => array('name' => 'Minify HTML', 'description' => 'Remove unnecessary whitespace from the HTML code of the page.'), 'minify-inline-scripts' => array('name' => 'Minify inline scripts and JSON', 'description' => 'Remove unnecessary whitespace from inline scripts and JSON data.')))))); } namespace Kibo\PhastPlugins\SDK\AJAX; /** * Handles AJAX requests to the plugin admin. * Use this class to handle requests to the plugin's admin AJAX end point * * Class RequestsDispatcher */ class RequestsDispatcher { const KEY_ACTION = 'phast-plugins-action'; /** * @var PhastUser */ private $user; /** * @var SDK */ private $sdk; /** * RequestsDispatcher constructor. * @param PhastUser $user * @param SDK $sdk */ public function __construct(\Kibo\PhastPlugins\SDK\Security\PhastUser $user, \Kibo\PhastPlugins\SDK\SDK $sdk) { $this->user = $user; $this->sdk = $sdk; } /** * Handles ajax requests to plugin's admin AJAX end point * * @param array $request The $_POST data to the request * @return mixed Must be json encoded and returned to the client */ public function dispatch(array $request) { if (!$this->user->mayModifySettings()) { return false; } $action = isset($request[self::KEY_ACTION]) ? $request[self::KEY_ACTION] : ''; if ($action == 'save-settings') { $this->sdk->getPluginConfiguration()->save($request); return $this->makeResponse(true, $this->sdk->getAdminPanelData()->get()); } if ($action == 'dismiss-notice') { $this->sdk->getPluginConfiguration()->hideActivationNotification(); return $this->makeResponse(true); } return $this->makeResponse(false); } private function makeResponse($success, $data = null) { return ['phast-success' => $success, 'phast-data' => $data]; } } namespace Kibo\PhastPlugins\SDK\Security; /** * Represents the user currently viewing the website (either backend or frontend) * * Interface PhastUser */ interface PhastUser { /** * Tells whether the user can access and manipulate * the plugin's settings * * @return bool */ public function mayModifySettings(); /** * Tells whether the user can access the website with Phast enabled in preview mode * * @return bool */ public function seesPreviewMode(); } namespace Kibo\PhastPlugins\SDK\Security; /** * Checks whether posted data to the server * contains the expected nonce. * * Interface NonceChecker */ interface NonceChecker { /** * Performs the check * * @param array $data The data posted to the server * @return bool TRUE if all is well, FALSE otherwise */ public function checkNonce(array $data); } namespace Kibo\PhastPlugins\SDK\Common; /** * Contains common implementations for methods * of the PluginHost interface * * @see PluginHost * Trait PluginHostTrait */ trait PluginHostTrait { public function getPluginName() { return 'Phast'; } public function isDev() { return $this->getPluginHostVersion() === '$VER' . 'SION$'; } public function onPhastConfigurationLoad(array $config) { return $config; } public function getLocale() { return 'en'; } public function getInstallNoticeRenderer() { return new \Kibo\PhastPlugins\SDK\AdminPanel\DefaultInstallNoticeRenderer(); } } namespace Kibo\PhastPlugins\SDK\Common; trait ServiceHostTrait { public function onServiceConfigurationLoad(array $config) { return $config; } } namespace Kibo\PhastPlugins\SDK\Common; trait PreviewCookieTrait { public function seesPreviewMode() { return isset($_COOKIE['PHAST_PREVIEW']) && (bool) $_COOKIE['PHAST_PREVIEW']; } } namespace Kibo\Phast\Environment\Exceptions; class PackageHasNoDiagnosticsException extends \Kibo\Phast\Exceptions\LogicException { } namespace Kibo\Phast\Environment\Exceptions; class PackageHasNoFactoryException extends \Kibo\Phast\Exceptions\LogicException { } namespace Kibo\Phast\Cache\File; class DiagnosticsLogWriter implements \Kibo\Phast\Logging\LogWriter { public function setLevelMask($mask) { } public function writeEntry(\Kibo\Phast\Logging\LogEntry $entry) { if ($entry->getLevel() > 2) { $needles = array_map(function ($key) { return '{' . $key . '}'; }, array_keys($entry->getContext())); $message = str_replace($needles, $entry->getContext(), $entry->getMessage()); throw new \Kibo\Phast\Exceptions\RuntimeException("Error: Level: {$entry->getLevel()}, Msg: {$message}"); } } } namespace Kibo\Phast\Cache\File; class Diagnostics implements \Kibo\Phast\Diagnostics\Diagnostics { public function diagnose(array $config) { \Kibo\Phast\Logging\Log::setLogger(new \Kibo\Phast\Logging\Logger(new \Kibo\Phast\Cache\File\DiagnosticsLogWriter())); $cache = new \Kibo\Phast\Cache\File\Cache($config['cache'], 'cache-self-diagnosis'); $v1 = $cache->get('test-key', function () { return 1; }, 2); $v2 = $cache->get('test-key', function () { return 2; }, 2); if ($v1 != $v2) { throw new \Kibo\Phast\Exceptions\RuntimeException('Cache failed, but no error was reported!'); } } } namespace Kibo\Phast\Filters\HTML\CommentsRemoval; class Filter implements \Kibo\Phast\Filters\HTML\HTMLStreamFilter { public function transformElements(\Traversable $elements, \Kibo\Phast\Filters\HTML\HTMLPageContext $context) { foreach ($elements as $element) { if (!$element instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Comment || $element->isIEConditional()) { (yield $element); } } } } namespace Kibo\Phast\Filters\HTML\ScriptsDeferring; class Filter extends \Kibo\Phast\Filters\HTML\BaseHTMLStreamFilter { use \Kibo\Phast\Filters\HTML\Helpers\JSDetectorTrait; protected function isTagOfInterest(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag) { return $tag->getTagName() == 'script'; } protected function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $script) { if ($this->isJSElement($script) && !$this->isDeferralDisabled($script)) { $this->rewrite($script); } (yield $script); } protected function afterLoop() { $this->context->addPhastJavaScript(\Kibo\Phast\ValueObjects\PhastJavaScript::fromString('/home/albert/code/phast/src/Build/../../src/Filters/HTML/ScriptsDeferring/scripts-loader.js', "var Promise=phast.ES6Promise;phast.ScriptsLoader={};phast.ScriptsLoader.getScriptsInExecutionOrder=function(a,b){var c=a.querySelectorAll('script[type=\"text/phast\"]');var d=[],e=[];for(var f=0;fcontext->addPhastJavaScript(\Kibo\Phast\ValueObjects\PhastJavaScript::fromString('/home/albert/code/phast/src/Build/../../src/Filters/HTML/ScriptsDeferring/rewrite.js', "var Promise=phast.ES6Promise;var go=phast.once(loadScripts);phast.on(document,\"DOMContentLoaded\").then(function(){if(phast.stylesLoading){phast.onStylesLoaded=go;setTimeout(go,4e3)}else{Promise.resolve().then(go)}});var loadFiltered=false;window.addEventListener(\"load\",function(a){if(!loadFiltered){a.stopImmediatePropagation()}loadFiltered=true});function loadScripts(){var b=new phast.ScriptsLoader.Scripts.Factory(document,fetchScript);var c=phast.ScriptsLoader.getScriptsInExecutionOrder(document,b);if(c.length===0){return}try{Object.defineProperty(document,\"readyState\",{configurable:true,get:function(){return\"loading\"}})}catch(d){console.error(\"[Phast] Unable to override document.readyState on this browser: \",d)}phast.ScriptsLoader.executeScripts(c).then(restoreReadyState)}function restoreReadyState(){window.requestAnimationFrame(function(){delete document[\"readyState\"];triggerEvent(document,\"readystatechange\");triggerEvent(document,\"DOMContentLoaded\");window.requestAnimationFrame(function(){if(loadFiltered){triggerEvent(window,\"load\")}else{loadFiltered=true}})})}function triggerEvent(e,f){var g=document.createEvent(\"Event\");g.initEvent(f,true,true);e.dispatchEvent(g)}function fetchScript(h){return phast.ResourceLoader.instance.get(phast.ResourceLoader.RequestParams.fromString(h.getAttribute(\"data-phast-params\")))}\n")); } private function rewrite(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $script) { if ($script->hasAttribute('type')) { $script->setAttribute('data-phast-original-type', $script->getAttribute('type')); } $script->setAttribute('type', 'text/phast'); if ($script->hasAttribute('data-phast-params')) { $script->removeAttribute('src'); } } private function isDeferralDisabled(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $script) { return $script->hasAttribute('data-phast-no-defer') || $script->hasAttribute('data-pagespeed-no-defer') || $script->getAttribute('data-cfasync') === 'false'; } } namespace Kibo\Phast\Filters\HTML\ImagesOptimizationService\Tags; class Factory implements \Kibo\Phast\Filters\HTML\HTMLFilterFactory { public function make(array $config) { return new \Kibo\Phast\Filters\HTML\ImagesOptimizationService\Tags\Filter((new \Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageURLRewriterFactory())->make($config, \Kibo\Phast\Filters\HTML\ImagesOptimizationService\Tags\Filter::class)); } } namespace Kibo\Phast\Filters\HTML\ImagesOptimizationService\CSS; class Factory implements \Kibo\Phast\Filters\HTML\HTMLFilterFactory { public function make(array $config) { return new \Kibo\Phast\Filters\HTML\ImagesOptimizationService\CSS\Filter((new \Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageURLRewriterFactory())->make($config, \Kibo\Phast\Filters\HTML\ImagesOptimizationService\CSS\Filter::class)); } } namespace Kibo\Phast\Filters\HTML\ImagesOptimizationService\CSS; class Filter extends \Kibo\Phast\Filters\HTML\BaseHTMLStreamFilter { /** * @var ImageURLRewriter */ protected $rewriter; /** * Filter constructor. * @param ImageURLRewriter $rewriter */ public function __construct(\Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageURLRewriter $rewriter) { $this->rewriter = $rewriter; } protected function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag) { if ($tag->hasAttribute('style')) { $tag->setAttribute('style', $this->rewriter->rewriteStyle($tag->getAttribute('style'))); } (yield $tag); } } namespace Kibo\Phast\Filters\HTML\PhastScriptsCompiler; class Factory implements \Kibo\Phast\Filters\HTML\HTMLFilterFactory { public function make(array $config) { $cache = new \Kibo\Phast\Cache\File\Cache($config['cache'], 'phast-scripts'); $compiler = new \Kibo\Phast\Filters\HTML\PhastScriptsCompiler\PhastJavaScriptCompiler($cache, $config['servicesUrl'], $config['serviceRequestFormat']); return new \Kibo\Phast\Filters\HTML\PhastScriptsCompiler\Filter($compiler); } } namespace Kibo\Phast\Filters\HTML\ScriptsProxyService; class Factory implements \Kibo\Phast\Filters\HTML\HTMLFilterFactory { public function make(array $config) { if (!isset($config['documents']['filters'][\Kibo\Phast\Filters\HTML\ScriptsProxyService\Filter::class]['serviceUrl'])) { $config['documents']['filters'][\Kibo\Phast\Filters\HTML\ScriptsProxyService\Filter::class]['serviceUrl'] = $config['servicesUrl']; } $filterConfig = $config['documents']['filters'][\Kibo\Phast\Filters\HTML\ScriptsProxyService\Filter::class]; $filterConfig['match'] = $config['scripts']['whitelist']; return new \Kibo\Phast\Filters\HTML\ScriptsProxyService\Filter($filterConfig, (new \Kibo\Phast\Security\ServiceSignatureFactory())->make($config), new \Kibo\Phast\Retrievers\LocalRetriever($config['retrieverMap']), (new \Kibo\Phast\Services\Bundler\TokenRefMakerFactory())->make($config)); } } namespace Kibo\Phast\Filters\HTML\CSSInlining; class Factory implements \Kibo\Phast\Filters\HTML\HTMLFilterFactory { public function make(array $config) { $localRetriever = new \Kibo\Phast\Retrievers\LocalRetriever($config['retrieverMap']); $retriever = new \Kibo\Phast\Retrievers\UniversalRetriever(); $retriever->addRetriever($localRetriever); $retriever->addRetriever(new \Kibo\Phast\Retrievers\CachingRetriever(new \Kibo\Phast\Cache\File\Cache($config['cache'], 'css'))); if (!isset($config['documents']['filters'][\Kibo\Phast\Filters\HTML\CSSInlining\Filter::class]['serviceUrl'])) { $config['documents']['filters'][\Kibo\Phast\Filters\HTML\CSSInlining\Filter::class]['serviceUrl'] = $config['servicesUrl']; } return new \Kibo\Phast\Filters\HTML\CSSInlining\Filter((new \Kibo\Phast\Security\ServiceSignatureFactory())->make($config), \Kibo\Phast\ValueObjects\URL::fromString($config['documents']['baseUrl']), $config['documents']['filters'][\Kibo\Phast\Filters\HTML\CSSInlining\Filter::class], $localRetriever, $retriever, new \Kibo\Phast\Filters\HTML\CSSInlining\OptimizerFactory($config), (new \Kibo\Phast\Filters\CSS\Composite\Factory())->make($config), (new \Kibo\Phast\Services\Bundler\TokenRefMakerFactory())->make($config)); } } namespace Kibo\Phast\Filters\HTML\Diagnostics; class Factory implements \Kibo\Phast\Filters\HTML\HTMLFilterFactory { public function make(array $config) { $url = isset($config['documents']['filters'][\Kibo\Phast\Filters\HTML\Diagnostics\Filter::class]['serviceUrl']) ? $config['documents']['filters'][\Kibo\Phast\Filters\HTML\Diagnostics\Filter::class]['serviceUrl'] : $config['servicesUrl'] . '?service=diagnostics'; return new \Kibo\Phast\Filters\HTML\Diagnostics\Filter($url); } } namespace Kibo\Phast\Filters\Image\Exceptions; class ImageProcessingException extends \Kibo\Phast\Exceptions\RuntimeException { } namespace Kibo\Phast\Filters\Image\ImageImplementations; class DummyImage extends \Kibo\Phast\Filters\Image\ImageImplementations\BaseImage implements \Kibo\Phast\Filters\Image\Image { /** * @var string */ private $imageString; private $transformationString; /** * DummyImage constructor. * * @param int $width * @param int $height */ public function __construct($width = null, $height = null) { $this->width = $width; $this->height = $height; } /** * @return int */ public function getWidth() { return $this->width; } /** * @return int */ public function getHeight() { return $this->height; } /** * @return string */ public function getType() { return $this->type; } /** * @param string $type */ public function setType($type) { $this->type = $type; } /** * @return int */ public function getCompression() { return $this->compression; } /** * @return string */ public function getAsString() { return $this->imageString; } /** * @param string $imageString */ public function setImageString($imageString) { $this->imageString = $imageString; } /** * @param mixed $transformationString */ public function setTransformationString($transformationString) { $this->transformationString = $transformationString; } protected function __clone() { $this->imageString = $this->transformationString; } } namespace Kibo\Phast\Filters\Service; class CachingServiceFilter implements \Kibo\Phast\Services\ServiceFilter { use \Kibo\Phast\Logging\LoggingTrait; /** * @var Cache */ private $cache; /** * @var CachedResultServiceFilter */ private $cachedFilter; /** * @var Retriever */ private $retriever; /** * CachingServiceFilter constructor. * @param Cache $cache * @param CachedResultServiceFilter $cachedFilter * @param Retriever $retriever */ public function __construct(\Kibo\Phast\Cache\Cache $cache, \Kibo\Phast\Filters\Service\CachedResultServiceFilter $cachedFilter, \Kibo\Phast\Retrievers\Retriever $retriever) { $this->cache = $cache; $this->cachedFilter = $cachedFilter; $this->retriever = $retriever; } /** * @param Resource $resource * @param array $request * @return Resource * @throws CachedExceptionException */ public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request) { $key = $this->cachedFilter->getCacheSalt($resource, $request); $this->logger()->info('Trying to get {url} from cache', ['url' => (string) $resource->getUrl()]); $result = $this->cache->get($key); if (isset($result['encoding']) && $result['encoding'] != 'identity') { $result = null; } if ($result && $this->checkDependencies($result)) { return $this->deserializeCachedData($result); } try { $result = $this->cachedFilter->apply($resource, $request); $this->cache->set($key, $this->serializeResource($result)); return $result; } catch (\Exception $e) { $cachingException = $this->serializeException($e); $this->cache->set($key, $cachingException); throw $this->deserializeException($cachingException); } } private function checkDependencies(array $data) { foreach ((array) @$data['dependencies'] as $dep) { $url = \Kibo\Phast\ValueObjects\URL::fromString($dep['url']); if ($this->retriever->getCacheSalt($url) >= $dep['cacheMarker']) { return false; } } return true; } private function deserializeCachedData(array $data) { if ($data['dataType'] == 'exception') { throw $this->deserializeException($data); } return $this->deserializeResource($data); } private function serializeResource(\Kibo\Phast\ValueObjects\Resource $resource) { return ['dataType' => 'resource', 'url' => $resource->getUrl()->toString(), 'mimeType' => $resource->getMimeType(), 'blob' => base64_encode($resource->getContent()), 'dependencies' => $this->serializeDependencies($resource)]; } private function serializeDependencies(\Kibo\Phast\ValueObjects\Resource $resource) { return array_map(function (\Kibo\Phast\ValueObjects\Resource $dep) { return ['url' => $dep->getUrl()->toString(), 'cacheMarker' => $dep->getCacheSalt()]; }, $resource->getDependencies()); } private function deserializeResource(array $data) { $params = [\Kibo\Phast\ValueObjects\URL::fromString($data['url']), base64_decode($data['blob']), $data['mimeType']]; return \Kibo\Phast\ValueObjects\Resource::makeWithContent(...$params); } private function serializeException(\Exception $e) { return ['dataType' => 'exception', 'class' => get_class($e), 'msg' => $e->getMessage(), 'code' => $e->getCode()]; } private function deserializeException(array $data) { return new \Kibo\Phast\Exceptions\CachedExceptionException(sprintf('Phast: %s: Type: %s, Msg: %s, Code: %s', static::class, $data['class'], $data['msg'], $data['code'])); } } namespace Kibo\Phast\Filters\Service; interface CachedResultServiceFilter extends \Kibo\Phast\Services\ServiceFilter { /** * @param Resource $resource * @param array $request * @return string */ public function getCacheSalt(\Kibo\Phast\ValueObjects\Resource $resource, array $request); } namespace Kibo\Phast\Filters\Service; class CompositeFilter implements \Kibo\Phast\Filters\Service\CachedResultServiceFilter { use \Kibo\Phast\Logging\LoggingTrait; /** * @var ServiceFilter[] */ private $filters = array(); public function addFilter(\Kibo\Phast\Services\ServiceFilter $filter) { $this->filters[] = $filter; } public function getCacheSalt(\Kibo\Phast\ValueObjects\Resource $resource, array $request) { $classes = array_map('get_class', $this->filters); $cached = array_filter($this->filters, function (\Kibo\Phast\Services\ServiceFilter $filter) { return $filter instanceof \Kibo\Phast\Filters\Service\CachedResultServiceFilter; }); $salts = array_map(function (\Kibo\Phast\Filters\Service\CachedResultServiceFilter $filter) use($resource, $request) { return $filter->getCacheSalt($resource, $request); }, $cached); return join("\n", array_merge($classes, $salts, [$resource->getUrl(), $resource->getCacheSalt()])); } public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request) { $this->logger()->info('Starting filtering for resource {url}', ['url' => $resource->getUrl()]); $result = array_reduce($this->filters, function (\Kibo\Phast\ValueObjects\Resource $resource, \Kibo\Phast\Services\ServiceFilter $filter) use($request) { $this->logger()->info('Starting {filter}', ['filter' => get_class($filter)]); try { return $filter->apply($resource, $request); } catch (\Kibo\Phast\Exceptions\RuntimeException $e) { $message = 'Phast RuntimeException: Filter: {filter} Exception: {exceptionClass} Msg: {message} Code: {code} File: {file} Line: {line}'; $this->logger()->critical($message, ['filter' => get_class($filter), 'exceptionClass' => get_class($e), 'message' => $e->getMessage(), 'code' => $e->getCode(), 'file' => $e->getFile(), 'line' => $e->getLine()]); return $resource; } }, $resource); $this->logger()->info('Done filtering for resource {url}', ['url' => $resource->getUrl()]); return $result; } } namespace Kibo\Phast\Filters\CSS\CSSMinifier; class Filter implements \Kibo\Phast\Services\ServiceFilter { /** * @param Resource $resource * @param array $request * @return Resource */ public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request) { $content = $resource->getContent(); // Normalize whitespace $content = preg_replace('~\\s+~', ' ', $content); // Remove whitespace before and after operators $chars = [',', '{', '}', ';']; foreach ($chars as $char) { $content = str_replace("{$char} ", $char, $content); $content = str_replace(" {$char}", $char, $content); } // Remove whitespace after colons $content = str_replace(': ', ':', $content); return $resource->withContent(trim($content)); } } namespace Kibo\Phast\Filters\CSS\CSSURLRewriter; class Filter implements \Kibo\Phast\Services\ServiceFilter { /** * @param Resource $resource * @param array $request * @return Resource */ public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request) { $baseUrl = $resource->getUrl(); $callback = function ($match) use($baseUrl) { if (preg_match('~^[a-z]+:|^#~i', $match[3])) { return $match[0]; } return $match[1] . \Kibo\Phast\ValueObjects\URL::fromString($match[3])->withBase($baseUrl) . $match[4]; }; $cssContent = preg_replace_callback('~ \\b ( url\\( ([\'"]?) ) ([A-Za-z0-9_/.:?&=+%,#@-]+) ( \\2 \\) ) ~x', $callback, $resource->getContent()); $cssContent = preg_replace_callback('~ ( @import \\s+ ([\'"]) ) ([A-Za-z0-9_/.:?&=+%,#@-]+) ( \\2 ) ~x', $callback, $cssContent); return $resource->withContent($cssContent); } } namespace Kibo\Phast\Filters\CSS\ImageURLRewriter; class Filter implements \Kibo\Phast\Filters\Service\CachedResultServiceFilter { /** * @var ImageURLRewriter */ private $rewriter; /** * Filter constructor. * @param ImageURLRewriter $rewriter */ public function __construct(\Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageURLRewriter $rewriter) { $this->rewriter = $rewriter; } public function getCacheSalt(\Kibo\Phast\ValueObjects\Resource $resource, array $request) { return $this->rewriter->getCacheSalt(); } public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request) { $content = $this->rewriter->rewriteStyle($resource->getContent()); $dependencies = $this->rewriter->getInlinedResources(); return $resource->withContent($content)->withDependencies($dependencies); } } namespace Kibo\Phast\Filters\CSS\Composite; class Filter extends \Kibo\Phast\Filters\Service\CompositeFilter implements \Kibo\Phast\Filters\Service\CachedResultServiceFilter { public function __construct() { $this->addFilter(new \Kibo\Phast\Filters\CSS\CommentsRemoval\Filter()); } } namespace Kibo\Phast\Filters\CSS\FontSwap; class Filter implements \Kibo\Phast\Services\ServiceFilter { const FONT_FACE_REGEXP = '/(@font-face\\s*\\{)([^}]*)/i'; const ICON_FONT_FAMILIES = array('Font Awesome', 'GeneratePress', 'Dashicons', 'Ionicons'); private $fontDisplayBlockPattern; public function __construct() { $this->fontDisplayBlockPattern = $this->getFontDisplayBlockPattern(); } public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request) { $css = $resource->getContent(); $filtered = preg_replace_callback(self::FONT_FACE_REGEXP, function ($match) { list($block, $start, $contents) = $match; $mode = preg_match($this->fontDisplayBlockPattern, $contents) ? 'block' : 'swap'; return $start . 'font-display:' . $mode . ';' . $contents; }, $css); return $resource->withContent($filtered); } private function getFontDisplayBlockPattern() { $patterns = []; foreach (self::ICON_FONT_FAMILIES as $family) { $chars = str_split($family); $chars = array_map(function ($char) { return preg_quote($char, '~'); }, $chars); $patterns[] = implode('\\s*', $chars); } return '~' . implode('|', $patterns) . '~i'; } } namespace Kibo\Phast\Filters\CSS\ImportsStripper; class Filter implements \Kibo\Phast\Filters\Service\CachedResultServiceFilter { use \Kibo\Phast\Logging\LoggingTrait; public function getCacheSalt(\Kibo\Phast\ValueObjects\Resource $resource, array $request) { return $this->shouldStripImports($request) ? 'strip-imports' : 'no-strip-imports'; } public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request) { if (!$this->shouldStripImports($request)) { $this->logger()->info('No import stripping requested! Skipping!'); return $resource; } $css = $resource->getContent(); $stripped = preg_replace(\Kibo\Phast\Filters\HTML\CSSInlining\Filter::CSS_IMPORTS_REGEXP, '', $css); return $resource->withContent($stripped); } private function shouldStripImports(array $request) { return isset($request['strip-imports']); } } namespace Kibo\Phast\Filters\CSS\CommentsRemoval; class Filter implements \Kibo\Phast\Services\ServiceFilter { public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request) { $content = preg_replace('~/\\*[^*]*\\*+([^/*][^*]*\\*+)*/~', '', $resource->getContent()); return $resource->withContent($content); } } namespace Kibo\Phast\Filters\Text\Decode; class Filter implements \Kibo\Phast\Services\ServiceFilter { const UTF8_BOM = "\357\273\277"; public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request = array()) { $content = $resource->getContent(); if (substr($content, 0, strlen(self::UTF8_BOM)) == self::UTF8_BOM) { $content = substr($content, strlen(self::UTF8_BOM)); } return $resource->withContent($content); } } namespace Kibo\Phast\Parsing\HTML\HTMLStreamElements; class ClosingTag extends \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Element { /** * @var string */ private $tagName; /** * ClosingTag constructor. * @param string $tagName */ public function __construct($tagName) { $this->tagName = strtolower($tagName); } /** * @return string */ public function getTagName() { return $this->tagName; } public function appendChild(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Element $element) { $this->stream->insertBeforeElement($this, $element); } public function dumpValue() { return $this->tagName; } } namespace Kibo\Phast\Parsing\HTML\HTMLStreamElements; class Tag extends \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Element { /** * @var string */ private $tagName; /** * @var array */ private $attributes = array(); /** * @var array */ private $newAttributes = array(); /** * @var \Iterator */ private $attributeReader; /** * @var string */ private $textContent = ''; /** * @var string */ private $closingTag = ''; private $dirty = false; /** * Tag constructor. * @param $tagName * @param array|\Traversable $attributes */ public function __construct($tagName, $attributes = array()) { $this->tagName = strtolower($tagName); if ($attributes instanceof \Iterator) { $this->attributeReader = $attributes; } elseif (is_array($attributes)) { $this->attributeReader = new \ArrayIterator($attributes); } else { throw new \InvalidArgumentException('Attributes must be array or Iterator'); } } /** * @return string */ public function getTagName() { return $this->tagName; } /** * @param string $attrName * @return bool */ public function hasAttribute($attrName) { return $this->getAttribute($attrName) !== null; } /** * @param string $attrName * @return mixed|null */ public function getAttribute($attrName) { if (array_key_exists($attrName, $this->newAttributes)) { return $this->newAttributes[$attrName]; } if (!array_key_exists($attrName, $this->attributes)) { $this->readUntilAttribute($attrName); } if (isset($this->attributes[$attrName])) { return $this->attributes[$attrName]; } } /** @return string[] */ public function getAttributes() { $this->readUntilAttribute(null); return array_filter($this->newAttributes + $this->attributes, function ($value) { return $value !== null; }); } private function readUntilAttribute($attrName) { if (!$this->attributeReader) { return; } while ($this->attributeReader->valid()) { $name = strtolower($this->attributeReader->key()); $value = $this->attributeReader->current(); $this->attributeReader->next(); if (!isset($this->attributes[$name])) { $this->attributes[$name] = $value; } if ($name == $attrName) { return; } } $this->attributeReader = null; } /** * @param string $attrName * @param string $value */ public function setAttribute($attrName, $value) { if ($this->getAttribute($attrName) === $value) { return; } $this->dirty = true; $this->newAttributes[$attrName] = $value; } /** * @param string $attrName */ public function removeAttribute($attrName) { $this->dirty = true; $this->newAttributes[$attrName] = null; } /** * @return string */ public function getTextContent() { return $this->textContent; } /** * @param string $textContent */ public function setTextContent($textContent) { $this->textContent = $textContent; } /** * @param $closingTag * @return Tag */ public function withClosingTag($closingTag) { $new = clone $this; $new->closingTag = $closingTag; return $new; } /** * @return string */ public function getClosingTag() { return $this->closingTag; } public function __toString() { return $this->getOpening() . $this->textContent . $this->getClosing(); } private function getOpening() { if ($this->dirty || !isset($this->originalString)) { return $this->generateOpeningTag(); } return parent::__toString(); } private function getClosing() { if ($this->closingTag) { return $this->closingTag; } if ($this->mustHaveClosing() && !$this->isFromParser()) { return 'tagName . '>'; } return ''; } private function generateOpeningTag() { $parts = ['<' . $this->tagName]; foreach ($this->getAttributes() as $name => $value) { $parts[] = $this->generateAttribute($name, $value); } return join(' ', $parts) . '>'; } private function generateAttribute($name, $value) { $result = $name; if ($value != '') { $result .= '=' . $this->quoteAttributeValue($value); } return $result; } private function quoteAttributeValue($value) { if (strpos($value, '"') === false) { return '"' . htmlspecialchars($value) . '"'; } return "'" . str_replace(['&', "'"], ['&', '''], $value) . "'"; } private function mustHaveClosing() { return !\Kibo\Phast\Parsing\HTML\HTMLInfo::isA($this->tagName, \Kibo\Phast\Parsing\HTML\HTMLInfo::VOID_TAG); } private function isFromParser() { return isset($this->originalString); } public function dumpValue() { $o = $this->tagName; foreach ($this->attributes as $name => $_) { $o .= " {$name}=\"" . $this->getAttribute($name) . '"'; } if ($this->textContent) { $o .= " content=[{$this->textContent}]"; } return $o; } } namespace Kibo\Phast\Common; class JSMinifier extends \JSMin\JSMin { protected $removeLicenseHeaders; public function __construct($input, $removeLicenseHeaders = false) { parent::__construct($input); $this->removeLicenseHeaders = $removeLicenseHeaders; } protected function consumeMultipleLineComment() { parent::consumeMultipleLineComment(); if ($this->removeLicenseHeaders) { $this->keptComment = preg_replace('~/\\*!.*?\\*/~s', '', $this->keptComment); } } } namespace Kibo\Phast\Logging\LogWriters\Dummy; class Writer implements \Kibo\Phast\Logging\LogWriter { public function setLevelMask($mask) { } public function writeEntry(\Kibo\Phast\Logging\LogEntry $entry) { } } namespace Kibo\Phast\Logging\LogWriters; abstract class BaseLogWriter implements \Kibo\Phast\Logging\LogWriter { protected $levelMask = ~0; protected abstract function doWriteEntry(\Kibo\Phast\Logging\LogEntry $entry); public function setLevelMask($mask) { $this->levelMask = $mask; } public function writeEntry(\Kibo\Phast\Logging\LogEntry $entry) { if ($this->levelMask & $entry->getLevel()) { $this->doWriteEntry($entry); } } } namespace Kibo\Phast\Services\Css; class Service extends \Kibo\Phast\Services\BaseService { protected function makeResponse(\Kibo\Phast\ValueObjects\Resource $resource, array $request) { $response = parent::makeResponse($resource, $request); $response->setHeader('Content-Type', 'text/css'); return $response; } } namespace Kibo\Phast\Services\Scripts; class Service extends \Kibo\Phast\Services\BaseService { protected function makeResponse(\Kibo\Phast\ValueObjects\Resource $resource, array $request) { $response = parent::makeResponse($resource, $request); $response->setHeader('Content-Type', 'application/javascript'); return $response; } } namespace Kibo\Phast\Services\Images; class Service extends \Kibo\Phast\Services\BaseService { protected function getParams(\Kibo\Phast\Services\ServiceRequest $request) { $params = parent::getParams($request); if ($this->proxySupportsAccept($request->getHTTPRequest())) { $params['varyAccept'] = true; if ($this->browserSupportsWebp($request->getHTTPRequest())) { $params['preferredType'] = \Kibo\Phast\Filters\Image\Image::TYPE_WEBP; \Kibo\Phast\Logging\Log::info('WebP will be served if possible!'); } } return $params; } protected function makeResponse(\Kibo\Phast\ValueObjects\Resource $resource, array $request) { $response = parent::makeResponse($resource, $request); $srcUrl = $resource->getUrl(); $response->setHeader('Link', "<{$srcUrl}>; rel=\"canonical\""); $response->setHeader('Content-Type', $resource->getMimeType()); if ($resource->getMimeType() != \Kibo\Phast\Filters\Image\Image::TYPE_PNG && @$request['varyAccept']) { $response->setHeader('Vary', 'Accept'); } return $response; } protected function validateIntegrity(\Kibo\Phast\Services\ServiceRequest $request) { if (!$this->config['images']['api-mode']) { parent::validateIntegrity($request); } } protected function validateWhitelisted(\Kibo\Phast\Services\ServiceRequest $request) { if (!$this->config['images']['api-mode']) { parent::validateWhitelisted($request); } } private function browserSupportsWebp(\Kibo\Phast\HTTP\Request $request) { return strpos($request->getHeader('accept'), 'image/webp') !== false; } private function proxySupportsAccept(\Kibo\Phast\HTTP\Request $request) { return !$request->isCloudflare(); } } namespace Kibo\PhastPlugins\SDK\AdminPanel; class DefaultInstallNoticeRenderer implements \Kibo\PhastPlugins\SDK\AdminPanel\InstallNoticeRenderer { public function render($notice, $onCloseJSFunction) { return $notice; } } namespace Kibo\PhastPlugins\SDK; interface PluginHost extends \Kibo\PhastPlugins\SDK\ServiceHost { /** * The name of the plugin used for displaying to the users * * @return string */ public function getPluginName(); /** * The name of the host system * * @return string */ public function getPluginHostName(); /** * The version of the plugin * * @return string */ public function getPluginHostVersion(); /** * Tells whether we are in production or development mode. * In development mode static files will be loaded from a dev server. * In production mode static files will be loaded from a prebuilt source. * * @return bool - TRUE for development, FALSE for production */ public function isDev(); /** * @return KeyValueStore */ public function getKeyValueStore(); /** * @return InstallNoticeRenderer */ public function getInstallNoticeRenderer(); /** * @return HostURLs */ public function getHostURLs(); /** * @return Nonce */ public function getNonce(); /** * @return NonceChecker */ public function getNonceChecker(); /** * @return PhastUser */ public function getPhastUser(); /** * Called right after phast's configuration * has been loaded. Use it to modify the config * and take any other needed action before * the filters are applied. * * @param array $config - The configuration that has been loaded * @return array - The configuration to use for phast */ public function onPhastConfigurationLoad(array $config); /** * Returns the current system locale * * @return string */ public function getLocale(); } namespace Kibo\Phast\Filters\JavaScript\Minification; class JSMinifierFilter implements \Kibo\Phast\Filters\Service\CachedResultServiceFilter { const VERSION = 2; private $removeLicenseHeaders = true; /** * JSMinifierFilter constructor. * @param bool $removeLicenseHeaders */ public function __construct($removeLicenseHeaders) { $this->removeLicenseHeaders = (bool) $removeLicenseHeaders; } public function getCacheSalt(\Kibo\Phast\ValueObjects\Resource $resource, array $request) { return http_build_query(['v' => self::VERSION, 'removeLicenseHeaders' => $this->removeLicenseHeaders]); } public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request) { $minified = (new \Kibo\Phast\Common\JSMinifier($resource->getContent(), $this->removeLicenseHeaders))->min(); return $resource->withContent($minified); } } namespace Kibo\Phast\Filters\Image\Composite; class Filter implements \Kibo\Phast\Filters\Service\CachedResultServiceFilter { use \Kibo\Phast\Logging\LoggingTrait; /** * @var ImageFactory */ private $imageFactory; /** * @var ImageInliningManager */ private $inliningManager; /** * @var ImageFilter[] */ private $filters = array(); /** * Filter constructor. * @param ImageFactory $imageFactory * @param ImageInliningManager $inliningManager */ public function __construct(\Kibo\Phast\Filters\Image\ImageFactory $imageFactory, \Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageInliningManager $inliningManager) { $this->imageFactory = $imageFactory; $this->inliningManager = $inliningManager; } public function addImageFilter(\Kibo\Phast\Filters\Image\ImageFilter $filter) { $this->filters[] = $filter; } public function getCacheSalt(\Kibo\Phast\ValueObjects\Resource $resource, array $request) { $filters = array_map('get_class', $this->filters); $salts = array_map(function (\Kibo\Phast\Filters\Image\ImageFilter $filter) use($request) { return $filter->getCacheSalt($request); }, $this->filters); return implode("\n", array_merge($filters, $salts, [$this->inliningManager->getMaxImageInliningSize(), $resource->getUrl(), $resource->getCacheSalt()])); } /** * @param Resource $resource * @param array $request * @return Resource */ public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request) { $image = $this->imageFactory->getForResource($resource); $filteredImage = $image; foreach ($this->filters as $filter) { $this->logger()->info('Applying {filter}', ['filter' => get_class($filter)]); try { $filteredImage = $filter->transformImage($filteredImage, $request); } catch (\Kibo\Phast\Filters\Image\Exceptions\ImageProcessingException $e) { $message = 'Image filter exception: Filter: {filter} Exception: {exceptionClass} Msg: {message} Code: {code} File: {file} Line: {line}'; $this->logger()->critical($message, ['filter' => get_class($filter), 'exceptionClass' => get_class($e), 'message' => $e->getMessage(), 'code' => $e->getCode(), 'file' => $e->getFile(), 'line' => $e->getLine()]); } } $sizeBefore = $filteredImage->getSizeAsString(); $sizeAfter = $image->getSizeAsString(); $sizeDifference = $sizeBefore - $sizeAfter; $this->logger()->info('Image processed. Size before/after: {sizeBefore}/{sizeAfter} ({sizeDifference})', ['sizeBefore' => $sizeBefore, 'sizeAfter' => $sizeAfter, 'sizeDifference' => $sizeDifference < 0 ? $sizeDifference : "+{$sizeDifference}"]); if ($sizeDifference < 0) { $this->logger()->info('Return filtered image and save {sizeDifference} bytes', ['sizeDifference' => -$sizeDifference]); $image = $filteredImage; } else { $this->logger()->info('Return original image'); } $processedResource = $resource->withContent($image->getAsString(), $image->getType()); $this->inliningManager->maybeStoreForInlining($processedResource); return $processedResource; } } namespace Kibo\Phast\Logging\LogWriters\JSONLFile; class Writer extends \Kibo\Phast\Logging\LogWriters\BaseLogWriter { use \Kibo\Phast\Logging\Common\JSONLFileLogTrait; /** * @param LogEntry $entry */ protected function doWriteEntry(\Kibo\Phast\Logging\LogEntry $entry) { $encoded = @\Kibo\Phast\Common\JSON::encode($entry->toArray()); if ($encoded) { $this->makeDirIfNotExists(); @file_put_contents($this->filename, $encoded . "\n", FILE_APPEND | LOCK_EX); } } private function makeDirIfNotExists() { if (!@file_exists($this->dir)) { @mkdir($this->dir, 0777, true); } } } namespace Kibo\Phast\Logging\LogWriters\Composite; class Writer extends \Kibo\Phast\Logging\LogWriters\BaseLogWriter { /** * @var Writer[] */ private $writers = array(); public function addWriter(\Kibo\Phast\Logging\LogWriter $writer) { $this->writers[] = $writer; } protected function doWriteEntry(\Kibo\Phast\Logging\LogEntry $entry) { foreach ($this->writers as $writer) { $writer->writeEntry($entry); } } } namespace Kibo\Phast\Logging\LogWriters\RotatingTextFile; class Writer extends \Kibo\Phast\Logging\LogWriters\BaseLogWriter { /** @var string */ private $path = 'phast.log'; /** @var int */ private $maxFiles = 2; /** @var int */ private $maxSize = 10 * 1024 * 1024; /** @var ObjectifiedFunctions */ private $funcs; /** * @param array $config * @param ?ObjectifiedFunctions $funcs */ public function __construct(array $config, \Kibo\Phast\Common\ObjectifiedFunctions $funcs = null) { if (isset($config['path'])) { $this->path = (string) $config['path']; } if (isset($config['maxFiles'])) { $this->maxFiles = (int) $config['maxFiles']; } if (isset($config['maxSize'])) { $this->maxSize = (int) $config['maxSize']; } $this->funcs = is_null($funcs) ? new \Kibo\Phast\Common\ObjectifiedFunctions() : $funcs; } protected function doWriteEntry(\Kibo\Phast\Logging\LogEntry $entry) { if (!($this->levelMask & $entry->getLevel())) { return; } $message = $this->interpolate($entry->getMessage(), $entry->getContext()); $line = sprintf("%s %s %s\n", gmdate('Y-m-d\\TH:i:s\\Z', $this->funcs->time()), \Kibo\Phast\Logging\LogLevel::toString($entry->getLevel()), $message); clearstatcache(true, $this->path); $this->rotate(strlen($line)); file_put_contents($this->path, $line, FILE_APPEND); } private function interpolate($message, $context) { $prefix = ''; $prefixKeys = ['requestId', 'service', 'class', 'method', 'line']; foreach ($prefixKeys as $key) { if (isset($context[$key])) { $prefix .= '{' . $key . "}\t"; } } return preg_replace_callback('/{(.+?)}/', function ($match) use($context) { return array_key_exists($match[1], $context) ? $context[$match[1]] : $match[0]; }, $prefix . $message); } private function rotate($bufferSize) { if (!$this->shouldRotate($bufferSize)) { return; } if (!($fp = fopen($this->path, 'r+'))) { return; } try { if (!flock($fp, LOCK_EX | LOCK_NB)) { return; } if (!$this->shouldRotate($bufferSize)) { return; } for ($i = $this->maxFiles - 1; $i > 0; $i--) { @rename($this->getName($i - 1), $this->getName($i)); } } finally { fclose($fp); } } private function getName($index) { if ($index <= 0) { return $this->path; } return $this->path . '.' . $index; } private function shouldRotate($bufferSize) { $currentSize = @filesize($this->path); if (!$currentSize) { return false; } $newSize = $currentSize + $bufferSize; return $newSize > $this->maxSize; } } namespace Kibo\Phast\Logging\LogWriters\PHPError; class Writer extends \Kibo\Phast\Logging\LogWriters\BaseLogWriter { private $messageType = 0; private $destination = null; private $extraHeaders = null; /** * @var ObjectifiedFunctions */ private $funcs; /** * PHPErrorLogWriter constructor. * @param array $config * @param ObjectifiedFunctions $funcs */ public function __construct(array $config, \Kibo\Phast\Common\ObjectifiedFunctions $funcs = null) { foreach (['messageType', 'destination', 'extraHeaders'] as $field) { if (isset($config[$field])) { $this->{$field} = $config[$field]; } } $this->funcs = is_null($funcs) ? new \Kibo\Phast\Common\ObjectifiedFunctions() : $funcs; } protected function doWriteEntry(\Kibo\Phast\Logging\LogEntry $entry) { if ($this->levelMask & $entry->getLevel()) { $this->funcs->error_log($this->interpolate($entry->getMessage(), $entry->getContext()), $this->messageType, $this->destination, $this->extraHeaders); } } private function interpolate($message, $context) { $prefix = ''; $prefixKeys = ['requestId', 'service', 'class', 'method', 'line']; foreach ($prefixKeys as $key) { if (isset($context[$key])) { $prefix .= '{' . $key . "}\t"; } } return preg_replace_callback('/{(.+?)}/', function ($match) use($context) { return array_key_exists($match[1], $context) ? $context[$match[1]] : $match[0]; }, $prefix . $message); } }