licenseKey = $licenseKey; $this->rootDir = $rootDir; $this->parse(); $this->ignoreParams(self::IG_PARAMS); $this->cacheFile = new Cache($this->getURL(), $this->rootDir); if (empty($licenseKey)) { $this->ignoreRequest('disconnected'); } $this->platform = [ 'plugin_cms' => 'php-sdk', 'cms_v' => defined('PHP_VERSION') ? PHP_VERSION : '', 'plugin_v' => '1.0.13' ]; } public function setDebug($debug) { $this->debug = $debug; $this->cacheFile->setDebug($this->debug); } public function getURL() { return $this->requestURL; } public function ignoreRequest($reason = 'default') { //replace newlines form reason text to avoid issues in appendFooter $reason = str_replace(array("\n", "\r"), '', $reason); $this->ignoreRead = true; $this->ignoreWrite = true; $this->ignoreReason = $reason; Util::sendHeader('x-rl-skip: ' . $this->ignoreReason, true); } public function skipForCookies($cookieNames) { if (!empty($cookieNames)) { foreach ($cookieNames as $c) { if (isset($_COOKIE[$c])) { $this->ignoreRequest("cookie-$c"); break; } } } } public function skipForPaths($patterns) { if (empty($patterns) || empty($this->requestURI)) { return; } #fnmatch(): the maximum allowed length for filename is 4096 characters $uriRaw = substr($this->requestURI, 0, 4096); $uriDecoded = substr(rawurldecode($this->requestURI), 0, 4096); // PHP 8 throws ValueError when fnmatch() receives a null byte. if (strpos($uriRaw, "\0") !== false || strpos($uriDecoded, "\0") !== false) { $this->ignoreRequest('malformed-uri'); return; } foreach ($patterns as $path_pattern) { if (empty($path_pattern)) { continue; } $path_pattern = trim($path_pattern); if ($path_pattern === '' || strpos($path_pattern, "\0") !== false) { continue; } $patterns_to_match = [$path_pattern]; if (!$this->patternContainsWildcard($path_pattern)) { $alternate_pattern = $this->getAlternateTrailingSlashPattern($path_pattern); if ($alternate_pattern !== $path_pattern) { $patterns_to_match[] = $alternate_pattern; } } foreach ($patterns_to_match as $candidate_pattern) { if (fnmatch($candidate_pattern, $uriRaw) || fnmatch($candidate_pattern, $uriDecoded)) { $this->ignoreRequest("skip-path-$path_pattern"); break 2; } } } } private function patternContainsWildcard($pattern) { return strpbrk($pattern, '*?[') !== false; } private function getAlternateTrailingSlashPattern($pattern) { if ($pattern === '/') { return $pattern; } $query = ''; $query_position = strpos($pattern, '?'); if ($query_position !== false) { $query = substr($pattern, $query_position); $pattern = substr($pattern, 0, $query_position); } if ($pattern === '') { return $query === '' ? $pattern : $pattern . $query; } if (substr($pattern, -1) === '/') { $pattern = rtrim($pattern, '/'); } else { $pattern .= '/'; } if ($pattern === '') { $pattern = '/'; } return $pattern . $query; } public function ignoreParams($paramNames) { if (empty($paramNames)) { return; } $parsed_url = parse_url($this->requestURL); $query = empty($parsed_url['query']) ? '' : trim($parsed_url['query']); if (!empty($query)) { try { parse_str($query, $qs_vars); if (!empty($paramNames)) { foreach ($paramNames as $p) { unset($qs_vars[trim($p)]); } } $query = http_build_query($qs_vars); $this->requestURI = trim(@$parsed_url['path']) . (empty($query) ? '' : '?' . $query);; $host = trim(@$parsed_url['host']); $scheme = trim(@$parsed_url['scheme']); $this->requestURL = $scheme . '://' . $host . $this->requestURI; } catch (\Throwable $e) { Exc::catch($e); } } } /** * @param array $ignore_params pass array containing the params to be ignored for caching. */ private function parse() { $rm = Util::getRequestMethod(); if (strcasecmp($rm, 'get') !== 0) { $this->ignoreRequest("request-method-$rm"); return; } //process request if (isset($_SERVER['REQUEST_URI'])) { list($urlpart, $qspart) = array_pad(explode('?', $_SERVER['REQUEST_URI']), 2, ''); parse_str($qspart, $qsvars); $varsLenO = count($qsvars); if (isset($qsvars['rl-no-optimization']) || isset($_SERVER['HTTP_RL_NO_OPTIMIZATION']) || isset($_SERVER['HTTP_RL_CSS'])) { unset($qsvars['rl-no-optimization']); $this->isNoOptimization = true; $this->ignoreRequest("no-optimization"); unset($_GET['rl-no-optimization']); } if (isset($qsvars['norl'])) { unset($qsvars['norl']); $this->isNoOptimization = true; $this->ignoreRequest("no-optimization"); unset($_GET['norl']); } if (isset($qsvars['rl-warmup'])) { unset($qsvars['rl-warmup']); $this->ignoreRead = true; $this->isWarmup = true; unset($_GET['rl-warmup']); } if (isset($qsvars['rltest'])) { $this->rlTest = true; unset($qsvars['rltest']); unset($_GET['rltest']); } if (isset($qsvars['rl-rand'])) { unset($qsvars['rl-rand']); unset($_GET['rl-rand']); } if (isset($qsvars['rl-only-after'])) { $this->onlyAfter = ($qsvars['rl-only-after'] / 1000); unset($qsvars['rl-only-after']); unset($_GET['rl-only-after']); } $varsLenM = count($qsvars); if ($varsLenO != $varsLenM) { $newqs = http_build_query($qsvars); $_SERVER['REQUEST_URI'] = $urlpart . (empty($newqs) ? '' : '?' . $newqs); } } $request_uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/'; $http_host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ''; if (empty($http_host)) { $http_host = 'localhost'; $this->ignoreRequest("missing-host"); } $raw_link = ($this->isHTTPS() ? "https" : "http") . "://$http_host$request_uri"; $parsed_url = parse_url($raw_link); $query = empty($parsed_url['query']) ? '' : trim($parsed_url['query']); if (empty($parsed_url['path'])) { $parsed_url['path'] = ''; } if (empty($parsed_url['host'])) { $parsed_url['host'] = 'localhost'; } if (empty($parsed_url['scheme'])) { $parsed_url['scheme'] = ''; } $this->requestURI = trim($parsed_url['path']) . (empty($query) ? '' : '?' . $query);; $host = trim($parsed_url['host']); $scheme = trim($parsed_url['scheme']); $this->requestURL = $scheme . '://' . $host . $this->requestURI; } private function isHTTPS() { return (isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443) || (isset($_SERVER['HTTPS']) && strcmp($_SERVER['HTTPS'], "off") !== 0); } private function serve() { if ($this->isWarmup) { if ($this->cacheFile->fresh(Cache::TTL_LONG, $this->onlyAfter)) { if (!empty($this->purgeCallback)) { call_user_func_array($this->purgeCallback, [$this->getURL()]); } Util::sendHeader('x-rl-cache: fresh', true); exit; } else { Util::sendHeader('x-rl-cache: stale', true); } } else { if ($this->meMode && !$this->rlTest) { Util::sendHeader('x-rl-skip: me-mode', true); } else if ($this->ignoreRead) { Util::sendHeader('x-rl-skip: ' . $this->ignoreReason, true); } else { if ($this->cacheFile->serve()) { exit; } else { Util::sendHeader('x-rl-cache: miss', true); } } } if ($this->isNoOptimization || $this->isWarmup) { Util::sendHeader('Cache-Control: no-store, no-cache, must-revalidate, max-age=0', true); Util::sendHeader('Cache-Control: post-check=0, pre-check=0', false); Util::sendHeader('Pragma: no-cache', true); } ob_start([$this, 'addFooter']); } /** * Saves buffer and append footer to all requests */ public function addFooter($buffer) { $code = http_response_code(); if ($code != 200) { $this->ignoreRequest('status-' . $code); } $validBuffer = is_string($buffer); if ($this->debug) { $bufferLen = $validBuffer ? strlen($buffer) : 0; $level = ob_get_level(); Util::sendHeader("x-rl-buffer: LN:$bufferLen LV:$level", false); } if ($validBuffer) { try { $bom = pack('H*', 'EFBBBF'); if ($bom !== false) { $buffer = preg_replace("/^($bom)*/", '', $buffer); } $headersList = headers_list(); $headers = []; $contentType = NULL; foreach ($headersList as $h) { $p = explode(':', $h, 2); $headers[trim($p[0])] = [trim($p[1])]; if (strcasecmp(trim($p[0]), 'content-type') === 0) { $contentType = $p[1]; } } $isHtml = ($contentType && stripos($contentType, 'text/html') !== false); $isAmp = preg_match("/)/", $buffer); if ($isHtml && !$isAmp) { $this->appendFooter($buffer); if ($this->isWarmup && !$this->ignoreWrite) { $this->cacheFile->save(Cache::TTL_SHORT, $buffer, $headers); $this->refresh($this->requestURL, true); } } if ($this->debug) { Util::sendHeader("x-rl-page: H:$isHtml A:$isAmp", false); } } catch (\Throwable $e) { if ($this->debug) { $buffer = $e->getMessage(); } Exc::catch($e); } catch (\Exception $e) { if ($this->debug) { $buffer = $e->getMessage(); } Exc::catch($e); } } return $buffer; } private function appendFooter(&$buffer) { $appended = Util::append($buffer, ''); if ($this->debug) { Util::sendHeader("x-rl-footer: $appended", false); } } public function process() { try { $this->serve(); } catch (\Throwable $e) { Exc::catch($e); } catch (\Exception $e) { Exc::catch($e); } } private function refresh($url, $force) { if ($this->cacheFile->get429()) { Util::sendHeader('x-rl-429: 1', true); return; } $api = new API($this->licenseKey, $this->platform); $api->setDebug($this->debug); //send HB before refresh to unblock previous un-installation if any if ($this->cacheFile->collectGarbage(strtotime('-15 minutes'))) { Util::sendHeader('x-rl-hb-pre: 1', true); $api->heartbeat(); Util::sendHeader('x-rl-hb-post: 1', true); } $response = $api->refreshV2($this->cacheFile, $url, $force); $resJson = json_encode($response); if ($resJson) { //print resJson string if encode success Util::sendHeader('x-rl-debug-refresh1:' . $resJson, true); } else { //print raw response if encode fails Util::sendHeader('x-rl-debug-refresh2:' . $response, true); } if (!empty($response['saved'])) { if (!empty($this->purgeCallback)) { call_user_func_array($this->purgeCallback, [$url]); } Util::sendHeader('x-rl-refresh-saved: 1', true); } else { if (!empty($response['message'])) { Util::sendHeader('x-rl-debug-refresh3:' . $resJson, true); if (in_array($response['message'], self::EC429)) { $this->cacheFile->set429(); } if ($response['message'] === 'BQE' || $response['message'] === 'EC224') { $this->cacheFile->deleteAll(); } } else { Util::sendHeader('x-rl-res-msg-empty: 1', true); } } exit; } public function setVariant($variant) { $this->cacheFile->setVariant($variant); } public function isWarmUp() { return $this->isWarmup; } public function registerPurgeCallback($cb) { $this->purgeCallback = $cb; } public function setMeMode() { return $this->meMode = true; } public function setPlatform($data) { if (is_array($data)) { foreach ($data as $key => $val) { $this->platform[$key] = $val; } } return; } }