From c62211e95782d4c2081276bf4205a96283d4fb4a Mon Sep 17 00:00:00 2001 From: Ben Croker Date: Wed, 4 Dec 2024 06:30:45 -0600 Subject: [PATCH] Fix purging subdomain URLs --- CHANGELOG.md | 6 +- src/controllers/RulesController.php | 2 +- src/helpers/UrlHelper.php | 5 +- src/resources/js/cp.js | 417 ++++++++++++++------------- tests/pest/Feature/UrlHelperTest.php | 22 +- 5 files changed, 219 insertions(+), 233 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fde441..91947b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,15 @@ # Release Notes for Cloudflare -## 3.0.2 - Unreleased +## 3.0.2 - 2024-12-04 ### Changed - Element URLs are now checked for null values for better compatibility with other plugins. +### Fixed + +- Fixed a bug in which URLs with a subdomain were not being purged ([#74](https://github.com/putyourlightson/craft-cloudflare/issues/74)). + ## 3.0.1 - 2024-06-18 ### Changed diff --git a/src/controllers/RulesController.php b/src/controllers/RulesController.php index e81082e..9e322af 100644 --- a/src/controllers/RulesController.php +++ b/src/controllers/RulesController.php @@ -21,7 +21,7 @@ public function actionSave(): Response { Cloudflare::$plugin->rules->saveRules(); - Craft::$app->getSession()->setNotice(Craft::t( + Craft::$app->getSession()->setSuccess(Craft::t( 'cloudflare', 'Cloudflare rules saved.' )); diff --git a/src/helpers/UrlHelper.php b/src/helpers/UrlHelper.php index b1c67e6..c7d7d49 100755 --- a/src/helpers/UrlHelper.php +++ b/src/helpers/UrlHelper.php @@ -68,17 +68,16 @@ public static function isPurgeableUrl(string $url, bool $includeZoneCheck): bool */ if ($includeZoneCheck) { if (!$urlDomain = parse_url($url, PHP_URL_HOST)) { - // bail if we couldn't even get a base domain return false; } - if (strtolower($urlDomain) !== strtolower($cfDomainName)) { + if (!str_ends_with(strtolower($urlDomain), strtolower($cfDomainName))) { Craft::info( sprintf('Ignoring URL outside zone: %s', $url), 'cloudflare' ); - return false; // base domain doesn't match Cloudflare zone + return false; } } diff --git a/src/resources/js/cp.js b/src/resources/js/cp.js index 3604e8c..08dd275 100644 --- a/src/resources/js/cp.js +++ b/src/resources/js/cp.js @@ -3,7 +3,7 @@ /** global Craft */ const credentialSpinner = document.getElementById( - "settings-credential-spinner" + "settings-credential-spinner" ); const zoneSelect = document.getElementById("settings-zone-select"); const zoneSelectWrap = document.getElementById("settings-zone-id-select"); @@ -17,254 +17,255 @@ const purgeAllButton = document.getElementById("settings-purge-all"); // widget purge URL pane toggle/heading const purgeUrlsToggle = document.querySelector( - ".purge-option.purge-individual-urls .heading" + ".purge-option.purge-individual-urls .heading" ); const purgeUrlsFormWrap = document.querySelector(".purge-urls-form"); if (verifyCredentialsButton) { - verifyCredentialsButton.addEventListener("click", (event) => { - event.preventDefault(); + verifyCredentialsButton.addEventListener("click", (event) => { + event.preventDefault(); - const settings = getAuthSettings(); + const settings = getAuthSettings(); - if (settings === false) { - Craft.cp.displayError( - Craft.t("cloudflare", "Please enter required API credentials.") - ); - return; - } + if (settings === false) { + Craft.cp.displayError( + Craft.t("cloudflare", "Please enter required API credentials.") + ); + return; + } - showSpinner(); - checkCredentials(settings); - }); + showSpinner(); + checkCredentials(settings); + }); } if (purgeUrlsButton) { - purgeUrlsButton.addEventListener("click", (event) => { - event.preventDefault(); - purgeUrls(purgeUrlsField.value); - }); + purgeUrlsButton.addEventListener("click", (event) => { + event.preventDefault(); + purgeUrls(purgeUrlsField.value); + }); } if (purgeAllButton) { - purgeAllButton.addEventListener("click", (event) => { - event.preventDefault(); - purgeAll(); - }); + purgeAllButton.addEventListener("click", (event) => { + event.preventDefault(); + purgeAll(); + }); } if (purgeUrlsToggle) { - purgeUrlsFormWrap.classList.add("hidden"); + purgeUrlsFormWrap.classList.add("hidden"); - purgeUrlsToggle.addEventListener("click", (event) => { - const heading = event.target; + purgeUrlsToggle.addEventListener("click", (event) => { + const heading = event.target; - purgeUrlsFormWrap.classList.toggle("hidden"); + purgeUrlsFormWrap.classList.toggle("hidden"); - setTimeout(() => { - heading.classList.toggle("active"); - }, 100); - }); + setTimeout(() => { + heading.classList.toggle("active"); + }, 100); + }); } function fetchZones() { - const settings = getAuthSettings(); - const selectedZoneId = zoneSelect.querySelector("option:checked") - ? zoneSelect.querySelector("option:checked").value - : false; - showSpinner(); - - Craft.postActionRequest( - "cloudflare/default/fetch-zones", - settings, - (response, statusText) => { - hideSpinner(); - - // check for errors - if (statusText === "error" || !response) { - Craft.cp.displayError( - Craft.t("cloudflare", "Could not verify API credentials.") - ); - - verifyContainer.classList.remove("verified"); - verifyContainer.classList.add("failed"); - - console.error( - "Credential verification failed with response: ", - response - ); + const settings = getAuthSettings(); + const selectedZoneId = zoneSelect.querySelector("option:checked") + ? zoneSelect.querySelector("option:checked").value + : false; + showSpinner(); - return false; - } - - // clear existing options - Array.from(zoneSelect.querySelectorAll("option")).forEach((option) => - option.remove() - ); - - // append zone options from Cloudflare - for (let i = 0; i < response.length; i++) { - const row = response[i]; - const option = document.createElement("option"); - - option.value = row.id; - option.textContent = row.name; - - zoneSelect.appendChild(option); - } - - // restore selection - if (selectedZoneId) { - zoneSelect.value = selectedZoneId; - } - - if (response.length === 0) { - // hide + disable menu, enable + display input - zoneSelect.disabled = true; - zoneSelectWrap.classList.add("hidden"); - zoneInputElement.disabled = false; - zoneInputWrap.classList.remove("hidden"); - } else { - // hide + disable input, enable + display menu - zoneSelect.disabled = false; - zoneSelectWrap.classList.remove("hidden"); - zoneInputElement.disabled = true; - zoneInputWrap.classList.add("hidden"); - } - - verifyContainer.classList.remove("failed"); - verifyContainer.classList.add("verified"); - } - ); + Craft.postActionRequest( + "cloudflare/default/fetch-zones", + settings, + (response, statusText) => { + hideSpinner(); + + // check for errors + if (statusText === "error" || !response) { + Craft.cp.displayError( + Craft.t("cloudflare", "Could not verify API credentials.") + ); + + verifyContainer.classList.remove("verified"); + verifyContainer.classList.add("failed"); + + console.error( + "Credential verification failed with response: ", + response + ); + + return false; + } + + // clear existing options + Array.from(zoneSelect.querySelectorAll("option")).forEach((option) => + option.remove() + ); + + // append zone options from Cloudflare + for (let i = 0; i < response.length; i++) { + const row = response[i]; + const option = document.createElement("option"); + + option.value = row.id; + option.textContent = row.name; + + zoneSelect.appendChild(option); + } + + // restore selection + if (selectedZoneId) { + zoneSelect.value = selectedZoneId; + } + + if (response.length === 0) { + // hide + disable menu, enable + display input + zoneSelect.disabled = true; + zoneSelectWrap.classList.add("hidden"); + zoneInputElement.disabled = false; + zoneInputWrap.classList.remove("hidden"); + } + else { + // hide + disable input, enable + display menu + zoneSelect.disabled = false; + zoneSelectWrap.classList.remove("hidden"); + zoneInputElement.disabled = true; + zoneInputWrap.classList.add("hidden"); + } + + verifyContainer.classList.remove("failed"); + verifyContainer.classList.add("verified"); + } + ); } function showSpinner() { - verifyContainer.classList.remove("verified", "failed"); - verifyCredentialsButton.classList.add("loading"); + verifyContainer.classList.remove("verified", "failed"); + verifyCredentialsButton.classList.add("loading"); } function hideSpinner() { - verifyCredentialsButton.classList.remove("loading"); + verifyCredentialsButton.classList.remove("loading"); } function checkCredentials(settings) { - Craft.sendActionRequest('POST', 'cloudflare/default/verify-connection', { data: settings }) - .then((response) => { - hideSpinner(); - - // if we succeeded, populate Cloudflare Zone options - fetchZones(); - }) - .catch(({response}) => { - // Handle non-2xx responses ... - hideSpinner(); - verifyContainer.classList.remove("verified"); - verifyContainer.classList.add("failed"); - - console.error( - "Credential verification failed with response: ", - response - ); - - if (typeof response.data.errors !== "undefined" && response.data.errors.length > 0) { - // Fail credentials, skip louder error + Craft.sendActionRequest('POST', 'cloudflare/default/verify-connection', {data: settings}) + .then((response) => { + hideSpinner(); + + // if we succeeded, populate Cloudflare Zone options + fetchZones(); + }) + .catch(({response}) => { + // Handle non-2xx responses ... + hideSpinner(); + verifyContainer.classList.remove("verified"); + verifyContainer.classList.add("failed"); + + console.error( + "Credential verification failed with response: ", + response + ); + + if (typeof response.data.errors !== "undefined" && response.data.errors.length > 0) { + // Fail credentials, skip louder error + return false; + } + + // Display louder error if we couldn’t even *check* the credentials + Craft.cp.displayError( + Craft.t("cloudflare", "Could not verify API credentials.") + ); + + return false; + }); +} + +function getAuthSettings() { + // fetch field references here since they may not have been available earlier + const authTypeField = document.getElementById("settings-authType"); + const apiTokenField = document.getElementById("settings-apiToken"); + const apiKeyField = document.getElementById("settings-apiKey"); + const emailField = document.getElementById("settings-email"); + + const authType = authTypeField.value || false; + const apiToken = apiTokenField.value || false; + const apiKey = apiKeyField.value || false; + const email = emailField.value || false; + + // make sure required fields exist + if ( + (authType === "key" && (!apiKey || !email)) || + (authType === "token" && !apiToken) + ) { return false; - } + } - // Display louder error if we couldn’t even *check* the credentials - Craft.cp.displayError( - Craft.t("cloudflare", "Could not verify API credentials.") - ); + if (authType === "key") { + return { + authType, + apiKey, + email, + }; + } - return false; - }); -} + if (authType === "token") { + return { + authType, + apiToken, + }; + } -function getAuthSettings() { - // fetch field references here since they may not have been available earlier - const authTypeField = document.getElementById("settings-authType"); - const apiTokenField = document.getElementById("settings-apiToken"); - const apiKeyField = document.getElementById("settings-apiKey"); - const emailField = document.getElementById("settings-email"); - - const authType = authTypeField.value || false; - const apiToken = apiTokenField.value || false; - const apiKey = apiKeyField.value || false; - const email = emailField.value || false; - - // make sure required fields exist - if ( - (authType === "key" && (!apiKey || !email)) || - (authType === "token" && !apiToken) - ) { return false; - } - - if (authType === "key") { - return { - authType, - apiKey, - email, - }; - } - - if (authType === "token") { - return { - authType, - apiToken, - }; - } - - return false; } function purgeUrls(urls) { - Craft.sendActionRequest('POST', 'cloudflare/default/purge-urls', { data: { urls: urls } }) - .then((response) => { - const wasSuccessful = typeof response.data.success !== "undefined" && response.data.success; - - if (! wasSuccessful) { - console.error("URL purge failed:", response); - Craft.cp.displayError(Craft.t("cloudflare", "URL purge failed.")); - return; - } - - // empty the URL field - purgeUrlsField.value = ""; - - Craft.cp.displayNotice( - Craft.t("cloudflare", "URL purge successful.") - ); - }) - .catch(({response}) => { - console.error("URL purge failed:", response); - Craft.cp.displayError(Craft.t("cloudflare", "URL purge failed.")); - return; - }); + Craft.sendActionRequest('POST', 'cloudflare/default/purge-urls', {data: {urls: urls}}) + .then((response) => { + const wasSuccessful = typeof response.data.success !== "undefined" && response.data.success; + + if (!wasSuccessful) { + console.error("URL purge failed:", response); + Craft.cp.displayError(Craft.t("cloudflare", "URL purge failed.")); + return; + } + + // empty the URL field + purgeUrlsField.value = ""; + + Craft.cp.displaySuccess( + Craft.t("cloudflare", "URL purge successful.") + ); + }) + .catch(({response}) => { + console.error("URL purge failed:", response); + Craft.cp.displayError(Craft.t("cloudflare", "URL purge failed.")); + return; + }); } function purgeAll() { - if ( - confirm( - Craft.t( - "cloudflare", - "You definitely want to purge the entire cache, right?" - ) - ) - ) { - Craft.sendActionRequest('POST', 'cloudflare/default/purge-all') - .then((response) => { - const wasSuccessful = typeof response.data.success !== "undefined" && response.data.success; - - if (! wasSuccessful) { - console.error("Zone purge failed:", response); - Craft.cp.displayError(Craft.t("cloudflare", "Zone purge failed.")); - return; - } - - Craft.cp.displayNotice( - Craft.t("cloudflare", "Zone purge successful.") - ); - }); - } + if ( + confirm( + Craft.t( + "cloudflare", + "You definitely want to purge the entire cache, right?" + ) + ) + ) { + Craft.sendActionRequest('POST', 'cloudflare/default/purge-all') + .then((response) => { + const wasSuccessful = typeof response.data.success !== "undefined" && response.data.success; + + if (!wasSuccessful) { + console.error("Zone purge failed:", response); + Craft.cp.displayError(Craft.t("cloudflare", "Zone purge failed.")); + return; + } + + Craft.cp.displaySuccess( + Craft.t("cloudflare", "Zone purge successful.") + ); + }); + } } diff --git a/tests/pest/Feature/UrlHelperTest.php b/tests/pest/Feature/UrlHelperTest.php index 422d2b7..de6ccc1 100644 --- a/tests/pest/Feature/UrlHelperTest.php +++ b/tests/pest/Feature/UrlHelperTest.php @@ -1,8 +1,8 @@ toBeFalse(); } }); - -test('The base domain is correctly returned from a URL', function() { - $urls = [ - 'snipcart.com' => 'https://snipcart.com/foo/bar', - 'example.org.au' => 'https://www.example.org.au/path/to/something', - 'foo.bar' => 'https://subdomain.foo.bar', - ]; - - foreach ($urls as $domain => $url) { - expect(UrlHelper::getBaseDomainFromUrl($url)) - ->toBe($domain); - } -}); - -test('A base domain that is not real is not returned from a URL', function() { - expect(UrlHelper::getBaseDomainFromUrl('www.nota.realdomain/foo/bar')) - ->toBeNull(); -});