PluginProbe ʕ •ᴥ•ʔ
NitroPack – Performance, Page Speed & Cache Plugin for Core Web Vitals, CDN & Image Optimization / 1.19.6
NitroPack – Performance, Page Speed & Cache Plugin for Core Web Vitals, CDN & Image Optimization v1.19.6
1.19.8 1.19.7 1.19.6 1.19.5 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.11.0 1.12.0 1.13.0 1.14.0 1.15.0 1.15.1 1.15.2 1.15.3 1.16.0 1.16.1 1.16.2 1.16.3 1.16.4 1.16.5 1.16.6 1.16.7 1.16.8 1.17.0 1.17.6 1.17.7 1.17.8 1.17.9 1.18.0 1.18.1 1.18.2 1.18.3 1.18.4 1.18.5 1.18.6 1.18.7 1.18.8 1.18.9 1.19.0 1.19.1 1.19.2 1.19.3 1.19.4 1.3.19 1.3.20 1.4.0 1.4.1 1.5.0 1.5.1 1.5.10 1.5.11 1.5.12 1.5.13 1.5.14 1.5.15 1.5.16 1.5.17 1.5.18 1.5.19 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.7.0 1.7.1 1.8.0 1.8.1 1.8.3 1.9.0 1.9.1 1.9.2
nitropack / nitropack-sdk / NitroPack / SDK / NitroPack.php
nitropack / nitropack-sdk / NitroPack / SDK Last commit date
Api 8 months ago Integrations 1 month ago StorageDriver 1 year ago Url 1 year ago Utils 1 year ago data 1 year ago Api.php 1 year ago Backlog.php 1 year ago BacklogReplayTimeoutException.php 1 year ago ChallengeProcessingException.php 1 year ago ChallengeVerificationException.php 1 year ago ConfigFetcherException.php 1 year ago Crypto.php 1 year ago Device.php 1 year ago DeviceType.php 1 year ago ElementRevision.php 1 year ago EmptyConfigException.php 1 year ago ExcludeEntry.php 1 year ago ExcludeOperationType.php 1 year ago FileHandle.php 1 year ago Filesystem.php 1 year ago HealthStatus.php 1 year ago IntegrationUrl.php 1 year ago NitroPack.php 1 month ago NoConfigException.php 1 year ago Pagecache.php 5 months ago PurgeType.php 1 year ago ServiceDownException.php 1 year ago StorageException.php 1 year ago VariationCookieException.php 1 year ago WebhookException.php 1 year ago Website.php 1 year ago
NitroPack.php
1263 lines
1 <?php
2 namespace NitroPack\SDK;
3
4 use \NitroPack\Url\Url;
5 use \NitroPack\SDK\Url\Embedjs;
6
7 class NitroPack {
8 const VERSION = '0.56.3';
9 const PAGECACHE_LOCK_EXPIRATION_TIME = 300; // in seconds
10 private $dataDir;
11 private $cachePath = array('data', 'pagecache');
12 private $configFile = array('data', 'config.json');
13 private $healthStatusFile = array('data', 'service-health');
14 private $timestampFile = array('data', 'time.mark');
15 private $pageCacheLockFile = array('data', 'get_cache.lock');
16 private $statefulCacheRevisionsFile = array('data', 'element-revision.json');
17 private $cachePathSuffix = NULL;
18 private $configTTL; // In seconds
19
20 private $siteId;
21 private $siteSecret;
22 private $userAgent; // Defaults to desktop Chrome
23
24 private $url;
25 private $config;
26 private $device;
27 private $api;
28 private $varnishProxyCacheHeaders = [];
29 private $referer;
30
31 public $backlog;
32 public $elementRevision;
33 public $healthStatus;
34 public $pageCache; // TODO: consider better ways of protecting/providing this outside the class
35 public $useCompression;
36
37 private static $cachePrefixes = array();
38 private static $cookieFilters = array();
39
40 public static function getRemoteAddr() {
41 // IP check order is: CloudFlare, Proxy, Client IP
42 $ipKeys = ["HTTP_X_FORWARDED_FOR", "HTTP_CF_CONNECTING_IP", "REMOTE_ADDR"];
43 foreach ($ipKeys as $key) {
44 if (!empty($_SERVER[$key])) {
45 return $_SERVER[$key];
46 }
47 }
48
49 return NULL;
50 }
51
52 public static function getCookies() {
53 $cookies = array();
54
55 foreach ($_COOKIE as $name=>$value) {
56 if (is_array($value)) {
57 foreach ($value as $k=>$v) {
58 $key = $name . "[$k]";
59 $cookies[$key] = $v;
60 }
61 } else {
62 $cookies[$name] = $value;
63 }
64 }
65
66 foreach (self::$cookieFilters as $cookieFilter) {
67 call_user_func_array($cookieFilter, array(&$cookies));
68 }
69
70 return $cookies;
71 }
72
73 public static function addCookieFilter($callback) {
74 if (is_callable($callback)) {
75 if (!in_array($callback, self::$cookieFilters, true)) {
76 self::$cookieFilters[] = $callback;
77 }
78 } else {
79 throw new \RuntimeException("Non-callable callback passed to " . __FUNCTION__);
80 }
81 }
82
83 public static function getExternalCustomPrefix() {
84 return !empty($_SERVER["HTTP_X_NITRO_CACHE_PREFIX"]) ? $_SERVER["HTTP_X_NITRO_CACHE_PREFIX"] : NULL;
85 }
86
87 public static function addCustomCachePrefix($prefix = "") {
88 if (!in_array($prefix, self::$cachePrefixes)) {
89 self::$cachePrefixes[] = $prefix;
90 }
91 }
92
93 public static function getCustomCachePrefix() {
94 return implode("-", self::$cachePrefixes);
95 }
96
97 public static function wildcardToRegex($str, $delim = "/") {
98 return implode(".*?", array_map(function($input) use ($delim) { return preg_quote($input, $delim); }, explode("*", $str)));
99 }
100
101 private function nitro_parse_str($qStr, &$resArr) {
102 if (strpos($qStr, '?') !== false) {
103 $tmpArr = explode('?', $qStr, 2);
104 parse_str($tmpArr[0], $resArr);
105 $completeValue = end($resArr) . $tmpArr[1];
106 $resArr[key($resArr)] = $completeValue;
107 reset($resArr);
108 } else {
109 parse_str($qStr, $resArr);
110 }
111 }
112
113 public function __construct($siteId, $siteSecret, $userAgent = NULL, $url = NULL, $dataDir = __DIR__, $referer = NULL) {
114 $this->configTTL = 3600;
115 $this->siteId = $siteId;
116 $this->siteSecret = $siteSecret;
117 $this->dataDir = $dataDir;
118 $this->referer = $referer;
119 $this->backlog = new Backlog($dataDir, $this);
120 $this->elementRevision = new ElementRevision($siteId, $this->getStatefulCacheRevisionFile());
121 $this->healthStatus = HealthStatus::HEALTHY;
122 $this->loadHealthStatus();
123
124 if (empty($userAgent)) {
125 $this->userAgent = !empty($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36';
126 } else {
127 $this->userAgent = $userAgent;
128 }
129
130 $this->loadConfig($siteId, $siteSecret);
131 $this->device = new Device($this->userAgent);
132
133 if(empty($url)) {
134 $host = !empty($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : "example.com";
135 $uri = !empty($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : "/";
136 $url = $this->getScheme() . $host . $uri;
137 }
138
139 $queryStr = parse_url($url, PHP_URL_QUERY);
140
141 if ($queryStr) {
142 $this->nitro_parse_str($queryStr, $queryParams);
143
144 if ($queryParams) {
145 if ($this->config->IgnoredParams) {
146 foreach ($this->config->IgnoredParams as $ignorePattern) {
147 $regex = "/^" . self::wildcardToRegex($ignorePattern) . "$/";
148 foreach($queryParams as $paramName => $paramValue) {
149 if (preg_match($regex, $paramName)) {
150 unset($queryParams[$paramName]);
151 }
152 }
153 }
154 }
155
156 ksort($queryParams);
157 $url = str_replace($queryStr, http_build_query($queryParams), $url);
158 }
159 }
160
161 $urlInfo = new Url($url);
162 $this->url = $urlInfo->getNormalized();
163
164 $this->pageCache = new Pagecache($this->url, $this->userAgent, $this->supportedCookiesFilter(self::getCookies()), $this->config->PageCache->SupportedCookies, $this->isAJAXRequest());
165 $this->pageCache->setCookiesProvider([$this, "getPagecacheCookies"]);
166
167 if ($this->config->PageCache->Geot->Status) {
168 $this->pageCache->setGeotVariations($this->config->PageCache->Geot->Variations);
169 }
170
171 if (!empty($this->config->UseNotModifiedHeader)) {
172 $this->pageCache->enableNotModifiedHeader();
173 }
174
175 if ($this->isAJAXRequest() && $this->isAllowedAJAXUrl($this->url) && !empty($_SERVER["HTTP_REFERER"]) && !$this->isAllowedStandaloneAJAXUrl($url)) {
176 $refererInfo = new Url($_SERVER["HTTP_REFERER"]);
177 $this->pageCache->setReferer($refererInfo->getNormalized());
178 }
179
180 if ($this->isServiceRequest()) {
181 $customPrefix = self::getExternalCustomPrefix();
182 if ($customPrefix) {
183 self::addCustomCachePrefix($customPrefix);
184 }
185 } else {
186 if (!!$this->config->LanguageVary->Status) {
187 $lang = $this->config->LanguageVary->DefaultLanguage;
188 $clientLanguagePreference = !empty($_SERVER["HTTP_ACCEPT_LANGUAGE"]) ? $_SERVER["HTTP_ACCEPT_LANGUAGE"] : "";
189 $languagePreferences = $this->parseLanguagePreferences($clientLanguagePreference);
190 $possibilities = [];
191 foreach ($languagePreferences as $pref) {
192 list($langCode, $langVariant, $weight) = $pref;
193
194 if ($langCode === "*") {
195 $langTries = [$this->config->LanguageVary->DefaultLanguage];
196 } else {
197 $langTries = ["$langCode-$langVariant", $langCode];
198 }
199
200 foreach ($langTries as $langTry) {
201 if (in_array($langTry, $this->config->LanguageVary->AvailableLanguages)) {
202 $possibilities[] = [$langTry, $weight];
203 }
204 }
205 }
206
207 if (!empty($possibilities)) {
208 usort($possibilities, function($a, $b) {
209 return $a[1] < $b[1] ? 1 : -1;
210 });
211 $lang = $possibilities[0][0];
212 }
213
214 self::addCustomCachePrefix("lang_$lang");
215 }
216 }
217
218 if (!empty($this->config->URLPathVersion)) {
219 $this->pageCache->setUrlPathVersion($this->config->URLPathVersion);
220 }
221
222
223 $this->api = new Api($this->siteId, $siteSecret);
224 $this->api->setBacklog($this->backlog);
225 $this->api->setNitroPack($this);
226
227 $this->pageCache->setDataDir($this->getCacheDir());
228
229 $this->useCompression = false;
230 }
231
232 private function parseLanguagePreferences($languagePreferences) {
233 $preferenceParts = array_filter(array_map("trim", explode(",", $languagePreferences)));
234 $preferences = [];
235 foreach ($preferenceParts as $lang) {
236 // Parse variants of the form en-US;q=0.8, en-US, en;q=0.8, en, *;q=0.8, *
237 if (preg_match("/^(?<code>[a-z\*]+)(-(?<variant>[A-Z]+))?(;q=(?<weight>[\d\.]+))?$/", $lang, $matches)) {
238 $langCode = $matches["code"];
239 $langVariant = !empty($matches["variant"]) ? $matches["variant"] : "";
240 $weight = !empty($matches["weight"]) ? (float)$matches["weight"] : 1;
241 $preferences[] = [$langCode, $langVariant, $weight];
242 }
243 }
244
245 return $preferences;
246 }
247
248 public function setReferer($referer) {
249 $refererInfo = new Url($referer);
250 $this->referer = $refererInfo->getNormalized();
251 $this->pageCache->setReferer($this->referer);
252 }
253
254 public function getPagecacheCookies() {
255 return $this->supportedCookiesFilter(self::getCookies());
256 }
257
258 public function supportedCookiesFilter($cookies) {
259 $supportedCookies = array();
260 foreach ($cookies as $cookieName=>$cookieValue) {
261 foreach ($this->config->PageCache->SupportedCookies as $cookie) {
262 if (preg_match('/^' . self::wildcardToRegex($cookie) . '$/', $cookieName)) {
263 $supportedCookies[$cookieName] = $cookieValue;
264 }
265 }
266 }
267 return $supportedCookies;
268 }
269
270 public function tagUrl($url, $tag) {
271 if ($this->isAllowedUrl($url)) {
272 return $this->api->tagUrl($url, $tag);
273 } else {
274 return false;
275 }
276 }
277
278 public function setCachePathSuffix($suffix) {
279 $this->cachePathSuffix = $suffix;
280 $this->pageCache->setDataDir($this->getCacheDir());
281 }
282
283 public function enableCompression() {
284 $this->pageCache->enableCompression();
285 }
286
287 public function disableCompression() {
288 $this->pageCache->disableCompression();
289 }
290
291 public function getUrl() {
292 return $this->url;
293 }
294
295 public function getApi() {
296 return $this->api;
297 }
298
299 public function getSiteId() {
300 return $this->siteId;
301 }
302
303 public function getConfig() {
304 return $this->config;
305 }
306
307 public function getCacheDir() {
308 $cachePath = $this->cachePath;
309 array_unshift($cachePath, $this->dataDir);
310 if ($this->cachePathSuffix) {
311 $cachePath[] = $this->cachePathSuffix;
312 }
313 return Filesystem::getOsPath($cachePath);
314 }
315
316 public function getStatefulCacheRevisionFile() {
317 $revisionFile = $this->statefulCacheRevisionsFile;
318 array_unshift($revisionFile, $this->dataDir);
319 return Filesystem::getOsPath($revisionFile);
320 }
321
322 public function getHealthStatus() {
323 return $this->healthStatus;
324 }
325
326 public function getHealthStatusFile() {
327 $healthStatusFile = $this->healthStatusFile;
328 array_unshift($healthStatusFile, $this->dataDir);
329 return Filesystem::getOsPath($healthStatusFile);
330 }
331
332 public function setHealthStatus($status) {
333 $this->healthStatus = $status;
334 Filesystem::filePutContents($this->getHealthStatusFile(), $status);
335 }
336
337 public function loadHealthStatus() {
338 if (defined("NITROPACK_DISABLE_BACKLOG")) return;
339 if (Filesystem::fileExists($this->getHealthStatusFile())) {
340 $this->healthStatus = Filesystem::fileGetContents($this->getHealthStatusFile());
341 } else {
342 $this->healthStatus = HealthStatus::HEALTHY;
343 }
344 }
345
346 public function checkHealthStatus() {
347 try {
348 // TODO: Potentially replace this with a dedicated method in the API
349 $this->fetchConfig(true);
350 $this->setHealthStatus(HealthStatus::HEALTHY);
351 return HealthStatus::HEALTHY;
352 } catch (\Exception $e) {
353 $this->setHealthStatus(HealthStatus::SICK);
354 return HealthStatus::SICK;
355 }
356 }
357
358 public function getTimestampFile() {
359 $timestampFile = $this->timestampFile;
360 array_unshift($timestampFile, $this->dataDir);
361 return Filesystem::getOsPath($timestampFile);
362 }
363
364 public function loadTimeMarks() {
365 if (Filesystem::fileExists($this->getTimestampFile())) {
366 try {
367 $marks = @json_decode(Filesystem::fileGetContents($this->getTimestampFile()), true);
368 if (!$marks) {
369 $marks = [];
370 }
371 } catch (\Exception $e) {
372 $marks = [];
373 }
374 } else {
375 $marks = [];
376 }
377
378 return $marks;
379 }
380
381 public function setTimeMark($name, $time = NULL) {
382 if ($time === NULL) $time = microtime(true);
383 $marks = $this->loadTimeMarks();
384 $marks[$name] = $time;
385 Filesystem::filePutContents($this->getTimestampFile(), json_encode($marks));
386 }
387
388 public function unsetTimeMark($name) {
389 $marks = $this->loadTimeMarks();
390 if (isset($marks[$name])) {
391 unset($marks[$name]);
392 }
393 Filesystem::filePutContents($this->getTimestampFile(), json_encode($marks));
394 }
395
396 public function getTimeMark($name) {
397 $marks = $this->loadTimeMarks();
398 if (!empty($marks[$name])) {
399 return $marks[$name];
400 } else {
401 return NULL;
402 }
403 }
404
405 public function getRemainingCacheTtl() {
406 return $this->pageCache->getRemainingTtl($this->config->PageCache->ExpireTime);
407 }
408
409 public function hasCache($layout = 'default') {
410 if ($this->hasLocalCache()) {
411 return true;
412 } else {
413 return $this->hasRemoteCache($layout);
414 }
415 }
416
417 public function hasLocalCache($checkIfRequestIsAllowed = true) {
418 if ($this->backlog->exists()) return false;
419 if (!$this->isAllowedUrl($this->url) || ($checkIfRequestIsAllowed && !$this->isAllowedRequest())) return false;
420 $cacheRevision = !empty($this->config->RevisionHash) ? $this->config->RevisionHash : NULL;
421 if ($this->getHealthStatus() !== HealthStatus::HEALTHY) {
422 return false;
423 }
424
425 if (!$this->pageCache->getUseInvalidated()) {
426 $ttl = $this->config->PageCache->ExpireTime;
427 } else {
428 $ttl = $this->config->PageCache->StaleExpireTime;
429 }
430
431 return $this->pageCache->hasCache() && !$this->pageCache->hasExpired($ttl, $cacheRevision);
432 }
433
434 public function hasRemoteCache($layout, $checkIfRequestIsAllowed = true) {
435 if ($this->backlog->exists()) return false;
436
437 if (
438 !$this->isAllowedUrl($this->url) ||
439 ($checkIfRequestIsAllowed && !$this->isAllowedRequest()) ||
440 ($this->pageCache->getParent() && !$this->pageCache->getParent()->hasCache())
441 ) return false;
442
443 $isWarmupRequest = !empty($_SERVER["HTTP_X_NITRO_WARMUP"]);
444 $isPageCacheLocked = $this->isPageCacheLocked();
445
446 if ($isPageCacheLocked && !$isWarmupRequest) {
447 return false;
448 }
449
450 $resp = $this->api->getCache($this->url, $this->userAgent, $this->supportedCookiesFilter(self::getCookies()), $this->isAJAXRequest(), $layout, $this->referer);
451
452 if ($isPageCacheLocked) {
453 return false;
454 }
455
456 if ($resp->getStatus() == Api\ResponseStatus::OK) {// We have cache response
457
458 // Check for invalidated cache and delete it if such is found
459 $this->pageCache->useInvalidated(true);
460 if ($this->pageCache->hasCache()) {
461 $path = $this->pageCache->getCachefilePath();
462 Filesystem::deleteFile($path);
463 Filesystem::deleteFile($path . ".gz");
464 Filesystem::deleteFile($path . ".stale");
465 Filesystem::deleteFile($path . ".stale.gz");
466 if (Filesystem::isDirEmpty(dirname($path))) {
467 Filesystem::deleteDir(dirname($path));
468 }
469 }
470 $this->pageCache->useInvalidated(false);
471 // End of check
472
473 list($headers, $content) = Filesystem::explodeByHeaders($resp->getBody());
474 $this->pageCache->setContent($content, $headers);
475 return true;
476 } else {
477 // The goal is to serve cache at all times even when it is slightly outdated. This approach should be ok because new cache has been requested and it should be ready soon
478 if ($this->pageCache->hasCache()) {
479 return true;
480 } else {
481 // Check for invalidated cache
482 $this->pageCache->useInvalidated(true);
483 if ($this->hasLocalCache(false)) {
484 return true;
485 } else {
486 $this->pageCache->useInvalidated(false);
487 }
488 }
489
490 return false;
491 }
492 }
493
494 public function hasRemoteCacheMulti($urls, $layout, $checkIfRequestIsAllowed = true) {
495 if ($this->backlog->exists()) return false;
496
497 foreach ($urls as $url) {
498 if (
499 !$this->isAllowedUrl($url) ||
500 ($checkIfRequestIsAllowed && !$this->isAllowedRequest()) ||
501 ($this->pageCache->getParent() && !$this->pageCache->getParent()->hasCache()) || // This check is works correctly despite the fact that it doesn't check the current iteration's url because the check is based on headers and all of the headers for the URLs are the same for batched webhooks
502 $this->isPageCacheLocked()
503 ) return false;
504 }
505
506 $hasCache = true; // hasCache is false if even one of the URLs doesn't have a cache
507 $responses = $this->api->getCacheMulti($urls, $this->userAgent, $this->supportedCookiesFilter(self::getCookies()), $this->isAJAXRequest(), $layout, $this->referer);
508
509 $originalURL = $this->url;
510 $originalPageCache = $this->pageCache;
511
512 foreach ($responses as $url => $resp) {
513 // Overwrite the url
514 $urlInfo = new Url($url);
515 $this->url = $urlInfo->getNormalized();
516
517 // Overwrite the page cache
518 $this->pageCache = new Pagecache($this->url, $this->userAgent, $this->supportedCookiesFilter(self::getCookies()), $this->config->PageCache->SupportedCookies, $this->isAJAXRequest());
519 $this->pageCache->setCookiesProvider([$this, "getPagecacheCookies"]);
520
521 if ($this->config->PageCache->Geot->Status) {
522 $this->pageCache->setGeotVariations($this->config->PageCache->Geot->Variations);
523 }
524
525 if ($this->isAJAXRequest() && $this->isAllowedAJAXUrl($this->url) && !empty($_SERVER["HTTP_REFERER"]) && !$this->isAllowedStandaloneAJAXUrl($url)) {
526 $refererInfo = new Url($_SERVER["HTTP_REFERER"]);
527 $this->pageCache->setReferer($refererInfo->getNormalized());
528 }
529 if (!empty($this->config->URLPathVersion)) {
530 $this->pageCache->setUrlPathVersion($this->config->URLPathVersion);
531 }
532 $this->pageCache->setDataDir($this->getCacheDir());
533
534 // Process the response
535 if ($resp->getStatus() == Api\ResponseStatus::OK) {// We have cache response
536
537 // Check for invalidated cache and delete it if such is found
538 $this->pageCache->useInvalidated(true);
539 if ($this->pageCache->hasCache()) {
540 $path = $this->pageCache->getCachefilePath();
541 Filesystem::deleteFile($path);
542 Filesystem::deleteFile($path . ".gz");
543 Filesystem::deleteFile($path . ".stale");
544 Filesystem::deleteFile($path . ".stale.gz");
545 if (Filesystem::isDirEmpty(dirname($path))) {
546 Filesystem::deleteDir(dirname($path));
547 }
548 }
549 $this->pageCache->useInvalidated(false);
550 // End of check
551
552 list($headers, $content) = Filesystem::explodeByHeaders($resp->getBody());
553 $this->pageCache->setContent($content, $headers);
554 continue;
555 } else {
556 // The goal is to serve cache at all times even when it is slightly outdated. This approach should be ok because new cache has been requested and it should be ready soon
557 if ($this->pageCache->hasCache()) {
558 continue;
559 } else {
560 // Check for invalidated cache
561 $this->pageCache->useInvalidated(true);
562 if ($this->hasLocalCache(false)) {
563 continue;
564 } else {
565 $this->pageCache->useInvalidated(false);
566 }
567 }
568
569 $hasCache = false;
570 continue;
571 }
572 }
573
574 // Restore the url and page cache
575 $this->url = $originalURL;
576 $this->pageCache = $originalPageCache;
577
578 return $hasCache;
579 }
580
581 public function invalidateCache($url = NULL, $tag = NULL, $reason = NULL) {
582 return $this->purgeCache($url, $tag, PurgeType::INVALIDATE | PurgeType::PAGECACHE_ONLY, $reason);
583 }
584
585 public function clearPageCache($reason = NULL) {
586 return $this->purgeCache(NULL, NULL, PurgeType::PAGECACHE_ONLY, $reason);
587 }
588
589 public function purgeCache($url = NULL, $tag = NULL, $purgeType = PurgeType::COMPLETE, $reason = NULL) {
590 @set_time_limit(0);
591 $this->lockPageCache(); // Set the page cache lock, expires after self::PAGECACHE_LOCK_EXPIRATION_TIME seconds
592
593 try {
594 $invalidate = !!($purgeType & PurgeType::INVALIDATE);
595 $pageCacheOnly = !!($purgeType & PurgeType::PAGECACHE_ONLY);
596 $lightPurge = !!($purgeType & PurgeType::LIGHT_PURGE);
597
598 if ($url || $tag) {
599 $localResult = true;
600 $apiResult = true;
601 if ($url) {
602 if (is_array($url)) {
603 foreach ($url as &$urlLink) {
604 $urlLink = $this->normalizeUrl($urlLink);
605 if ($invalidate) {
606 $localResult &= $this->invalidateLocalUrlCache($urlLink);
607 } else {
608 $localResult &= $this->purgeLocalUrlCache($urlLink);
609 }
610 }
611 } else {
612 $url = $this->normalizeUrl($url);
613 if ($invalidate) {
614 $localResult &= $this->invalidateLocalUrlCache($url);
615 } else {
616 $localResult &= $this->purgeLocalUrlCache($url);
617 }
618 }
619
620 try {
621 $apiResult &= $this->api->purgeCache($url, false, $reason, $lightPurge);
622 } catch (ServiceDownException $e) {
623 $apiResult = false;
624 // TODO: Potentially log this
625 }
626 }
627
628 if ($tag) {
629 $attemptsLeft = 10;
630 $purgedUrls = array();
631 do {
632 $hadError = false;
633
634 try {
635 $purgedUrls = $this->api->purgeCacheByTag($tag, $reason);
636
637 foreach ($purgedUrls as $url) {
638 if ($invalidate) {
639 $localResult &= $this->invalidateLocalUrlCache($url);
640 } else {
641 $localResult &= $this->purgeLocalUrlCache($url);
642 }
643 }
644 } catch (ServiceDownException $e) {
645 $this->purgeLocalCache(true); // TODO: This will leave stale cache files. Think of a way to delete them on systems that do not have a heartbeat (i.e custom integrations).
646 $apiResult = false;
647 // TODO: Log this
648 break;
649 } catch (\Exception $e) {
650 $hadError = true;
651 $attemptsLeft--;
652 sleep(3);
653 }
654 } while ($attemptsLeft > 0 && count($purgedUrls) > 0);
655 }
656 } else {
657 if ($invalidate) {
658 $localResult = $this->invalidateLocalCache();
659 $apiResult = $this->api->purgeCache(NULL, $pageCacheOnly, $reason, $lightPurge); // delete only page cache
660 } else {
661 $staleCacheDir = $this->purgeLocalCache(true);
662
663 // Call the cache purge method
664 $apiResult = $this->api->purgeCache(NULL, $pageCacheOnly, $reason, $lightPurge);
665
666 // Finally, delete the files of the stale directory
667 Filesystem::deleteDir($staleCacheDir);
668
669 $localResult = true; // We do not care if $staleCacheDir was not deleted successfully
670 }
671
672 $this->elementRevision->refresh();
673 }
674
675 $this->unlockPageCache(); // Purge cache is done, we can now unlock
676 } catch (\Exception $e) {
677 $this->unlockPageCache(); // Purge cache had an error, so just unlock
678
679 throw $e;
680 }
681
682 return $apiResult && $localResult;
683 }
684
685 public function purgeLocalCache($quick = false) {
686 $staleCacheDir = $this->getCacheDir() . '.stale.' . md5(microtime(true));
687 $staleCacheDirSuffix = "";
688 $counter = 0;
689 while (Filesystem::fileExists($staleCacheDir . $staleCacheDirSuffix)) {
690 $counter++;
691 $staleCacheDirSuffix = "_" . $counter;
692 }
693 $staleCacheDir .= $staleCacheDirSuffix;
694 $this->purgeProxyCache();
695 $this->config->LastFetch = 0;
696 $this->setConfig($this->config);
697
698 // Rename cache files directory
699 if (Filesystem::fileExists($this->getCacheDir()) && !Filesystem::rename($this->getCacheDir(), $staleCacheDir)) {
700 throw new \Exception("No write permissions to rename the directory: " . $this->getCacheDir());
701 }
702
703 // Create a new empty directory
704 if (!Filesystem::createDir($this->getCacheDir())) {
705 throw new \Exception("No write permissions to create the directory: " . $this->getCacheDir());
706 }
707
708 if (!$quick) {
709 // Finally, delete the files of the stale directory
710 Filesystem::deleteDir($staleCacheDir);
711 }
712
713 $this->elementRevision->refresh();
714
715 return $staleCacheDir;
716 }
717
718 public function fetchConfig($ignoreHealthStatus = false) {
719 // TODO: Record failures and repeat with a delay
720 $fetcher = new Api\RemoteConfigFetcher($this->siteId, $this->siteSecret);
721
722 if (!$ignoreHealthStatus) {
723 $fetcher->setBacklog($this->backlog);
724 $fetcher->setNitroPack($this);
725 }
726
727 try {
728 $configContents = $fetcher->get(); // this can throw in case of http errors or validation failures
729 } catch (\Exception $e) {
730 // TODO: Record this failure, possibly in the backlog
731 throw $e;
732 }
733
734 $config = json_decode($configContents);
735 if ($config) {
736 $config->SDKVersion = NitroPack::VERSION;
737 $config->LastFetch = time();
738
739 $this->setConfig($config);
740 return true;
741 } else {
742 throw new EmptyConfigException("Config response was empty");
743 }
744 }
745
746 public function setConfig($config) {
747 $file = $this->getConfigFile();
748 if (Filesystem::createDir(dirname($file))) {
749 if (Filesystem::filePutContents($file, json_encode($config))) {
750 return true;
751 } else {
752 throw new StorageException(sprintf("Config file %s cannot be saved to disk", $file));
753 }
754 } else {
755 throw new StorageException(sprintf("Storage directory %s cannot be created", dirname($file)));
756 }
757 }
758
759 public function setVarnishProxyCacheHeaders($newHeaders) {
760 $this->varnishProxyCacheHeaders = $newHeaders;
761 }
762
763 public function purgeProxyCache($url = NULL) {
764 if (empty($this->config->CacheIntegrations)) {
765 return;
766 }
767
768 if (!empty($this->config->CacheIntegrations->Varnish)) {
769 if (empty($this->config->CacheIntegrations->Varnish->PurgeConfigSet)) {
770 if ($url) {
771 $url = $this->normalizeUrl($url);
772 $varnish = new Integrations\Varnish(
773 $this->config->CacheIntegrations->Varnish->Servers,
774 $this->config->CacheIntegrations->Varnish->PurgeSingleMethod,
775 $this->varnishProxyCacheHeaders
776 );
777 $varnish->purge($url);
778 } else {
779 $varnish = new Integrations\Varnish(
780 $this->config->CacheIntegrations->Varnish->Servers,
781 $this->config->CacheIntegrations->Varnish->PurgeAllMethod,
782 $this->varnishProxyCacheHeaders
783 );
784 $varnish->purge($this->config->CacheIntegrations->Varnish->PurgeAllUrl);
785 }
786
787 return;
788 }
789
790 foreach ($this->config->CacheIntegrations->Varnish->PurgeConfigSet as $purgeSet) {
791 if ($url) {
792 foreach ($purgeSet->PurgeSingleHeadersTemplates as $headerTemplateTuple) {
793 $this->varnishProxyCacheHeaders[$headerTemplateTuple->HeaderName] = $this->valueFromTemplate($url, $headerTemplateTuple->HeaderTemplate);
794 }
795
796 $varnish = new Integrations\Varnish(
797 null,
798 $purgeSet->PurgeSingleMethod,
799 $this->varnishProxyCacheHeaders
800 );
801
802 $purgeUrl = $this->valueFromTemplate($url, $purgeSet->PurgeSingleTemplate);
803 $varnish->purge($purgeUrl, true);
804 } else {
805 $varnish = new Integrations\Varnish(
806 null,
807 $purgeSet->PurgeAllMethod,
808 $this->varnishProxyCacheHeaders
809 );
810 $varnish->purge($purgeSet->PurgeAllUrl, true);
811 }
812 }
813 }
814
815 //if (!empty($this->config->CacheIntegrations->LiteSpeed) && php_sapi_name() !== "cli") {
816 // if ($url) {
817 // $urlObj = new Url($url);
818 // $liteSpeedPath = $urlObj->getPath();
819 // if ($urlObj->getQuery()) {
820 // $liteSpeedPath .= "?" . $urlObj->getQuery();
821 // }
822 // header("X-LiteSpeed-Purge: $liteSpeedPath", false);
823 // } else {
824 // header("X-LiteSpeed-Purge: *", false);
825 // }
826 //}
827 }
828
829 public function isAllowedUrl($url) {
830 if (strpos($url, 'sucurianticache=') !== false) return false;
831
832 if ($this->config->EnabledURLs->Status) {
833 if (!empty($this->config->EnabledURLs->URLs)) {
834 foreach ($this->config->EnabledURLs->URLs as $enabledUrl) {
835 $enabledUrlModified = preg_replace("/^(https?:)?\/\//", "*", $enabledUrl);
836
837 if (preg_match('/^' . self::wildcardToRegex($enabledUrlModified) . '$/', $url)) {
838 return true;
839 }
840 }
841
842 return false;
843 }
844 } else if ($this->config->DisabledURLs->Status) {
845 if (!empty($this->config->DisabledURLs->URLs)) {
846 foreach ($this->config->DisabledURLs->URLs as $disabledUrl) {
847 $disabledUrlModified = preg_replace("/^(https?:)?\/\//", "*", $disabledUrl);
848
849 if (preg_match('/^' . self::wildcardToRegex($disabledUrlModified) . '$/', $url)) {
850 return false; // don't cache disabled URLs
851 }
852 }
853 }
854 }
855
856 return true;
857 }
858
859 public function isServiceRequest() {
860 return isset($_SERVER["HTTP_X_NITROPACK_REQUEST"]);
861 }
862
863 public function isAllowedRequest($allowServiceRequests = false) {
864 if (!$this->isAllowedUserAgent()) {
865 return false;
866 }
867
868 // TODO: Allow URLs which match a pattern in the AJAX URL whitelist
869 if (($this->isAJAXRequest() && !$this->isAllowedAJAX()) || !($this->isRequestMethod("GET") || $this->isRequestMethod("HEAD"))) {
870
871 return false; // don't cache ajax or not GET requests
872 }
873
874 if (!$allowServiceRequests && $this->isServiceRequest()) { // Skip requests coming from NitroPack
875 return false;
876 }
877
878 if (isset($_GET["nonitro"])) { // Skip requests having ?nonitro
879 return false;
880 }
881
882 if (!$this->isAllowedBrowser()) {
883 return false;
884 }
885
886 if (isset($this->config->ExcludedCookies) && $this->config->ExcludedCookies->Status) {
887 foreach ($this->config->ExcludedCookies->Cookies as $cookieExclude) {
888 foreach (self::getCookies() as $cookieName => $cookieValue) {
889 if (preg_match('/^' . self::wildcardToRegex($cookieExclude->name) . '$/', $cookieName)) {
890 if (count($cookieExclude->values) == 0) {
891 return false; // no excluded cookie values entered, reject all values
892 } else {
893 foreach ($cookieExclude->values as $val) {
894 if (preg_match('/^' . self::wildcardToRegex($val) . '$/', $cookieValue)) {
895 return false;
896 }
897 }
898 }
899 }
900 }
901 }
902 }
903
904 // Deactivate NitroPack for a desired % of journeys
905 if ($this->config->TestImpact->Status) {
906 $impactGroup = $this->getImpactGroupValue();
907 if ($impactGroup <= $this->config->TestImpact->ThresholdPercentage) {
908 return false;
909 }
910 }
911
912 return true;
913 }
914
915 private function getImpactGroupValue() {
916 if (!empty($_COOKIE['nitroImpactGroup'])) {
917 return $_COOKIE['nitroImpactGroup'];
918 } else {
919 $impactGroup = mt_rand(1,100);
920 setcookie('nitroImpactGroup', $impactGroup, time() + 86400, '/', $_SERVER['HTTP_HOST']);
921 return $impactGroup;
922 }
923 }
924
925 public function isAllowedBrowser() {
926 // Right now this function always returns true but is kept in case of possible future exclusions
927
928 if (empty($_SERVER["HTTP_USER_AGENT"])) {
929 return true;
930 }
931
932 return true;
933 }
934
935 public function getScheme() {
936 return $this->isSecure() ? 'https://' : 'http://';
937 }
938
939 public function isSecure() {
940 $result = (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && in_array("https", array_map("strtolower", array_map("trim", explode(",", $_SERVER['HTTP_X_FORWARDED_PROTO']))))) ||
941 (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
942 (!empty($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443) ||
943 (!empty($_SERVER['HTTP_SSL_FLAG']) && $_SERVER['HTTP_SSL_FLAG'] == 'SSL');
944
945 if (!$result && !empty($_SERVER['HTTP_CF_VISITOR'])) {
946 $visitor = json_decode($_SERVER['HTTP_CF_VISITOR']);
947 $result = $visitor && property_exists($visitor, "scheme") && strtolower($visitor->scheme) == "https";
948 }
949
950 return $result;
951 }
952
953 public function isAJAXRequest() {
954 return
955 (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') ||
956 $this->isAllowedAJAXUrl($this->url) ||
957 $this->isAllowedStandaloneAJAXUrl($this->url);
958 }
959
960 public function isRequestMethod($method) {
961 return empty($_SERVER['REQUEST_METHOD']) || $_SERVER['REQUEST_METHOD'] == $method;
962 }
963
964 public function isAllowedAJAX() {
965 if (!$this->isAllowedStandaloneAJAXUrl($this->url)) {
966 if (!$this->pageCache->getParent()) return false;
967 if (!$this->pageCache->getParent()->hasCache() || $this->pageCache->getParent()->hasExpired($this->config->PageCache->ExpireTime)) return false;
968 }
969 return true;
970 }
971
972 public function isAllowedAJAXUrl($url) {
973 if ($this->config->AjaxURLs->Status) {
974 if (!empty($this->config->AjaxURLs->URLs)) {
975 foreach ($this->config->AjaxURLs->URLs as $ajaxUrl) {
976 $ajaxUrlModified = preg_replace("/^(https?:)?\/\//", "*", $ajaxUrl);
977 if (preg_match('/^' . self::wildcardToRegex($ajaxUrlModified) . '$/', $url)) {
978 return true;
979 }
980 }
981 return false;
982 }
983 }
984 return false;
985 }
986
987 public function isAllowedStandaloneAJAXUrl($url)
988 {
989 if ($this->config->AjaxURLs->Status) {
990 if (!empty($this->config->AjaxURLs->StandaloneURLs)) {
991 foreach ($this->config->AjaxURLs->StandaloneURLs as $ajaxUrl) {
992 $ajaxUrlModified = preg_replace("/^(https?:)?\/\//", "*", $ajaxUrl);
993 if (preg_match('/^' . self::wildcardToRegex($ajaxUrlModified) . '$/', $url)) {
994 return true;
995 }
996 }
997 }
998 }
999 return false;
1000 }
1001
1002 public function isAllowedUserAgent()
1003 {
1004 if ($this->config->ExcludeByUserAgent->Status && !empty($this->config->ExcludeByUserAgent->UserAgents)) {
1005 foreach ($this->config->ExcludeByUserAgent->UserAgents as $uaValue) {
1006 if (preg_match('/^' . self::wildcardToRegex($uaValue) . '$/', $this->userAgent)) {
1007 return false;
1008 }
1009 }
1010 }
1011
1012 return true;
1013 }
1014
1015 public function isCacheAllowed() {
1016 return $this->isAllowedRequest() && $this->isAllowedUrl($this->url);
1017 }
1018
1019 public function isStatefulCacheSatisfied($type = NULL) {
1020 if ($this->config->StatefulCache->Status) {
1021 $foundTypeSelectors = false;
1022 foreach ($this->config->StatefulCache->Selectors as $selector) {
1023 if ($type !== NULL) {
1024 if ($selector->type === $type) {
1025 $foundTypeSelectors = true;
1026 } else {
1027 continue;
1028 }
1029 }
1030
1031 $selectorEncoded = str_replace("=", "", base64_encode($selector->string)); // base64 can produce == at the end which breaks is invalid for a cookie name, hence why we need to remove it
1032 $cookieKey = 'np-' . $selector->type . '-' . $selectorEncoded . '-override';
1033 if (empty($_COOKIE[$cookieKey]) || $_COOKIE[$cookieKey] != $this->elementRevision->get()) {
1034 return false;
1035 }
1036 }
1037
1038 return $type !== NULL ? $foundTypeSelectors : true;
1039 }
1040
1041 return false;
1042 }
1043
1044 public function purgeLocalUrlCache($url) {
1045 $url = $this->normalizeUrl($url);
1046 $this->purgeProxyCache($url);
1047 $localResult = true;
1048 $cacheDir = $this->getCacheDir();
1049 $knownDeviceTypes = Device::getKnownTypes();
1050 foreach ($knownDeviceTypes as $deviceType) {
1051 $urlDir = PageCache::getUrlDir($cacheDir . "/" . $deviceType, $url, false, $this->config->URLPathVersion);
1052 $invalidatedUrlDir = PageCache::getUrlDir($cacheDir . "/" . $deviceType, $url, true, $this->config->URLPathVersion);
1053 $localResult &= Filesystem::deleteDir($urlDir);
1054 $localResult &= Filesystem::deleteDir($invalidatedUrlDir);
1055 }
1056 return $localResult;
1057 }
1058
1059 public function invalidateLocalUrlCache($url) {
1060 $url = $this->normalizeUrl($url);
1061 $this->purgeProxyCache($url);
1062 $localResult = true;
1063 $cacheDir = $this->getCacheDir();
1064 $knownDeviceTypes = Device::getKnownTypes();
1065 foreach ($knownDeviceTypes as $deviceType) {
1066 $urlDir = PageCache::getUrlDir($cacheDir . "/" . $deviceType, $url, false, $this->config->URLPathVersion);
1067 $urlDirInvalid = PageCache::getUrlDir($cacheDir . "/" . $deviceType, $url, true, $this->config->URLPathVersion);
1068
1069 $this->invalidateDir($urlDir, $urlDirInvalid);
1070 }
1071 return $localResult;
1072 }
1073
1074 public function invalidateLocalCache() {
1075 $this->purgeProxyCache();
1076 $this->config->LastFetch = 0;
1077 $this->setConfig($this->config);
1078
1079 $cacheDir = $this->getCacheDir();
1080 $knownDeviceTypes = Device::getKnownTypes();
1081 foreach ($knownDeviceTypes as $deviceType) {
1082 $deviceTypeDir = $cacheDir . "/" . $deviceType;
1083 Filesystem::dirForeach($deviceTypeDir, function($urlDir) {
1084 if (substr($urlDir, -2) !== "_i") {
1085 $this->invalidateDir($urlDir, $urlDir . "_i");
1086 }
1087 });
1088 }
1089 return true;
1090 }
1091
1092 private function invalidateDir($urlDir, $urlDirInvalid) {
1093 if (Filesystem::fileExists($urlDirInvalid)) {
1094 Filesystem::dirForeach($urlDir, function($file) use ($urlDirInvalid) {
1095 Filesystem::rename($file, $urlDirInvalid . "/" . basename($file));
1096 });
1097
1098 Filesystem::deleteDir($urlDir);
1099 } else {
1100 if (Filesystem::rename($urlDir, $urlDirInvalid)) {
1101 Filesystem::touch($urlDirInvalid);
1102 }
1103 }
1104 }
1105
1106 public function integrationUrl($widget, $version = null) {
1107 $integration = new IntegrationUrl($widget, $this->siteId, $this->siteSecret, $version);
1108
1109 return $integration->getUrl();
1110 }
1111
1112 public function embedJsUrl() {
1113 $embedjs = new Embedjs();
1114
1115 return $embedjs->getUrl();
1116 }
1117
1118 public function enableSafeMode() {
1119 $this->api->safe_mode->enable();
1120 $this->fetchConfig();
1121 }
1122
1123 public function disableSafeMode() {
1124 $this->api->safe_mode->disable();
1125 $this->fetchConfig();
1126 }
1127
1128 public function enableCartCache() {
1129 $this->api->enableCartCache();
1130 }
1131
1132 public function disableCartCache() {
1133 $this->api->disableCartCache();
1134 }
1135
1136 public function getStatefulCacheHandlerScript() {
1137 if ($this->config->StatefulCache->Status && $this->config->StatefulCache->HandlerScript) {
1138 $keyRevision = $this->elementRevision->get();
1139 return '<script id="nitro-stateful-cache" nitro-exclude>' . str_replace("KEY_REVISION", $keyRevision, $this->config->StatefulCache->HandlerScript) . '</script>';
1140 }
1141
1142 return "";
1143 }
1144
1145 private function loadConfig() {
1146 $file = $this->getConfigFile();
1147
1148 $config = array();
1149 if (Filesystem::fileExists($file) || $this->fetchConfig(true)) {
1150 $config = json_decode(Filesystem::fileGetContents($file));
1151 if (empty($config->SDKVersion) || $config->SDKVersion !== NitroPack::VERSION || empty($config->LastFetch) || time() - $config->LastFetch >= $this->configTTL) {
1152 if ($this->getHealthStatus() === HealthStatus::HEALTHY) {
1153 if ($this->fetchConfig()) {
1154 $config = json_decode(Filesystem::fileGetContents($file));
1155 } else {
1156 throw new NoConfigException("Can't load config file");
1157 }
1158 }
1159 }
1160 $this->config = $config;
1161 } else {
1162 throw new NoConfigException("Can't load config file");
1163 }
1164 }
1165
1166 private function getConfigFile() {
1167 $configFile = $this->configFile;
1168
1169 $filename = array_pop($configFile);
1170
1171 $filename = $this->siteId . '-' . $filename;
1172
1173 array_push($configFile, $filename);
1174 array_unshift($configFile, $this->dataDir);
1175
1176 return Filesystem::getOsPath($configFile);
1177 }
1178
1179 private function lockPageCache() {
1180 $filename = $this->getPageCacheLockFilename();
1181
1182 if (Filesystem::fileExists($filename)) {
1183 $sem = 1 + (int)Filesystem::fileGetContents($filename);
1184 } else {
1185 $sem = 1;
1186 }
1187
1188 return !!Filesystem::filePutContents($filename, $sem);
1189 }
1190
1191 private function unlockPageCache() {
1192 $filename = $this->getPageCacheLockFilename();
1193
1194 if (Filesystem::fileExists($filename)) {
1195 $sem = (int)Filesystem::fileGetContents($filename);
1196
1197 $sem--;
1198
1199 if ($sem <= 0) {
1200 return !!Filesystem::deleteFile($filename);
1201 } else {
1202 return !!Filesystem::filePutContents($filename, $sem);
1203 }
1204 }
1205
1206 return false;
1207 }
1208
1209 private function isPageCacheLocked() {
1210 $filename = $this->getPageCacheLockFilename();
1211
1212 if (!Filesystem::fileExists($filename)) {
1213 return false;
1214 } else {
1215 if (time() - Filesystem::fileMTime($filename) <= self::PAGECACHE_LOCK_EXPIRATION_TIME) {
1216 return true;
1217 } else {
1218 Filesystem::deleteFile($filename);
1219
1220 return false;
1221 }
1222 }
1223
1224 // We should never get here, so consider this a default return value in case of future changes
1225 return false;
1226 }
1227
1228 private function getPageCacheLockFilename() {
1229 $pageCacheLockFile = $this->pageCacheLockFile;
1230 array_unshift($pageCacheLockFile, $this->dataDir);
1231 return Filesystem::getOsPath($pageCacheLockFile);
1232 }
1233
1234 private function normalizeUrl($url) {
1235 $urlObj = new Url($url);
1236 return $urlObj->getNormalized();
1237 }
1238
1239 /**
1240 * Substitutes values from a template based on URL components
1241 * @param string $url The URL, from which to extract substitution values
1242 * @param string $template The template to use
1243 * @return string
1244 */
1245 private function valueFromTemplate($url, $template) {
1246 $urlObj = new Url($url);
1247
1248 $proxyPurgeTemplate = array
1249 (
1250 '{{targetUrl}}' => $urlObj->getNormalized(),
1251 '{{targetProtocol}}' => $urlObj->getScheme(),
1252 '{{targetHost}}' => $urlObj->getHost(),
1253 '{{targetPath}}' => $urlObj->getPath(),
1254 '{{targetQuery}}' => $urlObj->getQuery(),
1255 );
1256
1257 $templateKeys = array_keys($proxyPurgeTemplate);
1258 $templateVals = array_values($proxyPurgeTemplate);
1259 $constructed = str_replace($templateKeys, $templateVals, $template);
1260
1261 return $constructed;
1262 }
1263 }