├── .gitignore ├── EDGE-MODULES.md ├── assets ├── screenshot-1.png ├── screenshot-2.png └── screenshot-3.png ├── build.sh ├── fastly_edge_modules ├── blackfire_integration.json ├── cors_headers.json ├── countryblock.json ├── datadome_integration.json ├── disabled │ ├── aclblacklist.json │ ├── error.json │ ├── normalise.json │ ├── redirects.json │ ├── stale.json │ └── ttloverride.json ├── force_cache_miss_on_hard_reload_for_admins.json ├── increase_timeouts_long_jobs.json ├── mobile_device_detection.json ├── netacea_integration.json ├── nocache.json ├── other_cms_integration.json ├── redirect_hosts.json └── url_rewrites.json ├── js ├── edgemodules.js ├── handlebars-v4.0.12.js └── jquery.serializejson.min.js ├── languages └── purgely.pot ├── purgely.php ├── readme.txt ├── src ├── classes │ ├── api.php │ ├── edgemodules.php │ ├── header-cache-control.php │ ├── header-surrogate-control.php │ ├── header-surrogate-keys.php │ ├── header.php │ ├── purge-request.php │ ├── related-surrogate-keys.php │ ├── settings.php │ ├── surrogate-key-collection.php │ ├── upgrades.php │ └── vcl-handler.php ├── config.php ├── settings-page.php ├── utils.php ├── wp-cli.php └── wp-purges.php ├── static └── logo_white.gif └── vcl_snippets ├── deliver.vcl ├── error.vcl ├── error_page └── deliver.vcl ├── fetch.vcl └── recv.vcl /.gitignore: -------------------------------------------------------------------------------- 1 | *.zip 2 | .idea -------------------------------------------------------------------------------- /EDGE-MODULES.md: -------------------------------------------------------------------------------- 1 | # Fastly Edge Modules 2 | 3 | This guide will show how to configure Fastly Edge Modules. Fastly Edge Modules 4 | is a flexible framework that allows definition of UI components and associated VCL 5 | code through a template. 6 | 7 | - [Blackfire integration](https://github.com/fastly/fastly-magento2/blob/master/Documentation/Guides/Edge-Modules/EDGE-MODULE-BLACKFIRE-INTEGRATION.md) - enable Fastly portion required for Blackfire profiling 8 | - [CORS headers](https://github.com/fastly/fastly-magento2/blob/master/Documentation/Guides/Edge-Modules/EDGE-MODULE-CORS-HEADERS.md) - Set CORS headers sent to the end user 9 | - [Datadome bot detection integration](https://github.com/fastly/fastly-magento2/blob/master/Documentation/Guides/Edge-Modules/EDGE-MODULE-DATADOME-INTEGRATION.md) - Datadome Bot Detection integration 10 | a filesystem URL 11 | to www.domain.com. Useful for redirecting apex/naked domains to www. 12 | - [Hard Reload Cache Bypass for admins](https://github.com/fastly/fastly-magento2/blob/master/Documentation/Guides/Edge-Modules/EDGE-MODULE-HARD-RELOAD-CACHE-BYPASS.md) - allows admin IPs to force cache bypass on browser hard reload. [More details here](https://github.com/fastly/fastly-magento2/issues/147) 13 | - [Increase timeouts for long running jobs](https://github.com/fastly/fastly-magento2/blob/master/Documentation/Guides/Edge-Modules/EDGE-MODULE-INCREASE-TIMEOUTS-LONG-JOBS.md) - Tweak timeouts for jobs/URLs that take longer than 1 minute 14 | - [Integrate other CMS/backend](https://github.com/fastly/fastly-magento2/blob/master/Documentation/Guides/Edge-Modules/EDGE-MODULE-OTHER-CMS-INTEGRATION.md) - configures integration of specific URLs to integrate other CMSes/backends into your site 15 | - [Mobile Theme Support](https://github.com/fastly/fastly-magento2/blob/master/Documentation/Guides/Edge-Modules/EDGE-MODULE-MOBILE-THEME-SUPPORT.md) - module required for supporting mobile device themes 16 | - [Netacea bot detection integration](https://github.com/fastly/fastly-magento2/blob/master/Documentation/Guides/Edge-Modules/EDGE-MODULE-NETACEA-INTEGRATION.md) - Netacea Bot Detection integration 17 | - [Redirect one domain to another](https://github.com/fastly/fastly-magento2/blob/master/Documentation/Guides/Edge-Modules/EDGE-MODULE-REDIRECT-DOMAIN.md) - redirect one domain to another e.g. domain.com 18 | - [URL rewrites](https://github.com/fastly/fastly-magento2/blob/master/Documentation/Guides/Edge-Modules/EDGE-MODULE-URL-REWRITES.md) - rewrite an incoming URL to a different backend URL. 19 | -------------------------------------------------------------------------------- /assets/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastly/WordPress-Plugin/84d7fd8d4638d7c814125655ac77fc07a3923e12/assets/screenshot-1.png -------------------------------------------------------------------------------- /assets/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastly/WordPress-Plugin/84d7fd8d4638d7c814125655ac77fc07a3923e12/assets/screenshot-2.png -------------------------------------------------------------------------------- /assets/screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastly/WordPress-Plugin/84d7fd8d4638d7c814125655ac77fc07a3923e12/assets/screenshot-3.png -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | version=`cat "fastly.php" | grep '\@version' | perl -pe 's/.*\@version ([^ ]+).*$/$1/'` 3 | file="fastly-${version}.zip" 4 | rm "$file" >/dev/null 2>&1 5 | zip -r "${file}" . -x "build.sh" -x ".*" -x "*.zip" 6 | -------------------------------------------------------------------------------- /fastly_edge_modules/blackfire_integration.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Necessary Fastly configuration to enable Blackfire profiling.", 3 | "id": "blackfire_integration", 4 | "name": "Blackfire integration", 5 | "properties": [ 6 | { 7 | "description": "ACL that contains IPs of users allowing to profile", 8 | "label": "ACL", 9 | "name": "Acl", 10 | "required": true, 11 | "type": "acl" 12 | } 13 | ], 14 | "test": { 15 | "origins": [ 16 | "https://httpbin.org" 17 | ], 18 | "reqUrl": "/status/500" 19 | }, 20 | "vcl": [ 21 | { 22 | "priority": 70, 23 | "template": " if (req.http.X-Blackfire-Query && req.http.Fastly-Client-IP ~ {{Acl}}) {\r\n if (req.esi_level > 0) {\r\n # ESI request should not be included in the profile.\r\n # Instead you should profile them separately, each one\r\n # in their dedicated profile.\r\n # Removing the Blackfire header avoids to trigger the profiling.\r\n # Not returning let it go through your usual workflow as a regular\r\n # ESI request without distinction.\r\n unset req.http.X-Blackfire-Query;\r\n } else {\r\n set req.http.X-Pass = \"1\";\r\n }\r\n }", 24 | "type": "recv" 25 | } 26 | ], 27 | "version": 1 28 | } 29 | -------------------------------------------------------------------------------- /fastly_edge_modules/cors_headers.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Set CORS headers", 3 | "id": "cors_headers", 4 | "name": "CORS headers", 5 | "properties": [ 6 | { 7 | "default": "anyone", 8 | "description": "What origins are allowed", 9 | "label": "Origins allowed", 10 | "name": "origin", 11 | "options": { 12 | "anyone": "Allow anyone (*)", 13 | "regex-match": "Regex matching set of origins. Do not supply http://" 14 | }, 15 | "required": true, 16 | "type": "select" 17 | }, 18 | { 19 | "default": "GET,HEAD,POST,OPTIONS", 20 | "description": "Allowed HTTP Methods that requestor can use", 21 | "label": "Allowed HTTP Methods", 22 | "name": "cors_allowed_methods", 23 | "required": true, 24 | "type": "string" 25 | }, 26 | { 27 | "description": "Allowed HTTP Headers that requestor can use", 28 | "label": "Allowed HTTP Headers", 29 | "name": "cors_allowed_headers", 30 | "required": false, 31 | "type": "string" 32 | }, 33 | { 34 | "description": "Regex matching origins that are allowed to access this service", 35 | "label": "Regex matching origins", 36 | "name": "cors_allowed_origins_regex", 37 | "required": false, 38 | "type": "string" 39 | } 40 | ], 41 | "test": { 42 | "origins": [ 43 | "https://httpbin.org" 44 | ], 45 | "reqUrl": "/html" 46 | }, 47 | "vcl": [ 48 | { 49 | "template": " if (req.http.Origin && !resp.http.Access-Control-Allow-Origin && !resp.http.Access-Control-Allow-Methods && !resp.http.Access-Control-Allow-Headers) {\n{{#ifEq origin \"anyone\"}}\n set resp.http.Access-Control-Allow-Origin = \"*\";\n{{/ifEq}}\n{{#ifEq origin \"regex-match\"}}\n if ( req.http.Origin ~ \"^https?://{{cors_allowed_origins_regex}}\" ) {\n set resp.http.Access-Control-Allow-Origin = req.http.origin;\n }\n{{/ifEq}}\n set resp.http.Access-Control-Allow-Methods = \"{{cors_allowed_methods}}\";\n{{#if cors_allowed_headers}}\n set resp.http.Access-Control-Allow-Headers = \"{{cors_allowed_headers}}\";\n{{/if}}\n set resp.http.Vary:Origin = \"\";\n \n }\n", 50 | "type": "deliver" 51 | } 52 | ], 53 | "version": 1 54 | } 55 | -------------------------------------------------------------------------------- /fastly_edge_modules/countryblock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "description": "Block requests from a set of countries", 4 | "name": "Country block", 5 | "test": { 6 | "reqHeaders": "Fastly-Debug: 1", 7 | "reqUrl": "/html", 8 | "origins": [ 9 | "https://httpbin.org" 10 | ] 11 | }, 12 | "vcl": [ 13 | { 14 | "type": "recv", 15 | "template": "if (fastly.ff.visits_this_service == 0 && client.geo.country_code ~ \"(?i)^({{replace countryList '[ ,]+' '|'}})$\") {\n error 921 \"[modly:countryblock]\";\n}" 16 | }, 17 | { 18 | "type": "error", 19 | "template": "if (obj.status == 921 && obj.response == \"[modly:countryblock]\") {\n set obj.status = 403;\n set obj.response = \"Forbidden\";\n if (req.http.Fastly-Debug) {\n set obj.http.Fastly-Country-Block = client.geo.country_code;\n }\n synthetic \"\";\n return (deliver);\n}" 20 | } 21 | ], 22 | "id": "countryblock", 23 | "properties": [ 24 | { 25 | "validation": "^\\w{2}(\\s+\\w{2})*$", 26 | "description": "List countries to block, using [ISO-3166-1 alpha 2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) codes, separated by spaces, eg `us de cn`", 27 | "name": "countryList", 28 | "required": true, 29 | "type": "string", 30 | "label": "Countries" 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /fastly_edge_modules/datadome_integration.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Set of VCLs required to integrate Datadome services. Please note for full functionality Fastly support needs to enable proper handling of POST requests. Do not enable unless this has been done.", 3 | "id": "datadome_integration", 4 | "name": "DataDome Bot Detection integration", 5 | "properties": [ 6 | { 7 | "label": "API Key", 8 | "name": "datadome_api_key", 9 | "type": "string", 10 | "description": "API Key", 11 | "required": true 12 | }, { 13 | "label": "Exclusion regex", 14 | "name": "datadome_exclusion_ext", 15 | "type": "string", 16 | "description": "The regex that will be applied to req.url.path", 17 | "default": "(?i)\\.(avi|flv|mka|mkv|mov|mp4|mpeg|mpg|mp3|flac|ogg|ogm|opus|wav|webm|webp|bmp|gif|ico|jpeg|jpg|png|svg|svgz|swf|eot|otf|ttf|woff|woff2|css|less|js|map)$", 18 | "required": true 19 | }, { 20 | "label": "Connection timeout", 21 | "name": "datadome_connect_timeout", 22 | "type": "integer", 23 | "description": "How long to wait for a timeout in milliseconds.", 24 | "default": "300", 25 | "required": true 26 | }, { 27 | "label": "First byte timeout", 28 | "name": "datadome_first_byte_timeout", 29 | "type": "integer", 30 | "description": "How long to wait for the first byte in milliseconds.", 31 | "default": "100", 32 | "required": true 33 | }, { 34 | "label": "Between bytes timeout", 35 | "name": "datadome_between_bytes_timeout", 36 | "type": "integer", 37 | "description": "How long to wait between bytes in milliseconds.", 38 | "default": "100", 39 | "required": true 40 | }, { 41 | "label": "Logging Endpoint for Debugging (Optional)", 42 | "name": "logging_endpoint", 43 | "type": "string", 44 | "description": "Name of a logging endpoint that has been already set up in Fastly.", 45 | "required": false 46 | } 47 | ], 48 | "test": { 49 | "origins": [ 50 | "https://httpbin.org" 51 | ], 52 | "reqUrl": "/sourcePath" 53 | }, 54 | "vcl": [ 55 | { 56 | "priority": 7, 57 | "template": "sub set_origin_header {\n if (req.backend.is_origin) {\n if (req.backend == datadome) {\n # Remove all unexpected headers\n header.filter_except(bereq, \"x-datadome-params\", \"accept-charset\", \"accept-language\", \"x-requested-with\", \"x-fl-productid\", \"x-flapi-session-id\", \"fastly-orig-accept-encoding\", \"cache-control\", \"client-id\", \"connection\", \"pragma\", \"accept\", \"headers-list\", \"host\", \"origin\", \"server-hostname\", \"server-name\", \"x-forwarded-for\", \"user-agent\", \"referer\", \"request\", \"content-type\", \"from\", \"true-client-ip\", \"via\", \"x-real-ip\", \"sec-ch-device-memory\", \"sec-ch-ua\", \"sec-ch-ua-arch\", \"sec-ch-ua-full-version-list\", \"sec-ch-ua-mobile\", \"sec-ch-ua-model\", \"sec-ch-ua-platform\", \"sec-fetch-dest\", \"sec-fetch-mode\", \"sec-fetch-site\", \"sec-fetch-user\");\n set bereq.http.x-datadome-params:key = \"{{datadome_api_key}}\";\n set bereq.http.x-datadome-params:requestmodulename = \"FastlyMagento\";\n set bereq.http.x-datadome-params:moduleversion = \"2.19.4\";\n set bereq.http.x-datadome-params:timerequest = time.start.usec;\n set bereq.http.x-datadome-params:servername = server.identity;\n set bereq.http.x-datadome-params:serverregion = server.region;\n set bereq.http.x-datadome-params:ip = urlencode(client.ip);\n set bereq.http.x-forwarded-proto = urlencode(req.protocol);\n set bereq.http.x-datadome-params:authorizationlen = std.strlen(req.http.authorization);\n # Truncating Headers - Start\n set bereq.http.accept-charset = substr(req.http.accept-charset, 0, 128);\n set bereq.http.accept-language = substr(req.http.accept-language, 0, 256);\n set bereq.http.x-requested-with = substr(req.http.x-requested-with, 0, 128);\n set bereq.http.x-fl-productid = substr(req.http.x-fl-productid, 0, 64);\n set bereq.http.x-flapi-session-id = substr(req.http.x-flapi-session-id, 0, 64);\n set bereq.http.fastly-orig-accept-encoding = substr(req.http.fastly-orig-accept-encoding, 0, 128);\n set bereq.http.cache-control = substr(req.http.cache-control, 0, 128);\n set bereq.http.client-id = substr(req.http.client-id, 0, 128);\n set bereq.http.connection = substr(req.http.connection, 0, 128);\n set bereq.http.pragma = substr(req.http.pragma, 0, 128);\n set bereq.http.accept = substr(req.http.accept, 0, 512);\n set bereq.http.headers-list = substr(req.http.headers-list, 0, 512);\n set bereq.http.host = substr(req.http.host, 0, 512);\n set bereq.http.origin = substr(req.http.origin, 0, 512);\n set bereq.http.server-hostname = substr(req.http.server-hostname, 0, 512);\n set bereq.http.server-name = substr(req.http.server-name, 0, 512);\n if( std.strlen(req.http.x-forwarded-for) \u003e 512 ) {\n # Truncate from the end\n set bereq.http.x-forwarded-for = substr(req.http.x-forwarded-for, -512);\n } else {\n set bereq.http.x-forwarded-for = req.http.x-forwarded-for;\n }\n set bereq.http.user-agent = substr(req.http.user-agent, 0, 768);\n set bereq.http.referer = substr(req.http.referer, 0, 1024);\n set bereq.http.request = substr(req.http.request, 0, 2048);\n set bereq.http.content-type = substr(req.http.content-type, 0, 64);\n set bereq.http.from = substr(req.http.from, 0, 128);\n set bereq.http.true-client-ip = substr(req.http.true-client-ip, 0, 128);\n set bereq.http.via = substr(req.http.via, 0, 256);\n set bereq.http.x-real-ip = substr(req.http.x-real-ip, 0, 128);\n set bereq.http.sec-ch-device-memory = substr(req.http.sec-ch-device-memory, 0, 8);\n set bereq.http.sec-ch-ua = substr(req.http.sec-ch-ua, 0, 128);\n set bereq.http.sec-ch-ua-arch = substr(req.http.sec-ch-ua-arch, 0, 16);\n set bereq.http.sec-ch-ua-full-version-list = substr(req.http.sec-ch-ua-full-version-list, 0, 256);\n set bereq.http.sec-ch-ua-mobile = substr(req.http.sec-ch-ua-mobile, 0, 8);\n set bereq.http.sec-ch-ua-model = substr(req.http.sec-ch-ua-model, 0, 128);\n set bereq.http.sec-ch-ua-platform = substr(req.http.sec-ch-ua-platform, 0, 32);\n set bereq.http.sec-fetch-dest = substr(req.http.sec-fetch-dest, 0, 32);\n set bereq.http.sec-fetch-mode = substr(req.http.sec-fetch-mode, 0, 32);\n set bereq.http.sec-fetch-site = substr(req.http.sec-fetch-site, 0, 64);\n set bereq.http.sec-fetch-user = substr(req.http.sec-fetch-user, 0, 8);\n # Truncating Headers - End\n if (req.http.x-datadome-clientid) {\n set bereq.http.x-datadome-params:clientid = urlencode(substr(req.http.x-datadome-clientid, 0, 128));\n set bereq.http.x-datadome-x-set-cookie = \"true\";\n } else {\n set bereq.http.x-datadome-params:clientid = urlencode(substr(req.http.cookie:datadome, 0, 128));\n }\n set bereq.http.x-datadome-params:cookieslen = std.strlen(req.http.cookie);\n # enforce gzip encoding between Fastly and DataDome\n set bereq.http.accept-encoding = \"gzip\";\n } else {\n # prevent leak of the key\n unset bereq.http.x-datadome-params;\n }\n }\n}\n\nbackend datadome {\n .host = \"api-fastly.datadome.co\";\n .port = \"8443\";\n .connect_timeout = {{datadome_connect_timeout}}ms;\n .first_byte_timeout = {{datadome_between_bytes_timeout}}ms;\n .between_bytes_timeout = {{datadome_between_bytes_timeout}}ms;\n .max_connections = 200;\n .ssl = true;\n .dynamic = true;\n .probe = {\n .request = \"HEAD /.well-known/healthcheck-datadome HTTP/1.1\" \"Host: api-fastly.datadome.co\" \"Connection: close\" \"User-Agent: Varnish/fastly (healthcheck)\";\n .expected_response = 200;\n .initial = 5;\n .interval = 2s;\n .threshold = 1;\n .timeout = 2s;\n .window = 5;\n }\n}", 58 | "type": "init" 59 | }, 60 | { 61 | "priority": 7, 62 | "template": "if (req.backend == datadome) {\n declare local var.status STRING;\n set var.status = beresp.status;\n # check that it is real ApiServer response\n if (var.status != beresp.http.x-datadomeresponse) {\n restart;\n }\n unset beresp.http.x-datadomeresponse;\n # copy datadome headers\n set req.http.x-datadome-headers-pairs:x-datadome-headers = urlencode(beresp.http.x-datadome-headers);\n\n if (beresp.http.x-datadome-headers ~ \"(?i)(^| )+x-set-cookie( |$)+\") {\n set req.http.x-datadome-headers-pairs:x-set-cookie = urlencode(beresp.http.x-set-cookie);\n }\n if (beresp.http.x-datadome-headers ~ \"(?i)(^| )+x-datadome-server( |$)+\") {\n set req.http.x-datadome-headers-pairs:x-datadome-server = urlencode(beresp.http.x-datadome-server);\n }\n if (beresp.http.x-datadome-headers ~ \"(?i)(^| )+x-datadome( |$)+\") {\n set req.http.x-datadome-headers-pairs:x-datadome = urlencode(beresp.http.x-datadome);\n }\n if (beresp.http.x-datadome-headers ~ \"(?i)(^| )+content-type( |$)+\") {\n set req.http.x-datadome-headers-pairs:content-type = urlencode(beresp.http.content-type);\n }\n if (beresp.http.x-datadome-headers ~ \"(?i)(^| )+charset( |$)+\") {\n set req.http.x-datadome-headers-pairs:charset = urlencode(beresp.http.charset);\n }\n if (beresp.http.x-datadome-headers ~ \"(?i)(^| )+cache-control( |$)+\") {\n set req.http.x-datadome-headers-pairs:cache-control = urlencode(beresp.http.cache-control);\n }\n if (beresp.http.x-datadome-headers ~ \"(?i)(^| )+pragma( |$)+\") {\n set req.http.x-datadome-headers-pairs:pragma = urlencode(beresp.http.pragma);\n }\n if (beresp.http.x-datadome-headers ~ \"(?i)(^| )+access-control-allow-credentials( |$)+\") {\n set req.http.x-datadome-headers-pairs:access-control-allow-credentials = urlencode(beresp.http.access-control-allow-credentials);\n }\n if (beresp.http.x-datadome-headers ~ \"(?i)(^| )+access-control-expose-headers( |$)+\") {\n set req.http.x-datadome-headers-pairs:access-control-expose-headers = urlencode(beresp.http.access-control-expose-headers);\n }\n if (beresp.http.x-datadome-headers ~ \"(?i)(^| )+access-control-allow-origin( |$)+\") {\n set req.http.x-datadome-headers-pairs:access-control-allow-origin = urlencode(beresp.http.access-control-allow-origin);\n }\n if (beresp.http.x-datadome-headers ~ \"(?i)(^| )+x-datadome-cid( |$)+\") {\n set req.http.x-datadome-headers-pairs:x-datadome-cid = urlencode(beresp.http.x-datadome-cid);\n }\n if (beresp.http.x-datadome-headers ~ \"(?i)(^| )+x-dd-b( |$)+\") {\n set req.http.x-datadome-headers-pairs:x-dd-b = urlencode(beresp.http.x-dd-b);\n }\n if (beresp.http.x-datadome-headers ~ \"(?i)(^| )+x-dd-type( |$)+\") {\n set req.http.x-datadome-headers-pairs:x-dd-type = urlencode(beresp.http.x-dd-type);\n }\n if (beresp.http.x-datadome-request-headers ~ \"(?i)(^| )+x-dd-type( |$)+\") {\n set req.http.x-dd-type = beresp.http.x-dd-type;\n }\n if (beresp.http.x-datadome-request-headers ~ \"(?i)(^| )+x-datadome-botname( |$)+\") {\n set req.http.x-datadome-botname = beresp.http.x-datadome-botname;\n }\n if (beresp.http.x-datadome-request-headers ~ \"(?i)(^| )+x-datadome-botfamily( |$)+\") {\n set req.http.x-datadome-botfamily = beresp.http.x-datadome-botfamily;\n }\n if (beresp.http.x-datadome-request-headers ~ \"(?i)(^| )+x-datadome-isbot( |$)+\") {\n set req.http.x-datadome-isbot = beresp.http.x-datadome-isbot;\n }\n if (beresp.http.x-datadome-request-headers ~ \"(?i)(^| )+x-datadome-captchapassed( |$)+\") {\n set req.http.x-datadome-captchapassed = beresp.http.x-datadome-captchapassed;\n }\n if (beresp.http.x-datadome-request-headers ~ \"(?i)(^| )+x-datadome-devicecheckpassed( |$)+\") {\n set req.http.x-datadome-devicecheckpassed = beresp.http.x-datadome-devicecheckpassed;\n }\n if (beresp.http.x-datadome-request-headers ~ \"(?i)(^| )+x-datadome-traffic-rule-response( |$)+\") {\n set req.http.x-datadome-traffic-rule-response = beresp.http.x-datadome-traffic-rule-response;\n }\n if (beresp.http.x-datadome-request-headers ~ \"(?i)(^| )+x-datadome-captchaurl( |$)+\") {\n set req.http.x-datadome-captchaurl = beresp.http.x-datadome-captchaurl;\n }\n if (beresp.http.x-datadome-request-headers ~ \"(?i)(^| )+x-datadome-requestid( |$)+\") {\n set req.http.x-datadome-requestid = beresp.http.x-datadome-requestid;\n }\n if (beresp.http.x-datadome-request-headers ~ \"(?i)(^| )+x-datadome-score( |$)+\") {\n set req.http.x-datadome-score = beresp.http.x-datadome-score;\n }\n if (beresp.http.x-datadome-request-headers ~ \"(?i)(^| )+x-datadome-ruletype( |$)+\") {\n set req.http.x-datadome-ruletype = beresp.http.x-datadome-ruletype;\n }\n if (beresp.http.x-datadome-request-headers ~ \"(?i)(^| )+x-datadome-matchedmodels( |$)+\") {\n set req.http.x-datadome-matchedmodels = beresp.http.x-datadome-matchedmodels;\n }\n if (beresp.http.x-datadome-request-headers ~ \"(?i)(^| )+x-datadome-sessionid( |$)+\") {\n set req.http.x-datadome-sessionid = beresp.http.x-datadome-sessionid;\n }\n # don\u0027t forget about ApiServer\u0027s cookies\n if (beresp.http.x-datadome-headers ~ \"(?i)(^| )+set-cookie( |$)+\") {\n set req.http.x-datadome-headers-pairs:set-cookie = urlencode(beresp.http.set-cookie);\n }\n\n # Continue only if ApiServer returns expected blocked status\n if (beresp.status != 403 \u0026\u0026 beresp.status != 401 \u0026\u0026 beresp.status != 301 \u0026\u0026 beresp.status != 302) {\n unset beresp.http.x-datadome-headers;\n unset beresp.http.x-datadome-request-headers;\n set req.http.x-datadome-cookie = beresp.http.x-datadome-cookie; # Allow Session Feature\n restart;\n }\n\n # ok, it is banned request, cleanup it a bit\n if (beresp.http.x-datadome-request-headers ~ \"(?i)(^| )+x-dd-type( |$)+\") {\n if (beresp.http.x-datadome-headers !~ \"(?i)(^| )+x-dd-type( |$)+\") {\n unset beresp.http.x-dd-type;\n }\n }\n if (beresp.http.x-datadome-request-headers ~ \"(?i)(^| )+x-datadome-botname( |$)+\") {\n if (beresp.http.x-datadome-headers !~ \"(?i)(^| )+x-datadome-botname( |$)+\") {\n unset beresp.http.x-datadome-botname;\n }\n }\n if (beresp.http.x-datadome-request-headers ~ \"(?i)(^| )+x-datadome-botfamily( |$)+\") {\n if (beresp.http.x-datadome-headers !~ \"(?i)(^| )+x-datadome-botfamily( |$)+\") {\n unset beresp.http.x-datadome-botfamily;\n }\n }\n if (beresp.http.x-datadome-request-headers ~ \"(?i)(^| )+x-datadome-isbot( |$)+\") {\n if (beresp.http.x-datadome-headers !~ \"(?i)(^| )+x-datadome-isbot( |$)+\") {\n unset beresp.http.x-datadome-isbot;\n }\n }\n if (beresp.http.x-datadome-request-headers ~ \"(?i)(^| )+x-datadome-captchapassed( |$)+\") {\n if (beresp.http.x-datadome-headers !~ \"(?i)(^| )+x-datadome-captchapassed( |$)+\") {\n unset beresp.http.x-datadome-captchapassed;\n }\n }\n if (beresp.http.x-datadome-request-headers ~ \"(?i)(^| )+x-datadome-devicecheckpassed( |$)+\") {\n if (beresp.http.x-datadome-headers !~ \"(?i)(^| )+x-datadome-devicecheckpassed( |$)+\") {\n unset beresp.http.x-datadome-devicecheckpassed;\n }\n }\n if (beresp.http.x-datadome-request-headers ~ \"(?i)(^| )+x-datadome-traffic-rule-response( |$)+\") {\n if (beresp.http.x-datadome-headers !~ \"(?i)(^| )+x-datadome-traffic-rule-response( |$)+\") {\n unset beresp.http.x-datadome-traffic-rule-response;\n }\n }\n if (beresp.http.x-datadome-request-headers ~ \"(?i)(^| )+x-datadome-captchaurl( |$)+\") {\n if (beresp.http.x-datadome-headers !~ \"(?i)(^| )+x-datadome-captchaurl( |$)+\") {\n unset beresp.http.x-datadome-captchaurl;\n }\n }\n if (beresp.http.x-datadome-request-headers ~ \"(?i)(^| )+x-datadome-requestid( |$)+\") {\n if (beresp.http.x-datadome-headers !~ \"(?i)(^| )+x-datadome-requestid( |$)+\") {\n unset beresp.http.x-datadome-requestid;\n }\n }\n if (beresp.http.x-datadome-request-headers ~ \"(?i)(^| )+x-datadome-score( |$)+\") {\n if (beresp.http.x-datadome-headers !~ \"(?i)(^| )+x-datadome-score( |$)+\") {\n unset beresp.http.x-datadome-score;\n }\n }\n if (beresp.http.x-datadome-request-headers ~ \"(?i)(^| )+x-datadome-ruletype( |$)+\") {\n if (beresp.http.x-datadome-headers !~ \"(?i)(^| )+x-datadome-ruletype( |$)+\") {\n unset beresp.http.x-datadome-ruletype;\n }\n }\n if (beresp.http.x-datadome-request-headers ~ \"(?i)(^| )+x-datadome-matchedmodels( |$)+\") {\n if (beresp.http.x-datadome-headers !~ \"(?i)(^| )+x-datadome-matchedmodels( |$)+\") {\n unset beresp.http.x-datadome-matchedmodels;\n }\n }\n if (beresp.http.x-datadome-request-headers ~ \"(?i)(^| )+x-datadome-sessionid( |$)+\") {\n if (beresp.http.x-datadome-headers !~ \"(?i)(^| )+x-datadome-sessionid( |$)+\") {\n unset beresp.http.x-datadome-sessionid;\n }\n }\n unset beresp.http.x-datadome-headers;\n unset beresp.http.x-datadome-request-headers;\n}", 63 | "type": "fetch" 64 | }, 65 | { 66 | "priority": 7, 67 | "template": "if (req.backend == datadome) {\n restart;\n}", 68 | "type": "error" 69 | }, 70 | { 71 | "priority": 7, 72 | "template": "# copy datadome headers if it isn\u0027t datadome request\nif (fastly.ff.visits_this_service == 0 \u0026\u0026 req.backend != datadome) {\n declare local var.x-datadome-headers STRING;\n set var.x-datadome-headers = urldecode(req.http.x-datadome-headers-pairs:x-datadome-headers);\n if (var.x-datadome-headers ~ \"(?i)(^| )+x-set-cookie( |$)+\") {\n set resp.http.x-set-cookie = urldecode(req.http.x-datadome-headers-pairs:x-set-cookie);\n }\n if (var.x-datadome-headers ~ \"(?i)(^| )+x-datadome-server( |$)+\") {\n set resp.http.x-datadome-server = urldecode(req.http.x-datadome-headers-pairs:x-datadome-server);\n }\n if (var.x-datadome-headers ~ \"(?i)(^| )+x-datadome( |$)+\") {\n set resp.http.x-datadome = urldecode(req.http.x-datadome-headers-pairs:x-datadome);\n }\n if (var.x-datadome-headers ~ \"(?i)(^| )+content-type( |$)+\") {\n set resp.http.content-type = urldecode(req.http.x-datadome-headers-pairs:content-type);\n }\n if (var.x-datadome-headers ~ \"(?i)(^| )+charset( |$)+\") {\n set resp.http.charset = urldecode(req.http.x-datadome-headers-pairs:charset);\n }\n if (var.x-datadome-headers ~ \"(?i)(^| )+cache-control( |$)+\") {\n set resp.http.cache-control = urldecode(req.http.x-datadome-headers-pairs:cache-control);\n }\n if (var.x-datadome-headers ~ \"(?i)(^| )+pragma( |$)+\") {\n set resp.http.pragma = urldecode(req.http.x-datadome-headers-pairs:pragma);\n }\n if (var.x-datadome-headers ~ \"(?i)(^| )+access-control-allow-credentials( |$)+\") {\n set resp.http.access-control-allow-credentials = urldecode(req.http.x-datadome-headers-pairs:access-control-allow-credentials);\n }\n if (var.x-datadome-headers ~ \"(?i)(^| )+access-control-expose-headers( |$)+\") {\n set resp.http.access-control-expose-headers = urldecode(req.http.x-datadome-headers-pairs:access-control-expose-headers);\n }\n if (var.x-datadome-headers ~ \"(?i)(^| )+access-control-allow-origin( |$)+\") {\n set resp.http.access-control-allow-origin = urldecode(req.http.x-datadome-headers-pairs:access-control-allow-origin);\n }\n if (var.x-datadome-headers ~ \"(?i)(^| )+x-datadome-cid( |$)+\") {\n set resp.http.x-datadome-cid = urldecode(req.http.x-datadome-headers-pairs:x-datadome-cid);\n }\n if (var.x-datadome-headers ~ \"(?i)(^| )+x-dd-b( |$)+\") {\n set resp.http.x-dd-b = urldecode(req.http.x-datadome-headers-pairs:x-dd-b);\n }\n if (var.x-datadome-headers ~ \"(?i)(^| )+x-dd-type( |$)+\") {\n set resp.http.x-dd-type = urldecode(req.http.x-datadome-headers-pairs:x-dd-type);\n }\n # Allow Session Feature\n if (resp.http.x-datadome-cookie) {\n add resp.http.set-cookie = resp.http.x-datadome-cookie;\n if (req.http.x-datadome-clientid) {\n set resp.http.x-set-cookie = resp.http.x-datadome-cookie;\n }\n unset resp.http.x-datadome-cookie;\n } elseif (var.x-datadome-headers ~ \"(?i)(^| )+set-cookie( |$)+\") {\n # don\u0027t forget about ApiServer\u0027s cookies\n add resp.http.set-cookie = urldecode(req.http.x-datadome-headers-pairs:set-cookie);\n }\n}", 73 | "type": "deliver" 74 | }, 75 | { 76 | "priority": 7, 77 | "template": "\n {{#if logging_endpoint}}\n ## Debug DataDome\n log {\"syslog \"} req.service_id {\" {{logging_endpoint}} :: \"}\n \" timestamp=%22\" now\n \"%22 client_ip=\" req.http.Fastly-Client-IP\n \" request=\" req.method\n \" url=%22\" cstr_escape(req.url.path)\n \"%22 restarts=\" req.restarts\n \" DataDomeDebug=\" \"Before_DataDome\"\n \" fastlyFF=\" fastly.ff.visits_this_service;\n ##\n {{/if}}# Configure the regular expression below to match URLs that\n# should be checked by DataDome\nif (fastly.ff.visits_this_service == 0 \u0026\u0026 req.restarts == 0 \u0026\u0026 req.method != \"FASTLYPURGE\" \u0026\u0026 !(req.url.path ~ \"{{datadome_exclusion_ext}}\" \u0026\u0026 (req.method == \"GET\" || req.method == \"HEAD\"))) {\n\n set req.backend = datadome;\n unset req.http.x-datadome-params;\n # Configure the string below to include your DataDome API key\n set req.http.x-datadome-params:method = urlencode(req.method);\n set req.http.x-datadome-params:postparamlen = urlencode(req.http.content-length);\n set req.method = \"GET\";\n set req.http.x-datadome-params:tlsprotocol = urlencode(tls.client.protocol);\n set req.http.x-datadome-params:tlscipherslist = urlencode(tls.client.ciphers_list);\n set req.http.x-datadome-params:tlsextensionslist = urlencode(tls.client.tlsexts_list);\n set req.http.x-datadome-params:ja3 = urlencode(tls.client.ja3_md5);\n {{#if logging_endpoint}}\n ## Debug DataDome\n log {\"syslog \"} req.service_id {\" {{logging_endpoint}} :: \"}\n \" timestamp=%22\" now\n \"%22 client_ip=\" req.http.Fastly-Client-IP\n \" request=\" req.method\n \" host=\" req.http.host\n \" url=%22\" cstr_escape(req.url)\n \"%22 request_referer=%22\" cstr_escape(req.http.Referer)\n \"%22 request_user_agent=%22\" cstr_escape(req.http.User-Agent)\n \"%22 request_accept_language=%22\" cstr_escape(req.http.Accept-Language)\n \"%22 request_accept_charset=%22\" cstr_escape(req.http.Accept-Charset)\n \"%22 contentLength=\" req.http.Content-Length\n \" restarts=\" req.restarts\n \" DataDomeDebug=\" \"To_DataDome\"\n \" fastlyFF=\" fastly.ff.visits_this_service;\n ##\n {{/if}}\n return (pass);\n} else {\n if (req.http.x-datadome-params:method) {\n set req.method = urldecode(req.http.x-datadome-params:method);\n # After a restart, clustering is disabled. This re-enables it.\n set req.http.fastly-force-shield = \"1\";\n }\n unset req.http.x-datadome-params;\n {{#if logging_endpoint}}\n ## Debug DataDome\n log {\"syslog \"} req.service_id {\" {{logging_endpoint}} :: \"}\n \" timestamp=%22\" now\n \"%22 client_ip=\" req.http.Fastly-Client-IP\n \" request=\" req.method\n \" host=\" req.http.host\n \" url=%22\" cstr_escape(req.url)\n \"%22 request_referer=%22\" cstr_escape(req.http.Referer)\n \"%22 request_user_agent=%22\" cstr_escape(req.http.User-Agent)\n \"%22 request_accept_language=%22\" cstr_escape(req.http.Accept-Language)\n \"%22 request_accept_charset=%22\" cstr_escape(req.http.Accept-Charset)\n \"%22 contentLength=\" req.http.Content-Length\n \" restarts=\" req.restarts\n \" DataDomeDebug=\" \"Bypass_DataDome\"\n \" fastlyFF=\" fastly.ff.visits_this_service;\n ##\n {{/if}}\n}\n\n# we\u0027re using the first restart for datadome, update a part of fastly code\n# we can\u0027t replace whole macros because we haven\u0027t got any idea about backends\nif (req.restarts == 1) {\n if (!req.http.x-timer) {\n set req.http.x-timer = \"S\" time.start.sec \".\" time.start.usec_frac;\n }\n set req.http.x-timer = req.http.x-timer \",VS0\";\n}\n\nset var.fastly_req_do_shield = (req.restarts \u003c= 1);", 78 | "type": "recv" 79 | }, 80 | { 81 | "priority": 7, 82 | "template": "call set_origin_header;", 83 | "type": "miss" 84 | }, 85 | { 86 | "priority": 7, 87 | "template": "call set_origin_header;", 88 | "type": "pass" 89 | } 90 | ], 91 | "version": "2.19.4" 92 | } 93 | -------------------------------------------------------------------------------- /fastly_edge_modules/disabled/aclblacklist.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Block users by IP or subnet", 3 | "id": "aclblacklist", 4 | "description": "Use an Access Control List (ACL) to block users from specific IPs or\nnetwork", 5 | "version": 1, 6 | "properties": [ 7 | { 8 | "name": "externalAcl", 9 | "label": "ACL", 10 | "description": "Referencing a named access control list defined on your service enables\nyou to [update it via our API](https://docs.fastly.com/guides/access-control-lists/creating-and-manipulating-edge-acl-entries).", 11 | "type": "acl", 12 | "required": false 13 | }, 14 | { 15 | "name": "localAcl", 16 | "label": "IPs/masks", 17 | "description": "List specific IPs and subnets to block, separated by newlines", 18 | "type": "longstring", 19 | "required": false, 20 | "validation": "^(\"(\\d{3}" 21 | }, 22 | { 23 | "name": "mode", 24 | "label": "Mode", 25 | "type": "select", 26 | "options": { 27 | "allow": "Allow only the listed IPs", 28 | "block": "Allow all except the listed IPs" 29 | }, 30 | "required": true 31 | } 32 | ], 33 | "vcl": [ 34 | { 35 | "type": "init", 36 | "template": "{{#if localAcl}}\nacl modly_aclblacklist_local {\n {{localAcl}}\n}\n{{/if}}" 37 | }, 38 | { 39 | "type": "recv", 40 | "template": "{{#ifEq mode \"allow\"}}\n {{#if ../localAcl}}\n {{#if ../externalAcl}}\n if (client.ip !~ modly_aclblacklist_local && client.ip !~ {{../externalAcl}}) {\n error 921 \"[modly:aclblacklist]\";\n }\n {{else}}\n if (client.ip !~ modly_aclblacklist_local) {\n error 921 \"[modly:aclblacklist]\";\n }\n {{/if}}\n {{else}}\n if (client.ip !~ {{../externalAcl}}) {\n error 921 \"[modly:aclblacklist]\";\n }\n {{/if}}\n{{else}}\n {{#if ../externalAcl}}\n if (client.ip ~ {{../externalAcl}}) {\n error 921 \"[modly:aclblacklist]\";\n }\n {{/if}}\n {{#if ../localAcl}}\n if (client.ip ~ modly_aclblacklist_local) {\n error 921 \"[modly:aclblacklist]\";\n }\n {{/if}}\n{{/ifEq}}" 41 | }, 42 | { 43 | "type": "error", 44 | "template": "if (obj.status == 921 && obj.response == \"[modly:aclblacklist]\") {\n synthetic \"\";\n set obj.status = 403;\n set obj.response = \"Forbidden\";\n set obj.http.Content-Type = \"text/html\";\n if (req.http.Fastly-Debug) {\n set obj.http.Fastly-ACL = \"Ban in effect for \" client.ip;\n } \n return (deliver);\n}" 45 | } 46 | ], 47 | "test": { 48 | "origins": [ 49 | "https://httpbin.org" 50 | ], 51 | "reqUrl": "/status/500" 52 | } 53 | } -------------------------------------------------------------------------------- /fastly_edge_modules/disabled/error.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "description": "In the event of 4xx or 5xx errors being returned from your origin server, replace the output with something else at the edge.", 4 | "name": "Custom error pages", 5 | "test": { 6 | "reqUrl": "/status/500", 7 | "origins": [ 8 | "https://httpbin.org" 9 | ] 10 | }, 11 | "vcl": [ 12 | { 13 | "type": "fetch", 14 | "template": "{{#each responses}}\nif (beresp.status == {{type}}) {\n error 921 \"[modly:customerrors]{{type}}\";\n}\n{{/each}}" 15 | }, 16 | { 17 | "type": "error", 18 | "template": "if (obj.status == 921 && obj.response ~ \"^\\[modly:customerrors\\](.*)$\") {\n {{#each responses}}\n if (re.group.1 == \"{{type}}\") {\n synthetic {\"{{responseBody}}\"};\n return (deliver);\n }\n {{/each}}\n}\n#TODO: errors during fetch ('nofetch')" 19 | } 20 | ], 21 | "id": "customerrors", 22 | "properties": [ 23 | { 24 | "label": "Responses", 25 | "type": "group", 26 | "properties": [ 27 | { 28 | "label": "Error type", 29 | "required": true, 30 | "type": "select", 31 | "options": { 32 | "404": "404 Not Found", 33 | "nofetch": "Origin offline", 34 | "401": "401 Unauthorized", 35 | "403": "403 Forbidden", 36 | "503": "503 Service Unavailable", 37 | "500": "500 Internal Server Error" 38 | }, 39 | "name": "type" 40 | }, 41 | { 42 | "required": true, 43 | "type": "longstring", 44 | "name": "responseBody", 45 | "label": "Response content" 46 | } 47 | ], 48 | "name": "responses", 49 | "entryTemplate": "{{type}}" 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /fastly_edge_modules/disabled/normalise.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "description": "Normalise requests to increase cache hit ratio", 4 | "name": "Normalise requests", 5 | "test": { 6 | "reqHeaders": "User-Agent: test\nAccept-Language: en-GB,en-US;q=0.9,en;q=0.8", 7 | "reqUrl": "/html?FOO=42&bar=TESTtest&aa=", 8 | "origins": [ 9 | "https://httpbin.org" 10 | ] 11 | }, 12 | "vcl": [ 13 | { 14 | "type": "recv", 15 | "template": "{{#if lower }}\nset req.url = std.tolower(req.url);\n{{/if}}\n\n{{#if qsWhitelist }}\nset req.url = querystring.filter_except(req.url,\n \"{{replace qsWhitelist '\\W' '\" querystring.filtersep() \"'}}\"\n);\n{{/if}}\n\n{{#if qsSort }}\nset req.url = querystring.sort(req.url);\n{{/if}}\n\n{{#if unsetHeaders }}\nunset req.http.{{replace unsetHeaders \"\\s+\" \"; unset req.http.\"}};\n{{/if}}\n\n{{#if acceptLangs }}\nset req.http.Accept-Language = accept.language_lookup(\n \"{{replace acceptLangs \"[,\\s&]+\" \":\"}}\",\n \"{{extract acceptLangs \"^(\\w+)([,\\s&].*)?$\"}}\",\n req.http.Accept-Language\n);\n{{/if}}" 16 | } 17 | ], 18 | "id": "normalisereqs", 19 | "properties": [ 20 | { 21 | "label": "Strip all query params except", 22 | "required": false, 23 | "type": "string", 24 | "description": "List the query parameters that are valid for your site, separated by `&`, `,` or spaces. All others will be removed.", 25 | "name": "qsWhitelist" 26 | }, 27 | { 28 | "description": "Sorting the querystring improves cache efficiency without changing any of the data", 29 | "name": "qsSort", 30 | "default": false, 31 | "required": false, 32 | "type": "boolean", 33 | "label": "Sort querystring parameters" 34 | }, 35 | { 36 | "description": "Converts entire URL to lowercase, including path segments, query keys and values.", 37 | "name": "lower", 38 | "default": false, 39 | "required": false, 40 | "type": "boolean", 41 | "label": "Convert to lowercase" 42 | }, 43 | { 44 | "label": "Remove headers", 45 | "required": false, 46 | "type": "longstring", 47 | "description": "Headers that you would like to strip from the incoming request, one per line.", 48 | "name": "unsetHeaders" 49 | }, 50 | { 51 | "label": "Accepted languages", 52 | "required": false, 53 | "type": "string", 54 | "description": "ISO-639-1 language codes recognised by your origin server, separated by space. Incoming `Accept-Language` headers will be normalised to one of these values. List your default language first.", 55 | "name": "acceptLangs" 56 | } 57 | ] 58 | } -------------------------------------------------------------------------------- /fastly_edge_modules/disabled/redirects.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Redirects", 3 | "id": "redirects", 4 | "description": "Emit redirects directly from the edge for known URLs", 5 | "version": 1, 6 | "properties": [ 7 | { 8 | "name": "rules", 9 | "label": "Rules", 10 | "type": "group", 11 | "entryTemplate": "{{source}} => {{dest}} ({{type}})", 12 | "properties": [ 13 | { 14 | "name": "source", 15 | "label": "Source path", 16 | "description": "URL path of incoming request to redirect", 17 | "type": "path", 18 | "required": true 19 | }, 20 | { 21 | "name": "dest", 22 | "label": "Destination path or URL", 23 | "description": "URL path to which to redirect", 24 | "type": "string", 25 | "required": true 26 | }, 27 | { 28 | "name": "type", 29 | "label": "Type of redirect", 30 | "description": "Permanent and temporary redirects send HTTP responses to the client. Internal\nredirects accept the incoming request and remap it invisibly at the Edge. Internal\nredirects cannot change the domain.", 31 | "type": "select", 32 | "required": false, 33 | "default": "308", 34 | "options": { 35 | "307": "Temporary", 36 | "308": "Permanent", 37 | "internal": "Internal" 38 | } 39 | }, 40 | { 41 | "name": "matchQS", 42 | "label": "Match querystring", 43 | "description": "Whether to include the querystring when matching against the source\npath. Querystring is always lowercased and sorted prior to matching", 44 | "type": "boolean", 45 | "required": false, 46 | "default": false 47 | } 48 | ] 49 | } 50 | ], 51 | "vcl": [ 52 | { 53 | "type": "recv", 54 | "template": "if (req.url == \"xxxxxxx\") {\n{{#each rules}}\n} elseif ({{#if matchQS}}req.url{{else}}req.url.path{{/if}} == \"{{source}}\") {\n {{#ifEq type \"internal\"}}\n set req.url = \"{{../dest}}\";\n {{else}}\n error 921 \"[modly:redirect]{{../dest}},{{../type}}\";\n {{/ifEq}}\n{{/each}}\n}" 55 | }, 56 | { 57 | "type": "error", 58 | "template": "if (obj.status == 921 && obj.response ~ \"^\\[modly\\:redirect\\](.*),(\\d+)\") {\n set obj.status = std.atoi(re.group.2);\n set obj.response = if (re.group.2 == \"308\", \"Permanent redirect\", \"Temporary redirect\");\n set obj.http.Location = re.group.1;\n return (deliver);\n}" 59 | } 60 | ], 61 | "test": { 62 | "origins": [ 63 | "https://httpbin.org" 64 | ], 65 | "reqUrl": "/sourcePath" 66 | } 67 | } -------------------------------------------------------------------------------- /fastly_edge_modules/disabled/stale.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "description": "If your origin server is unreachable, serve a stale version of the content from the Fastly cache", 4 | "name": "Serve stale on origin failure", 5 | "test": { 6 | "reqUrl": "/html", 7 | "origins": [ 8 | "https://httpbin.org" 9 | ] 10 | }, 11 | "vcl": [ 12 | { 13 | "type": "recv", 14 | "template": "if (req.http.Fastly-FF) {\n set req.max_stale_while_revalidate = 0s;\n set req.max_stale_if_error = 0s;\n}" 15 | }, 16 | { 17 | "type": "fetch", 18 | "template": "if (beresp.status >= 500 && beresp.status < 600) {\n if (stale.exists) {\n return(deliver_stale);\n }\n if (req.restarts < 1 && (req.request == \"GET\" || req.request == \"HEAD\")) {\n restart;\n }\n error 503;\n}\n{{#if sie}}set beresp.stale_if_error = {{sie}};{{/if}}\n{{#if swr}}set beresp.stale_while_revalidate = {{swr}};{{/if}}" 19 | }, 20 | { 21 | "type": "deliver", 22 | "template": "if (resp.status >= 500 && resp.status < 600 && stale.exists) {\n restart;\n}" 23 | }, 24 | { 25 | "type": "error", 26 | "template": "if (obj.status >= 500 && obj.status < 600) {\n if (stale.exists) {\n return(deliver_stale);\n }\n set obj.status = 503;\n set obj.response = \"Service unavailable\";\n set obj.http.Content-Type = \"text/html\";\n synthetic {\"{{offlineContent}}\"};\n return(deliver);\n}" 27 | } 28 | ], 29 | "id": "servestale", 30 | "properties": [ 31 | { 32 | "description": "Maximum time in seconds to allow stale content to be served while waiting for updated content from origin", 33 | "name": "swr", 34 | "default": "60s", 35 | "required": false, 36 | "type": "rtime", 37 | "label": "Stale lifetime for async revalidation" 38 | }, 39 | { 40 | "description": "Maximum time in seconds to allow stale content to be served while origin is unreachable", 41 | "name": "sie", 42 | "default": "604800s", 43 | "required": false, 44 | "type": "rtime", 45 | "label": "Stale lifetime for origin failure" 46 | }, 47 | { 48 | "description": "Content to serve (with a 503 Service Unavailable response) if the origin is offline and a stale version is not available", 49 | "name": "offlineContent", 50 | "default": "Sorry, we are currently experiencing a problem.", 51 | "required": false, 52 | "type": "longstring", 53 | "label": "Offline page content" 54 | } 55 | ] 56 | } -------------------------------------------------------------------------------- /fastly_edge_modules/disabled/ttloverride.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Override Edge TTL / cache time", 3 | "id": "ttloverride", 4 | "description": "For selected requests, override caching directives received from the origin server. Often used to increase TTL for static assets.", 5 | "version": 1, 6 | "properties": [ 7 | { 8 | "name": "rules", 9 | "label": "Override rules", 10 | "type": "group", 11 | "entryTemplate": "{{pathpattern}}: {{ttl}}s", 12 | "properties": [ 13 | { 14 | "name": "pathpattern", 15 | "label": "Path pattern", 16 | "description": "Regular expressions are supported", 17 | "type": "string", 18 | "required": true 19 | }, 20 | { 21 | "name": "ttl", 22 | "label": "Freshness duration", 23 | "type": "rtime", 24 | "required": true 25 | }, 26 | { 27 | "name": "swr", 28 | "label": "Stale while revalidate duration", 29 | "type": "rtime", 30 | "required": false 31 | }, 32 | { 33 | "name": "sie", 34 | "label": "Stale if error duration", 35 | "type": "rtime", 36 | "required": false 37 | } 38 | ] 39 | } 40 | ], 41 | "vcl": [ 42 | { 43 | "type": "fetch", 44 | "template": "{{#each rules}}\nif (req.url ~ \"{{pathpattern}}\") {\n set beresp.ttl = {{ttl}};\n {{#if swr}}set beresp.stale-while-revalidate = {{swr}};{{/if}}\n {{#if sie}}set beresp.stale-if-error = {{sie}};{{/if}}\n}\n{{/each}}" 45 | } 46 | ], 47 | "test": { 48 | "origins": [ 49 | "https://httpbin.org" 50 | ], 51 | "reqUrl": "/html" 52 | } 53 | } -------------------------------------------------------------------------------- /fastly_edge_modules/force_cache_miss_on_hard_reload_for_admins.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Hard Reload cache bypass for set of admin IPs", 3 | "id": "force_cache_miss_on_hard_reload_for_admins", 4 | "description": "Force cache miss for users on allowlist. Invoke it on your browser by pressing CMD/CTRL + SHIFT + R or SHIFT + F5 depending on your browser. It only affects your own session. It will not affected already cached content.", 5 | "version": 1, 6 | "properties": [ 7 | { 8 | "name": "Acl", 9 | "label": "ACL", 10 | "description": "ACL that contains IPs of users allowing to force cache misses", 11 | "type": "acl", 12 | "required": true 13 | } 14 | ], 15 | "vcl": [ 16 | { 17 | "type": "recv", 18 | "template": "if ( req.http.Fastly-Client-IP ~ {{Acl}} && req.http.pragma ~ \"no-cache\" ) {\n set req.hash_always_miss = true;\n}" 19 | }, 20 | { 21 | "type": "hash", 22 | "template": "if ( req.http.Fastly-Client-IP ~ {{Acl}} && req.http.pragma ~ \"no-cache\" ) {\n set req.hash += \"NOCACHE\";\n\n}" 23 | } 24 | ], 25 | "test": { 26 | "origins": [ 27 | "https://httpbin.org" 28 | ], 29 | "reqUrl": "/status/500" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /fastly_edge_modules/increase_timeouts_long_jobs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Increase timeouts for long running jobs", 3 | "id": "increase_timeouts_long_jobs", 4 | "description": "For selected requests, override default backend timeout. Often used for long running jobs that take over 1 minute. Please note these paths will no longer be cached. Fastly imposes hard limit 10 minute timeout.", 5 | "version": 1, 6 | "properties": [ 7 | { 8 | "name": "rules", 9 | "label": "Timeout override rules", 10 | "type": "group", 11 | "properties": [ 12 | { 13 | "name": "pathpattern", 14 | "label": "Path pattern (regular expression)", 15 | "description": "Regular expressions are supported", 16 | "type": "string", 17 | "required": true 18 | }, 19 | { 20 | "name": "timeout", 21 | "label": "Timeout in seconds", 22 | "type": "integer", 23 | "default": "300", 24 | "required": true 25 | } 26 | ] 27 | } 28 | ], 29 | "vcl": [ 30 | { 31 | "type": "recv", 32 | "template": "if (req.restarts == 0) {\n unset req.http.x-edge-module-timeout;\n}{{#each rules}}\nif (req.url ~ \"{{pathpattern}}\") {\n set req.http.x-pass = \"1\";\n set req.http.x-edge-module-timeout = \"{{timeout}}\";\n}\n{{/each}}", 33 | "priority": 80 34 | }, 35 | { 36 | "type": "pass", 37 | "template": "if (req.http.x-edge-module-timeout) {\n set bereq.first_byte_timeout = std.atof(req.http.x-edge-module-timeout);\n}" 38 | } 39 | ], 40 | "test": { 41 | "origins": [ 42 | "https://httpbin.org" 43 | ], 44 | "reqUrl": "/html" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /fastly_edge_modules/mobile_device_detection.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mobile Theme support", 3 | "id": "mobile_device_detection", 4 | "description": "By default Fastly caches a single version of a page ignoring device type e.g. mobile/desktop. This module adds Vary-ing by a device type. It supports iPhone, Android and Tizen mobile device detection. It will cache separate page versions for mobile and desktop", 5 | "version": 1, 6 | "vcl": [ 7 | { 8 | "priority": 45, 9 | "type": "recv", 10 | "template": " # Mobile device detection for mobile themes\n set req.http.X-UA-Device = \"desktop\";\n\n if (req.http.User-Agent ~ \"(?i)ip(hone|od)\") {\n set req.http.X-UA-Device = \"mobile\";\n } elsif (req.http.User-Agent ~ \"(?i)android.*(mobile|mini)\") {\n set req.http.X-UA-Device = \"mobile\";\n } elsif (req.http.User-Agent ~ \"(?i)tizen.*mobile\") {\n set req.http.X-UA-Device = \"mobile\";\n }" 11 | }, 12 | { 13 | "priority": 70, 14 | "type": "fetch", 15 | "template": " # Add X-UA-Device Vary for HTML\n if ( beresp.http.Content-Type ~ \"text/html\" ) {\n set beresp.http.Vary:X-UA-Device = \"\";\n }" 16 | }, 17 | { 18 | "priority": 70, 19 | "type": "deliver", 20 | "template": " # Execute only on the edge nodes\n if ( fastly.ff.visits_this_service == 0 && !req.http.Fastly-Debug ) {\n unset resp.http.Vary:X-UA-Device;\n }" 21 | } 22 | ], 23 | "test": { 24 | "origins": [ 25 | "https://httpbin.org" 26 | ], 27 | "reqUrl": "/status/500" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /fastly_edge_modules/nocache.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Disable caching", 3 | "id": "disablecache", 4 | "description": "For selected requests, disable caching, either on Fastly, or in the browser, or both.", 5 | "version": 1, 6 | "properties": [ 7 | { 8 | "name": "rules", 9 | "label": "Disable caching for", 10 | "type": "group", 11 | "entryTemplate": "{{pathpattern}}: {{mode}}", 12 | "properties": [ 13 | { 14 | "name": "pathpattern", 15 | "label": "Path pattern", 16 | "description": "Regular expressions are supported", 17 | "type": "string", 18 | "required": true 19 | }, 20 | { 21 | "name": "mode", 22 | "label": "Where to disable caching", 23 | "type": "select", 24 | "options": { 25 | "browser": "Browser", 26 | "fastly": "Fastly edge cache", 27 | "both": "Browser and Fastly" 28 | }, 29 | "required": true 30 | } 31 | ] 32 | } 33 | ], 34 | "vcl": [ 35 | { 36 | "type": "recv", 37 | "template": "{{#each rules}}\n{{#ifMatch mode 'both|fastly'}}\nif (req.url ~ \"{{../pathpattern}}\") {\n set req.http.x-pass = \"1\";\n}\n{{/ifMatch}}\n{{/each}}" 38 | }, 39 | { 40 | "type": "deliver", 41 | "template": "{{#each rules}}\n{{#ifMatch mode 'both|browser'}}\nif (req.url ~ \"{{../pathpattern}}\" && fastly.ff.visits_this_service == 0 ) {\n set resp.http.Cache-Control = \"no-cache, private\";\n unset resp.http.Expires;\n unset resp.http.Pragma;\n}\n{{/ifMatch}}\n{{/each}}" 42 | } 43 | ], 44 | "test": { 45 | "origins": [ 46 | "https://httpbin.org" 47 | ], 48 | "reqUrl": "/html" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /fastly_edge_modules/other_cms_integration.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Other CMS/backend integration", 3 | "id": "other_cms_integration", 4 | "description": "This edge module is intended to integrate other CMSes/backend into your shop e.g. Wordpress blog etc. Sometimes referred to as domain masking", 5 | "version": 1, 6 | "properties": [ 7 | { 8 | "name": "urls_dict", 9 | "label": "URL prefixes Dictionary", 10 | "description": "Pick the dictionary that contains a list of prefixes that should be sent to another CMS/backend", 11 | "type": "dict", 12 | "required": true 13 | }, 14 | { 15 | "name": "override_backend_hostname", 16 | "label": "Override Backend Hostname", 17 | "description": "Optional hostname to send to the backend. DEFAULT doesn't modify Host header sent to the origin.", 18 | "type": "string", 19 | "default": "DEFAULT", 20 | "required": true 21 | } 22 | ], 23 | "vcl": [ 24 | { 25 | "priority": 70, 26 | "type": "recv", 27 | "template": " # Make sure X-ExternalCMS is not set before proceeding\n if ( req.restarts == 0 ) {\n remove req.http.X-ExternalCMS;\n }\n\n # Extract first part of the path from a URL\n if ( req.url.path ~ \"^/?([^:/\\s]+).*$\" ) {\n # check if first part of the url is in the wordpress urls table\n if ( table.lookup({{urls_dict}}, re.group.1, \"NOTFOUND\") != \"NOTFOUND\" ) {\n set req.http.X-ExternalCMS = \"1\";\n # There is an issue with load-scripts.php in Wordpress where the ordering of query arguments matter\n # in compressing the JS. By default Magento sorts query arguments. Here we undo the sorting for wp-admin URLs\n if ( req.url.path ~ \"/wp-admin\" ) {\n set req.url = req.http.Magento-Original-URL;\n }\n }\n }\n" 28 | }, 29 | { 30 | "type": "miss", 31 | "template": "{{#ifEq override_backend_hostname \"DEFAULT\"}}\n # Intentionally empty {{else}} \n if ( req.backend.is_origin && req.http.X-ExternalCMS ) {\n set bereq.http.host = \"{{override_backend_hostname}}\";\n }\n{{/ifEq}}" 32 | }, 33 | { 34 | "type": "pass", 35 | "template": "{{#ifEq override_backend_hostname \"DEFAULT\"}}\n # Intentionally empty {{else}} \n if ( req.backend.is_origin && req.http.X-ExternalCMS ) {\n set bereq.http.host = \"{{override_backend_hostname}}\";\n }\n{{/ifEq}}" 36 | } 37 | ], 38 | "test": { 39 | "origins": [ 40 | "https://httpbin.org" 41 | ], 42 | "reqUrl": "/status/500" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /fastly_edge_modules/redirect_hosts.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Set up domain/host redirects (301) e.g. domain.com => www.domain.com", 3 | "id": "redirect_hosts", 4 | "name": "Redirect one domain to another", 5 | "properties": [ 6 | { 7 | "entryTemplate": "{{incomingHost}} => {{destinationHost}}", 8 | "label": "Rules", 9 | "name": "rules", 10 | "properties": [ 11 | { 12 | "description": "Incoming domain/host e.g. www.domain.com", 13 | "label": "Incoming Domain/Host", 14 | "name": "incomingHost", 15 | "required": true, 16 | "type": "string" 17 | }, 18 | { 19 | "description": "Destination domain/host", 20 | "label": "Destination domain/host", 21 | "name": "destinationHost", 22 | "required": true, 23 | "type": "string" 24 | }, 25 | { 26 | "default": false, 27 | "description": "Strip incoming path and set it to /. Default only rewrites host retaining the path e.g. http://domain.com/category is redirected to https://www.domain.com/category", 28 | "label": "Ignore path", 29 | "name": "ignorePath", 30 | "required": true, 31 | "type": "boolean" 32 | } 33 | ], 34 | "type": "group" 35 | } 36 | ], 37 | "test": { 38 | "origins": [ 39 | "https://httpbin.org" 40 | ], 41 | "reqUrl": "/sourcePath" 42 | }, 43 | "vcl": [ 44 | { 45 | "priority": 4, 46 | "template": "{{#each rules}}\nif (req.http.host == \"{{incomingHost}}\") {\n set req.http.host = \"{{destinationHost}}\";\n {{#if ignorePath }}\n set req.url = \"/\";\n{{/if}} error 801;\n}\n{{/each}}", 47 | "type": "recv" 48 | } 49 | ], 50 | "version": 1 51 | } 52 | -------------------------------------------------------------------------------- /fastly_edge_modules/url_rewrites.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Rewrite URL path to point to the correct URL on the backend. NOT a redirect.", 3 | "id": "url_rewrites", 4 | "name": "URL rewrites", 5 | "properties": [ 6 | { 7 | "entryTemplate": "{{source}} => {{dest}}", 8 | "label": "Rules", 9 | "name": "rules", 10 | "properties": [ 11 | { 12 | "default": "/sitemap.xml", 13 | "description": "Incoming URL path only (no host) exact match e.g. /sitemap.xml", 14 | "label": "Source path", 15 | "name": "source", 16 | "required": true, 17 | "type": "path" 18 | }, 19 | { 20 | "default": "/media/sitemap.xml", 21 | "description": "Destination path on origin (no host)", 22 | "label": "Destination path", 23 | "name": "dest", 24 | "required": true, 25 | "type": "string" 26 | } 27 | ], 28 | "type": "group" 29 | } 30 | ], 31 | "test": { 32 | "origins": [ 33 | "https://httpbin.org" 34 | ], 35 | "reqUrl": "/sourcePath" 36 | }, 37 | "vcl": [ 38 | { 39 | "template": "{{#each rules}}\nif (req.backend.is_origin && req.url.path == \"{{source}}\") {\n set bereq.url = \"{{dest}}\";\n}\n{{/each}}", 40 | "type": "miss" 41 | }, 42 | { 43 | "template": "{{#each rules}}\nif (req.backend.is_origin && req.url.path == \"{{source}}\") {\n set bereq.url = \"{{dest}}\";\n}\n{{/each}}", 44 | "type": "pass" 45 | } 46 | ], 47 | "version": 1 48 | } 49 | -------------------------------------------------------------------------------- /js/edgemodules.js: -------------------------------------------------------------------------------- 1 | class EdgeModules 2 | { 3 | static removeGroup(e) 4 | { 5 | e.closest('div').remove(); 6 | return false; 7 | } 8 | 9 | static addGroup(id, name) 10 | { 11 | var template = document.querySelector(`#${id}-template`); 12 | var target = template.parentElement; 13 | 14 | var clone = template.content.cloneNode(true); 15 | var count = document.querySelectorAll(`div[id^="${id}"]`).length; 16 | clone.querySelector('#container').id = `${id}-${count}`; 17 | 18 | var list = clone.querySelectorAll(`[name^="${name}"]`); 19 | for (let element of list) { 20 | element.name = element.name.replace('[x]', `[${count}]`); 21 | } 22 | 23 | target.appendChild(clone); 24 | return false; 25 | } 26 | 27 | static setupHanldebars() 28 | { 29 | Handlebars.registerHelper('replace', (inp, re, repl) => inp.replace(new RegExp(re, 'g'), repl)); 30 | Handlebars.registerHelper('ifEq', function (a, b, options) { 31 | if (a === b) { 32 | return options.fn(this); 33 | } else { 34 | return options.inverse(this); 35 | } 36 | }); 37 | Handlebars.registerHelper('ifMatch', (a, pat, opts) => opts[a.match(new RegExp(pat)) ? 'fn':'inverse'](this)); 38 | Handlebars.registerHelper('extract', (a, pat) => (a.match(new RegExp(pat)) || [])[1]); 39 | } 40 | 41 | static disableModule(name) 42 | { 43 | return document.querySelector(`form[id="${name}-disable-form"]`).submit(); 44 | } 45 | 46 | static submit(form) 47 | { 48 | this.setupHanldebars(); 49 | 50 | // let parsedVcl = JSON.stringify(parseVcl(fieldData)); 51 | var key = form.querySelector(`[id$="key"]`).value; 52 | var snippet = form.querySelector(`[id$="snippet"]`); 53 | var vcl = JSON.parse(unescape(form.querySelector(`[id$="vcl"]`).value)); 54 | 55 | snippet.value = this.generateSnippet(vcl, this.getSnippetData(form, key)); 56 | 57 | return true; 58 | } 59 | 60 | static getSnippetData(form, key) 61 | { 62 | var formData = jQuery(form).serializeJSON()[key] 63 | delete formData['snippet']; 64 | delete formData['vcl']; 65 | return formData; 66 | } 67 | 68 | static generateSnippet(vcls, data) 69 | { 70 | let templates = []; 71 | for (const vcl of vcls) { 72 | let vclTemplate = Handlebars.compile(vcl.template); 73 | templates.push({ 74 | "type": vcl.type, 75 | "priority": vcl.priority ? vcl.priority : 45, 76 | "snippet": vclTemplate(data) 77 | }); 78 | } 79 | 80 | return escape(JSON.stringify(templates)); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /js/jquery.serializejson.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | SerializeJSON jQuery plugin. 3 | https://github.com/marioizquierdo/jquery.serializeJSON 4 | version 2.9.0 (Jan, 2018) 5 | 6 | Copyright (c) 2012-2018 Mario Izquierdo 7 | Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) 8 | and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. 9 | */ 10 | !function(e){if("function"==typeof define&&define.amd)define(["jquery"],e);else if("object"==typeof exports){var n=require("jquery");module.exports=e(n)}else e(window.jQuery||window.Zepto||window.$)}(function(e){"use strict";e.fn.serializeJSON=function(n){var r,s,t,i,a,u,l,o,p,c,d,f,y;return r=e.serializeJSON,s=this,t=r.setupOpts(n),i=s.serializeArray(),r.readCheckboxUncheckedValues(i,t,s),a={},e.each(i,function(e,n){u=n.name,l=n.value,p=r.extractTypeAndNameWithNoType(u),c=p.nameWithNoType,(d=p.type)||(d=r.attrFromInputWithName(s,u,"data-value-type")),r.validateType(u,d,t),"skip"!==d&&(f=r.splitInputNameIntoKeysArray(c),o=r.parseValue(l,u,d,t),(y=!o&&r.shouldSkipFalsy(s,u,c,d,t))||r.deepSet(a,f,o,t))}),a},e.serializeJSON={defaultOptions:{checkboxUncheckedValue:void 0,parseNumbers:!1,parseBooleans:!1,parseNulls:!1,parseAll:!1,parseWithFunction:null,skipFalsyValuesForTypes:[],skipFalsyValuesForFields:[],customTypes:{},defaultTypes:{string:function(e){return String(e)},number:function(e){return Number(e)},boolean:function(e){return-1===["false","null","undefined","","0"].indexOf(e)},null:function(e){return-1===["false","null","undefined","","0"].indexOf(e)?e:null},array:function(e){return JSON.parse(e)},object:function(e){return JSON.parse(e)},auto:function(n){return e.serializeJSON.parseValue(n,null,null,{parseNumbers:!0,parseBooleans:!0,parseNulls:!0})},skip:null},useIntKeysAsArrayIndex:!1},setupOpts:function(n){var r,s,t,i,a,u;u=e.serializeJSON,null==n&&(n={}),t=u.defaultOptions||{},s=["checkboxUncheckedValue","parseNumbers","parseBooleans","parseNulls","parseAll","parseWithFunction","skipFalsyValuesForTypes","skipFalsyValuesForFields","customTypes","defaultTypes","useIntKeysAsArrayIndex"];for(r in n)if(-1===s.indexOf(r))throw new Error("serializeJSON ERROR: invalid option '"+r+"'. Please use one of "+s.join(", "));return i=function(e){return!1!==n[e]&&""!==n[e]&&(n[e]||t[e])},a=i("parseAll"),{checkboxUncheckedValue:i("checkboxUncheckedValue"),parseNumbers:a||i("parseNumbers"),parseBooleans:a||i("parseBooleans"),parseNulls:a||i("parseNulls"),parseWithFunction:i("parseWithFunction"),skipFalsyValuesForTypes:i("skipFalsyValuesForTypes"),skipFalsyValuesForFields:i("skipFalsyValuesForFields"),typeFunctions:e.extend({},i("defaultTypes"),i("customTypes")),useIntKeysAsArrayIndex:i("useIntKeysAsArrayIndex")}},parseValue:function(n,r,s,t){var i,a;return i=e.serializeJSON,a=n,t.typeFunctions&&s&&t.typeFunctions[s]?a=t.typeFunctions[s](n):t.parseNumbers&&i.isNumeric(n)?a=Number(n):!t.parseBooleans||"true"!==n&&"false"!==n?t.parseNulls&&"null"==n?a=null:t.typeFunctions&&t.typeFunctions.string&&(a=t.typeFunctions.string(n)):a="true"===n,t.parseWithFunction&&!s&&(a=t.parseWithFunction(a,r)),a},isObject:function(e){return e===Object(e)},isUndefined:function(e){return void 0===e},isValidArrayIndex:function(e){return/^[0-9]+$/.test(String(e))},isNumeric:function(e){return e-parseFloat(e)>=0},optionKeys:function(e){if(Object.keys)return Object.keys(e);var n,r=[];for(n in e)r.push(n);return r},readCheckboxUncheckedValues:function(n,r,s){var t,i,a;null==r&&(r={}),e.serializeJSON,t="input[type=checkbox][name]:not(:checked):not([disabled])",s.find(t).add(s.filter(t)).each(function(s,t){if(i=e(t),null==(a=i.attr("data-unchecked-value"))&&(a=r.checkboxUncheckedValue),null!=a){if(t.name&&-1!==t.name.indexOf("[]["))throw new Error("serializeJSON ERROR: checkbox unchecked values are not supported on nested arrays of objects like '"+t.name+"'. See https://github.com/marioizquierdo/jquery.serializeJSON/issues/67");n.push({name:t.name,value:a})}})},extractTypeAndNameWithNoType:function(e){var n;return(n=e.match(/(.*):([^:]+)$/))?{nameWithNoType:n[1],type:n[2]}:{nameWithNoType:e,type:null}},shouldSkipFalsy:function(n,r,s,t,i){var a=e.serializeJSON.attrFromInputWithName(n,r,"data-skip-falsy");if(null!=a)return"false"!==a;var u=i.skipFalsyValuesForFields;if(u&&(-1!==u.indexOf(s)||-1!==u.indexOf(r)))return!0;var l=i.skipFalsyValuesForTypes;return null==t&&(t="string"),!(!l||-1===l.indexOf(t))},attrFromInputWithName:function(e,n,r){var s,t;return s=n.replace(/(:|\.|\[|\]|\s)/g,"\\$1"),t='[name="'+s+'"]',e.find(t).add(e.filter(t)).attr(r)},validateType:function(n,r,s){var t,i;if(i=e.serializeJSON,t=i.optionKeys(s?s.typeFunctions:i.defaultOptions.defaultTypes),r&&-1===t.indexOf(r))throw new Error("serializeJSON ERROR: Invalid type "+r+" found in input name '"+n+"', please use one of "+t.join(", "));return!0},splitInputNameIntoKeysArray:function(n){var r;return e.serializeJSON,r=n.split("["),""===(r=e.map(r,function(e){return e.replace(/\]/g,"")}))[0]&&r.shift(),r},deepSet:function(n,r,s,t){var i,a,u,l,o,p;if(null==t&&(t={}),(p=e.serializeJSON).isUndefined(n))throw new Error("ArgumentError: param 'o' expected to be an object or array, found undefined");if(!r||0===r.length)throw new Error("ArgumentError: param 'keys' expected to be an array with least one element");i=r[0],1===r.length?""===i?n.push(s):n[i]=s:(a=r[1],""===i&&(o=n[l=n.length-1],i=p.isObject(o)&&(p.isUndefined(o[a])||r.length>2)?l:l+1),""===a?!p.isUndefined(n[i])&&e.isArray(n[i])||(n[i]=[]):t.useIntKeysAsArrayIndex&&p.isValidArrayIndex(a)?!p.isUndefined(n[i])&&e.isArray(n[i])||(n[i]=[]):!p.isUndefined(n[i])&&p.isObject(n[i])||(n[i]={}),u=r.slice(1),p.deepSet(n[i],u,s,t))}}}); -------------------------------------------------------------------------------- /languages/purgely.pot: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016 Purgely 2 | # This file is distributed under the same license as the Purgely package. 3 | msgid "" 4 | msgstr "" 5 | "Project-Id-Version: Purgely 1.0.0\n" 6 | "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/purgely\n" 7 | "POT-Creation-Date: 2016-01-11 03:08:41+00:00\n" 8 | "MIME-Version: 1.0\n" 9 | "Content-Type: text/plain; charset=UTF-8\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | "PO-Revision-Date: 2016-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: LANGUAGE \n" 14 | 15 | #. #-#-#-#-# purgely.pot (Purgely 1.0.0) #-#-#-#-# 16 | #. Plugin Name of the plugin/theme 17 | #: src/settings-page.php:52 src/settings-page.php:53 18 | msgid "Purgely" 19 | msgstr "" 20 | 21 | #: src/settings-page.php:78 22 | msgid "Fastly settings" 23 | msgstr "" 24 | 25 | #: src/settings-page.php:86 26 | msgid "API Key" 27 | msgstr "" 28 | 29 | #: src/settings-page.php:94 30 | msgid "Service ID" 31 | msgstr "" 32 | 33 | #: src/settings-page.php:102 34 | msgid "API Endpoint" 35 | msgstr "" 36 | 37 | #: src/settings-page.php:111 38 | msgid "General settings" 39 | msgstr "" 40 | 41 | #: src/settings-page.php:118 42 | msgid "Cache TTL for end users (in Seconds)" 43 | msgstr "" 44 | 45 | #: src/settings-page.php:126 46 | msgid "Default Purge Type" 47 | msgstr "" 48 | 49 | #: src/settings-page.php:134 50 | msgid "Allow Full Cache Purges" 51 | msgstr "" 52 | 53 | #: src/settings-page.php:143 54 | msgid "Content revalidation settings" 55 | msgstr "" 56 | 57 | #: src/settings-page.php:150 58 | msgid "Enable Stale while Revalidate" 59 | msgstr "" 60 | 61 | #: src/settings-page.php:158 62 | msgid "Stale while Revalidate TTL (in Seconds)" 63 | msgstr "" 64 | 65 | #: src/settings-page.php:166 66 | msgid "Enable Stale if Error" 67 | msgstr "" 68 | 69 | #: src/settings-page.php:174 70 | msgid "Stale if Error TTL (in Seconds)" 71 | msgstr "" 72 | 73 | #: src/settings-page.php:189 74 | msgid "" 75 | "Please enter details related to your Fastly account. A Fastly API token and " 76 | "service ID are required for some operations (e.g., surrogate key and full " 77 | "cache purges). " 78 | msgstr "" 79 | 80 | #: src/settings-page.php:203 src/settings-page.php:231 81 | msgid "Required for surrogate key and full cache purges" 82 | msgstr "" 83 | 84 | #: src/settings-page.php:205 85 | msgid "API token for the Fastly account associated with this site." 86 | msgstr "" 87 | 88 | #: src/settings-page.php:208 src/settings-page.php:236 89 | msgid "Please see Fastly's documentation for %s." 90 | msgstr "" 91 | 92 | #: src/settings-page.php:212 93 | msgid "more information on finding your API token" 94 | msgstr "" 95 | 96 | #: src/settings-page.php:233 97 | msgid "Fastly service ID for this site." 98 | msgstr "" 99 | 100 | #: src/settings-page.php:240 101 | msgid "more information on finding your service ID" 102 | msgstr "" 103 | 104 | #: src/settings-page.php:260 105 | msgid "API endpoint for this service." 106 | msgstr "" 107 | 108 | #: src/settings-page.php:273 109 | msgid "" 110 | "This section allows you to configure general cache settings. Note that " 111 | "changes to these settings can cause destabilization to your site if " 112 | "misconfigured. The default setting should be sufficient for most sites." 113 | msgstr "" 114 | 115 | #: src/settings-page.php:288 116 | msgid "" 117 | "This setting controls the \"surrogate-control\" header's \"max-age\" value. " 118 | "It defines the cache duration for all pages on the site." 119 | msgstr "" 120 | 121 | #: src/settings-page.php:304 122 | msgid "Soft" 123 | msgstr "" 124 | 125 | #: src/settings-page.php:305 126 | msgid "Instant" 127 | msgstr "" 128 | 129 | #: src/settings-page.php:316 130 | msgid "" 131 | "The purge type setting controls the manner in which the cache is purged. " 132 | "Instant purging causes the cached object(s) to be purged immediately. Soft " 133 | "purging causes the origin to revalidate the cache and Fastly will serve " 134 | "stale content until revalidation is completed. For more information, please " 135 | "see %s." 136 | msgstr "" 137 | 138 | #: src/settings-page.php:320 139 | msgid "Fastly's documentation for more information on soft purging" 140 | msgstr "" 141 | 142 | #: src/settings-page.php:341 143 | msgid "" 144 | "The full cache purging behavior available to WP CLI must be explicitly " 145 | "enabled in order for it to work. Purging the entire cache can cause " 146 | "significant site stability issues and is disabled by default." 147 | msgstr "" 148 | 149 | #: src/settings-page.php:354 150 | msgid "" 151 | "This section allows you to configure how content is handled as it is " 152 | "revalidated. It is important that proper consideration is given to how " 153 | "content is regenerated after it expires from cache. The default settings " 154 | "take a conservative approach by allowing stale content to be served while " 155 | "new content is regenerated in the background." 156 | msgstr "" 157 | 158 | #: src/settings-page.php:372 159 | msgid "" 160 | "Turn the \"stale while revalidate\" behavior on or off. The stale while " 161 | "revalidate behavior allows stale content to be served while content is " 162 | "regenerated in the background. Please see %s" 163 | msgstr "" 164 | 165 | #: src/settings-page.php:376 166 | msgid "Fastly's documentation for more information on stale while revalidate" 167 | msgstr "" 168 | 169 | #: src/settings-page.php:396 170 | msgid "" 171 | "This setting determines the amount of time that stale content will be served " 172 | "while new content is generated." 173 | msgstr "" 174 | 175 | #: src/settings-page.php:416 176 | msgid "" 177 | "Turn the \"stale if error\" behavior on or off. The stale if error behavior " 178 | "allows stale content to be served while the origin is returning an error " 179 | "state. Please see %s" 180 | msgstr "" 181 | 182 | #: src/settings-page.php:420 183 | msgid "Fastly's documentation for more information on stale if error" 184 | msgstr "" 185 | 186 | #: src/settings-page.php:440 187 | msgid "" 188 | "This setting determines the amount of time that stale content will be served " 189 | "while the origin is returning an error state." 190 | msgstr "" 191 | 192 | #: src/settings-page.php:458 193 | msgid "Purgely Settings" 194 | msgstr "" 195 | 196 | #: src/wp-cli.php:111 197 | msgid "purge type is unknown" 198 | msgstr "" 199 | 200 | #: src/wp-cli.php:118 src/wp-cli.php:126 201 | msgid "purged %s" 202 | msgstr "" 203 | 204 | #: src/wp-cli.php:120 205 | msgid "URL could not be purged" 206 | msgstr "" 207 | 208 | #: src/wp-cli.php:128 209 | msgid "key could not be purged" 210 | msgstr "" 211 | 212 | #: src/wp-cli.php:134 213 | msgid "purged all" 214 | msgstr "" 215 | 216 | #: src/wp-cli.php:136 217 | msgid "cache could not be purged" 218 | msgstr "" 219 | 220 | #: src/wp-cli.php:143 221 | msgid "purged %d %s" 222 | msgstr "" 223 | 224 | #. Description of the plugin/theme 225 | msgid "A plugin to manage Fastly caching behavior and purging." 226 | msgstr "" 227 | 228 | #. Author of the plugin/theme 229 | msgid "Zack Tollman, WIRED Tech Team" 230 | msgstr "" 231 | -------------------------------------------------------------------------------- /purgely.php: -------------------------------------------------------------------------------- 1 | root_dir = dirname(__FILE__); 160 | $this->src_dir = $this->root_dir . '/src'; 161 | $this->vcl_dir = $this->root_dir . '/vcl_snippets'; 162 | $this->edge_modules_dir = $this->root_dir . '/fastly_edge_modules'; 163 | $this->file_path = $this->root_dir . '/' . basename(__FILE__); 164 | $this->url_base = untrailingslashit(plugins_url('/', __FILE__)); 165 | $this->current_version = get_option("fastly-schema-version", false); 166 | 167 | $include_files = array( 168 | $this->src_dir . '/config.php', 169 | $this->src_dir . '/utils.php', 170 | $this->src_dir . '/classes/api.php', 171 | $this->src_dir . '/classes/edgemodules.php', 172 | $this->src_dir . '/classes/settings.php', 173 | $this->src_dir . '/classes/upgrades.php', 174 | $this->src_dir . '/classes/vcl-handler.php', 175 | $this->src_dir . '/classes/related-surrogate-keys.php', 176 | $this->src_dir . '/classes/purge-request.php', 177 | $this->src_dir . '/classes/surrogate-key-collection.php', 178 | $this->src_dir . '/classes/header.php', 179 | $this->src_dir . '/classes/header-surrogate-control.php', 180 | $this->src_dir . '/classes/header-cache-control.php', 181 | $this->src_dir . '/classes/header-surrogate-keys.php', 182 | $this->src_dir . '/wp-purges.php' 183 | ); 184 | 185 | // Include dependent files. 186 | $currently_included = get_included_files(); 187 | foreach($include_files as $file) { 188 | if(!in_array($file, $currently_included)) { 189 | include $file; 190 | } 191 | } 192 | 193 | // First install DB schema changes 194 | $upgrades = new Upgrades($this); 195 | $upgrades->check_and_run_upgrades(); 196 | 197 | // Initialize custom cache taxonomy if activated 198 | if(Purgely_Settings::get_setting('use_fastly_cache_tags')) { 199 | add_action( 'init', array($this, 'init_fastly_cache_taxonomy')); 200 | } 201 | 202 | // Initialize the key collector. 203 | $this::$surrogate_keys_header = new Purgely_Surrogate_Keys_Header(); 204 | 205 | // Initialize cache control header. 206 | $this::$cache_control_headers = new Purgely_Cache_Control_Header(); 207 | 208 | // Initialize the surrogate control header. 209 | $this::$surrogate_control_header = new Purgely_Surrogate_Control_Header(); 210 | 211 | // Add the surrogate keys. 212 | add_action('wp', array($this, 'set_standard_keys'), 100); 213 | 214 | // Send the surrogate keys. 215 | add_action('wp', array($this, 'send_surrogate_keys'), 101); 216 | 217 | // Set and send the surrogate control header. 218 | add_action('wp', array($this, 'send_surrogate_control'), 101); 219 | 220 | // Set and send the surrogate control header. 221 | add_action('wp', array($this, 'send_cache_control'), 101); 222 | 223 | //Image optimization 224 | if (!is_admin() && Purgely_Settings::get_setting('io_enable_wp')) { 225 | if(Purgely_Settings::get_setting('io_adaptive_pixel_ratios')) { 226 | add_action('wp', array($this, 'image_optimization_device_pixel_ratios'), 101); 227 | } 228 | } 229 | 230 | // Load in WP CLI. 231 | if (defined('WP_CLI') && WP_CLI) { 232 | include $this->src_dir . '/wp-cli.php'; 233 | } 234 | 235 | // Set plugin url 236 | if (!defined('FASTLY_PLUGIN_URL')) { 237 | define('FASTLY_PLUGIN_URL', plugin_dir_url(__FILE__)); 238 | } 239 | 240 | // Set version 241 | if (!defined('FASTLY_VERSION')) { 242 | define('FASTLY_VERSION', $this->version); 243 | } 244 | 245 | // Load the textdomain. 246 | add_action('plugins_loaded', array($this, 'load_plugin_textdomain')); 247 | add_action('plugins_loaded', array($this, 'load_admin_settings')); 248 | 249 | // Load custom JS for EdgeModules 250 | add_action( 'admin_enqueue_scripts', array($this, 'enqueue_admin_script_edgemodules')); 251 | } 252 | 253 | /** 254 | * Set all the surrogate keys for the requests. 255 | * 256 | * @return void 257 | */ 258 | public function set_standard_keys() 259 | { 260 | 261 | if (is_user_logged_in()) { 262 | return; 263 | } 264 | 265 | global $wp_query; 266 | $key_collection = new Purgely_Surrogate_Key_Collection($wp_query); 267 | 268 | $this::$surrogate_keys_collection = $key_collection; 269 | 270 | $keys = $key_collection->get_keys(); 271 | $this::$surrogate_keys_header->add_keys($keys); 272 | } 273 | 274 | /** 275 | * Send the currently registered surrogate keys. 276 | * 277 | * This function takes all of the surrogate keys that are currently recorded and flattens them into a single header 278 | * and sends the header. Any other keys need to be set by 3rd party code before "init", 101. 279 | * 280 | * This function does allow for a filtering of the keys before they are sent, to allow for the keys to be 281 | * de-registered when and if necessary. 282 | * 283 | * @return void 284 | */ 285 | public function send_surrogate_keys() 286 | { 287 | 288 | if (is_user_logged_in()) { 289 | return; 290 | } 291 | 292 | $keys_header = $this::$surrogate_keys_header; 293 | $keys = apply_filters('purgely_surrogate_keys', $keys_header->get_keys()); 294 | $keys_header->set_keys($keys); 295 | 296 | do_action('purgely_pre_send_keys', $keys_header); 297 | $keys_header->send_header(); 298 | do_action('purgely_post_send_keys', $keys_header); 299 | } 300 | 301 | /** 302 | * Set the TTL for the object and send the header. 303 | * 304 | * This is the main function for setting the TTL for the page. 305 | * To change it, use the "purgely_pre_send_surrogate_control" and "purgely_post_send_surrogate_control" 306 | * actions. 307 | * 308 | * Note that any alterations must be done before init, 101. 309 | * 310 | * The default set here is 5 minutes. This has proven to be a reasonable default for caches for WordPress pages. 311 | * 312 | * @return void 313 | */ 314 | public function send_surrogate_control() 315 | { 316 | /** 317 | * If a user is logged in, surrogate control headers should be ignored. We do not want to cache any logged in 318 | * user views. WordPress sets a "Cache-Control:no-cache, must-revalidate, max-age=0" header for logged in views 319 | * and this should be sufficient for keeping logged in views uncached. 320 | */ 321 | if (is_user_logged_in()) { 322 | return; 323 | } 324 | 325 | $surrogate_control = $this::$surrogate_control_header; 326 | 327 | $custom_ttl = $this::$surrogate_keys_collection->get_custom_ttl(); 328 | if($custom_ttl) { 329 | $surrogate_control->edit_headers(array('max-age' => $custom_ttl)); 330 | } 331 | 332 | do_action('purgely_pre_send_surrogate_control', $surrogate_control); 333 | $surrogate_control->send_header(); 334 | do_action('purgely_post_send_surrogate_control', $surrogate_control); 335 | } 336 | 337 | /** 338 | * Send each of the control control headers. 339 | * 340 | * @return void 341 | */ 342 | public function send_cache_control() 343 | { 344 | /** 345 | * If a user is logged in, surrogate control headers should be ignored. We do not want to cache any logged in 346 | * user views. WordPress sets a "Cache-Control:no-cache, must-revalidate, max-age=0" header for logged in views 347 | * and this should be sufficient for keeping logged in views uncached. 348 | */ 349 | if (is_user_logged_in()) { 350 | return; 351 | } 352 | 353 | $cache_control = $this::$cache_control_headers; 354 | 355 | do_action('purgely_pre_send_cache_control', $cache_control); 356 | $cache_control->send_header(); 357 | do_action('purgely_post_send_cache_control', $cache_control); 358 | } 359 | 360 | /** 361 | * Load the plugin text domain. 362 | * 363 | * @return void 364 | */ 365 | public function load_plugin_textdomain() 366 | { 367 | load_plugin_textdomain('purgely', false, basename(dirname(__FILE__)) . '/languages/'); 368 | } 369 | 370 | public function load_admin_settings() 371 | { 372 | if (is_admin()) { 373 | include $this->src_dir . '/settings-page.php'; 374 | } 375 | } 376 | 377 | /** 378 | * Initialize Fastly custom cache tags taxonomy 379 | */ 380 | public function init_fastly_cache_taxonomy() 381 | { 382 | $labels = array( 383 | 'name' => _x( 'Fastly Cache Tags', 'taxonomy general name', 'purgely' ), 384 | 'singular_name' => _x( 'Fastly Cache Tag', 'taxonomy singular name', 'purgely' ), 385 | 'search_items' => __( 'Search Fastly Tags', 'purgely' ), 386 | 'popular_items' => __( 'Popular Fastly Tags', 'purgely' ), 387 | 'all_items' => __( 'All Fastly Tags', 'purgely' ), 388 | 'parent_item' => null, 389 | 'parent_item_colon' => null, 390 | 'edit_item' => __( 'Edit Fastly Tags', 'purgely' ), 391 | 'update_item' => __( 'Update Fastly Tags', 'purgely' ), 392 | 'add_new_item' => __( 'Add New Fastly Tags', 'purgely' ), 393 | 'new_item_name' => __( 'New Fastly Tags Name', 'purgely' ), 394 | 'separate_items_with_commas' => __( 'Separate Fastly Tags with commas', 'purgely' ), 395 | 'add_or_remove_items' => __( 'Add or remove Fastly Tags', 'purgely' ), 396 | 'choose_from_most_used' => __( 'Choose from the most used Fastly Tags', 'purgely' ) 397 | ); 398 | 399 | // create a new taxonomy 400 | register_taxonomy( 401 | 'fastly_cache_tag', 402 | array('post', 'page', 'attachment', 'nav_menu_item'), 403 | array( 404 | 'labels' => $labels, 405 | 'rewrite' => array( 'slug' => 'fastly_cache_tag' ), 406 | 'capabilities' => array( 407 | 'manage_terms' => 'manage_categories', 408 | 'edit_terms' => 'manage_categories', 409 | 'delete_terms' => 'manage_categories', 410 | 'assign_terms' => 'edit_posts', 411 | ) 412 | ) 413 | ); 414 | 415 | // Include custom post types if activated 416 | if(Purgely_Settings::get_setting('use_fastly_cache_tags_for_custom_post_type')) { 417 | $custom_post_types = get_post_types(array('_builtin' => false)); 418 | if(is_array($custom_post_types) && !empty($custom_post_types)) { 419 | foreach ($custom_post_types as $post_type) { 420 | $result = register_taxonomy_for_object_type( 'fastly_cache_tag', $post_type ); 421 | if(!$result && Purgely_Settings::get_setting('fastly_debug_mode')) { 422 | error_log('Error when registering Fastly cache tags to custom post type: ' . $post_type); 423 | } 424 | } 425 | } 426 | } 427 | } 428 | 429 | public function image_optimization_device_pixel_ratios() { 430 | add_filter( 'wp_get_attachment_image_attributes', array($this, 'fastly_io_pixel_ratios_attachment'), 100 , 3 ); 431 | add_filter('wp_calculate_image_sizes', array($this, 'fastly_io_pixel_ratios_sizes'), 100); 432 | 433 | if(Purgely_Settings::get_setting('io_adaptive_pixel_ratios_content')) { 434 | add_filter( 'wp_calculate_image_srcset', array($this, 'fastly_io_pixel_ratios_content'), 100 , 5 ); 435 | add_filter( 'the_content', array($this, 'fastly_io_pixel_ratios_the_content_filter')); 436 | } 437 | } 438 | 439 | /** 440 | * Set attachment srcset and unset sizes for Fastly IO pixels 441 | * @param $attr 442 | * @return mixed 443 | */ 444 | function fastly_io_pixel_ratios_attachment($attr, $attachment, $size) { 445 | 446 | $image_data = wp_get_attachment_image_src($attachment->ID, $size); 447 | $width = $image_data[1]; 448 | $src = $attachment->guid; 449 | $query = '?width=' . $width; 450 | $attr['src'] = $src . $query; 451 | $sizes = Purgely_Settings::get_setting('io_adaptive_pixel_ratio_sizes'); 452 | 453 | $dprCount = count($sizes); 454 | $attr['srcset'] = ''; 455 | foreach($sizes as $key => $size) { 456 | $size_int = trim($size, 'x'); 457 | $attr['srcset'] .= $src . $query . "&dpr=$size_int {$size}"; 458 | if(($key+1) < $dprCount) { 459 | $attr['srcset'] .= ', '; 460 | } 461 | } 462 | 463 | unset($attr['sizes']); 464 | $attr['alt'] = isset($attachment->post_name) ? $attachment->post_name : 'image'; 465 | 466 | return $attr; 467 | } 468 | 469 | /** 470 | * Set pixel ratios srcset on content images 471 | * @param $size_array 472 | * @param $image_src 473 | * @param $src 474 | * @param $attachment_id 475 | * @return array 476 | */ 477 | function fastly_io_pixel_ratios_content( $size_array, $image_src, $src, $attachment_id, $id ) { 478 | 479 | $attachment_original = wp_get_attachment_url($id); 480 | $width = isset($image_src[0]) ? $image_src[0] : $attachment_id['width']; 481 | $query = "?width={$width}"; 482 | $sizes = Purgely_Settings::get_setting('io_adaptive_pixel_ratio_sizes'); 483 | $srcset = array(); 484 | $main = array(); 485 | foreach($sizes as $size) { 486 | $size_int = trim($size, 'x'); 487 | $srcset['url'] = $attachment_original . $query . "&dpr=$size_int"; 488 | $srcset['value'] = $size; 489 | $main[] = $srcset; 490 | } 491 | 492 | return $main; 493 | } 494 | 495 | /** 496 | * Fake sizes to unset it 497 | * @return bool 498 | */ 499 | function fastly_io_pixel_ratios_sizes() { 500 | return true; 501 | } 502 | 503 | /** 504 | * Adjust content images to IO format 505 | * @param $content 506 | * @return mixed 507 | */ 508 | function fastly_io_pixel_ratios_the_content_filter($content) { 509 | if ( ! preg_match_all( '/]+>/', $content, $matches ) ) { 510 | return $content; 511 | } 512 | 513 | $selected_images = $attachment_ids = array(); 514 | 515 | foreach( $matches[0] as $image ) { 516 | if ( true == strpos( $image, ' src=' ) && preg_match( '/wp-image-([0-9]+)/i', $image, $class_id ) && 517 | ( $attachment_id = absint( $class_id[1] ) ) ) { 518 | 519 | /* 520 | * If exactly the same image tag is used more than once, overwrite it. 521 | * All identical tags will be replaced later with 'str_replace()'. 522 | */ 523 | $selected_images[ $image ] = $attachment_id; 524 | // Overwrite the ID when the same image is included more than once. 525 | $attachment_ids[ $attachment_id ] = true; 526 | } 527 | } 528 | 529 | if ( count( $attachment_ids ) > 1 ) { 530 | /* 531 | * Warm the object cache with post and meta information for all found 532 | * images to avoid making individual database calls. 533 | */ 534 | _prime_post_caches( array_keys( $attachment_ids ), false, true ); 535 | } 536 | 537 | foreach ( $selected_images as $image => $attachment_id ) { 538 | 539 | $image_src = preg_match( '/src="([^"]+)"/', $image, $match_src ) ? $match_src[1] : ''; 540 | list( $image_src ) = explode( '?', $image_src ); 541 | 542 | // Return early if we couldn't get the image source. 543 | if ( ! $image_src ) { 544 | continue; 545 | } 546 | 547 | // Extract width 548 | $width = preg_match( '/ width="([0-9]+)"/', $image, $match_width ) ? (int) $match_width[1] : 0; 549 | 550 | $image_src_param = 'src="' . $image_src . '?width=' . $width . '"'; 551 | $image_src = 'src="' . $image_src . '"'; 552 | 553 | // Replace edited image src with src containing parameter 554 | $replacement = str_replace( $image_src, $image_src_param, $image ); 555 | // Replace edited IO image in content 556 | $content = str_replace( $image, $replacement, $content ); 557 | } 558 | return $content; 559 | } 560 | 561 | /** 562 | * Get Edge Modules data 563 | * @return array 564 | */ 565 | function fastly_edge_modules() { 566 | $result = []; 567 | foreach ( glob( $this->edge_modules_dir . "/*.json" ) as $file ) { 568 | if (is_file($file) && $json = wp_json_file_decode($file)) { 569 | $result[$json->id] = $json; 570 | } 571 | } 572 | return $result; 573 | } 574 | 575 | public function enqueue_admin_script_edgemodules($hook) 576 | { 577 | if ('fastly_page_fastly-edge-modules' != $hook) { 578 | return; 579 | } 580 | wp_enqueue_script( 'fastly_edgemodules_jquery_serializejson_library', plugin_dir_url( __FILE__ ) . 'js/jquery.serializejson.min.js', array(), '1.0' ); 581 | wp_enqueue_script( 'fastly_edgemodules_handlebars_library', plugin_dir_url( __FILE__ ) . 'js/handlebars-v4.0.12.js', array(), '1.0' ); 582 | wp_enqueue_script( 'fastly_edgemodules_script', plugin_dir_url( __FILE__ ) . 'js/edgemodules.js', array(), '1.0' ); 583 | } 584 | } 585 | 586 | /** 587 | * Instantiate or return the one Purgely instance. 588 | * 589 | * @return Purgely 590 | */ 591 | function get_purgely_instance() 592 | { 593 | return Purgely::instance(); 594 | } 595 | 596 | get_purgely_instance(); 597 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === Fastly === 2 | Contributors: Fastly, Inchoo, CondeNast 3 | Tags: fastly, cdn, performance, speed, spike, spike-protection, caching, dynamic, comments, ddos 4 | Requires at least: 4.6.2 5 | Tested up to: 6.8.0 6 | Stable tag: 1.2.28 7 | License: GPLv2 8 | 9 | Integrates Fastly with WordPress publishing tools. 10 | 11 | This is the official Fastly plugin for WordPress. 12 | 13 | The official code repository for this plugin is available here: 14 | 15 | https://github.com/fastly/WordPress-Plugin/ 16 | 17 | == Description == 18 | 19 | Installation: 20 | 21 | You can either install from source (you\'re looking at it), or from the WordPress [plugin directory](http://wordpress.org/plugins/fastly/). 22 | 23 | 1. To proceed with configuration you will need to [sign up for Fastly](https://www.fastly.com/signup) and create and activate a new service (unless you already have one). Details of how to create and activate a new service can be found [here](https://docs.fastly.com/guides/basic-setup/sign-up-and-create-your-first-service). You will also need to find your Service ID and make a note of the string. 24 | 2. You will need to create an API token with the Global API access option selected. [Click here for token management screen](https://manage.fastly.com/account/personal/tokens). 25 | 3. Set up the Fastly plugin inside your WordPress admin panel 26 | 4. In your Wordpress blog admin panel, Under Fastly->General, enter & save your Fastly API token and Service ID 27 | 5. Verify connection by pressing `TEST CONNECTION` button. 28 | 6. In order to get the most value out of Fastly we recommend you upload VCL snippets from https://github.com/fastly/WordPress-Plugin/tree/master/vcl_snippets. These snippets will add code for following 29 | - Force certain paths to be passed (not cached) e.g. wp-admin, wp-login.php 30 | - Makes sure that logged in user sessions are never cached 31 | - Handling for serving [stale on error](https://docs.fastly.com/guides/performance-tuning/serving-stale-content.html) 32 | 33 | You can upload them by hand or press `Update VCL` button in the UI. 34 | 35 | For more information, or if you have any problems, please email us. 36 | 37 | _Note: you may have to disable other caching plugins like W3TotalCache or WP Rocket to avoid getting odd cache behaviour._ 38 | 39 | - Pulls in the [Fastly API](http://docs.fastly.com/api) 40 | - Integrates purging in post/page/taxonomies publishing 41 | - Includes an admin panel in `wp-admin` 42 | - Integrates some of the advanced purging options from Fastly API 43 | - Allows to monitor purging using webhooks for slack 44 | 45 | Using this plugin means you won't have to purge content in Fastly when you make changes to your WordPress content. Purges will automatically happen with no need for manual intervention. 46 | 47 | == Customization == 48 | 49 | = Edge Modules = 50 | 51 | Edge Modules are a framework that enables specific functions to be enabled on Fastly Edge without 52 | the need to write VCL. Current list of functions that can be included includes 53 | 54 | - Enable Blackfire metrics and tracing 55 | - Set CORS headers 56 | - Enable support for bot detection partners such as Datadome/Netacea 57 | - Redirect one domain to another e.g. domain.com => www.domain.com 58 | - Rewrite URLs going to a backend e.g. /sitemap.xml => /media/sitemap.xml 59 | - Domain masking an external backend/origin 60 | 61 | More details can be found at https://github.com/fastly/WordPress-Plugin/blob/master/EDGE-MODULES.md 62 | 63 | = Image optimization = 64 | Image Optimization is a separately contracted feature. Please contact support@fastly.com to request 65 | pricing and activation. 66 | 67 | Once activated on service level, you will be able to enable it in your blog under Fastly->Advanced. 68 | 69 | Breakdown of IO options: 70 | Enable Image Optimization in Fastly configuration - Activating this uploads VCL that steers image traffic to the image optimization service 71 | 72 | Enable Image Optimization in Wordpress - Main switch to activate IO which is needed for all other options to work. 73 | 74 | Enable adaptive pixel ratios - Switch for adaptive pixel ratios implementation. This replaces adaptive pixels srcset to format which Fastly IO can parse and replace. Initially works only on inserted attachments like featured images, but can be applied on content images if enabled. 75 | 76 | Adaptive pixel ratio sizes - Select pixel ratios that will be generated when creating image srcset html. 77 | 78 | Enable image optimization for content images - Safe switch for Image optimization of content images (due to difference from featured images, those are processed differently). To fully utilize, insert full size images in content. 79 | 80 | 81 | = Wordpress Hooks = 82 | 83 | Available wordpress hooks (add_action) on: 84 | 85 | Editing related (purging) keys for a given post 86 | purgely_related_keys 87 | 88 | Editing surrogate keys output 89 | purgely_pre_send_keys 90 | purgely_post_send_keys 91 | functions: add_keys 92 | 93 | Editing surrogate control headers output(max-age, stale-while-revalidate, stale-if-error) 94 | purgely_pre_send_surrogate_control 95 | purgely_post_send_surrogate_control 96 | functions: edit_headers, unset_headers 97 | 98 | Edit cache control headers output (max-age) 99 | purgely_pre_send_cache_control 100 | purgely_post_send_cache_control 101 | functions: edit_headers, unset_headers 102 | 103 | Example: 104 | add_action(\'purgely_pre_send_surrogate_control\', \'custom_headers_edit\'); 105 | function custom_headers_edit($header_object) 106 | { 107 | $header_object->edit_headers(array(\'custom-header\' => \'555\', \'max-age\' => \'99\')); 108 | } 109 | 110 | add_filter(\'purgely_related_keys\', \'custom_related_keys\', 10, 2); 111 | function custom_related_keys($keys_array, $post_object) { 112 | $keys_array[] = \'custom-key\'; 113 | return $keys_array; 114 | } 115 | 116 | add_action(\'purgely_pre_send_keys\', \'custom_surrogate_keys\'); 117 | function custom_surrogate_keys($keys_object) { 118 | $keys_object->add_key(\'custom-key\'); 119 | } 120 | 121 | Note: you may have to disable other caching plugins like W3TotalCache to avoid getting odd cache behaviour. 122 | 123 | == Screenshots == 124 | 1. Fastly General Tab 125 | 2. Fastly Advanced Tab 126 | 3. Fastly Webhooks Tab 127 | 128 | == Changelog == 129 | 130 | = 1.2.28 131 | 132 | * Small fixes reported in log files 133 | * Bumping tested up WP version to 6.8 134 | 135 | = 1.2.27 136 | 137 | * Allowing usage of Fastly service without stored credentials 138 | 139 | = 1.2.26 140 | 141 | * Code cleanup, improvements in data sanitization and escaping input 142 | 143 | = 1.2.25 144 | 145 | * Assignment fix for constants https://github.com/fastly/WordPress-Plugin/pull/99 146 | 147 | = 1.2.24 148 | 149 | * Updates to Datadome and Netacea edge modules 150 | 151 | = 1.2.23 152 | 153 | * Compatibility fixes for Wordpress 6.2 154 | 155 | = 1.2.22 156 | 157 | * Fixes for PHP 8.1 158 | 159 | = 1.2.20 160 | 161 | * Another fix for PHP 8 162 | 163 | = 1.2.19 164 | 165 | * Fixes for PHP 8 166 | 167 | = 1.2.18 168 | 169 | * ‘front page’ should be automatically purged just like the ‘home' page https://github.com/fastly/WordPress-Plugin/pull/87 170 | 171 | = 1.2.17 172 | 173 | * Fix for edge modules with option values defaulting to a single value https://github.com/fastly/WordPress-Plugin/pull/83 174 | 175 | = 1.2.16 = 176 | 177 | * Scheduling posts would purge content at submission and activation. Make sure we don't purge at submission time https://github.com/fastly/WordPress-Plugin/pull/82 178 | 179 | = 1.2.15 = 180 | 181 | * Always purged keys sanitization callback was overly aggresive stripping underscores 182 | 183 | = 1.2.14 = 184 | 185 | * Add missing files that were missing in 1.2.13 deploy 186 | 187 | = 1.2.13 = 188 | 189 | * Introduce Edge Modules https://github.com/fastly/WordPress-Plugin/pull/79 190 | 191 | = 1.2.12 = 192 | 193 | * Removed restart logic from VCL snippets as it may clash with Image Optimization 194 | 195 | = 1.2.11 = 196 | 197 | * API token sanitization was too aggressive stripping off underscores which are now legitimate characters in a token 198 | 199 | = 1.2.10 = 200 | 201 | * Remove a call to a now deprecated function https://github.com/fastly/WordPress-Plugin/issues/72 202 | 203 | = 1.2.9 = 204 | 205 | * Added fix for scheduled posts transition to published 206 | 207 | = 1.2.8 = 208 | 209 | * Minor fixes 210 | 211 | = 1.2.7 = 212 | 213 | * Fixed duplicated API calls on admin page loads 214 | 215 | = 1.2.6 = 216 | 217 | * Add Image Optimization configuration 218 | 219 | = 1.2.5 = 220 | * Added fix for including only always purged keys if existing 221 | * Added fix for header surrogate key number larger than limit 222 | 223 | = 1.2.4 = 224 | * Added fix for not yet existing pages not being purged (404 pages key issue) 225 | * Added admin entry for always purged keys 226 | * Make surrogate keys comply with multi-site configurations 227 | 228 | = 1.2.3 = 229 | * wp_cli added configuration listing and updating functionality 230 | * Enabled setting of HTML for Maintenance/Error page (503) 231 | * Minor fixes 232 | 233 | = 1.2.2 = 234 | * Action Hooks fix 235 | 236 | = 1.2.1 = 237 | * Minor VCL clean up 238 | 239 | = 1.2.0 = 240 | * Added purge by url 241 | * Changes regarding logging logic 242 | * VCL update User Interface changes 243 | * Fixed and enabled support for wp_cli 244 | 245 | = 1.1.1 = 246 | * Some Purgely plugin functionalities integrated into Fastly (along with some advanced options) 247 | * Purging by Surrogate-Keys is used instead of purging by url 248 | * Added webhooks support (Slack focused) to log purges and other critical events 249 | * Added debugging logs option, purge all button for emergency 250 | * Advanced options: Surrogate Cache TTL, Cache TTL, Default Purge Type, Allow Full Cache Purges, Log purges in error log, 251 | Debug mode, Enable Stale while Revalidate, Stale while Revalidate TTL, Enable Stale if Error, Stale if Error TTL. 252 | * Fastly VCL update 253 | * Curl no longer needed 254 | 255 | = 1.1 = 256 | * Include fixes for header sending 257 | * Enable \"soft\" purging 258 | 259 | = 1.0 = 260 | * Mark as deprecated 261 | * Recommend Purgely from Condé Nast 262 | * Add in link to GitHub repo 263 | 264 | = 0.99 = 265 | * Add a guard function for cURL prequisite 266 | * Bring up to date with WP Plugin repo standards 267 | 268 | = 0.98 = 269 | * Security fixes for XSS/CSRF 270 | * Only load CSS/JS on admin page 271 | * Properly enqueue scripts and styles 272 | * Use WP HTTP API methods 273 | * Properly register scripts 274 | 275 | = 0.94 = 276 | * Change to using PURGE not POST for purges 277 | * Correct URL building for comments purger 278 | 279 | = 0.92 = 280 | * Fix bug in port addition 281 | 282 | = 0.91 = 283 | * Make work in PHP 5.3 284 | 285 | = 0.9 = 286 | * Fix comment purging 287 | 288 | = 0.8 = 289 | * Fix url purging 290 | 291 | = 0.7 = 292 | * Fix category purging 293 | 294 | = 0.6 = 295 | * Remove bogus error_log call 296 | 297 | = 0.5 = 298 | * Switch to using curl 299 | * Change PURGE methodology 300 | * Performance enhancements 301 | 302 | == About Fastly == 303 | 304 | Fastly is the only real-time content delivery network designed to seamlessly integrate with your development stack. 305 | 306 | Fastly provides real-time updating of content and the ability to cache dynamic as well as static content. For any content that is truly uncacheable, we'll accelerate it. 307 | 308 | In addition we allow you to update your configuration in seconds, provide real time log and stats streaming, powerful edge scripting capabilities, and TLS termination (amongst many other features). 309 | 310 | == License == 311 | 312 | Fastly.com WordPress Plugin 313 | Copyright (C) 2011,2012,2013,2014,2015,2016,2017 Fastly.com 314 | 315 | This program is free software: you can redistribute it and/or modify 316 | it under the terms of the GNU General Public License as published by 317 | the Free Software Foundation, either version 3 of the License, or 318 | (at your option) any later version. 319 | 320 | This program is distributed in the hope that it will be useful, 321 | but WITHOUT ANY WARRANTY; without even the implied warranty of 322 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 323 | GNU General Public License for more details. 324 | 325 | You should have received a copy of the GNU General Public License 326 | along with this program. If not, see . 327 | 328 | == Upgrade Notice == 329 | Additional features with improvements in purging precision and Fastly API options 330 | -------------------------------------------------------------------------------- /src/classes/api.php: -------------------------------------------------------------------------------- 1 | headers_get = [ 27 | 'Fastly-Key' => Purgely_Settings::get_setting('fastly_api_key'), 28 | 'Accept' => 'application/json' 29 | ]; 30 | $this->headers_post = [ 31 | 'Fastly-Key' => Purgely_Settings::get_setting('fastly_api_key'), 32 | 'Accept' => 'application/json', 33 | 'Content-Type' => 'application/x-www-form-urlencoded' 34 | ]; 35 | $this->base_url = implode('', [ 36 | trailingslashit(Purgely_Settings::get_setting('fastly_api_hostname')), 37 | trailingslashit('service'), 38 | trailingslashit(Purgely_Settings::get_setting('fastly_service_id')), 39 | ]); 40 | } 41 | 42 | /** 43 | * @return Fastly_Api 44 | */ 45 | public static function getInstance() 46 | { 47 | if (is_null(self::$instance)) { 48 | $instance = new self(); 49 | $instance->validate(); 50 | self::$instance = $instance; 51 | } 52 | return self::$instance; 53 | } 54 | 55 | protected function validate() 56 | { 57 | try { 58 | $this->get_active_version(); 59 | } catch (\Exception $e) { 60 | wp_die("
61 |

Unable to connect to Fastly. Please go to General settings and review your settings.

62 |
"); 63 | } 64 | } 65 | 66 | public function get_active_version() 67 | { 68 | if (is_null($this->active_version)) { 69 | $response = Requests::get($this->base_url.'version', $this->headers_get); 70 | 71 | if ($response->status_code !== 200) { 72 | throw new \Exception("Invalid response while fetching active version."); 73 | } 74 | 75 | foreach (json_decode($response->body) as $version) { 76 | if ($version->active) { 77 | $this->active_version = $version; 78 | break; 79 | } 80 | } 81 | } 82 | return $this->active_version; 83 | } 84 | 85 | public function clone_active_version() 86 | { 87 | return $this->clone_version($this->get_active_version()->number); 88 | } 89 | 90 | public function clone_version($version) 91 | { 92 | $url = $this->base_url . "version/{$version}/clone"; 93 | return json_decode(Requests::put($url, $this->headers_post)->body); 94 | } 95 | 96 | 97 | public function validate_version($version) 98 | { 99 | $url = $this->base_url . "version/{$version}/validate"; 100 | $result = json_decode(Requests::get($url, $this->headers_get)->body); 101 | if ($result->status === 'error') { 102 | $this->show_error($result->msg); 103 | return false; 104 | } 105 | return true; 106 | } 107 | 108 | public function activate_version($version) 109 | { 110 | $url = $this->base_url . "version/{$version}/activate"; 111 | $this->active_version = json_decode(Requests::put($url, $this->headers_post)->body); 112 | return $this->active_version; 113 | } 114 | 115 | public function get_all_snippets($version = null) 116 | { 117 | $v = 0; 118 | if(!$version){ 119 | $activeVersion = $this->get_active_version(); 120 | if($activeVersion){ 121 | $v = $this->get_active_version()->number; 122 | } 123 | }else{ 124 | $v = $version; 125 | } 126 | 127 | if($v){ 128 | $url = $this->base_url . "version/{$v}/snippet"; 129 | $response = Requests::get($url, $this->headers_get)->body; 130 | }else{ 131 | $response = '{}'; 132 | } 133 | return json_decode($response); 134 | } 135 | 136 | public function get_snippet($name, $version = null) 137 | { 138 | $v = is_null($version) ? $this->get_active_version()->number : $version; 139 | $url = $this->base_url . "version/{$v}/snippet/{$name}"; 140 | return json_decode(Requests::get($url, $this->headers_get)->body); 141 | } 142 | 143 | public function snippet_exists($name, $version = null) 144 | { 145 | $result = $this->get_snippet($name, $version); 146 | return (bool) ($result->id ?? false); 147 | } 148 | 149 | public function upload_snippet($version, $snippet) 150 | { 151 | $url = $this->base_url . "version/{$version}/snippet"; 152 | if (!$this->snippet_exists($snippet['name'], $version)) { 153 | $verb = Requests::POST; 154 | } else { 155 | $verb = Requests::PUT; 156 | if (!isset($snippet['dynamic']) || $snippet['dynamic'] != 1) { 157 | $url .= '/'.$snippet['name']; 158 | unset($snippet['name'], $snippet['type'], $snippet['dynamic'], $snippet['priority']); 159 | } else { 160 | $snippet['name'] = $this->get_snippet($snippet['name'], $version)->id; 161 | $url = $this->base_url . "snippet/{$snippet['name']}"; 162 | } 163 | } 164 | 165 | $result = json_decode(Requests::request($url, $this->headers_post, $snippet, $verb)->body); 166 | if (!isset($result->id) || !$result->id) { 167 | $this->show_error($result->detail); 168 | return false; 169 | } 170 | return true; 171 | } 172 | 173 | public function delete_snippet($version, $name) 174 | { 175 | $url = $this->base_url . "version/{$version}/snippet/{$name}"; 176 | $result = json_decode(Requests::delete($url, $this->headers_get)->body); 177 | if (isset($result->status) && $result->status !== 'ok') { 178 | $this->show_error($result->detail ?? ''); 179 | return false; 180 | } 181 | return true; 182 | } 183 | 184 | public function get_all_acls() 185 | { 186 | $activeVersion = $this->get_active_version(); 187 | if($activeVersion){ 188 | $url = $this->base_url . "version/{$activeVersion->number}/acl"; 189 | $data = Requests::get($url, $this->headers_get)->body; 190 | }else{ 191 | $data = '{}'; 192 | } 193 | return json_decode($data); 194 | } 195 | 196 | public function get_all_dictionaries() 197 | { 198 | $url = $this->base_url . "version/{$this->get_active_version()->number}/dictionary"; 199 | return json_decode(Requests::get($url, $this->headers_get)->body); 200 | } 201 | 202 | public function show_error($message) 203 | { 204 | $this->error_message = $message; 205 | add_action('admin_notices', array($this, 'error_notice')); 206 | } 207 | 208 | public function error_notice() 209 | { 210 | ?> 211 |
212 |

error_message ); ?>

213 |
214 | getModulesWithData(); 36 | ?> 37 |
38 |
39 |

40 | fastly
41 | version: 42 |

43 |
44 | Fastly Edge Modules is a framework that allows you to enable specific functionality on Fastly without needing to write any VCL code. 45 | Below is a list of functions you can enable. Some may have additional options you can configure. To enable or disable click 46 | on the Manage button next to the functionality you want to enable, configure any available options then click Upload. 47 | To disable/remove the module click on Manage then click on Disable. 48 | 49 | 50 | 51 | 52 | 58 | 64 | 68 | 69 | 70 | 71 |
53 | name ); ?>
54 |

55 | description ); ?> 56 |

57 |
59 | 60 | enabled) && $module->enabled) ? esc_html__('Enabled', 'purgely') : esc_html__('Disabled', 'purgely'); ?>
61 | Uploaded: data['uploaded_at']) ? esc_html( gmdate('Y/m/d' , strtotime($module->data['uploaded_at'] ) ) ) : esc_html__('never', 'purgely'); ?> 62 |
63 |
65 | Manage 67 |
72 | 73 |
74 | 75 | 116 | 117 | get_all_snippets(); 123 | $localData = get_option(self::SETTINGS, []); 124 | return array_map(function ($module) use ($apiData, $localData) { 125 | $module->data = $localData[$module->id] ?? []; 126 | $query = self::EDGE_PREFIX.'_'.$module->id; 127 | foreach ($apiData as $apiModule) { 128 | if (substr($apiModule->name, 0, strlen($query)) === $query) { 129 | $module->enabled = true; 130 | } 131 | } 132 | return $module; 133 | }, get_purgely_instance()->fastly_edge_modules()); 134 | } 135 | 136 | protected function renderGroup($group, $values, $suffix) 137 | { 138 | $values = $values ? $values : []; 139 | $name = "{$suffix}[{$group->name}]"; 140 | $suffix = "{$suffix}-{$group->name}"; 141 | ?> 142 | 143 | 144 | 147 | 148 | 149 | Add group 150 | 151 | 152 | 153 | 154 | $value): ?> 155 | renderGroupProperties($group->properties, $value, "{$name}[$key]", "{$suffix}-{$key}"); ?> 156 | 157 | 160 | 161 | 162 | 168 |
169 | 170 | 171 | 172 | renderProperty($name, $property, $values[$property->name] ?? ""); ?> 173 | 174 | 175 |
176 | 177 | Remove group 178 | 179 |
180 |
181 | 187 | 188 | 189 | 192 | 193 | 194 | renderField($name, $property, $value); ?> 195 |

description ?? "" ); ?>

196 | 197 | 198 | name; 204 | $name = "{$name}[{$property->name}]"; 205 | 206 | if(!$value && isset($property->default)){ 207 | $value = $property->default; 208 | } 209 | 210 | 211 | $required = $property->required ? 'required' : ''; 212 | 213 | switch ($property->type) { 214 | case 'acl': 215 | echo ""; 221 | break; 222 | case 'dict': 223 | echo ""; 229 | break; 230 | case 'select': 231 | echo ""; 238 | break; 239 | case 'boolean': 240 | echo ""; 245 | break; 246 | case 'integer': 247 | case 'float': 248 | echo ""; 250 | break; 251 | case 'string': 252 | case 'path': 253 | default: 254 | 255 | echo ""; 257 | break; 258 | } 259 | } 260 | 261 | protected function getAcls() 262 | { 263 | if (is_null($this->acls)) { 264 | $this->acls = fastly_api()->get_all_acls(); 265 | } 266 | return $this->acls; 267 | } 268 | 269 | protected function getDictionaries() 270 | { 271 | if (is_null($this->dictionaries)) { 272 | $this->dictionaries = fastly_api()->get_all_dictionaries(); 273 | } 274 | return $this->dictionaries; 275 | } 276 | 277 | public function processFormSubmission($data) 278 | { 279 | unset($data['nonce']); 280 | 281 | $clone = fastly_api()->clone_active_version(); 282 | foreach ($data as $key => $datum) { 283 | $snippets = json_decode(rawurldecode($datum['snippet'])); 284 | foreach ($snippets as $snippet) { 285 | $success = fastly_api()->upload_snippet($clone->number, [ 286 | 'name' => self::EDGE_PREFIX.'_'.$key.'_'.$snippet->type, 287 | 'type' => $snippet->type, 288 | 'dynamic' => "0", 289 | 'priority' => $snippet->priority, 290 | 'content' => $snippet->snippet 291 | ]); 292 | if (!$success) { 293 | return; 294 | } 295 | } 296 | } 297 | if (!fastly_api()->validate_version($clone->number)) { 298 | return; 299 | } 300 | fastly_api()->activate_version($clone->number); 301 | 302 | $currentData = get_option(self::SETTINGS, []); 303 | $data = array_merge($currentData , array_map(function ($d) { 304 | unset($d['snippet']); 305 | $d['uploaded_at'] = gmdate(DATE_ISO8601); 306 | return $d; 307 | }, $data)); 308 | update_option(self::SETTINGS, $data); 309 | } 310 | 311 | public function processFormSubmissionDisable($data) 312 | { 313 | $clone = fastly_api()->clone_active_version(); 314 | foreach ($data['types'] as $type) { 315 | $name = self::EDGE_PREFIX.'_'.$data['module_name'].'_'.$type; 316 | if (!fastly_api()->delete_snippet($clone->number, $name)) { 317 | return; 318 | } 319 | } 320 | 321 | if (!fastly_api()->validate_version($clone->number)) { 322 | return; 323 | } 324 | fastly_api()->activate_version($clone->number); 325 | 326 | $currentData = get_option(self::SETTINGS, []); 327 | unset($currentData[$data['module_name']]); 328 | update_option(self::SETTINGS, $currentData); 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /src/classes/header-cache-control.php: -------------------------------------------------------------------------------- 1 | _headers['max-age'] = absint(Purgely_Settings::get_setting('cache_control_ttl')); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/classes/header-surrogate-control.php: -------------------------------------------------------------------------------- 1 | _headers['max-age'] = absint(Purgely_Settings::get_setting('surrogate_control_ttl')); 26 | } 27 | if (true === Purgely_Settings::get_setting('enable_stale_while_revalidate')) { 28 | $this->_headers['stale-while-revalidate'] = absint(Purgely_Settings::get_setting('stale_while_revalidate_ttl')); 29 | } 30 | if (true === Purgely_Settings::get_setting('enable_stale_if_error')) { 31 | $this->_headers['stale-if-error'] = absint(Purgely_Settings::get_setting('stale_if_error_ttl')); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/classes/header-surrogate-keys.php: -------------------------------------------------------------------------------- 1 | get_keys(); 33 | 34 | // Combine keys. 35 | $keys = array_merge($current_keys, $keys); 36 | 37 | // De-dupe keys. 38 | $keys = array_unique($keys); 39 | 40 | // Rekey the keys. 41 | $keys = array_values($keys); 42 | 43 | $this->set_keys($keys); 44 | } 45 | 46 | /** 47 | * Add a key to the list. 48 | * 49 | * @param string $key The key to add to the list. 50 | * @return array The full list of keys. 51 | */ 52 | public function add_key($key) 53 | { 54 | $keys = $this->get_keys(); 55 | $keys[] = $key; 56 | 57 | $this->set_keys($keys); 58 | return $keys; 59 | } 60 | 61 | /** 62 | * Return the value of the header, overwritten from parent for Keys special case 63 | * Also test header size, if too big, set key that will always be purged 64 | * 65 | * @return string The header value. 66 | */ 67 | public function get_value() 68 | { 69 | $keys_string = $this->prepare_keys(); 70 | $header_string = $this->_header_name . ': ' . $keys_string; 71 | $header_size_bytes = mb_strlen($header_string, '8bit'); 72 | if ($header_size_bytes >= FASTLY_MAX_HEADER_SIZE) { 73 | // Set to be always purged 74 | $siteId = false; 75 | if(is_multisite()) { 76 | $siteId = get_current_blog_id(); 77 | } elseif($sitecode = Purgely_Settings::get_setting('sitecode')) { 78 | $siteId = $sitecode; 79 | } 80 | if($siteId) { 81 | return $siteId . '-' . 'holos'; 82 | } 83 | return 'holos'; 84 | } 85 | return $keys_string; 86 | } 87 | 88 | /** 89 | * Prepare the keys into a header value string. 90 | * 91 | * @return string Space delimited list of sanitized keys. 92 | */ 93 | public function prepare_keys() 94 | { 95 | $keys = $this->get_keys(); 96 | 97 | $siteId = false; 98 | if(is_multisite()) { 99 | $siteId = get_current_blog_id(); 100 | } elseif($sitecode = Purgely_Settings::get_setting('sitecode')) { 101 | $siteId = $sitecode; 102 | } 103 | if($siteId) { 104 | foreach($keys as $index => $key) { 105 | $keys[$index] = $siteId . '-' . $key; 106 | } 107 | } 108 | 109 | $keys = array_map(array($this, 'sanitize_key'), $keys); 110 | return rtrim(implode(' ', $keys), ' '); 111 | } 112 | 113 | /** 114 | * Sanitize a surrogate key. 115 | * 116 | * @param string $key The unsanitized key. 117 | * @return string The sanitized key. 118 | */ 119 | public function sanitize_key($key) 120 | { 121 | return purgely_sanitize_surrogate_key($key); 122 | } 123 | 124 | /** 125 | * Set the keys for the Surrogate Keys header. 126 | * 127 | * @param array $keys The keys for the header. 128 | * @return void 129 | */ 130 | public function set_keys($keys) 131 | { 132 | $this->_keys = $keys; 133 | } 134 | 135 | /** 136 | * Key the list of Surrogate Keys. 137 | * 138 | * @return array The list of Surrogate Keys. 139 | */ 140 | public function get_keys() 141 | { 142 | return $this->_keys; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/classes/header.php: -------------------------------------------------------------------------------- 1 | set_header_name($this->_header_name); 38 | $this->build_original_headers(); 39 | } 40 | 41 | /** 42 | * Sets original headers 43 | */ 44 | public function build_original_headers() 45 | { 46 | } 47 | 48 | /** 49 | * Send the key by setting the header. 50 | * 51 | * @return void 52 | */ 53 | public function send_header() 54 | { 55 | header($this->_header_name . ': ' . $this->get_value(), false); 56 | } 57 | 58 | /** 59 | * Set the header name. 60 | * 61 | * @param string $header_name The header name. 62 | * @return void 63 | */ 64 | public function set_header_name($header_name) 65 | { 66 | $this->_header_name = $header_name; 67 | } 68 | 69 | /** 70 | * Build header string based on current headers 71 | * 72 | * @return string 73 | */ 74 | public function build_header_value() 75 | { 76 | $headers = ''; 77 | foreach ($this->_headers as $name => $val) { 78 | $headers .= $name . '=' . $val . ', '; 79 | } 80 | 81 | return rtrim($headers, ', '); 82 | } 83 | 84 | /** 85 | * Set the header value. 86 | * 87 | * @param string $value The header value. 88 | * @return void 89 | */ 90 | public function set_value($value) 91 | { 92 | $this->_value = $value; 93 | } 94 | 95 | /** 96 | * Return the value of the header. 97 | * 98 | * @return string The header value. 99 | */ 100 | public function get_value() 101 | { 102 | return $this->build_header_value(); 103 | } 104 | 105 | /** 106 | * Edit headers - allows to overwrite or add new headers 107 | * 108 | * @param array $key 109 | */ 110 | public function edit_headers($key) 111 | { 112 | if (is_array($key)) { 113 | foreach ($key as $k => $val) { 114 | $this->_headers[$k] = $val; 115 | } 116 | } 117 | } 118 | 119 | /** 120 | * Remove wanted headers 121 | * 122 | * @param array|string $key 123 | */ 124 | public function unset_headers($key) 125 | { 126 | if (is_array($key)) { 127 | foreach ($key as $k) { 128 | if (!empty($this->_headers[$k])) { 129 | unset($this->_headers[$k]); 130 | } 131 | } 132 | } else { 133 | if (!empty($this->_headers[$key])) { 134 | unset($this->_headers[$key]); 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/classes/purge-request.php: -------------------------------------------------------------------------------- 1 | set_type($type); 59 | $this->set_thing($thing); 60 | 61 | // Build up headers & request url 62 | $headers = $this->_build_headers(); 63 | $request_uri = $this->_build_request_uri_for_purge($type); 64 | $request_method = $this->_build_request_method_type($type); 65 | 66 | if (($request_uri && !empty($thing)) || ($request_uri && $type = self::ALL)) { 67 | try { 68 | $response = Requests::request($request_uri, $headers, array(), $request_method); 69 | 70 | // Do logging where needed 71 | $message = $this->_get_purge_data_message(); 72 | handle_logging($response, $message); 73 | 74 | return $response->success; 75 | } catch (Exception $e) { 76 | error_log($e->getMessage()); 77 | } 78 | } 79 | return false; 80 | } 81 | 82 | /** 83 | * Builds request headers 84 | * 85 | * @return array 86 | */ 87 | protected function _build_headers() 88 | { 89 | $headers = array(); 90 | 91 | // Credentials 92 | $headers['Fastly-Key'] = Purgely_Settings::get_setting('fastly_api_key'); 93 | 94 | // Purge type 95 | if (Purgely_Settings::get_setting('default_purge_type') === 'soft') { 96 | $headers['Fastly-Soft-Purge'] = 1; 97 | } 98 | 99 | // Add Surrogate-Key header 100 | $thing = $this->get_thing(); 101 | if (!empty($thing) && $this->get_type() === Purgely_Purge::KEY_COLLECTION) { 102 | $keys = implode(' ', $this->get_thing()); 103 | $headers['Surrogate-Key'] = $keys; 104 | } 105 | 106 | return $headers; 107 | } 108 | 109 | /** 110 | * Build the URI for the purge request. 111 | * 112 | * @type string Type of the purge request (key, url, all) 113 | * @return string The purge URI to purge all items. 114 | */ 115 | protected function _build_request_uri_for_purge($type) 116 | { 117 | $api_endpoint = Purgely_Settings::get_setting('fastly_api_hostname'); 118 | $fastly_service_id = Purgely_Settings::get_setting('fastly_service_id'); 119 | 120 | switch ($type) { 121 | case 'key-collection': 122 | return trailingslashit($api_endpoint) . 'service/' . $fastly_service_id . '/purge'; 123 | case 'url': 124 | return $this->get_thing(); 125 | case 'all': 126 | return trailingslashit($api_endpoint) . 'service/' . $fastly_service_id . '/purge_all'; 127 | default : 128 | return false; 129 | } 130 | } 131 | 132 | /** 133 | * Sets Request method type 134 | * @param $type 135 | * @return string 136 | */ 137 | protected function _build_request_method_type($type) 138 | { 139 | if ($type === Purgely_Purge::URL) { 140 | return Purgely_Purge::PURGE; 141 | } else { 142 | return Requests::POST; 143 | } 144 | } 145 | 146 | /** 147 | * Set the thing to purge. 148 | * 149 | * @param string|array $thing The identifier for the purged item. 150 | * @return void 151 | */ 152 | public function set_thing($thing) 153 | { 154 | $this->_thing = $thing; 155 | } 156 | 157 | /** 158 | * Get the thing to purge. 159 | * 160 | * @return string|array The identifier for the purged item. 161 | */ 162 | public function get_thing() 163 | { 164 | return $this->_thing; 165 | } 166 | 167 | /** 168 | * Set the type of purge. 169 | * 170 | * @param string $type The type of purge to perform. 171 | * @return void 172 | */ 173 | public function set_type($type) 174 | { 175 | $this->_type = $type; 176 | } 177 | 178 | /** 179 | * Get the type of purge. 180 | * 181 | * @return string The type of purge being performed. 182 | */ 183 | public function get_type() 184 | { 185 | return $this->_type; 186 | } 187 | 188 | /** 189 | * Prepare message for logging with data being purged 190 | * @return string 191 | */ 192 | protected function _get_purge_data_message() 193 | { 194 | if ($this->get_type() === self::URL) { 195 | $msg = "Purging URL - " . $this->get_thing(); 196 | } elseif ($this->get_type() === self::KEY_COLLECTION) { 197 | $msg = "Purging Keys *" . implode(' ', $this->get_thing()) . "*"; 198 | } else { 199 | $msg = 'Initiated Purge All'; 200 | } 201 | return $msg; 202 | } 203 | 204 | /** 205 | * Get possible purge types 206 | * @return array 207 | */ 208 | static function get_purge_types() 209 | { 210 | return self::$_purge_types; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/classes/related-surrogate-keys.php: -------------------------------------------------------------------------------- 1 | ID; 42 | } 43 | 44 | // Pull the post object from the $identifiers array and setup a standard post object. 45 | $this->set_post_id($identifier); 46 | $this->set_post(get_post($identifier)); 47 | // Insert identifier 48 | $this->_collection[] = 'p-' . $identifier; 49 | } 50 | 51 | /** 52 | * Determine all surrogate keys 53 | * 54 | * @return array Related surrogate keys 55 | */ 56 | public function locate_all() 57 | { 58 | // Collect and store keys 59 | $this->locate_surrogate_taxonomies($this->get_post_id()); 60 | $this->locate_author_surrogate_key($this->get_post_id()); 61 | $this->include_always_purged_types(); 62 | 63 | $this->_collection = apply_filters('purgely_related_keys', $this->_collection, $this->get_post()); 64 | 65 | $sitecode = Purgely_Settings::get_setting('sitecode'); 66 | 67 | if(is_multisite() or $sitecode) { 68 | $this->appendMultiSiteIdToCollection(); 69 | } 70 | 71 | $num = count($this->_collection); 72 | // Split keys for multiple requests if needed 73 | if ($num >= FASTLY_MAX_HEADER_KEY_SIZE) { 74 | $parts = $num / FASTLY_MAX_HEADER_KEY_SIZE; 75 | $additional = ($parts > (int)$parts) ? 1 : 0; 76 | $parts = (int)$parts + (int)$additional; 77 | $chunks = ceil($num/$parts); 78 | $this->_collection = array_chunk($this->_collection, $chunks); 79 | } else { 80 | $this->_collection = array($this->_collection); 81 | } 82 | 83 | return $this->_collection; 84 | } 85 | 86 | /** 87 | * Includes types that get purged always (for custom themes) 88 | */ 89 | public function include_always_purged_types() 90 | { 91 | $always_purged = $this->get_always_purged_types(); 92 | $this->_collection = array_merge($this->_collection, $always_purged); 93 | } 94 | 95 | /** 96 | * Fetches types that get purged always (for custom themes) 97 | * 98 | * @return array Keys that always get purged. 99 | */ 100 | public static function get_always_purged_types() 101 | { 102 | $always_purged_keys = Purgely_Settings::get_setting('always_purged_keys'); 103 | $always_purged_keys = explode(',', $always_purged_keys); 104 | 105 | $always_purged_templates = array( 106 | 'tm-post', 107 | 'tm-home', 108 | 'tm-front_page', 109 | 'tm-feed', 110 | 'holos', 111 | 'tm-404' 112 | ); 113 | 114 | $always_purged = array_merge($always_purged_templates, $always_purged_keys); 115 | return $always_purged; 116 | } 117 | 118 | /** 119 | * Get the term link pages for all terms associated with a post in a particular taxonomy. 120 | * 121 | * @param int $post_id Post ID. 122 | */ 123 | public function locate_surrogate_taxonomies($post_id) 124 | { 125 | 126 | $taxonomies = apply_filters('purgely_taxonomy_keys', (array)get_taxonomies()); 127 | 128 | foreach ($taxonomies as $taxonomy) { 129 | $this->locate_surrogate_taxonomy_single($post_id, $taxonomy); 130 | } 131 | } 132 | 133 | /** 134 | * Locate single taxonomy terms for post_id 135 | * 136 | * @param $post_id 137 | * @param $taxonomy 138 | */ 139 | public function locate_surrogate_taxonomy_single($post_id, $taxonomy) 140 | { 141 | $terms = wp_get_post_terms($post_id, $taxonomy, array('fields' => 'ids')); 142 | 143 | if (is_array($terms)) { 144 | foreach ($terms as $term) { 145 | if ($term) { 146 | $key = 't-' . $term; 147 | $this->_collection[] = $key; 148 | } 149 | } 150 | } 151 | } 152 | 153 | /** 154 | * Get author key 155 | * 156 | * @param int $post_id The post ID to search for related author information. 157 | */ 158 | public function locate_author_surrogate_key($post_id) 159 | { 160 | 161 | if ($post = $this->get_post($post_id)) { 162 | $key = 'a-' . absint($post->post_author); 163 | $this->_collection[] = $key; 164 | } 165 | } 166 | 167 | /** 168 | * Append Multisite ID to surrogate keys 169 | * @return array 170 | */ 171 | public function appendMultiSiteIdToCollection() 172 | { 173 | $siteId = "0"; // a default 174 | if (is_multisite()) { 175 | $siteId = get_current_blog_id(); 176 | } else { 177 | $siteId = Purgely_Settings::get_setting('sitecode'); 178 | } 179 | 180 | foreach($this->_collection as $index => $key) { 181 | if(empty($key)) { 182 | continue; 183 | } 184 | $this->_collection[$index] = $siteId . '-' .$key; 185 | } 186 | 187 | return $this->_collection; 188 | } 189 | 190 | /** 191 | * Get the main post ID. 192 | * 193 | * @return int The main post ID. 194 | */ 195 | public function get_post_id() 196 | { 197 | return $this->_post_id; 198 | } 199 | 200 | /** 201 | * Set the main post ID. 202 | * 203 | * @param int $post_id The main post ID. 204 | * @return void 205 | */ 206 | public function set_post_id($post_id) 207 | { 208 | $this->_post_id = $post_id; 209 | } 210 | 211 | /** 212 | * Get the main post object. 213 | * 214 | * @return WP_Post|false The main post object. 215 | */ 216 | public function get_post() 217 | { 218 | if ($this->_post) { 219 | return $this->_post; 220 | } 221 | 222 | $post = get_post($this->get_post_id()); 223 | 224 | if (!$post) { 225 | return false; 226 | } else { 227 | $this->set_post($post); 228 | return $post; 229 | } 230 | } 231 | 232 | /** 233 | * Set the main post object. 234 | * 235 | * @param WP_Post $post The main post object. 236 | * @return void 237 | */ 238 | public function set_post($post) 239 | { 240 | $this->_post = $post; 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /src/classes/settings.php: -------------------------------------------------------------------------------- 1 | 'fastly-settings-general', 15 | self::FASTLY_CONFIGURATION_LIST_ADVANCED => 'fastly-settings-advanced', 16 | self::FASTLY_CONFIGURATION_LIST_ADVANCED => 'fastly-settings-io', 17 | self::FASTLY_CONFIGURATION_LIST_WEBHOOKS => 'fastly-settings-webhooks' 18 | ); 19 | 20 | /** 21 | * Possible pixel ratio sizes 22 | */ 23 | const POSSIBLE_PIXEL_RATIOS = array('1x', '1.5x', '2x', '3x', '3.5x', '4x'); 24 | 25 | /** 26 | * The settings values for the plugin. 27 | * 28 | * @var array Holds all of the individual settings for the plugin. 29 | */ 30 | public static $settings = array(); 31 | 32 | /** 33 | * Get the valid settings for the plugin. 34 | * 35 | * @return array The valid settings including default values and sanitize callback. 36 | */ 37 | public static function get_registered_settings() 38 | { 39 | return array( 40 | 'fastly_api_key' => array( 41 | 'sanitize_callback' => 'purgely_sanitize_key', 42 | 'default' => PURGELY_FASTLY_KEY, 43 | ), 44 | 'fastly_service_id' => array( 45 | 'sanitize_callback' => 'purgely_sanitize_key', 46 | 'default' => PURGELY_FASTLY_SERVICE_ID, 47 | ), 48 | 'allow_purge_all' => array( 49 | 'sanitize_callback' => 'purgely_sanitize_checkbox', 50 | 'default' => PURGELY_ALLOW_PURGE_ALL, 51 | ), 52 | 'fastly_log_purges' => array( 53 | 'sanitize_callback' => 'purgely_sanitize_checkbox', 54 | 'default' => PURGELY_FASTLY_LOG_PURGES, 55 | ), 56 | 'fastly_vcl_version' => array( 57 | 'sanitize_callback' => 'purgely_sanitize_checkbox', 58 | 'default' => PURGELY_FASTLY_VCL_VERSION, 59 | ), 60 | 'fastly_debug_mode' => array( 61 | 'sanitize_callback' => 'purgely_sanitize_checkbox', 62 | 'default' => PURGELY_FASTLY_DEBUG_MODE, 63 | ), 64 | 'fastly_api_hostname' => array( 65 | 'sanitize_callback' => 'esc_url', 66 | 'default' => PURGELY_API_ENDPOINT, 67 | ), 68 | 'enable_stale_while_revalidate' => array( 69 | 'sanitize_callback' => 'purgely_sanitize_checkbox', 70 | 'default' => PURGELY_ENABLE_STALE_WHILE_REVALIDATE, 71 | ), 72 | 'stale_while_revalidate_ttl' => array( 73 | 'sanitize_callback' => 'absint', 74 | 'default' => PURGELY_STALE_WHILE_REVALIDATE_TTL, 75 | ), 76 | 'enable_stale_if_error' => array( 77 | 'sanitize_callback' => 'purgely_sanitize_checkbox', 78 | 'default' => PURGELY_ENABLE_STALE_IF_ERROR, 79 | ), 80 | 'stale_if_error_ttl' => array( 81 | 'sanitize_callback' => 'absint', 82 | 'default' => PURGELY_STALE_IF_ERROR_TTL, 83 | ), 84 | 'use_fastly_cache_tags' => array( 85 | 'sanitize_callback' => 'purgely_sanitize_checkbox', 86 | 'default' => PURGELY_USE_FASTLY_CACHE_TAGS, 87 | ), 88 | 'use_fastly_cache_tags_for_custom_post_type' => array( 89 | 'sanitize_callback' => 'purgely_sanitize_checkbox', 90 | 'default' => PURGELY_USE_FASTLY_CACHE_TAGS_FOR_CUSTOM_POST_TYPE, 91 | ), 92 | 'always_purged_keys' => array( 93 | 'sanitize_callback' => 'purgely_sanitize_key', 94 | 'default' => PURGELY_ALWAYS_PURGED_KEYS, 95 | ), 96 | 'surrogate_control_ttl' => array( 97 | 'sanitize_callback' => 'absint', 98 | 'default' => PURGELY_SURROGATE_CONTROL_TTL, 99 | ), 100 | 'cache_control_ttl' => array( 101 | 'sanitize_callback' => 'absint', 102 | 'default' => PURGELY_CACHE_CONTROL_TTL, 103 | ), 104 | 'default_purge_type' => array( 105 | 'sanitize_callback' => 'sanitize_key', 106 | 'default' => PURGELY_DEFAULT_PURGE_TYPE, 107 | ), 108 | 'sitecode' => array( 109 | 'sanitize_callback' => 'sanitize_key', 110 | 'default' => FASTLY_SITECODE, 111 | ), 112 | 'custom_ttl_templates' => array( 113 | 'sanitize_callback' => 'purgely_sanitize_ttl_templates', 114 | 'default' => array(), 115 | ), 116 | 'io_adaptive_pixel_ratios' => array( 117 | 'sanitize_callback' => 'purgely_sanitize_checkbox', 118 | 'default' => PURGELY_USE_FASTLY_IO_ADAPTIVE_PIXELS, 119 | ), 120 | 'io_enable_wp' => array( 121 | 'sanitize_callback' => 'purgely_sanitize_checkbox', 122 | 'default' => PURGELY_USE_FASTLY_IO_WORDPRESS, 123 | ), 124 | 'io_adaptive_pixel_ratios_content' => array( 125 | 'sanitize_callback' => 'purgely_sanitize_checkbox', 126 | 'default' => PURGELY_USE_FASTLY_IO_ADAPTIVE_PIXELS_CONTENT, 127 | ), 128 | 'io_adaptive_pixel_ratio_sizes' => array( 129 | 'sanitize_callback' => 'purgely_sanitize_pixel_ratios', 130 | 'default' => PURGELY_FASTLY_IO_ADAPTIVE_PIXEL_SIZES, 131 | ), 132 | 'webhooks_url_endpoint' => array( 133 | 'sanitize_callback' => 'esc_url', 134 | 'default' => PURGELY_WEBHOOKS_URL_ENDPOINT, 135 | ), 136 | 'webhooks_username' => array( 137 | 'sanitize_callback' => 'sanitize_key', 138 | 'default' => PURGELY_WEBHOOKS_USERNAME, 139 | ), 140 | 'webhooks_channel' => array( 141 | 'sanitize_callback' => 'sanitize_key', 142 | 'default' => PURGELY_WEBHOOKS_CHANNEL, 143 | ), 144 | 'webhooks_activate' => array( 145 | 'sanitize_callback' => 'purgely_sanitize_checkbox', 146 | 'default' => PURGELY_WEBHOOKS_ACTIVATE, 147 | ), 148 | ); 149 | } 150 | 151 | /** 152 | * Get an array of settings values. 153 | * 154 | * This method negotiates the database values and the constant values to determine what the current value should be. 155 | * The database value takes precedence over the constant value. 156 | * 157 | * @return array The current settings values. 158 | */ 159 | public static function get_settings() 160 | { 161 | $negotiated_settings = self::$settings; 162 | 163 | if (empty($negotiated_settings)) { 164 | $registered_settings = self::get_registered_settings(); 165 | $saved_settings = get_option('fastly-settings-general', array()); 166 | $saved_settings = array_merge($saved_settings, get_option('fastly-settings-advanced', array())); 167 | $saved_settings_io = get_option('fastly-settings-io', array()); 168 | if($saved_settings_io && is_array($saved_settings_io)) { 169 | $saved_settings = array_merge($saved_settings, $saved_settings_io); 170 | } 171 | $saved_settings = array_merge($saved_settings, get_option('fastly-settings-webhooks', array())); 172 | $negotiated_settings = array(); 173 | 174 | foreach ($registered_settings as $key => $values) { 175 | $value = ''; 176 | 177 | if ( ! empty( $saved_settings[ $key ] ) ) { 178 | $value = $saved_settings[ $key ]; 179 | } else if ( ! empty( $values['default'] ) ) { 180 | $value = $values['default']; 181 | } 182 | 183 | if ( ! empty ( $values['sanitize_callback'] ) ) { 184 | $value = call_user_func( $values['sanitize_callback'], $value ); 185 | } 186 | 187 | $negotiated_settings[$key] = $value; 188 | } 189 | 190 | self::set_settings($negotiated_settings); 191 | } 192 | 193 | return $negotiated_settings; 194 | } 195 | 196 | /** 197 | * Get an array of settings section strictly from database. 198 | * 199 | * @param $section 200 | * @return array 201 | */ 202 | public static function get_database_section_settings($section) 203 | { 204 | return get_option($section, array()); 205 | } 206 | 207 | /** 208 | * Get the value of an individual setting. 209 | * 210 | * @param string $setting The setting name. 211 | * @return mixed The setting value. 212 | */ 213 | public static function get_setting($setting) 214 | { 215 | $value = ''; 216 | 217 | $negotiated_settings = self::get_settings(); 218 | $registered_settings = self::get_registered_settings(); 219 | 220 | if (isset($negotiated_settings[$setting])) { 221 | $value = $negotiated_settings[$setting]; 222 | } elseif (isset($registered_settings[$setting]['default'])) { 223 | $value = $registered_settings[$setting]['default']; 224 | } 225 | 226 | return $value; 227 | } 228 | 229 | /** 230 | * Set the settings values. 231 | * 232 | * @param array $settings The current settings values. 233 | * @return void 234 | */ 235 | public static function set_settings($settings) 236 | { 237 | self::$settings = $settings; 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/classes/surrogate-key-collection.php: -------------------------------------------------------------------------------- 1 | _add_key_post_ids($wp_query); 63 | 64 | // Get the query type. 65 | $template_key = $this->_add_key_query_type($wp_query); 66 | 67 | // Get all taxonomy terms and author info if on a single post. 68 | $term_keys = array(); 69 | 70 | if ($wp_query->is_single()) { 71 | $taxonomies = apply_filters('purgely_taxonomy_keys', (array)get_taxonomies()); 72 | 73 | foreach ($taxonomies as $taxonomy) { 74 | // $wp_query->post can be null, and then PHP 8 will throw a warning when trying to access post->ID. 75 | // In lower versions of PHP, there would be no warning, but the end result in $term_keys would be the 76 | // same, as it would merge an empty array to current array. So, check if there is $wp_query->post and 77 | // skip if it's null. 78 | if (!$wp_query->post) continue; 79 | 80 | $term_keys = array_merge($term_keys, $this->_add_key_terms_single($wp_query->post->ID, $taxonomy)); 81 | } 82 | 83 | // Get author information. 84 | $term_keys = array_merge($term_keys, $this->_add_key_author($wp_query->post)); 85 | 86 | } else { 87 | if ($wp_query->is_category() || $wp_query->is_tag() || $wp_query->is_tax()) { 88 | $term_keys = $this->_add_key_terms_taxonomy(); 89 | } 90 | } 91 | 92 | // Merge, de-dupe, and prune empties. 93 | $keys = array_merge( 94 | $keys, 95 | $template_key, 96 | $term_keys 97 | ); 98 | 99 | $keys = array_unique($keys); 100 | $keys = array_filter($keys); 101 | 102 | // If there is always purge key existing, remove all others 103 | $always_purged = Purgely_Related_Surrogate_Keys::get_always_purged_types(); 104 | foreach($always_purged as $k) { 105 | if (in_array($k, $template_key)) { 106 | $keys = $template_key; 107 | break; 108 | } 109 | } 110 | 111 | $this->set_keys($keys); 112 | } 113 | 114 | /** 115 | * Add a key for each post ID to all pages that include the post. 116 | * 117 | * @param WP_Query $wp_query The main query. 118 | * @return array $keys The "post-{ID}" keys. 119 | */ 120 | private function _add_key_post_ids($wp_query) 121 | { 122 | $keys = array(); 123 | 124 | foreach ($wp_query->posts as $post) { 125 | $keys[] = 'p-' . absint($post->ID); 126 | } 127 | 128 | return $keys; 129 | } 130 | 131 | /** 132 | * Determine the type of WP template being displayed. 133 | * 134 | * @param WP_Query $wp_query The query object to inspect. 135 | * @return array $key The template key. 136 | */ 137 | private function _add_key_query_type($wp_query) 138 | { 139 | $template_type = ''; 140 | $key = ''; 141 | 142 | /** 143 | * This function has the potential to be called in the admin context. Unfortunately, in the admin context, 144 | * $wp_query, is not a WP_Query object. Bad things happen when call_user_func is applied below. As such, lets' be 145 | * cautious and make sure that the $wp_query object is indeed a WP_Query object. 146 | */ 147 | if (is_a($wp_query, 'WP_Query')) { 148 | // List of all "is" calls. 149 | $types = $this::$types; 150 | 151 | /** 152 | * Foreach "is" call, if it is a callable function, call and see if it returns true. If it does, we know what type 153 | * of template we are currently on. Break the loop and return that value. 154 | */ 155 | foreach ($types as $type) { 156 | $callable = array($wp_query, 'is_' . $type); 157 | if (method_exists($wp_query, 'is_' . $type) && is_callable($callable)) { 158 | if (true === call_user_func($callable)) { 159 | $template_type = $type; 160 | break; 161 | } 162 | } 163 | } 164 | } 165 | 166 | // Only set the key if it exists. 167 | if (!empty($template_type)) { 168 | $key = self::FASTLY_TEMPLATE_KEY_PREFIX . $template_type; 169 | } 170 | 171 | $this->set_custom_ttl($template_type); 172 | 173 | return (array)$key; 174 | } 175 | 176 | public function set_custom_ttl($template_type) 177 | { 178 | $custom_ttls = Purgely_Settings::get_setting('custom_ttl_templates'); 179 | $ttl = isset($custom_ttls[$template_type]) ? $custom_ttls[$template_type] : false; 180 | $this->custom_ttl = (int)$ttl; 181 | } 182 | 183 | public function get_custom_ttl() 184 | { 185 | return $this->custom_ttl; 186 | } 187 | 188 | /** 189 | * Get the term keys for every term associated with a post. 190 | * 191 | * @param int $post_id Post ID. 192 | * @param string $taxonomy The taxonomy to look for associated terms. 193 | * @return array The term slug/taxonomy combos for the post. 194 | */ 195 | private function _add_key_terms_single($post_id, $taxonomy) 196 | { 197 | $keys = array(); 198 | $terms = get_the_terms($post_id, $taxonomy); 199 | 200 | if ($terms) { 201 | foreach ($terms as $term) { 202 | if (isset($term->term_id)) { 203 | $keys[] = 't-' . $term->term_id; 204 | } 205 | } 206 | } 207 | 208 | return $keys; 209 | } 210 | 211 | /** 212 | * Get the term keys for taxonomies. 213 | * 214 | * @return array The taxonomy combos for the post. 215 | */ 216 | private function _add_key_terms_taxonomy() 217 | { 218 | $keys = array(); 219 | 220 | $queried_object = get_queried_object(); 221 | // archive page? author page? single post? 222 | 223 | if (!empty($queried_object->term_id) && !empty($queried_object->taxonomy)) { 224 | $keys[] = 't-' . absint($queried_object->term_id); 225 | } 226 | 227 | return $keys; 228 | } 229 | 230 | 231 | /** 232 | * Get author related to this post. 233 | * 234 | * @param WP_Post $post The post object to search for related author information. 235 | * @return array The related author key. 236 | */ 237 | private function _add_key_author($post) 238 | { 239 | if (!$post) return array(); 240 | 241 | $author = absint($post->post_author); 242 | $key = array(); 243 | 244 | if ($author > 0) { 245 | $key[] = 'a-' . absint($author); 246 | } 247 | 248 | return $key; 249 | } 250 | 251 | /** 252 | * Set the keys variable. 253 | * 254 | * @param array $keys Array of Purgely_Surrogate_Key objects. 255 | * @return void 256 | */ 257 | public function set_keys($keys) 258 | { 259 | $this->_keys = $keys; 260 | } 261 | 262 | /** 263 | * Set an individual key. 264 | * 265 | * @param Purgely_Surrogate_Keys_Header $key Purgely_Surrogate_Key object. 266 | * @return void 267 | */ 268 | public function set_key($key) 269 | { 270 | $keys = $this->get_keys(); 271 | $keys[] = $key; 272 | 273 | $this->set_keys($keys); 274 | } 275 | 276 | /** 277 | * Get all of the keys to be sent in the headers. 278 | * 279 | * @return array Array of Purgely_Surrogate_Key objects 280 | */ 281 | public function get_keys() 282 | { 283 | return $this->_keys; 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /src/classes/upgrades.php: -------------------------------------------------------------------------------- 1 | _main_instance = $object; 25 | } 26 | 27 | 28 | /** 29 | * Check version and run new upgrades if there are any 30 | */ 31 | public function check_and_run_upgrades() 32 | { 33 | // Upgrade to 1.1.1 34 | if (version_compare($this->_main_instance->current_version, '1.1.1', '<')) { 35 | $this->upgrade1_1_1(); 36 | } 37 | } 38 | 39 | /** 40 | * Upgrades to 1.1.1 version 41 | * 42 | * @return void 43 | */ 44 | protected function upgrade1_1_1() 45 | { 46 | // Convert old fastly credentials to new storing type 47 | $data_general = array(); 48 | $data_advanced = array(); 49 | $data_general['fastly_api_hostname'] = get_option('fastly_api_hostname', false); 50 | $data_general['fastly_api_key'] = get_option('fastly_api_key', false); 51 | $data_general['fastly_service_id'] = get_option('fastly_service_id', false); 52 | $data_advanced['fastly_log_purges'] = get_option('fastly_log_purges', false); 53 | 54 | foreach ($data_general as $k => $single) { 55 | if ($single === false || empty($single)) { 56 | unset($data_general[$k]); 57 | } 58 | } 59 | 60 | foreach ($data_advanced as $k => $single) { 61 | if ($single === false || empty($single)) { 62 | unset($data_advanced[$k]); 63 | } 64 | } 65 | 66 | // Update data 67 | update_option('fastly-settings-general', $data_general); 68 | update_option('fastly-settings-advanced', $data_advanced); 69 | 70 | // Update version 71 | update_option("fastly-schema-version", '1.1.1'); 72 | } 73 | 74 | /** 75 | * Manual update of vcl, conditions and settings to 1.1.1 version 76 | * @param bool 77 | * @return bool|array 78 | */ 79 | public function vcl_upgrade_1_1_1($activate) 80 | { 81 | // Update VCL 82 | $vcl_dir = $this->_main_instance->vcl_dir; 83 | $data = array( 84 | 'vcl' => array( 85 | array( 86 | 'vcl_dir' => $vcl_dir, 87 | 'type' => 'recv' 88 | ), 89 | array( 90 | 'vcl_dir' => $vcl_dir, 91 | 'type' => 'deliver', 92 | ), 93 | array( 94 | 'vcl_dir' => $vcl_dir, 95 | 'type' => 'error', 96 | ), 97 | array( 98 | 'vcl_dir' => $vcl_dir, 99 | 'type' => 'fetch', 100 | ) 101 | ), 102 | 'condition' => array( 103 | array( 104 | 'name' => self::WORDPRESS_MODULE_NAME . '_request1', 105 | 'statement' => 'req.http.x-pass', 106 | 'type' => 'REQUEST', 107 | 'priority' => 90 108 | ) 109 | ), 110 | 'setting' => array( 111 | array( 112 | 'name' => self::WORDPRESS_MODULE_NAME . '_setting1', 113 | 'action' => 'pass', 114 | 'request_condition' => 'wordpressplugin_request1' 115 | ) 116 | ) 117 | ); 118 | 119 | $errors = array(); 120 | 121 | $vcl = new Vcl_Handler($data); 122 | if (!$vcl->execute($activate)) { 123 | //Log if enabled 124 | if (Purgely_Settings::get_setting('fastly_debug_mode')) { 125 | foreach ($vcl->get_errors() as $error) { 126 | error_log($error); 127 | } 128 | } 129 | 130 | $errors = array_merge($errors, $vcl->get_errors()); 131 | } 132 | 133 | if (!empty($errors)) { 134 | return $errors; 135 | } 136 | 137 | return true; 138 | } 139 | 140 | /** 141 | * Update of maintenance/error page HTML 142 | * @param string 143 | * @param bool 144 | * @return bool|array 145 | */ 146 | public function maintenance_html_update($html, $activate) 147 | { 148 | // Update HTML VCL snippets 149 | $vcl_dir = $this->_main_instance->vcl_dir; 150 | $data = array( 151 | 'vcl' => array( 152 | array( 153 | 'vcl_dir' => $vcl_dir, 154 | 'subdirectory' => 'error_page', 155 | 'type' => 'deliver', 156 | ), 157 | ), 158 | 'condition' => array( 159 | array( 160 | 'name' => self::WORDPRESS_MODULE_NAME . '_error_page_condition', 161 | 'statement' => 'req.http.ResponseObject == "WORDPRESS_ERROR_PAGE"', 162 | 'type' => 'REQUEST', 163 | 'priority' => 90, 164 | ) 165 | ), 166 | 'response' => array( 167 | array( 168 | 'name' => self::WORDPRESS_MODULE_NAME . '_error_page_response_object', 169 | 'request_condition' => self::WORDPRESS_MODULE_NAME . '_error_page_condition', 170 | 'content' => $html, 171 | 'status' => '503', 172 | 'response' => 'Service Temporarily Unavailable' 173 | ) 174 | ) 175 | ); 176 | 177 | $errors = array(); 178 | 179 | $vcl = new Vcl_Handler($data); 180 | if (!$vcl->execute($activate)) { 181 | //Log if enabled 182 | if (Purgely_Settings::get_setting('fastly_debug_mode')) { 183 | foreach ($vcl->get_errors() as $error) { 184 | error_log($error); 185 | } 186 | } 187 | 188 | $errors = array_merge($errors, $vcl->get_errors()); 189 | } 190 | 191 | if (!empty($errors)) { 192 | return $errors; 193 | } 194 | 195 | return true; 196 | } 197 | 198 | 199 | /** 200 | * Enable image optimization 201 | * @param string 202 | * @param bool 203 | * @return bool|array 204 | */ 205 | public function image_optimization_toggle($activate) 206 | { 207 | // Update HTML VCL snippets 208 | $data = array( 209 | 'condition' => array( 210 | array( 211 | 'name' => self::WORDPRESS_MODULE_NAME . '_image_optimization', 212 | 'statement' => 'req.url.ext ~ "(?i)^(gif|png|jpe?g|webp)$"', 213 | 'type' => 'REQUEST', 214 | 'priority' => 10, 215 | ) 216 | ), 217 | 'header' => array( 218 | array( 219 | 'name' => self::WORDPRESS_MODULE_NAME . '_image_optimization', 220 | 'type' => 'request', 221 | 'action' => 'set', 222 | 'dst' => 'http.x-fastly-imageopto-api', 223 | 'src' => '"fastly"', 224 | 'ignore_if_set' => 0, 225 | 'priority' => "1", 226 | 'request_condition' => self::WORDPRESS_MODULE_NAME . '_image_optimization' 227 | ) 228 | ) 229 | ); 230 | 231 | $errors = array(); 232 | $vcl = new Vcl_Handler(array()); 233 | $io_enabled = $vcl->check_io_active_on_fastly(); 234 | if($io_enabled) { 235 | // Set for deletion 236 | $data = array( 237 | 'condition' => array( 238 | array( 239 | 'name' => self::WORDPRESS_MODULE_NAME . '_image_optimization', 240 | 'statement' => 'req.url.ext ~ "(?i)^(gif|png|jpe?g|webp)$"', 241 | 'type' => 'REQUEST', 242 | 'priority' => 10, 243 | 'delete' => true 244 | ) 245 | ), 246 | 'header' => array( 247 | array( 248 | 'name' => self::WORDPRESS_MODULE_NAME . '_image_optimization', 249 | 'type' => 'request', 250 | 'action' => 'set', 251 | 'dst' => 'http.x-fastly-imageopto-api', 252 | 'src' => '"fastly"', 253 | 'ignore_if_set' => 0, 254 | 'priority' => "1", 255 | 'request_condition' => self::WORDPRESS_MODULE_NAME . '_image_optimization', 256 | 'delete' => true 257 | ) 258 | ) 259 | ); 260 | } 261 | 262 | $vcl = new Vcl_Handler($data); 263 | 264 | if (!$vcl->execute($activate)) { 265 | //Log if enabled 266 | if (Purgely_Settings::get_setting('fastly_debug_mode')) { 267 | foreach ($vcl->get_errors() as $error) { 268 | error_log($error); 269 | } 270 | } 271 | 272 | $errors = array_merge($errors, $vcl->get_errors()); 273 | } 274 | 275 | if (!empty($errors)) { 276 | return $errors; 277 | } 278 | 279 | return true; 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/config.php: -------------------------------------------------------------------------------- 1 | $v) { 106 | if(!is_string($v)) { 107 | $key[$k] = ''; 108 | } else { 109 | $key[$k] = preg_replace('/[^0-9]/', '', $v); 110 | } 111 | } 112 | } 113 | return $key; 114 | } 115 | 116 | /** 117 | * Callback function for sanitizing a checkbox setting. 118 | * 119 | * @param mixed $value Unsanitized setting. 120 | * @return bool Whether or not value is valid. 121 | */ 122 | function purgely_sanitize_checkbox($value) 123 | { 124 | return (in_array($value, array('1', 1, 'true', true), true)); 125 | } 126 | 127 | /** 128 | * Callback function for sanitizing a array setting. 129 | * 130 | * @param mixed $value Unsanitized setting. 131 | * @return array Whether or not value is valid. 132 | */ 133 | function purgely_sanitize_pixel_ratios($value) 134 | { 135 | $result = array_intersect($value, Purgely_Settings::POSSIBLE_PIXEL_RATIOS); 136 | return $result; 137 | } 138 | 139 | /** 140 | * Function for testing Fastly API connection 141 | * @param $hostname 142 | * @param $service_id 143 | * @param $api_key 144 | * @return array 145 | */ 146 | function test_fastly_api_connection($hostname, $service_id, $api_key) 147 | { 148 | 149 | if (empty($hostname) || empty($service_id) || empty($api_key)) { 150 | return array('status' => false, 'message' => __('Please enter credentials first', 'purgely')); 151 | } 152 | 153 | $url = trailingslashit($hostname) . 'service/' . $service_id; 154 | $headers = array( 155 | 'Fastly-Key' => $api_key, 156 | 'Accept' => 'application/json' 157 | ); 158 | 159 | $purgely_instance = Purgely::instance(); 160 | if(empty($purgely_instance->connection_status)) { 161 | try { 162 | $response = Requests::get($url, $headers); 163 | if ($response->success) { 164 | $response_body = json_decode($response->body); 165 | $service_name = $response_body->name; 166 | $purgely_instance->service_name = $service_name; 167 | /* translators: %s: Name of the Fastly service for which we have tested connection */ 168 | $message = sprintf(__('Connection Successful on service *%s*', 'purgely'), $service_name); 169 | } else { 170 | handle_logging($response); 171 | $message = json_decode($response->body); 172 | $message = $message->msg; 173 | } 174 | $purgely_instance->connection_status = array('status' => $response->success, 'message' => $message); 175 | } catch (Exception $e) { 176 | $purgely_instance->connection_status = array('status' => false, 'message' => $e->getMessage()); 177 | } 178 | } 179 | return $purgely_instance->connection_status; 180 | } 181 | 182 | /** 183 | * Sends message to slack via webhooks 184 | * @param $message 185 | */ 186 | function send_web_hook($message) 187 | { 188 | 189 | if (!Purgely_Settings::get_setting('webhooks_activate')) { 190 | return; 191 | } 192 | 193 | $webhook_url = Purgely_Settings::get_setting('webhooks_url_endpoint'); 194 | $username = Purgely_Settings::get_setting('webhooks_username'); 195 | $channel = Purgely_Settings::get_setting('webhooks_channel'); 196 | 197 | $headers = array('Content-type: application/json'); 198 | $data = wp_json_encode( 199 | array( 200 | 'text' => $message, 201 | 'username' => $username, 202 | 'channel' => '#' . $channel, 203 | 'icon_emoji' => ':airplane:' 204 | ) 205 | ); 206 | 207 | try { 208 | $response = Requests::request($webhook_url, $headers, $data, Requests::POST); 209 | if (!$response->success) { 210 | if (Purgely_Settings::get_setting('fastly_debug_mode')) { 211 | error_log("Webhooks request failed, error: " . json_decode($response->body)); 212 | } 213 | } 214 | } catch (Exception $e) { 215 | error_log($e->getMessage()); 216 | } 217 | } 218 | 219 | /** 220 | * Test slack webhooks connection in admin 221 | * @return array 222 | */ 223 | function test_web_hook() 224 | { 225 | 226 | $webhook_url = Purgely_Settings::get_setting('webhooks_url_endpoint'); 227 | $username = Purgely_Settings::get_setting('webhooks_username'); 228 | $channel = Purgely_Settings::get_setting('webhooks_channel'); 229 | 230 | $headers = array('Content-type: application/json'); 231 | $data = wp_json_encode( 232 | array( 233 | 'text' => 'Webhook connection successful!', 234 | 'username' => $username, 235 | 'channel' => '#' . $channel, 236 | 'icon_emoji' => ':airplane:' 237 | ) 238 | ); 239 | 240 | try { 241 | $response = Requests::request($webhook_url, $headers, $data, Requests::POST); 242 | $message = $response->success ? __('Connection Successful!', 'purgely') : $response->body; 243 | 244 | if (Purgely_Settings::get_setting('fastly_debug_mode')) { 245 | error_log('Webhooks - test connection: ' . $response->body); 246 | } 247 | 248 | return array('status' => $response->success, 'message' => $message); 249 | } catch (Exception $e) { 250 | if (Purgely_Settings::get_setting('fastly_debug_mode')) { 251 | error_log('Webhooks - test connection: ' . $e->getMessage()); 252 | } 253 | return array('status' => false, 'message' => $e->getMessage()); 254 | } 255 | } 256 | 257 | /** 258 | * Do logging where needed 259 | * @param \WpOrg\Requests\Response $response 260 | * @param $message 261 | */ 262 | function handle_logging($response, $message = false) 263 | { 264 | $debug_mode = Purgely_Settings::get_setting('fastly_debug_mode'); 265 | $log_purges = Purgely_Settings::get_setting('fastly_log_purges'); 266 | $log_slack = Purgely_Settings::get_setting('webhooks_activate'); 267 | 268 | if ($debug_mode || $log_purges || $log_slack) { 269 | $msg = get_message_by_status_code($response->status_code); 270 | if ($message) { 271 | $msg = $msg . ' - ' . $message; 272 | } 273 | } else { 274 | return; 275 | } 276 | 277 | // Log purges in logs, don't log twice 278 | if ($log_purges || $debug_mode) { 279 | if ($log_purges) { 280 | error_log($msg); 281 | } elseif ($debug_mode) { 282 | if (!$response->success) { 283 | error_log($msg); 284 | } 285 | } 286 | } 287 | 288 | // Log message in Slack via Webhooks 289 | if ($log_slack) { 290 | send_web_hook($msg); 291 | } 292 | } 293 | 294 | /** 295 | * Returns response message based on status code 296 | * @param $code 297 | * @return string 298 | */ 299 | function get_message_by_status_code($code) 300 | { 301 | switch ($code) { 302 | case 200: 303 | $msg = '200 - OK'; 304 | break; 305 | case 203: 306 | $msg = '203 - Non-Authoritative Information'; 307 | break; 308 | case 300: 309 | $msg = '300 - Multiple Choices'; 310 | break; 311 | case 301: 312 | $msg = '301 - Moved Permanently'; 313 | break; 314 | case 302: 315 | $msg = '302 - Moved Temporarily'; 316 | break; 317 | case 401: 318 | $msg = '401 - Unauthorized'; 319 | break; 320 | case 404: 321 | $msg = '404 - Not Found'; 322 | break; 323 | case 410: 324 | $msg = '410 - Gone'; 325 | break; 326 | default: 327 | $msg = __('Error occurred, turn on debugging options and check your logs.', 'purgely'); 328 | break; 329 | } 330 | return $msg; 331 | } 332 | 333 | /** 334 | * Determine if the first arg is a URL. 335 | * 336 | * @param string $thing The first argument passed to the function. 337 | * @return bool True if the thing is a URL, false if not. 338 | */ 339 | function is_url($thing) 340 | { 341 | return 0 === strpos($thing, 'http') && esc_url_raw($thing) === $thing; 342 | } 343 | 344 | 345 | function get_maintenance_html() 346 | { 347 | $handler = new Vcl_Handler(array()); 348 | $name = Upgrades::WORDPRESS_MODULE_NAME . '_error_page_response_object'; 349 | $response_object = $handler->get_response_object_data($name); 350 | 351 | if($response_object && !empty($response_object->body)) { 352 | $data = json_decode($response_object->body); 353 | $html = !empty($data->content) ? $data->content : false; 354 | if($html) { 355 | return htmlentities($html); 356 | } 357 | } 358 | 359 | return false; 360 | } 361 | -------------------------------------------------------------------------------- /src/wp-cli.php: -------------------------------------------------------------------------------- 1 | locate_all(); 68 | // Issue purge request 69 | foreach ($thing as $tg) { 70 | $result = $purgely->purge($type, $tg); 71 | } 72 | } else { 73 | $result = $purgely->purge($type, $thing); 74 | } 75 | 76 | if ($type === Purgely_Purge::ALL) { 77 | $message = 'all'; 78 | } elseif ($type === Purgely_Purge::KEY_COLLECTION) { 79 | $message = 'ID:' . $args[1]; 80 | } elseif ($type === Purgely_Purge::URL) { 81 | $message = esc_url($thing); 82 | } else { 83 | $message = $thing; 84 | } 85 | 86 | if ($result) { 87 | /* translators: %s: information which parameter was purged - all sites, selected post or specific url */ 88 | WP_CLI::success(sprintf(__('Successfully purged - %s', 'purgely'), $message)); 89 | } else { 90 | /* translators: %s: information for which parameter purge was attempted - all sites, selected post or specific url */ 91 | WP_CLI::error(sprintf(__('Purge failed - %s - (enable and check logging for more information)'), $message)); 92 | } 93 | } 94 | 95 | /** 96 | * Sets wanted configuration or lists available configuration options 97 | * @param $args 98 | */ 99 | public function configset($args) 100 | { 101 | $config_section = !empty($args[0]) ? $args[0] : false; 102 | $config_option = !empty($args[1]) ? $args[1] : false; 103 | $config_value = isset($args[2]) ? $args[2] : false; 104 | 105 | if($config_section === false || $config_option === false || $config_value === false) { 106 | $message = $this->_color('red', 'Missing section, option or value'); 107 | $msg = $this->_color('green', 'wp fastly configset {section} {option} {value}'); 108 | $message .= "\nUsage: {$msg}"; 109 | $msg = $this->_color('gold', 'general, advanced, webhooks'); 110 | $message .= "\nSections: $msg"; 111 | $msg = $this->_color('green', 'wp fastly configlist {section}'); 112 | $message .= "\nTo list options from certain section run: $msg"; 113 | $msg = $this->_color('green', '{true|false}'); 114 | $message .= "\nFor yes/no configuration options use : $msg"; 115 | WP_CLI::error($message, 'purgely'); 116 | return; 117 | } 118 | 119 | // Sanitize 120 | $registered_settings = Purgely_Settings::get_registered_settings(); 121 | if(array_key_exists($config_option, $registered_settings)) { 122 | $callback = $registered_settings[$config_option]['sanitize_callback']; 123 | // False will return false when checking validity 124 | if(in_array($config_value, array('1', 1, 'true'), true)) { 125 | $config_value = 'true'; 126 | } 127 | 128 | if(in_array($config_value, array('0', 0, 'false'), true)) { 129 | $config_value = 'false'; 130 | } else { 131 | $config_value = call_user_func($callback, $config_value); 132 | // TODO standardization issue(visual) 133 | if($config_value === true) { 134 | $config_value = 'true'; 135 | } 136 | } 137 | } 138 | 139 | // List configuration options and determine if option and section exists 140 | if(array_key_exists($config_section, Purgely_Settings::$lists)) { 141 | $section = Purgely_Settings::$lists[$config_section]; 142 | $settings_list = Purgely_Settings::get_database_section_settings($section); 143 | if(array_key_exists($config_option, $settings_list)) { 144 | // Update value 145 | if($config_value === false){ 146 | WP_CLI::error(sprintf(__('Invalid configuration option value'))); 147 | return; 148 | } 149 | $settings_list[$config_option] = $config_value; 150 | } else { 151 | WP_CLI::error(sprintf(__('Invalid configuration option'))); 152 | return; 153 | } 154 | } else { 155 | WP_CLI::error(sprintf(__('Invalid configuration section'))); 156 | return; 157 | } 158 | 159 | if(update_option($section, $settings_list)) { 160 | $config_option = $this->_color('gold', $config_option); 161 | $config_value = $this->_color('red', $config_value); 162 | /* translators: %1\$s: option which is updated, %2\$s: value to which option is set */ 163 | WP_CLI::success(sprintf(__( 164 | "Successfully saved %1\$s option in configuration with value %2\$s", 'purgely'), $config_option, $config_value) 165 | ); 166 | } else { 167 | WP_CLI::error(__('Failed to save option. Please update value.', 'purgely')); 168 | } 169 | } 170 | 171 | /** 172 | * Lists configuration by sections 173 | * @param $args 174 | */ 175 | public function configlist($args) 176 | { 177 | $conf_list = !empty($args[0]) ? $args[0] : false; 178 | 179 | if(!$conf_list) { 180 | $msg = $this->_color('gold', 'wp fastly configlist {general|advanced|webhooks}'); 181 | 182 | /* translators: %s: configlist message */ 183 | WP_CLI::error(sprintf(__("Usage: %s", 'purgely'), $msg)); 184 | return; 185 | } 186 | 187 | // List configuration options 188 | if(array_key_exists($conf_list, Purgely_Settings::$lists)) { 189 | $section = Purgely_Settings::$lists[$conf_list]; 190 | $settings_list = Purgely_Settings::get_database_section_settings($section); 191 | foreach($settings_list as $key => $value) { 192 | 193 | // Output 1 and 0 as true/false TODO issue with standardization of admin and cli saving (only visual) 194 | if(in_array($value, array('1', 1, 'true', true), true)) { 195 | $value = 'true'; 196 | } elseif (in_array($value, array('0', 0, 'false', false), true)){ 197 | $value = 'false'; 198 | } 199 | 200 | $key = $this->_color('gold', $key); 201 | $value = $this->_color('red', $value); 202 | /* translators: %1\$s: name of current setting, %2\$s: value of a setting */ 203 | WP_CLI::line( sprintf( __( "Setting %1\$s = %2\$s", 'purgely' ), $key, $value ) ); 204 | } 205 | return; 206 | } 207 | $msg = $this->_color('gold', 'wp fastly configlist {general|advanced|webhooks}'); 208 | /* translators: %s: configlist message */ 209 | WP_CLI::error(sprintf(__("Invalid config list selected. Usage: %s", 'purgely'), $msg)); 210 | return; 211 | } 212 | 213 | /** 214 | * Set color on string 215 | * @param $color 216 | * @param $string 217 | * @return string 218 | */ 219 | protected function _color($color, $string) 220 | { 221 | if(!$color || $string === false) { 222 | return $string; 223 | } 224 | switch ($color){ 225 | case ('red'): 226 | $string = "\e[31m" . $string . "\e[0m"; 227 | break; 228 | case ('gold'): 229 | $string = "\e[33m" . $string . "\e[0m"; 230 | break; 231 | case ('green'): 232 | $string = "\e[32m" . $string . "\e[0m"; 233 | break; 234 | default: 235 | break; 236 | } 237 | return $string; 238 | } 239 | } 240 | endif; 241 | 242 | WP_CLI::add_command('fastly', 'Purgely_Command'); 243 | -------------------------------------------------------------------------------- /src/wp-purges.php: -------------------------------------------------------------------------------- 1 | _purge_actions() as $action) { 37 | add_action($action, array($this, 'purge'), 10, 1); 38 | } 39 | } 40 | 41 | /** 42 | * Callback for post changing events to purge keys. 43 | * 44 | * @param int $post_id Post ID. 45 | * @return void 46 | */ 47 | public function purge($post_id) 48 | { 49 | if (in_array($this->getPostId($post_id), $this->postIdsProcessed)) { 50 | return; 51 | } 52 | 53 | array_push($this->postIdsProcessed, $this->getPostId($post_id)); 54 | if (!in_array(get_post_status($post_id), array('publish', 'trash', 'draft'))) { 55 | return; 56 | } 57 | 58 | // Check credentials 59 | $fastly_hostname = Purgely_Settings::get_setting('fastly_api_hostname'); 60 | $fastly_service_id = Purgely_Settings::get_setting('fastly_service_id'); 61 | $fastly_api_key = Purgely_Settings::get_setting('fastly_api_key'); 62 | $test = test_fastly_api_connection($fastly_hostname, $fastly_service_id, $fastly_api_key); 63 | if (!$test['status']) { 64 | return; 65 | } 66 | 67 | $related_collection_object = new Purgely_Related_Surrogate_Keys($post_id); 68 | $collections = $related_collection_object->locate_all(); 69 | 70 | $purgely = new Purgely_Purge(); 71 | foreach($collections as $collection) 72 | { 73 | $purgely->purge('key-collection', $collection, array()); 74 | } 75 | } 76 | 77 | private function getPostId($post) 78 | { 79 | if ($post instanceof WP_Post) { 80 | return $post->ID; 81 | } 82 | return $post; 83 | } 84 | 85 | /** 86 | * A list of actions to purge URLs. 87 | * 88 | * @return array List of actions. 89 | */ 90 | private function _purge_actions() 91 | { 92 | return array( 93 | 'save_post', 94 | 'deleted_post', 95 | 'trashed_post', 96 | 'delete_attachment', 97 | 'future_to_publish' 98 | ); 99 | } 100 | } 101 | 102 | /** 103 | * Instantiate or return the one Purgely_Purges instance. 104 | * 105 | * @return Purgely_Purges 106 | */ 107 | function get_purgely_purges_instance() 108 | { 109 | return Purgely_Purges::instance(); 110 | } 111 | 112 | get_purgely_purges_instance(); 113 | -------------------------------------------------------------------------------- /static/logo_white.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastly/WordPress-Plugin/84d7fd8d4638d7c814125655ac77fc07a3923e12/static/logo_white.gif -------------------------------------------------------------------------------- /vcl_snippets/deliver.vcl: -------------------------------------------------------------------------------- 1 | # Add an easy way to see whether custom Fastly VCL has been uploaded 2 | if ( req.http.Fastly-Debug ) { 3 | set resp.http.Fastly-WordPress-VCL-Uploaded = "1.2.17"; 4 | } else { 5 | remove resp.http.Fastly-WordPress-VCL-Uploaded; 6 | } 7 | -------------------------------------------------------------------------------- /vcl_snippets/error.vcl: -------------------------------------------------------------------------------- 1 | /* handle 503s */ 2 | if (obj.status >= 500 && obj.status < 600) { 3 | 4 | /* deliver stale object if it is available */ 5 | if (stale.exists) { 6 | return(deliver_stale); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /vcl_snippets/error_page/deliver.vcl: -------------------------------------------------------------------------------- 1 | # If we are about to serve a 5xx we need to restart then in vcl_recv error out to 2 | # get the holding page 3 | if ( resp.status >= 500 && resp.status < 600 && !req.http.ResponseObject ) { 4 | set req.http.ResponseObject = "WORDPRESS_ERROR_PAGE"; 5 | restart; 6 | } 7 | -------------------------------------------------------------------------------- /vcl_snippets/fetch.vcl: -------------------------------------------------------------------------------- 1 | # just in case the request snippet for x-pass is not set we pass here 2 | if ( req.http.x-pass ) { 3 | return(pass); 4 | } 5 | 6 | /* handle 5XX (or any other unwanted status code) */ 7 | if (beresp.status >= 500 && beresp.status < 600) { 8 | 9 | /* deliver stale if the object is available */ 10 | if (stale.exists) { 11 | return(deliver_stale); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /vcl_snippets/recv.vcl: -------------------------------------------------------------------------------- 1 | if (fastly.ff.visits_this_service > 0) { 2 | # Needed for proper handling of stale while revalidated when shielding is involved 3 | set req.max_stale_while_revalidate = 0s; 4 | } 5 | 6 | ## always cache these images & static assets 7 | if (req.request == "GET" && req.url.ext ~ "(?i)(css|js|gif|jpg|jpeg|bmp|png|ico|img|tga|webp|wmf)") { 8 | remove req.http.cookie; 9 | } else if (req.request == "GET" && req.url.path ~ "(xmlrpc\.php|wlmanifest\.xml)") { 10 | remove req.http.cookie; 11 | } 12 | 13 | ### do not cache these files: 14 | ## never cache the admin pages, or the server-status page 15 | if (req.request == "GET" && (req.url.path ~ "(wp-admin|bb-admin|server-status)")) { 16 | set req.http.X-Pass = "1"; 17 | } else if (req.http.X-Requested-With == "XMLHttpRequest" && req.url !~ "recent_reviews") { 18 | # Do not cache ajax requests except for recent reviews 19 | set req.http.X-Pass = "1"; 20 | } else if (req.url.qs ~ "nocache" || 21 | req.url.path ~ "(control\.php|wp-comments-post\.php|wp-login\.php|bb-login\.php|bb-reset-password\.php|register\.php)") { 22 | set req.http.X-Pass = "1"; 23 | # Woocommerce sets cart as cacheable. Need to make sure we never cache it 24 | } else if (req.url.path ~ "/cart/?$" ) { 25 | set req.http.X-Pass = "1"; 26 | } 27 | 28 | # Remove wordpress_test_cookie except on non-cacheable paths 29 | if (!req.http.X-Pass && req.http.Cookie:wordpress_test_cookie) { 30 | remove req.http.Cookie:wordpress_test_cookie; 31 | } 32 | 33 | if ( req.http.Cookie ) { 34 | ### do not cache authenticated sessions 35 | if (req.http.Cookie ~ "(wordpress_|PHPSESSID)") { 36 | set req.http.X-Pass = "1"; 37 | } else if (!req.http.X-Pass) { 38 | # Cleans up cookies by removing everything except vendor_region, PHPSESSID and themetype2 39 | set req.http.Cookie = ";" req.http.Cookie; 40 | set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";"); 41 | set req.http.Cookie = regsuball(req.http.Cookie, ";(vendor_region|PHPSESSID|themetype2|.*woocommerce.*)=", "; \1="); 42 | set req.http.Cookie = regsuball(req.http.Cookie, ";[^ ][^;]*", ""); 43 | set req.http.Cookie = regsuball(req.http.Cookie, "^[; ]+|[; ]+$", ""); 44 | 45 | if (req.http.Cookie == "") { 46 | remove req.http.Cookie; 47 | } 48 | } 49 | } 50 | --------------------------------------------------------------------------------