wp-plugin-cloudflare-cache/assets/js/worker_template.js

937 lines
24 KiB
JavaScript

// Worker version: 2.8.0
// Default cookie prefixes for cache bypassing
const DEFAULT_BYPASS_COOKIES = [
'wordpress_logged_in_',
'comment_',
'woocommerce_',
'wordpressuser_',
'wordpresspass_',
'wordpress_sec_',
'yith_wcwl_products',
'edd_items_in_cart',
'it_exchange_session_',
'comment_author',
'dshack_level',
'auth',
'noaffiliate_',
'mp_session',
'mp_globalcart_',
'xf_'
]
// Third party query parameter that we need to ignore in a URL
const THIRD_PARTY_QUERY_PARAMETERS = [
'Browser',
'C',
'GCCON',
'MCMP',
'MarketPlace',
'PD',
'Refresh',
'Sens',
'ServiceVersion',
'Source',
'Topic',
'__WB_REVISION__',
'__cf_chl_jschl_tk__',
'__d',
'__hsfp',
'__hssc',
'__hstc',
'__s',
'_branch_match_id',
'_bta_c',
'_bta_tid',
'_com',
'_escaped_fragment_',
'_ga',
'_ga-ft',
'_gl',
'_hsmi',
'_ke',
'_kx',
'_paged',
'_sm_byp',
'_sp',
'_szp',
'3x',
'a',
'a_k',
'ac',
'acpage',
'action-box',
'action_object_map',
'action_ref_map',
'action_type_map',
'activecampaign_id',
'ad',
'ad_frame_full',
'ad_frame_root',
'ad_name',
'adclida',
'adid',
'adlt',
'adsafe_ip',
'adset_name',
'advid',
'aff_sub2',
'afftrack',
'afterload',
'ak_action',
'alt_id',
'am',
'amazingmurphybeds',
'amp;',
'amp;amp',
'amp;amp;amp',
'amp;amp;amp;amp',
'amp;utm_campaign',
'amp;utm_medium',
'amp;utm_source',
'ampStoryAutoAnalyticsLinker',
'ampstoryautoanalyticslinke',
'an',
'ap',
'ap_id',
'apif',
'apipage',
'as_occt',
'as_q',
'as_qdr',
'askid',
'atFileReset',
'atfilereset',
'aucid',
'auct',
'audience',
'author',
'awt_a',
'awt_l',
'awt_m',
'b2w',
'back',
'bannerID',
'blackhole',
'blockedAdTracking',
'blog-reader-used',
'blogger',
'br',
'bsft_aaid',
'bsft_clkid',
'bsft_eid',
'bsft_ek',
'bsft_lx',
'bsft_mid',
'bsft_mime_type',
'bsft_tv',
'bsft_uid',
'bvMethod',
'bvTime',
'bvVersion',
'bvb64',
'bvb64resp',
'bvplugname',
'bvprms',
'bvprmsmac',
'bvreqmerge',
'cacheburst',
'campaign',
'campaign_id',
'campaign_name',
'campid',
'catablog-gallery',
'channel',
'checksum',
'ck_subscriber_id',
'cmplz_region_redirect',
'cmpnid',
'cn-reloaded',
'code',
'comment',
'content_ad_widget',
'cost',
'cr',
'crl8_id',
'crlt.pid',
'crlt_pid',
'crrelr',
'crtvid',
'ct',
'cuid',
'daksldlkdsadas',
'dcc',
'dfp',
'dm_i',
'domain',
'dosubmit',
'dsp_caid',
'dsp_crid',
'dsp_insertion_order_id',
'dsp_pub_id',
'dsp_tracker_token',
'dt',
'dur',
'durs',
'e',
'ee',
'ef_id',
'el',
'env',
'erprint',
'et_blog',
'exch',
'externalid',
'fb_action_ids',
'fb_action_types',
'fb_ad',
'fb_source',
'fbclid',
'fbzunique',
'fg-aqp',
'fireglass_rsn',
'fo',
'fp_sid',
'fpa',
'fref',
'fs',
'furl',
'fwp_lunch_restrictions',
'ga_action',
'gclid',
'gclsrc',
'gdffi',
'gdfms',
'gdftrk',
'gf_page',
'gidzl',
'goal',
'gooal',
'gpu',
'gtVersion',
'haibwc',
'hash',
'hc_location',
'hemail',
'hid',
'highlight',
'hl',
'home',
'hsa_acc',
'hsa_ad',
'hsa_cam',
'hsa_grp',
'hsa_kw',
'hsa_mt',
'hsa_net',
'hsa_src',
'hsa_tgt',
'hsa_ver',
'ias_campId',
'ias_chanId',
'ias_dealId',
'ias_dspId',
'ias_impId',
'ias_placementId',
'ias_pubId',
'ical',
'ict',
'ie',
'igshid',
'im',
'ipl',
'jw_start',
'jwsource',
'k',
'key1',
'key2',
'klaviyo',
'ksconf',
'ksref',
'l',
'label',
'lang',
'ldtag_cl',
'level1',
'level2',
'level3',
'level4',
'limit',
'lng',
'load_all_comments',
'lt',
'ltclid',
'ltd',
'lucky',
'm',
'm?sales_kw',
'matomo_campaign',
'matomo_cid',
'matomo_content',
'matomo_group',
'matomo_keyword',
'matomo_medium',
'matomo_placement',
'matomo_source',
'max-results',
'mc_cid',
'mc_eid',
'mdrv',
'mediaserver',
'memset',
'mibextid',
'mkcid',
'mkevt',
'mkrid',
'mkwid',
'ml_subscriber',
'ml_subscriber_hash',
'mobileOn',
'mode',
'month',
'msID',
'msclkid',
'msg',
'mtm_campaign',
'mtm_cid',
'mtm_content',
'mtm_group',
'mtm_keyword',
'mtm_medium',
'mtm_placement',
'mtm_source',
'murphybedstoday',
'mwprid',
'n',
'native_client',
'navua',
'nb',
'nb_klid',
'o',
'okijoouuqnqq',
'org',
'pa_service_worker',
'partnumber',
'pcmtid',
'pcode',
'pcrid',
'pfstyle',
'phrase',
'pid',
'piwik_campaign',
'piwik_keyword',
'piwik_kwd',
'pk_campaign',
'pk_keyword',
'pk_kwd',
'placement',
'plat',
'platform',
'playsinline',
'pp',
'pr',
'prid',
'print',
'q',
'q1',
'qsrc',
'r',
'rd',
'rdt_cid',
'redig',
'redir',
'ref',
'reftok',
'relatedposts_hit',
'relatedposts_origin',
'relatedposts_position',
'remodel',
'replytocom',
'reverse-paginate',
'rid',
'rnd',
'rndnum',
'robots_txt',
'rq',
'rsd',
's_kwcid',
'sa',
'safe',
'said',
'sales_cat',
'sales_kw',
'sb_referer_host',
'scrape',
'script',
'scrlybrkr',
'search',
'sellid',
'sersafe',
'sfn_data',
'sfn_trk',
'sfns',
'sfw',
'sha1',
'share',
'shared',
'showcomment',
'si',
'sid',
'sid1',
'sid2',
'sidewalkShow',
'sig',
'site',
'site_id',
'siteid',
'slicer1',
'slicer2',
'source',
'spref',
'spvb',
'sra',
'src',
'srk',
'srp',
'ssp_iabi',
'ssts',
'stylishmurphybeds',
'subId1 ',
'subId2 ',
'subId3',
'subid',
'swcfpc',
'tail',
'teaser',
'test',
'timezone',
'toWww',
'triplesource',
'trk_contact',
'trk_module',
'trk_msg',
'trk_sid',
'tsig',
'turl',
'u',
'up_auto_log',
'upage',
'updated-max',
'uptime',
'us_privacy',
'usegapi',
'usqp',
'utm',
'utm_campa',
'utm_campaign',
'utm_content',
'utm_expid',
'utm_id',
'utm_medium',
'utm_reader',
'utm_referrer',
'utm_source',
'utm_sq',
'utm_ter',
'utm_term',
'v',
'vc',
'vf',
'vgo_ee',
'vp',
'vrw',
'vz',
'wbraid',
'webdriver',
'wing',
'wpdParentID',
'wpmp_switcher',
'wref',
'wswy',
'wtime',
'x',
'zMoatImpID',
'zarsrc',
'zeffdn'
]
// List of Static File Extensions for which we don't need to run the whole logic
// Just fetch them and send the response
const STATIC_FILE_EXTENSIONS = [
'.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', '.avif', '.tiff', '.ico', '.3gp', '.wmv', '.avi', '.asf', '.asx', '.mpg', '.mpeg', '.webm', '.ogg', '.ogv', '.mp4', '.mkv', '.pls', '.mp3', '.mid', '.wav', '.swf', '.flv', '.exe', '.zip', '.tar', '.rar', '.gz', '.tgz', '.bz2', '.uha', '.7z', '.doc', '.docx', '.pdf', '.iso', '.test', '.bin', '.js', '.json', '.css', '.eot', '.ttf', '.woff', '.woff2', '.webmanifest'
]
/**
* Function to check if the response status code is within the range
* of 3XX, 4XX, 5XX and if so, then return TRUE else FALSE
*
* @param {Response} response - The origin server response
* @return {Boolean} has_unusual_response_code - If the response has a status code is
* within the defined list then return TRUE else FALSE
*/
function has_unusual_origin_server_response_code(response) {
const responseStatusCode = String( response?.status )
if( responseStatusCode.startsWith( '3' ) || responseStatusCode.startsWith( '4' ) || responseStatusCode.startsWith( '5' ) ) {
response.headers?.set('x-wp-cf-super-cache-worker-origin-response', responseStatusCode)
return true
} else {
return false
}
}
/**
* Function to normalize the URL by removing promotional query parameters from the URL and cache the original URL
* @param {Object} event - Event Object
* @return {URL} reqURL - Request URL without promotional query strings
*/
function url_normalize(event) {
try {
// Fetch the Request URL from the event
// Parse the URL for better handling
const reqURL = new URL(event?.request?.url)
// Loop through the promo queries (THIRD_PARTY_QUERY_PARAMETERS) and see if we have any of these queries present in the URL, if so remove them
for ( let i = 0; i < THIRD_PARTY_QUERY_PARAMETERS.length; i++ ) {
// Create the REGEX to text the URL with our desired parameters
const promoUrlQuery = new RegExp( '(&?)(' + THIRD_PARTY_QUERY_PARAMETERS[i] + '=\\S+)', 'g' )
// Check if the reqURL.search has these search query parameters
if(promoUrlQuery.test( reqURL.search )) {
// The URL has promo query parameters that we need to remove
const urlSearchParams = reqURL.searchParams
urlSearchParams.delete( THIRD_PARTY_QUERY_PARAMETERS[i] )
}
}
return reqURL
} catch (err) {
return {
error: true,
errorMessage: `URL Handling Error: ${err.message}`,
errorStatusCode: 400
}
}
}
/**
* Function to check if the current request should be BYPASSed or Cached based on exclusion cookies
* entered by the user in the plugin settings
* @param {String} cookieHeader - The cookie header of the current request
* @param {Array} cookies_list - List of cookies which should not be cached
* @return {Boolean} blackListedCookieExists - If blacklisted cookie exists in the current request
*/
function are_blacklisted_cookies(cookieHeader, cookies_list) {
let blackListedCookieExists = false
// Make sure both cookieHeader & cookies_list are defined & the length of both cookieHeader & cookies_list > 0
if (
cookieHeader?.length > 0 &&
cookies_list?.length > 0
) {
// Split the received request cookie header by semicolon to an Array
const cookies = cookieHeader.split(';')
// Loop through the cookies in the request header and check if there is any cookie present there
// which is also mentioned in our bypassed cookies array
// if there is then set blackListedCookieExists as true and break out of the loops
for ( let i = 0; i < cookies.length; i++ ) {
for ( let j = 0; j < cookies_list.length; j++ ) {
if (cookies[i].trim().includes(cookies_list[j].trim())) {
blackListedCookieExists = true
// Found item. Break out from the loop
break
}
}
// Check if blackListedCookieExists is true then break out of this loop. Else continue the loop
if( blackListedCookieExists ) {
break
}
}
}
return blackListedCookieExists // value -> TRUE | FALSE
}
/**
* Function to add extra response headers for BYPASSed Requests
* @param {Response} res - The response object
* @param {String} reason - The string that hold the bypass reason
*/
function add_bypass_custom_headers(res, reason) {
if (res && (reason?.length > 0)) {
// BYPASS the request and add our custom headers
res?.headers?.set('x-wp-cf-super-cache-worker-status', 'bypass')
res?.headers?.set('x-wp-cf-super-cache-worker-bypass-reason', reason)
res?.headers?.set('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
}
}
/**
* The function that handles the Request
* @param {Object} event - Received Event object
* @return {Response} response - Response object that is being returned to the user
*/
async function handleRequest(event) {
const request = event?.request
const requestURL = url_normalize(event)
// Check if we have received any error in the url_normalize() call, if so return that error message
if( requestURL?.error ) {
return new Response(
requestURL.errorMessage,
{ status: requestURL.errorStatusCode, statusText: requestURL.errorMessage }
)
}
let response = false
let bypassCache = false
const bypassReason = {
'req_method': false,
'admin_req': false,
'file_path_ext': false,
'page_excluded': false,
'file_excluded': false,
'cookie': false
}
let bypassReasonDetails = ''
const cookieHeader = request?.headers?.get('cookie')
const reqDetails = {
'contentTypeHTML': false
}
// ---------------------------------------------------------
// Check - If the request is for an static file,
// then no need to go further, just fetch the file and return
// ---------------------------------------------------------
const requestPath = requestURL?.pathname
let isStaticFile = false
// Loop through the STATIC_FILE_EXTENSIONS and check if the request path has any of the extensions
for ( let i = 0; i < STATIC_FILE_EXTENSIONS.length; i++ ) {
if( requestPath.endsWith( STATIC_FILE_EXTENSIONS[i] ) ) {
// Set isStaticFile to TRUE and break out of the loop
isStaticFile = true
// Found item. Break out from the loop
break
}
}
if( isStaticFile ) {
let staticFileResponse
try {
staticFileResponse = await fetch(request)
} catch (err) {
return new Response(
`Error: ${err.message}`,
{ status: 500, statusText: "Unable to fetch the static file from the origin server" }
)
}
return new Response(staticFileResponse?.body, staticFileResponse)
}
// ---------------------------------------------------------
// Check - Bypass Request ? - Only Based on Request Headers
// ---------------------------------------------------------
// 1. BYPASS any requests whose request method is not GET or HEAD
const allowedReqMethods = ['GET', 'HEAD']
if (!bypassCache && request) {
if (!allowedReqMethods.includes(request?.method)) {
bypassCache = true
bypassReason.req_method = true
bypassReasonDetails = `Caching not possible for req method ${request.method}`
}
}
// 2. BYPASS the cache for WP Admin HTML Requests & Any File That has /wp-admin/ in it & API endpoints
// Get the Accept header of the request being received by the CF Worker
const accept = request?.headers?.get('Accept')
if (!bypassCache && accept) {
// List of path regex that we will BYPASS caching
// Path includes - WP Admin Paths, WP REST API, WooCommerce API, EDD API Endpoints
const bypass_admin_path = new RegExp(/(\/(wp-admin)(\/?))/g)
const bypass_cache_paths = new RegExp(/(\/((wp-admin)|(wc-api)|(edd-api))(\/?))/g)
// List of file extensions to be BYPASSed
const bypass_file_ext = new RegExp(/\.(xsl|xml)$/)
// Check if the request is for WP Admin endpoint & accept type includes text/html i.e. the main HTML request
if ( accept?.includes('text/html') ) {
reqDetails.contentTypeHTML = true
}
// Check if the request URL is an admin URL for HTML type requests
if ( reqDetails.contentTypeHTML && bypass_admin_path.test(requestPath) ) {
bypassCache = true
bypassReason.admin_req = true
bypassReasonDetails = 'WP Admin HTML request'
} else if ( bypass_cache_paths.test(requestPath) || bypass_file_ext.test(requestPath) ) {
// This is for files which starts with /wp-admin/ but not supposed to be cached
// E.g. /wp-admin/load-styles.php || /wp-admin/admin-ajax.php
// Also API endpoints and xml/xsl files to ensure sitemap isn't cached
bypassCache = true
bypassReason.file_path_ext = true
bypassReasonDetails = 'Dynamic File'
}
}
// 3. BYPASS the cache if DEFAULT_BYPASS_COOKIES is present in the request
// AND also only for the HTML type requests
if (
!bypassCache &&
reqDetails.contentTypeHTML &&
cookieHeader?.length > 0 &&
DEFAULT_BYPASS_COOKIES.length > 0
) {
// Separate the request cookies by semicolon and create an Array
const cookies = cookieHeader.split(';')
// Loop through the cookies Array to see if there is any cookies present that is present in DEFAULT_BYPASS_COOKIES
let foundDefaultBypassCookie = false
for ( let i = 0; i < cookies.length; i++ ) {
for ( let j = 0; j < DEFAULT_BYPASS_COOKIES.length; j++ ) {
if ( cookies[i].trim().startsWith( DEFAULT_BYPASS_COOKIES[j].trim() ) ) {
bypassCookieName = cookies[i].trim().split('=')
bypassCache = true
bypassReason.cookie = true
bypassReasonDetails = `Default Bypass Cookie [${bypassCookieName[0]}] Present`
foundDefaultBypassCookie = true
// Stop the loop
break
}
}
// Stop the loop if foundDefaultBypassCookie is TRUE else continue
if( foundDefaultBypassCookie ) {
break
}
}
}
/**
* Check if the Request has been Bypassed so far.
* If not, then check if the request exists in CF Edge Cache & if it does, send it
* If it does not exists in CF Edge Cache, then check if the request needs to be Bypassed based on the headers
* present in the Response.
*/
if (!bypassCache) { // bypassCache is still FALSE
// Check if the Request present in the CF Edge Cache
const cacheKey = new Request(requestURL, request)
const cache = caches?.default // Get global CF cache object for this zone
// Try to Get this request from this zone's cache
try {
response = await cache?.match(cacheKey)
} catch (err) {
return new Response(
`Error: ${err.message}`,
{ status: 500, statusText: "Unable to fetch cache from Cloudflare" }
)
}
if (response) { // Cache is present for this request in the CF Edge. Nothing special needs to be done.
// This request is already cached in the CF Edge. So, simply create a response and set custom headers
response = new Response(response?.body, response)
response?.headers?.set('x-wp-cf-super-cache-worker-status', 'hit')
} else { // Cache not present in CF Edge. Check if Req needs to be Bypassed or Cached based on Response header data
// Fetch the response of this given request normally without any special parameters
// so that we can use the response headers set by the plugin at the server level
let fetchedResponse
try {
fetchedResponse = await fetch(request)
} catch(err) {
return new Response(
`Error: ${err.message}`,
{ status: 500, statusText: "Unable to fetch content from the origin server" }
)
}
// If the above if check fails that means we have a good response and lets proceed
response = new Response(fetchedResponse.body, fetchedResponse)
// Check if the response has any unusual origin server response code & if so then return the response
if( has_unusual_origin_server_response_code(response) ) {
return response
}
// ---------------------------------------------------------
// Check - Bypass Request ? - Based on RESPONSE Headers
// ---------------------------------------------------------
// 4. BYPASS the HTML page requests which are excluded from caching (via WP Admin plugin settings or page level settings)
if (
!bypassCache &&
response?.headers?.get('content-type')?.includes('text/html') &&
!response?.headers?.has('x-wp-cf-super-cache-active')
) {
bypassCache = true
bypassReason.page_excluded = true
bypassReasonDetails = 'This page is excluded from caching'
}
// 5. BYPASS the static files (non HTML) which has x-wp-cf-super-cache response header set to no-cache
if (!bypassCache &&
!response?.headers?.get('content-type')?.includes('text/html') &&
(response?.headers?.get('x-wp-cf-super-cache') === 'no-cache')
) {
bypassCache = true
bypassReason.file_excluded = true
bypassReasonDetails = 'This file is excluded from caching'
}
// 6. BYPASS cache if any custom cookie mentioned by the user in the plugin settings is present in the request
// Check only for HTML type requests
if (
!bypassCache &&
cookieHeader?.length > 0 &&
response?.headers?.get('content-type')?.includes('text/html') &&
response?.headers?.has('x-wp-cf-super-cache-cookies-bypass')
) {
// Make sure the feature is enabled first
if (response?.headers?.get('x-wp-cf-super-cache-cookies-bypass') !== 'swfpc-feature-not-enabled') {
// Get the list of cookie names entered by the user in the plugin settings
let cookies_blacklist = response?.headers?.get('x-wp-cf-super-cache-cookies-bypass')
if (cookies_blacklist?.length > 0) {
// Split the received cookie list with | separated and make an Array
cookies_blacklist = cookies_blacklist.split('|')
if (are_blacklisted_cookies(cookieHeader, cookies_blacklist)) {
bypassCache = true
bypassReason.cookie = true
bypassReasonDetails = 'User provided excluded cookies present in request'
}
}
}
}
//-----------------------------------------------------
// Check if the request needs to be BYPASSed or Cached
//-----------------------------------------------------
if (!bypassCache) { // bypassCache is still FALSE. Cache the item in the CF Edge
// Check if the response status code is not 206 or request method is not HEAD to cache using cache.put(),
// as any request with status code === 206 or req.method HEAD cache.put() will not work.
// More info: https://developers.cloudflare.com/workers/runtime-apis/cache#put
if (response.status !== 206 || request?.method !== 'HEAD') {
// If the response header has x-wp-cf-super-cache-active overwrite the cache-control header provided by the server value with x-wp-cf-super-cache-active value just to be safe
if (response.headers?.has('x-wp-cf-super-cache-active')) {
response.headers?.set('Cache-Control', response.headers?.get('x-wp-cf-super-cache-cache-control'))
}
// Set the worker status as miss and put the item in CF cache
response.headers?.set('x-wp-cf-super-cache-worker-status', 'miss')
// Add page in cache using cache.put()
try {
event.waitUntil( cache.put( cacheKey, response.clone() ) )
} catch (err) {
return new Response(
`Cache Put Error: ${err.message}`,
{ status: 500, statusText: `Cache Put Error: ${err.message}` }
)
}
} else {
// Try to fetch this request again with cacheEverything set to TRUE as that is the only way to cache it
// More info: https://developers.cloudflare.com/workers/runtime-apis/request#requestinitcfproperties
try {
response = await fetch(request, { cf: { cacheEverything: true } })
} catch (err) {
return new Response(
`Error: ${err.message}`,
{ status: 500, statusText: "Unable to fetch content from the origin server with cacheEverything flag" }
)
}
response = new Response(response.body, response)
// Check if the response has any unusual origin server response code & if so then return the response
if( has_unusual_origin_server_response_code(response) ) {
return response
}
// Set the worker status as miss and put the item in CF cache
response.headers?.set('x-wp-cf-super-cache-worker-status', 'miss')
}
} else { // bypassCache -> TRUE || Bypass the Request
// BYPASS the request and add our custom headers
add_bypass_custom_headers(response, bypassReasonDetails)
}
}
} else { // bypassCache -> TRUE
// Fetch the request from the origin server and send it by adding our custom bypass headers
let bypassedResponse
try {
bypassedResponse = await fetch(request)
} catch (err) {
return new Response(
`Error: ${err.message}`,
{ status: 500, statusText: "Unable to fetch the bypassed content from the origin server" }
)
}
response = new Response(bypassedResponse?.body, bypassedResponse)
// Check if the response has any unusual origin server response code & if so then return the response
if( has_unusual_origin_server_response_code(response) ) {
return response
}
// BYPASS the request and add our custom headers
add_bypass_custom_headers(response, bypassReasonDetails)
}
return response
}
/**
* Adding event lister to the fetch event to catch the requests and manage them accordingly
* @param {Object} event
*/
addEventListener('fetch', event => {
try {
return event.respondWith(handleRequest(event))
} catch (err) {
return event.respondWith(
new Response(
`Error thrown: ${err.message}`,
{ status: 500, statusText: `Error thrown: ${err.message}` }
)
)
}
})