diff --git a/.gitignore b/.gitignore index 71deb5c..40b1aa5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /tests/assets/ +/.phpunit.result.cache diff --git a/.travis.yml b/.travis.yml index 15f0f1e..0d622d3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,29 +19,24 @@ cache: matrix: include: - - php: 5.6 + - php: 7.4 env: - DB=MYSQL - - RECIPE_VERSION=1.0.x-dev + - RECIPE_VERSION=1.13.x-dev - PHPCS_TEST=1 - - php: 7.0 + - php: 8.1 env: - DB=MYSQL - - RECIPE_VERSION=1.1.x-dev - # This doesn't work due to Multisites not being built to be compatible. - #- MULTISITES_VERSION=5.0.2 # NOTE: Test at a version we know definitely works. - - php: 7.1 + - RECIPE_VERSION=2.0.x-dev + - php: 8.1 env: - DB=MYSQL - - PHPSTAN_TEST=1 - - RECIPE_VERSION=1.2.x-dev - - php: 7.2 + - RECIPE_VERSION=2.1.x-dev + - php: 7.4 env: - DB=MYSQL - PHPSTAN_TEST=1 - - RECIPE_VERSION=1.2.x-dev - # This doesn't work due to Multisites not being built to be compatible. - #- MULTISITES_VERSION=master # NOTE: Test against the tip for regressions. This could cause failures! + - RECIPE_VERSION=1.13.x-dev before_script: - phpenv rehash diff --git a/composer.json b/composer.json index c2dfb9d..c609401 100644 --- a/composer.json +++ b/composer.json @@ -17,13 +17,13 @@ } ], "require": { - "php": ">=5.6", - "silverstripe/cms": "^4.0", - "jamesryanbell/cloudflare": "~1.11" + "php": ">=7.4", + "silverstripe/cms": "^4|^5", + "cloudflare/sdk": "~1.3" }, "require-dev": { "squizlabs/php_codesniffer": "^3.0", - "phpunit/phpunit": "^5.7" + "phpunit/phpunit": "^9.5" }, "scripts": { "phpcbf": "phpcbf -n src/ tests/" diff --git a/docs/en/quick-start.md b/docs/en/quick-start.md index 83fe016..4dafebb 100644 --- a/docs/en/quick-start.md +++ b/docs/en/quick-start.md @@ -4,6 +4,14 @@ 2. Configure in YML, example below: ```yml +Symbiote\Cloudflare\Cloudflare: + enabled: true + api_token: '24ca61e15fb2aa62a31-212a90f2674f_3451f8' # Needs the the "Cache Purge" permission: + zone_id: '73a40b2c0c10f468cb658f67b9d46fff' +``` + +Alternatively you can use you Global API Key: +```yml Symbiote\Cloudflare\Cloudflare: enabled: true email: 'silverstripe@gmail.com' @@ -11,6 +19,16 @@ Symbiote\Cloudflare\Cloudflare: zone_id: '73a40b2c0c10f468cb658f67b9d46fff' ``` +Note the `email`, `auth_key`, `zone_id`, and `api_token` yaml options can also be represented with service properties so you can store this information in the environment file. For example: + +```yml +Symbiote\Cloudflare\Cloudflare: + enabled: true + api_token: "`CLOUDFLARE_API_TOKEN`" + zone_id: "`CLOUDFLARE_ZONE_ID`" +``` + + 3. Publishing / unpublishing a page from the CMS will now clear the Cloudflare cache for that URL. 4. For clearing CSS, JavaScript and images from the Cloudflare cache, see the [Advanced Usage](advanced-usage.md) section. diff --git a/phpunit.xml.dist b/phpunit.xml.dist index b543ea6..e398543 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,13 +1,14 @@ - - - tests/ - - - - src/ - - tests/ - - - + + + + + src/ + + + tests/ + + + + tests/ + diff --git a/src/Cloudflare.php b/src/Cloudflare.php index 48f5d48..f2929ee 100644 --- a/src/Cloudflare.php +++ b/src/Cloudflare.php @@ -2,19 +2,21 @@ namespace Symbiote\Cloudflare; -use Exception; -use Symbiote\Multisites\Model\Site; -use Cloudflare\Api; -use Cloudflare\Zone\Cache; -use SilverStripe\Core\Injector\Injector; -use SilverStripe\CMS\Model\SiteTree; +use Cloudflare\API\Adapter\Guzzle as Cloudflare_Guzzle; +use Cloudflare\API\Auth\APIKey as Cloudflare_APIKey; +use Cloudflare\API\Auth\APIToken as Cloudflare_APIToken; +use Cloudflare\API\Endpoints\Zones as Cloudflare_Zones; use SilverStripe\Assets\File; -use SilverStripe\Control\Director; +use SilverStripe\CMS\Model\SiteTree; use SilverStripe\Control\Controller; -use SilverStripe\View\Requirements; +use SilverStripe\Control\Director; +use SilverStripe\Core\Config\Configurable; use SilverStripe\Core\Extensible; use SilverStripe\Core\Injector\Injectable; -use SilverStripe\Core\Config\Configurable; +use SilverStripe\Core\Injector\Injector; +use SilverStripe\View\Requirements; +use Symbiote\Multisites\Model\Site; +use Exception; class Cloudflare { @@ -23,9 +25,9 @@ class Cloudflare use Configurable; /** - * Cloudflare can only purge 500 files per request. + * Cloudflare can only purge 30 files per request. */ - const MAX_PURGE_FILES_PER_REQUEST = 500; + const MAX_PURGE_FILES_PER_REQUEST = 30; /** * String representation of this class. @@ -73,6 +75,16 @@ class Cloudflare */ private static $auth_key = ''; + /** + * API Token + * + * eg. 24ca61e15fb2aa62a312-12a90f2674f_3451f8 + * + * @var string + * @config + */ + private static $api_token = ''; + /** * Zone ID * @@ -108,7 +120,7 @@ class Cloudflare ); /** - * @var \Cloudflare\Api + * @var Cloudflare_Zones */ protected $client; @@ -121,7 +133,24 @@ public function __construct() { $this->filesystem = Injector::inst()->get(self::FILESYSTEM_CLASS); if ($this->config()->enabled) { - $this->client = new Api($this->config()->email, $this->config()->auth_key); + if ($this->config()->api_token) { + $this->client = new Cloudflare_Zones( + new Cloudflare_Guzzle( + new Cloudflare_APIToken( + Injector::inst()->convertServiceProperty($this->config()->api_token) + ), + ) + ); + } else { + $this->client = new Cloudflare_Zones( + new Cloudflare_Guzzle( + new Cloudflare_APIKey( + Injector::inst()->convertServiceProperty($this->config()->email), + Injector::inst()->convertServiceProperty($this->config()->auth_key) + ), + ) + ); + } } } @@ -145,10 +174,13 @@ public function purgeAll() if (!$this->client) { return null; } - $cache = new Cache($this->client); - $response = $cache->purge($this->getZoneIdentifier(), true); - $result = new CloudflareResult(array(), $response->errors); - return $result; + + try { + $this->client->cachePurgeEverything($this->getZoneIdentifier()); + return new CloudflareResult([], []); + } catch (Exception $e) { + return new CloudflareResult([], [$e->getMessage()]); + } } /** @@ -176,11 +208,11 @@ public function purgeImages() public function purgeCSSAndJavascript() { return $this->purgeFilesByExtensions( - array( + [ 'css', 'js', 'json', - ) + ] ); } @@ -219,7 +251,7 @@ public function purgeURLs(array $absoluteOrRelativeURLList) */ public function getZoneIdentifier() { - return $this->config()->zone_id; + return Injector::inst()->convertServiceProperty($this->config()->zone_id); } /** @@ -230,20 +262,20 @@ protected function purgeFilesByExtensions(array $fileExtensions) if (!$this->client) { return null; } + $files = $this->getFilesToPurgeByExtensions($fileExtensions, false); // Purge files - $cache = new Cache($this->client); $zoneIdentifier = $this->getZoneIdentifier(); $errors = array(); foreach (array_chunk($files, self::MAX_PURGE_FILES_PER_REQUEST) as $filesChunk) { - $response = $cache->purge_files($zoneIdentifier, $filesChunk); - if (!$response->success) { - $errors = array_merge($errors, $response->errors); + try { + $this->client->cachePurge($zoneIdentifier, $filesChunk); + } catch (Exception $e) { + $errors[] = $e->getMessage(); } } - // $result = new CloudflareResult($files, $errors); return $result; } @@ -307,7 +339,7 @@ private function getFilesToPurgeByExtensions(array $fileExtensions, $ignoreDatab // Scan files in the project directory to purge $folderList = array( // Get all files built by `Requirements` system (*.css, *.js) - Director::baseFolder().'/'.ASSETS_DIR.'/'.Requirements::backend()->getCombinedFilesFolder(), + Director::baseFolder() . '/' . (defined('PUBLIC_DIR') ? PUBLIC_DIR . '/' : '') . ASSETS_DIR . '/' . Requirements::backend()->getCombinedFilesFolder(), // Get all module / theme files Director::baseFolder() ); @@ -324,7 +356,7 @@ private function getFilesToPurgeByExtensions(array $fileExtensions, $ignoreDatab } $fileRecordList = File::get()->filter( array( - 'Filename:EndsWith' => $fileExtensionsPrefixedWithDot + 'FileFilename:EndsWith' => $fileExtensionsPrefixedWithDot ) ); $files = array_merge($files, $fileRecordList->map('ID', 'Link')->toArray()); @@ -338,21 +370,15 @@ private function getFilesToPurgeByExtensions(array $fileExtensions, $ignoreDatab */ private function purgeFiles(array $filesToPurge) { - $cache = new Cache($this->client); - $response = $cache->purge_files($this->getZoneIdentifier(), $filesToPurge); $errors = []; - if (!$response->success) { - if (isset($response->errors)) { - $errors = $response->errors; - } else { - throw new \Exception($response->error); - //if (isset($response->error)) { - // $error = new \stdClass; - // $error->message = $response->error; - // $errors[] = $error; - //} - } + try { + $this->client->cachePurge($this->getZoneIdentifier(), $filesToPurge); + } catch (Exception $e) { + $errors[] = $e->getMessage(); + + throw $e; } + $result = new CloudflareResult($filesToPurge, $errors); return $result; } diff --git a/src/CloudflareResult.php b/src/CloudflareResult.php index 7419f73..d214c35 100644 --- a/src/CloudflareResult.php +++ b/src/CloudflareResult.php @@ -16,24 +16,8 @@ class CloudflareResult public function __construct(array $files, array $errorRecords) { - // Determine what purged files were un-successful. - $purgedFiles = $files; - foreach ($errorRecords as $errorRecord) { - foreach ($purgedFiles as $key => $url) { - if (strpos($errorRecord->message, $url) !== false) { - unset($purgedFiles[$key]); - } - } - } - // Apply to this object - $this->successes = $purgedFiles; - if ($errorRecords) { - $this->errors = array(); - foreach ($errorRecords as $errorRecord) { - $this->errors[] = $errorRecord->message; - } - } + $this->errors = $errorRecords; } /** diff --git a/src/PurgeTask.php b/src/PurgeTask.php index 16002b8..67b7e46 100644 --- a/src/PurgeTask.php +++ b/src/PurgeTask.php @@ -2,10 +2,8 @@ namespace Symbiote\Cloudflare; -use Psr\Log\LoggerInterface; -use SilverStripe\Core\Injector\Injector; use SilverStripe\Control\Director; -use SilverStripe\Dev\BuildTask; +use SilverStripe\Core\Injector\Injector; // // NOTE(Jake): 2018-04-26 @@ -42,17 +40,6 @@ public function endRun($request) $result = $this->callPurgeFunction($client); $timeTakenInSeconds = number_format(microtime(true) - $startTime, 2, '.', ''); - // Show output - $status = 'PURGE SUCCESS'; - - $successes = $result->getSuccesses(); - if ($successes) { - $this->log('Successes:'); - foreach ($successes as $success) { - $this->log($success); - } - } - $errors = $result->getErrors(); if ($errors) { @@ -69,10 +56,10 @@ public function endRun($request) // If no successes or errors, assume success. // ie. this is for purge everything. echo Director::is_cli() ? "\n" : '
'; - if (!$successes && !$errors) { - $this->log($status.'.'); + if (!$errors) { + $this->log('SUCCESS'); } else { - $this->log($status.'. ('.count($successes).' successes, '.count($errors).' failed)'); + $this->log($status.'. ('.count($errors).' failed)'); } $this->log('Time taken: '.$timeTakenInSeconds.' seconds.'); } diff --git a/tests/CloudflarePurgeFileTest.php b/tests/CloudflarePurgeFileTest.php index 8ad959d..a497835 100644 --- a/tests/CloudflarePurgeFileTest.php +++ b/tests/CloudflarePurgeFileTest.php @@ -2,14 +2,12 @@ namespace Symbiote\Cloudflare\Tests; -use ReflectionObject; -use SilverStripe\CMS\Model\SiteTree; -use SilverStripe\Core\Injector\Injector; -use SilverStripe\View\Requirements; use SilverStripe\Core\Config\Config; +use SilverStripe\Core\Injector\Injector; use SilverStripe\Dev\FunctionalTest; +use SilverStripe\View\Requirements; use Symbiote\Cloudflare\Cloudflare; -use Symbiote\Cloudflare\Filesystem; +use ReflectionObject; class CloudflarePurgeFileTest extends FunctionalTest { @@ -22,7 +20,7 @@ class CloudflarePurgeFileTest extends FunctionalTest * This is used to determine if the 'framework' folder was scanned * for CSS/JS files. */ - const FRAMEWORK_CSS_FILE = 'vendor/silverstripe/framework/src/Dev/Install/client/styles/install.css'; + const FRAMEWORK_CSS_FILE = 'vendor/silverstripe/framework/client/styles/debug.css'; protected static $disable_themes = true; @@ -105,7 +103,7 @@ public function testPurgeCSSAndJS() */ public function testAllowBlacklistedDirectories() { - Config::inst()->update(Cloudflare::FILESYSTEM_CLASS, 'disable_default_blacklist_absolute_pathnames', true); + Config::inst()->set(Cloudflare::FILESYSTEM_CLASS, 'disable_default_blacklist_absolute_pathnames', true); $files = $this->getFilesToPurgeByExtensions( array( 'css', @@ -113,7 +111,7 @@ public function testAllowBlacklistedDirectories() 'json', ) ); - Config::inst()->update(Cloudflare::FILESYSTEM_CLASS, 'disable_default_blacklist_absolute_pathnames', false); + Config::inst()->set(Cloudflare::FILESYSTEM_CLASS, 'disable_default_blacklist_absolute_pathnames', false); // If it has a file from the 'framework' module, fail this test as it should be ignored. $hasFramework = false; diff --git a/tests/CloudflarePurgePageTest.php b/tests/CloudflarePurgePageTest.php index 4949054..a38f6d5 100644 --- a/tests/CloudflarePurgePageTest.php +++ b/tests/CloudflarePurgePageTest.php @@ -2,15 +2,14 @@ namespace Symbiote\Cloudflare\Tests; -use ReflectionObject; -use SilverStripe\Core\Injector\Injector; -use SilverStripe\Control\Director; +use Cloudflare\API\Endpoints\EndpointException; +use SilverStripe\CMS\Controllers\RootURLController; use SilverStripe\CMS\Model\SiteTree; use SilverStripe\Core\Config\Config; +use SilverStripe\Core\Injector\Injector; use SilverStripe\Dev\FunctionalTest; use Symbiote\Cloudflare\Cloudflare; -use SilverStripe\CMS\Controllers\RootURLController; -use SilverStripe\Control\Controller; +use ReflectionObject; //use Symbiote\Multisites\Model\Site; @@ -18,7 +17,7 @@ class CloudflarePurgePageTest extends FunctionalTest { protected static $disable_themes = true; - public function setUp() + public function setUp(): void { parent::setUp(); if (!defined('SS_BASE_URL')) { @@ -96,14 +95,14 @@ public function testPurgeRandomPage() */ public function testPurgePage() { - Config::inst()->update(Cloudflare::class, 'enabled', true); + Config::inst()->set(Cloudflare::class, 'enabled', true); $wasPurgePageCalled = false; $record = SiteTree::create(); $record->write(); try { $record->publishSingle(); - } catch (\Cloudflare\Exception\AuthenticationException $e) { + } catch (EndpointException $e) { // NOTE(Jake): 2018-04-26 // // This is expected behaviour. Since we're running `purgePage` with Cloudflare