Blame | Last modification | View Log | Download
<?phpnamespace ProcessWire;class ProcessCacheControl extends Process implements Module{/** @var string The name of the GET parameter that determines the action to perform. */public const ACTION_PARAMETER_NAME = 'action';/** @var string The base name for the required permission for using this module. */public const MODULE_PERMISSION = 'cache-control';/** @var string The format for optional action-specific permissions. @see self::getActionPermissionName */public const ACTION_PERMISSION_FORMAT = '%1$s-%2$s';/** @var string The ID of the default cache action. */public const DEFAULT_ACTION_ID = 'all';public static function getModuleInfo(){return ['title' => __('Cache Control'),'summary' => __('Adds an entry to the setup menu to clear all caches. Provides an API to add cache management actions and an interface to execute them.'),'author' => "Moritz L'Hoest",'href' => 'https://github.com/MoritzLost/ProcessCacheControl','version' => '1.1.1','icon' => 'floppy-o','singular' => true,'useNavJSON' => true,'installs' => 'CacheControlTools','requires' => ['ProcessWire>=3.0.130','PHP>=7.1',],'permission' => self::MODULE_PERMISSION,'permissions' => [self::MODULE_PERMISSION => __('Use the cache control interface provided by the ProcessCacheControl module and execute cache control actions.')],'page' => ['name' => 'cache-control','parent' => 'setup','title' => __('Cache Control'),],];}/*** Check if the user can instantiate and use this module. Check this as a* static method BEFORE instantiating the module, because it will throw an* error if the user can't access it.** @param User|null $user The user whose access to check. Defaults to the current user.* @return boolean*/public static function canUseModule(?User $user = null): bool{$user = $user ?? wire('user');return $user->hasPermission(self::MODULE_PERMISSION);}/*** Get all actions that a user is allowed to execute.** @param User|null $user The user whose access to check. Defaults to the current user.* @return array*/public function getAllowedActions(?User $user = null): array{$user = $user ?? wire('user');return array_filter($this->getActionDefinitions(), function (array $action) use ($user) {return $this->canExecuteAction($action['id'], $user);});}/*** Get all available cache actions. This method can be hooked to add* additional cache actions. Action definitions are associative arrays. The* following array keys are required:* - id (string): The ID for this job, used as a GET parameter.* - title (string): The display title for the job.* - callback (callable): Any callable or closure to call when this method gets executed.** @return array An array of action definitions (associative arrays).*/public function ___getActionDefinitions(): array{return [['id' => self::DEFAULT_ACTION_ID,'title' => $this->_('Clear all'),'icon' => 'floppy-o','callback' => [$this, 'clearAll'],],];}/*** Perform the action specified by the passed action ID. Actions can be added* through hooks, @see ProcessCacheControl::getActionDefinitions.** @param string $actionId* @return void*/public function ___executeAction(string $actionId): void{// get the action definition, if it exists$actionDefinition = $this->getActionDefinitionById($actionId);if (null === $actionDefinition) {throw new \InvalidArgumentException(sprintf($this->_('Action with ID %s does not exist.'), $actionId));}// if a specific permission for this action exists, the user needs it in order// to execute this action. otherwise, the user only needs the general// cache-control permission$actionPermissionName = $this->getActionPermissionName($actionId);$actionPermissionExists = $this->permissions->has($actionPermissionName);$actionAllowed = $this->canExecuteAction($actionId, $this->user);if (!$actionAllowed) {$permissionDeniedMessage = $actionPermissionExists? $this->_('Prevented user %1$s from executing cache control action %2$s because it requires the following permissions: %3$s, %4$s'): $this->_('Prevented user %1$s from executing cache control action %2$s because it requires the following permission: %3$s');$this->modules->get('CacheControlTools')->logMessage(sprintf($permissionDeniedMessage,$this->user->name,$actionId,self::MODULE_PERMISSION,$actionPermissionName));} else {call_user_func($actionDefinition['callback'], $this->modules->get('CacheControlTools'));}}/*** Clear all caches according to the module configuration. This is the default* action that the module provides out of the box.** @param CacheControlTools|null $tools* @return void*/public function ___clearAll(?CacheControlTools $tools): void{// automatically instantiate $tools so the method can be called without argumentsif (null === $tools) $tools = $this->modules->get('CacheControlTools')->verbose();// clear all configured specified entries / namespaces in the database cache$cache = $this->wire('cache');if ($this->WireCacheExpireAll) {$cache->expireAll();$tools->logMessage($this->_('Expired all WireCache entries that have an expiration data ($cache->expireAll()).'));}if ($this->WireCacheDeleteAll) {$cache->deleteAll();$tools->logMessage($this->_('Deleted all WireCache entries except for reserved system entries ($cache->deleteAll()).'));}if (!empty($namespaceList = $this->parseNamespaceList($this->WireCacheDeleteNamespaces))) {$tools->clearWireCacheByNamespaces($namespaceList);}// clear selected cache directoriesif ($this->ClearCacheDirectories) {foreach ($this->ClearCacheDirectories as $dir) {$tools->clearCacheDirectoryContent($dir);}}// clear procache through the api, if it is installedif ($this->ClearProCache) {$procache = $this->wire('procache');if (null !== $procache) {$procache->clearAll();$tools->logMessage($this->_('Cleared the ProCache module cache ($procache->clearAll()).'));}}// clear all stored asset versionsif ($this->ClearAllAssetVersions) {$tools->clearAllAssetVersions();}}/*** Check if a user can execute the given action.** @param string $actionId The ID of the action to check.* @param User|null $user The user whose access to check. Defaults to the current user.* @return array*/public function canExecuteAction(string $actionId, ?User $user = null): bool{$user = $user ?? wire('user');$actionPermissionName = $this->getActionPermissionName($actionId);$hasModulePermission = $user->hasPermission(self::MODULE_PERMISSION);$actionPermissionExists = $this->permissions->has($actionPermissionName);// the user "lacks" the permissions for this action only if it exists and they don't have it$lacksActionPermission = $actionPermissionExists && !$user->hasPermission($actionPermissionName);return $hasModulePermission && !$lacksActionPermission;}/*** Get the name of the optional permission for the passed action ID.** @param string $actionId The ID to get the permission name for.* @return string*/public function getActionPermissionName(string $actionId): string{return sprintf(self::ACTION_PERMISSION_FORMAT,self::MODULE_PERMISSION,$actionId);}/*** Find an action definition by it's ID.** @param string $action The ID of the action to find.* @return array|null*/public function getActionDefinitionById(string $action): ?array{foreach ($this->getActionDefinitions() as $actionDefinition) {if ($actionDefinition['id'] === $action) {return $actionDefinition;}}return null;}/*** Check if the specified action exists.** @param string $actionId The ID of the action to check.* @return boolean*/public function actionExists(string $actionId): bool{return $this->getActionDefinitionById($actionId) !== null;}/*** Get the URL of the Process Page in the backend, or a URL that directly* executes an action if an $actionId is provided.** @param string|null $actionId Optional ID of the action to get an URL for.* @return string*/public function getProcessUrl(?string $actionId = null): string{$ProcessPage = wire('pages')->get(2)->findOne('name=cache-control, include=all');return $ProcessPage->url(['data' => $actionId ? [self::ACTION_PARAMETER_NAME => $actionId] : []]);}/*** Called by ProcessWire during module installation.** @return void*/public function ___install(){// save an installation message to force creation of the cache-control log file if it doesnt exist yet$tools = $this->modules->get('CacheControlTools');$tools->logMessage($this->_('ProcessCacheControl installed successfully. Creating the dedicated log file.'));return parent::___install();}/*** The main Process that gets executed on the module's page. Performs the* action specified as a GET parameter (if any) and outputs all new log* messages. Will also display buttons for all available cache actions.** @return void*/public function ___execute(){// save the current amount of log messages, so we can determine all new// log messages after executing the cache clear action$previousLogTotal = $this->log->getTotalEntries(CacheControlTools::LOG_NAME);// if the URL specifies an action, execute it$actionId = $this->wire('input')->get(self::ACTION_PARAMETER_NAME);if ($actionId) {$actionExists = $this->actionExists($actionId);$userCanExecute = $actionExists && $this->canExecuteAction($actionId, $this->user);$actionDisplayName = $actionExists ? $this->getActionDefinitionById($actionId)['title'] : $actionId;if (!$actionExists) {$this->error(sprintf($this->_("Requested action '%s' does not exist."), $actionDisplayName));} elseif (!$userCanExecute) {$this->error(sprintf($this->_("You lack the permission to execute the action '%s'."), $actionDisplayName));} else {$this->executeAction($actionId);$this->message(sprintf($this->_("Successfully executed action '%s'. Check the log output below for more information."), $actionDisplayName));}}// get all new log messages that were added by the cache clear action$newLogTotal = $this->log->getTotalEntries(CacheControlTools::LOG_NAME);$newLogEntries = $newLogTotal > $previousLogTotal? array_reverse($this->log->getEntries(CacheControlTools::LOG_NAME, ['limit' => $newLogTotal - $previousLogTotal,])): null;// the following code generates the module page output// list available actions$buttonWrapper = $this->modules->get('InputfieldFieldset');$buttonWrapper->label = $this->_('Available Cache Control Actions');$buttonWrapper->columnWidth = 50;$buttonWrapper->collapsed = Inputfield::collapsedNever;foreach ($this->getAllowedActions() as $actionDefinition) {$action = $this->modules->get('InputfieldButton');$action->value = $actionDefinition['title'];$action->href = $this->page->url(['data' => ['action' => $actionDefinition['id']]]);$buttonWrapper->add($action);}// list new log entries$logOutput = $this->modules->get('InputfieldMarkup');$logOutput->label = $this->_('Log output for the current action');$logOutput->columnWidth = 50;$logOutput->collapsed = Inputfield::collapsedNever;if ($newLogEntries) {$logOutput->value = "<code><pre>" . implode("\n", array_map(function ($line) {return $line['text'];}, $newLogEntries)) . "</code></pre>";} else {$logOutput->value = sprintf("<p>%s</p>", $this->_('No new log entries. Start a job by selecting one of the options to the left.'));}// wrap everything inside a form inputfield$actionForm = $this->modules->get('InputfieldForm');$actionForm->add($buttonWrapper);$actionForm->add($logOutput);return $actionForm->render();}/*** Called by ProcessWire to create the flyout menu in the ProcessWire Admin.** @param array $options* @return void*/public function ___executeNavJSON(array $options = []){$options['itemLabel'] = 'title';$options['add'] = false;$options['edit'] = sprintf("?%s={id}", self::ACTION_PARAMETER_NAME);$options['items'] = $this->getAllowedActions();return parent::___executeNavJSON($options);}/*** Parses a list of newline-seperated cache namespaces, filtering out blank lines.** @param string $namespaces The multiline string to parse.* @return array*/protected function parseNamespaceList(string $namespaces): array{return array_filter(preg_split("/[\r\n]+/", $namespaces),function ($namespace) {return !empty($namespace);});}}