// 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}` } ) ) } })