From 27fca8b738e68a7a55b376fa71460e3cd4e91322 Mon Sep 17 00:00:00 2001 From: EC2 Default User Date: Thu, 18 Jul 2019 07:20:05 +0000 Subject: [PATCH] first commit --- README.md | 194 ++++++ composer.json | 7 + includes/cache_purge_rule.ui.inc | 82 +++ nexteuropa_varnish.admin.inc | 153 +++++ nexteuropa_varnish.drush.inc | 30 + nexteuropa_varnish.helper.inc | 88 +++ nexteuropa_varnish.info | 13 + nexteuropa_varnish.install | 182 ++++++ nexteuropa_varnish.module | 918 +++++++++++++++++++++++++++++ nexteuropa_varnish.rules.admin.inc | 210 +++++++ phpunit.xml.dist | 8 + src/Entity/PurgeRule.php | 58 ++ src/PurgeRuleController.php | 50 ++ src/PurgeRuleType.php | 54 ++ src/Tests/.htaccess | 1 + src/Tests/HelperMethodsTest.php | 73 +++ src/Tests/bootstrap.php | 12 + 17 files changed, 2133 insertions(+) create mode 100644 README.md create mode 100644 composer.json create mode 100644 includes/cache_purge_rule.ui.inc create mode 100644 nexteuropa_varnish.admin.inc create mode 100644 nexteuropa_varnish.drush.inc create mode 100644 nexteuropa_varnish.helper.inc create mode 100644 nexteuropa_varnish.info create mode 100644 nexteuropa_varnish.install create mode 100644 nexteuropa_varnish.module create mode 100644 nexteuropa_varnish.rules.admin.inc create mode 100644 phpunit.xml.dist create mode 100644 src/Entity/PurgeRule.php create mode 100644 src/PurgeRuleController.php create mode 100644 src/PurgeRuleType.php create mode 100644 src/Tests/.htaccess create mode 100644 src/Tests/HelperMethodsTest.php create mode 100644 src/Tests/bootstrap.php diff --git a/README.md b/README.md new file mode 100644 index 0000000..b658e0f --- /dev/null +++ b/README.md @@ -0,0 +1,194 @@ +Table of Contents +- [Nexteuropa Varnish](#nexteuropa-varnish) + - [Requirements](#requirements) + - [General settings](#general-settings-first-tab) + - [Purge rules](#purge-rules-second-tab) + - [Rules logic](#purge-rules-logic) + - [Tests](#tests-and-custom-behat-feature-context) + - [Developer's notes](#developers-notes) + - [Blocking of the rules](#blocking-temporary-the-purge-mechanism) + - [Varnish Mock](#testing-varnish-calls) + + +# Nexteuropa Varnish + +Varnish is a very fast reverse-proxy system which serves static +files and anonymous page views based on the previously processed +requests. +The Nexteuropa Varnish module provides functionality which allows to +send customized HTTP request to the Varnish server based on the +configured 'purge rules' in the form of regular expressions. +The main purpose of those requests is to invalidate the Varnish cache to +display recently published content changes. + +## Requirements +This feature can be enabled only with the support of the QA/Maintenance +team. + +The following environment specific variables have to be configured +before enabling the feature: +``` + 'nexteuropa_varnish_request_user' - a string with the username + 'nexteuropa_varnish_request_password' - a string with the password + 'nexteuropa_varnish_http_targets', - an array with the urls ex. 'http://localhost' + 'nexteuropa_varnish_tag', - a string with the tag + 'nexteuropa_varnish_request_method' - a string with the HTTP request method + 'nexteuropa_varnish_http_timeout' - a float representing the maximum number + of seconds the function call may take (by default 2.0) +``` + +In order to enable the feature make sure that above variables are set +and if so then go to the `admin/structure/feature-set` page, +select the 'Rule-based web frontend cache purging' feature +and click on the 'Validate' button. + +Nexteuropa Varnish provides a 'Administer frontend cache purge rules' +permission which allows to create and maintain 'purge rules'. + +## General settings (First Tab) +Configuration page is located here `admin/config/system/nexteuropa-varnish`. + +### "Purge all caches" Button + +The configuration page provides a "Purge all caches" button. + +Once it is clicked, this button will trigger: +- The cleaning of the "Drupal cache" if **Clear drupal cache as well** is also ticked; +- The purging of **ALL** site's entries indexed in the Varnish cache. + +Checking "Drupal cache" has impact on the site's performance as it forces +Drupal to rebuild all requested pages, use cautiously. + +### Default rule "Enable the default purge rule" +See description in UI. + +## "Purge rules" (Second Tab) +The module provides an custom entity type allowing to define purge rules: + +`Purge rule - machine name: 'nexteuropa_varnish_cache_purge_rule'` + +Click the **'Add cache purge rule'** link. +You will be redirected to the **'Add cache purge rule'** form. + +### Content type + +The `Content type` select box allows to limit triggering a rule to one type of *node*. +For example, you want to trigger the clearing of `my-news` only when content of type `news` is added, +edited or removed. + +Selecting 'All' will apply to all node types. + +### What should be purged +#### Purge the edited node ! +If the default purge rule is disabled, the option **`Paths of the node the action is performed on`** will appear. + +Just save it. The behaviour is similar to the default, except you can restrict to content types. +#### Purge nodes that match this regex! +The option **`A specific list of regex`** allows defining a set of regex matching the paths you want to clear. + +The field description provides hints for testing validity of the regex you entered in the field. + +The `Check scope` button allows evaluating if the regex is built to return the paths you would expect to clear. + +After setting up a rule you need to submit it by clicking the **'Save'** button. +If the regular expression you entered is not valid, the following warning will be shown +``` +Regex is invalid. +Please check your expression at the Regex101 page. +``` + +After the creation of a new rule you will be redirected to the page with the list of rules. +From that page you can use option to add a new rule or edit, delete existing rules. + +## Purge rules logic +The Nexteuropa Varnish provides hardcoded logic for triggering +configured rules. Current version implements two workflow cases for: +- content types moderated via the workbench moderation module +- content types without additional moderation (default Drupal settings) + +### Content moderated via the workbench moderation module +For the content types which are controlled via the workbench moderation +module, created purge rules will be triggered in the following cases: +- when a given content has a workflow state change to 'Published' +- when a given content has a workflow state change from 'Published' to any other + +### Content without moderation +For the content types which are not moderated (default Drupal content +type with two states: published and unpublished), created purge rules +will be triggered in the following cases: +- when a node of the given content type is created and saved with the 'Publish' state +- when a published node of the given content type is updated + +## Tests and custom Behat Feature Context +The Nexteuropa Varnish provides complete a Behat test suite and additional +Feature Context located in the FrontendCacheContext class. + +Tests are performed against a mocked HTTP server. The only difference is that +the mocked HTTP server doesn't support 'PURGE' method and uses +the 'POST' method instead. + +You can find the Behat scenarios in the frontend_cache_purge.feature file +located under the test folder. + +## Developer's notes +### Specificities +Nexteuropa Varnish uses the https://www.drupal.org/project/chr module +which overrides the default `drupal_http_request()` function. + +A custom patch was created for this specific feature +The patch can be found [here](https://www.drupal.org/files/issues/chr-purge-2825701-2.patch) + +The patch adds the 'PURGE' HTTP method, which is commonly used by systems such +as Varnish, Squid and SAAS CDNs like Fastly to clear cached versions of +certain paths. + +All of HTTP requests are send by the `_nexteuropa_varnish_purge_paths()` +function. + +### Blocking temporary the purge mechanism + +Next Europa Varnish feature provides a feature to prevent from sending all supported purge requests. + +To do so, the following line must be added to the settings file: +`$conf['nexteuropa_varnish_prevent_purge'] = TRUE;` + +Once it is added, no purge request will be sent to Varnish and the "Purge all caches" button will be disabled. + +Nevertheless, it is still possible to manage the purge rules during the blocking period. + +### Testing varnish calls locally + +In order to test varnish on C9 environement, please perform these steps: + +1. Make sure your environement is up to date. See FAQs on confluence for more information or type : +``` +sudo salt-call state.apply tools.varnish-mock +``` + +2. Add this to your settings.php file +``` +$conf['nexteuropa_varnish_request_method'] = "PURGE"; +$conf['nexteuropa_varnish_http_targets'] = array ("http://127.0.0.1:6081"); +$conf['nexteuropa_varnish_tag'] = "drupal-760"; +$conf['nexteuropa_varnish_request_user'] = "user"; +$conf['nexteuropa_varnish_request_password'] = "password"; +$conf['nexteuropa_varnish_http_timeout'] = "30"; +``` +3. Launch the mock +```varnish-mock``` + +4. By default, the mock prints information on console. Output file can be used with the "-filePath" parameter: +Type ```varnish-mock -h``` to get help + +Please note that the value of the `base_path` variable on your site has an impact on the `path of the node` sent. + +### Upgrading from a previous version +Because previous versions of the module did not support fully regular expressions , the validation of the rules +exceptions have been added upon saving a new rule **and** upon triggering a rule. +Therefore, if you entered a rule in a previous version and this rule does not match the more strict criterias now +in place, a warning will appear : +``` +Please check your varnish rules , the regex ^an-already-inserted/rule you are trying to flush is not valid. +We suggest you review and save your regex rules again using the documentation available and the +"Check Scope" button. In case of doubt, please contact your site administrator or the devops team. +``` diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..6054bfd --- /dev/null +++ b/composer.json @@ -0,0 +1,7 @@ +{ + "name": "ec-europa/digit-ne-varnish", + "description": "The module provides functionality which allows to send customized HTTP request to the Varnish server.", + "type": "drupal-module", + "license": "EUPL-1.1", + "homepage": "https://github.com/ec-europa/digit-ne-varnish", +} \ No newline at end of file diff --git a/includes/cache_purge_rule.ui.inc b/includes/cache_purge_rule.ui.inc new file mode 100644 index 0000000..aedd709 --- /dev/null +++ b/includes/cache_purge_rule.ui.inc @@ -0,0 +1,82 @@ +' . t('paths of the node') . ''; + + if ($entity->type() == PurgeRuleType::PATHS) { + $paths_cell_content = check_plain(implode(', ', $entity->paths())); + } + array_unshift($additional_cols, $paths_cell_content); + $all = array('all' => 'All'); + $content_type_names = $all + node_type_get_names(); + $content_type_label = $content_type_names[$entity->content_type]; + array_unshift($additional_cols, $content_type_label); + + $row = parent::overviewTableRow($conditions, $id, $entity, $additional_cols); + + // We remove the 'Label' row, because our entity has no meaningful label. + array_shift($row); + + return $row; + } + + /** + * {@inheritdoc} + */ + public function overviewTable($conditions = array()) { + + // If the purge mechanism is prevented to work, then a warning message must + // be displayed. + _nexteuropa_varnish_temporary_message(); + + $render = parent::overviewTable($conditions); + + // Add a unique id to the table. This will make it easier to target it + // in acceptance tests. + $render['#attributes']['id'] = 'frontend-cache-purge-rules'; + + return $render; + } + + /** + * {@inheritdoc} + */ + public function hook_menu() { + $items = parent::hook_menu(); + $items[$this->path]['title'] = t('Purge rules'); + $items[$this->path]['type'] = MENU_LOCAL_TASK; + + return $items; + } + +} diff --git a/nexteuropa_varnish.admin.inc b/nexteuropa_varnish.admin.inc new file mode 100644 index 0000000..84a27ca --- /dev/null +++ b/nexteuropa_varnish.admin.inc @@ -0,0 +1,153 @@ + 'fieldset', + '#title' => t('Purge caches'), + '#description' => t('The operation will clear Varnish cache. + Click the checkbox to also clear Drupal cache.'), + ); + + // If the purge mechanism is prevented to work, then the button must be + // disabled and a warning message must be displayed. + $disabled = _nexteuropa_varnish_temporary_message(); + + $form['purge_cache']['purge_drupal'] = array( + '#type' => 'checkbox', + '#title' => t('Clear drupal cache as well'), + '#return_value' => 1, + '#default_value' => 0, + ); + + $form['purge_cache']['purge'] = array( + '#type' => 'submit', + '#value' => t('Purge caches'), + '#submit' => array('nexteuropa_varnish_purge_all_confirm'), + '#disabled' => $disabled, + ); + + $form['settings'] = array( + '#type' => 'fieldset', + '#title' => t('Settings'), + ); + + $form['settings']['nexteuropa_varnish_default_purge_rule'] = array( + '#type' => 'checkbox', + '#title' => t('Enable the default purge rule'), + '#description' => t('Activates the default purge rule for all content types. + The rule invalidates the Varnish cache entries whenever content changes + are having an impact on the published/unpublished state.'), + '#default_value' => variable_get('nexteuropa_varnish_default_purge_rule', FALSE), + ); + + $form['#submit'][] = 'nexteuropa_varnish_admin_settings_cache_clear_submit'; + return system_settings_form($form); +} + +/** + * Submit callback for clearing the purge rules cache table. + * + * @see nexteuropa_varnish_admin_settings_form() + */ +function nexteuropa_varnish_admin_settings_cache_clear_submit($form, &$form_state) { + cache_clear_all('*', NEXTEUROPA_VARNISH_CACHE_TABLE, TRUE); +} + +/** + * Redirect the user from "General" page to the "purge all" confirmation form. + */ +function nexteuropa_varnish_purge_all_confirm($form, &$form_state) { + if ($form_state['values']['purge_drupal']) { + $form_state['redirect'] = 'admin/config/system/nexteuropa-varnish/purge/all'; + } + else { + $form_state['redirect'] = 'admin/config/system/nexteuropa-varnish/purge/varnish'; + } +} + +/** + * Generates the form allowing triggering the full cache purge. + */ +function nexteuropa_varnish_purge_all_form($form, &$form_state, $arg) { + $description = t("The action you are about to perform has a deep impact on the site's performance!"); + $arg == 'all' ? $type = "Varnish and Drupal" : $type = "Varnish"; + $confirm_message = t("Are you sure you want to purge !type cache ?", array('!type' => $type)); + return confirm_form($form, + $confirm_message, + 'admin/config/system/nexteuropa-varnish/general', + $description, + t('Continue'), + t('Cancel') + ); +} + +/** + * Processes the flush cache triggered by the "Purge caches" button. + * + * @see nexteuropa_varnish_admin_settings_form() + */ +function nexteuropa_varnish_purge_all_form_submit($form, &$form_state) { + $arg = $form_state['build_info']['args'][0]; + $arg == 'all' ? $type = "Drupal and Varnish" : $type = "Varnish"; + $confirm_message = t('The !type caches have been fully flushed.', array('!type' => $type)); + $message_level = 'status'; + // First clear the actual backend cache (usually DrupalDatabaseCache). + // Otherwise the web frontend cache will receive again outdated cached + // versions of pages. + if ($arg == 'all') { + drupal_flush_all_caches(); + } + + // Treating Varnish flushing (inspired by "flexibe_purge" contrib module). + $send_success = _nexteuropa_varnish_varnish_requests_send(); + if (!$send_success) { + $confirm_message = t('The Varnish caches have not been purged correctly. Please consult logs for more information.'); + $message_level = 'error'; + } + + drupal_set_message($confirm_message, $message_level); + $form_state['redirect'] = 'admin/config/system/nexteuropa-varnish/general'; +} + +/** + * Implements hook_FORM_ID_form_validate(). + */ +function nexteuropa_varnish_admin_settings_form_validate($form, &$form_state) { + $rule_state = $form_state['values']['nexteuropa_varnish_default_purge_rule']; + if ($rule_state && _nexteuropa_varnish_check_node_rules()) { + form_set_error( + 'settings', + t('You can not enable the default purge rule while "Purge rules" of type "node" exist.') + ); + } +} + +/** + * Checks if rules type of node exist. + * + * @return bool + * TRUE / FALSE depends on the results of the query. + */ +function _nexteuropa_varnish_check_node_rules() { + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'nexteuropa_varnish_cache_purge_rule') + ->propertyCondition('paths', ''); + + $result = $query->execute(); + + return isset($result['nexteuropa_varnish_cache_purge_rule']); +} diff --git a/nexteuropa_varnish.drush.inc b/nexteuropa_varnish.drush.inc new file mode 100644 index 0000000..0115f99 --- /dev/null +++ b/nexteuropa_varnish.drush.inc @@ -0,0 +1,30 @@ + "Varnish clear cache.", + 'examples' => array( + 'drush varnish-flush-cache' => 'Varnish clear cache.', + ), + 'aliases' => array('vcc'), + ); + + return $items; +} + +/** + * Varnish clear cache. + */ +function drush_nexteuropa_varnish_varnish_flush_cache() { + _nexteuropa_varnish_varnish_requests_send(); +} diff --git a/nexteuropa_varnish.helper.inc b/nexteuropa_varnish.helper.inc new file mode 100644 index 0000000..b633c58 --- /dev/null +++ b/nexteuropa_varnish.helper.inc @@ -0,0 +1,88 @@ + $var_item_name) { + $value = variable_get($param_name); + + // In case of "nexteuropa_varnish_http_targets" contains only one value, + // we ensure that it is an array anyway. + if (($param_name == 'nexteuropa_varnish_http_targets') && (!empty($value) && !is_array($value))) { + $value = array($value); + } + $settings[$var_item_name] = $value; + } + + if ($validity_control && !(_nexteuropa_varnish_check_configuration($settings))) { + throw new InvalidArgumentException(t('The module is not correctly set.')); + } + } + return $settings; +} + +/** + * Check the module settings to see if they are correctly defined. + * + * @param array $settings + * The module settings to check. + * + * @return bool + * FALSE if at least one of the parameters is not correctly defined; + * otherwise TRUE. + */ +function _nexteuropa_varnish_check_configuration($settings) { + $all_configurations = _nexteuropa_varnish_get_settings_item_names(); + foreach ($all_configurations as $configuration) { + if (empty($settings[$configuration])) { + return FALSE; + } + } + + return TRUE; +} + +/** + * Gets the key names used in the module settings array. + * + * @return array + * The key names of settings array. The array is keyed by the actual variable + * name as defined the "settings.php" file. + */ +function _nexteuropa_varnish_get_settings_item_names() { + return array( + 'nexteuropa_varnish_request_method' => 'varnish_request_method', + 'nexteuropa_varnish_http_targets' => 'varnish_http_targets', + 'nexteuropa_varnish_tag' => 'varnish_tag', + 'nexteuropa_varnish_request_user' => 'varnish_request_user', + 'nexteuropa_varnish_request_password' => 'varnish_request_password', + 'nexteuropa_varnish_http_timeout' => 'varnish_http_timeout', + ); +} diff --git a/nexteuropa_varnish.info b/nexteuropa_varnish.info new file mode 100644 index 0000000..fd905e3 --- /dev/null +++ b/nexteuropa_varnish.info @@ -0,0 +1,13 @@ +name = NextEuropa Varnish +description = Custom Varnish rules and actions. +core = 7.x +package = NextEuropa + +dependencies[] = workbench_moderation +dependencies[] = entity +dependencies[] = registry_autoload + +configure = admin/config/system/nexteuropa-varnish +registry_autoload[] = PSR-4 + +files[] = includes/cache_purge_rule.ui.inc diff --git a/nexteuropa_varnish.install b/nexteuropa_varnish.install new file mode 100644 index 0000000..807530b --- /dev/null +++ b/nexteuropa_varnish.install @@ -0,0 +1,182 @@ + 'Purge rules', + 'fields' => array( + 'id' => array( + 'type' => 'serial', + 'not null' => TRUE, + 'description' => 'Primary Key: Unique purge rule ID.', + ), + 'content_type' => array( + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'description' => 'The machine name of the content type a rule applies for.', + ), + 'paths' => array( + 'type' => 'text', + 'description' => 'Paths to purge, one per line.', + ), + ), + 'primary key' => array('id'), + ); + + $schema[NEXTEUROPA_VARNISH_CACHE_TABLE] = drupal_get_schema_unprocessed('system', 'cache'); + $schema[NEXTEUROPA_VARNISH_CACHE_TABLE]['description'] = 'Cache table for the nexteuropa_varnish purge rules.'; + + return $schema; +} + +/** + * Implements hook_requirements(). + * + * At installation and run time phase, All Varnish feature settings' parameters + * must be set. + * The control status returns which parameters are missing. + */ +function nexteuropa_varnish_requirements($phase) { + $requirements = array(); + $t = get_t(); + + $control_phase = array( + 'install', + 'runtime', + ); + + if (in_array($phase, $control_phase)) { + $required_vars = _nexteuropa_varnish_get_settings_item_names(); + $settings = _nexteuropa_varnish_get_varnish_settings(FALSE); + + foreach ($required_vars as $param_name => $var_item) { + if (empty($settings[$var_item])) { + $t_var = array('@var' => $param_name); + $requirements[$var_item] = array( + 'title' => $t('NextEuropa Varnish: "@var" is missing', $t_var), + 'description' => $t('"@var" must be set. Please ask your support team to check the server configuration.', $t_var), + 'severity' => REQUIREMENT_ERROR, + ); + } + } + + } + + return $requirements; +} + +/** + * Implements hook_enable(). + */ +function nexteuropa_varnish_enable() { + $administrator = user_role_load_by_name('administrator'); + + if ($administrator) { + user_role_grant_permissions( + $administrator->rid, + array( + 'administer frontend cache purge rules', + ) + ); + } +} + +/** + * Implements hook_install(). + */ +function nexteuropa_varnish_install() { + // Set the weight to 2. Pathauto has weight 1 and we need to react after it. + db_update('system') + ->fields(array('weight' => 2)) + ->condition('type', 'module') + ->condition('name', 'nexteuropa_varnish') + ->execute(); + + // Enabling the default purge rule. + variable_set('nexteuropa_varnish_default_purge_rule', TRUE); +} + +/** + * Implements hook_uninstall(). + */ +function nexteuropa_varnish_uninstall() { + $vars = array( + 'nexteuropa_varnish_default_purge_rule', + 'nexteuropa_varnish_http_targets', + 'nexteuropa_varnish_tag', + 'nexteuropa_varnish_request_method', + ); + + foreach ($vars as $var) { + variable_del($var); + } +} + +/** + * Add the cache purge rules table. + */ +function nexteuropa_varnish_update_7100() { + db_create_table( + 'nexteuropa_varnish_cache_purge_rule', + array( + 'description' => 'Purge rules', + 'fields' => array( + 'id' => array( + 'type' => 'serial', + 'not null' => TRUE, + 'description' => 'Primary Key: Unique purge rule ID.', + ), + 'content_type' => array( + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'description' => 'The machine name of the content type a rule applies for.', + ), + 'paths' => array( + 'type' => 'text', + 'description' => 'Paths to purge, one per line.', + ), + ), + 'primary key' => array('id'), + ) + ); +} + +/** + * Alter the module weight so it comes after pathauto. + */ +function nexteuropa_varnish_update_7101() { + db_update('system') + ->fields(array('weight' => 2)) + ->condition('type', 'module') + ->condition('name', 'nexteuropa_varnish') + ->execute(); +} + +/** + * NEPT-799: Add cache table for the purge rules. + */ +function nexteuropa_varnish_update_7102() { + $schema[NEXTEUROPA_VARNISH_CACHE_TABLE] = drupal_get_schema_unprocessed('system', 'cache'); + $schema[NEXTEUROPA_VARNISH_CACHE_TABLE]['description'] = 'Cache table for the purge rules of nexteuropa_varnish.'; + db_create_table(NEXTEUROPA_VARNISH_CACHE_TABLE, $schema[NEXTEUROPA_VARNISH_CACHE_TABLE]); +} diff --git a/nexteuropa_varnish.module b/nexteuropa_varnish.module new file mode 100644 index 0000000..6498f86 --- /dev/null +++ b/nexteuropa_varnish.module @@ -0,0 +1,918 @@ + 'Next Europa Varnish - Configuration', + 'description' => 'Configuration of the Varnish cache invalidation mechanism.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('nexteuropa_varnish_admin_settings_form'), + 'access arguments' => array('administer frontend cache purge rules'), + 'file' => 'nexteuropa_varnish.admin.inc', + ); + + $items['admin/config/system/nexteuropa-varnish/purge/%'] = array( + 'title' => 'Next Europa Varnish - Purge confirmation', + 'description' => 'Confirmation form for Drupal and Varnish purge.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('nexteuropa_varnish_purge_all_form', 5), + 'access callback' => '_nexteuropa_varnish_purge_all_access', + 'file' => 'nexteuropa_varnish.admin.inc', + 'type' => MENU_CALLBACK, + ); + + $items['admin/config/system/nexteuropa-varnish/general'] = array( + 'title' => 'General settings', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => -10, + ); + + return $items; +} + +/** + * Implements hook_entity_info(). + */ +function nexteuropa_varnish_entity_info() { + return array( + 'nexteuropa_varnish_cache_purge_rule' => array( + 'module' => 'nexteuropa_varnish', + 'label' => t('Cache purge rule'), + 'fieldable' => FALSE, + 'entity keys' => array( + 'id' => 'id', + ), + 'label callback' => 'nexteuropa_varnish_cache_purge_rule_label', + 'base table' => 'nexteuropa_varnish_cache_purge_rule', + 'entity class' => 'Drupal\\nexteuropa_varnish\\Entity\\PurgeRule', + 'controller class' => 'Drupal\\nexteuropa_varnish\\PurgeRuleController', + 'access callback' => 'nexteuropa_varnish_cache_purge_rule_access', + 'admin ui' => array( + 'path' => 'admin/config/system/nexteuropa-varnish/purge_rules', + 'controller class' => 'NexteuropaVarnishCachePurgeRuleEntityUIController', + 'file' => 'nexteuropa_varnish.rules.admin.inc', + ), + ), + ); +} + +/** + * Implements hook_flush_caches(). + */ +function nexteuropa_varnish_flush_caches() { + return array(NEXTEUROPA_VARNISH_CACHE_TABLE); +} + +/** + * Implements hook_permission(). + */ +function nexteuropa_varnish_permission() { + return array( + 'administer frontend cache purge rules' => array( + 'title' => 'Administer frontend cache purge rules', + 'description' => '', + ), + ); +} + +/** + * Access callback for the cache purge rule entity.. + * + * @param string $op + * The operation being performed. One of 'view', 'update', 'create', 'delete' + * or just 'edit' (being the same as 'create' or 'update'). + * @param object $cache_purge_rule + * (optional) A cache purge rule to check access for. If nothing is given, + * access for all cache purge rules is determined. + * @param object $account + * (optional) The user to check for. Leave it to NULL to check for the + * global user. + * + * @return bool + * Whether access is allowed or not. + */ +function nexteuropa_varnish_cache_purge_rule_access($op, $cache_purge_rule = NULL, $account = NULL) { + return user_access('administer frontend cache purge rules', $account); +} + +/** + * Label callback for the cache purge rule entity. + * + * @param object $purge_rule + * The cache purge rule. + */ +function nexteuropa_varnish_cache_purge_rule_label($purge_rule) { + if (isset($purge_rule->id)) { + return t('cache purge rule !id', array('!id' => $purge_rule->id)); + } + + return t('new cache purge rule'); +} + +/** + * Implements hook_workbench_moderation_transition(). + */ +function nexteuropa_varnish_workbench_moderation_transition($node, $previous_state, $new_state) { + if (_nexteuropa_varnish_prevent_purge()) { + return; + } + + // No rule defined for the node: Ignore the rest of the process. + if (!nexteuropa_varnish_node_has_rules($node)) { + return; + } + + // Paths of the previous node revision are retrieved to control if they have + // changed. + // If it is the case and the node is already published, that means some + // node URLs are not valid anymore, and their references in the Varnish index + // must be purged. + $updated_node_paths = array(); + if (empty($node->is_new) && (isset($node->workbench_moderation['published']))) { + $updated_node_paths = _nexteuropa_varnish_get_updated_node_paths($node); + } + // Something must be sent to Varnish when a new content + // revision is published. + $publishing = ($new_state == workbench_moderation_state_published()); + + // Something must be sent to Varnish when the content + // is unpublished (no published revision anymore). + $unpublishing = !$publishing && + ($node->workbench_moderation['my_revision']->published == 1)&& + ($node->status == NODE_NOT_PUBLISHED); + + if ($publishing || $unpublishing || !empty($updated_node_paths)) { + _nexteuropa_varnish_process_paths_sending($node, $updated_node_paths); + } +} + +/** + * Implements hook_node_insert(). + */ +function nexteuropa_varnish_node_insert($node) { + if ($node->status == NODE_PUBLISHED) { + nexteuropa_varnish_node_update($node); + } +} + +/** + * Implements hook_node_update(). + */ +function nexteuropa_varnish_node_update($node) { + // Ignore moderated nodes, they are handled by the workbench moderation hooks + // implemented in this module. + if (workbench_moderation_node_moderated($node)) { + return; + } + + if (_nexteuropa_varnish_prevent_purge()) { + return; + } + + // No rule defined for the node: Ignore the rest of the process. + if (!nexteuropa_varnish_node_has_rules($node)) { + return; + } + + // Ignore updates on existing, unpublished content that remains unpublished. + if (($node->status == NODE_NOT_PUBLISHED) && + (isset($node->original) && ($node->original->status == NODE_NOT_PUBLISHED)) + ) { + return; + } + + // Paths of the previous node revision are retrieved to control if they have + // changed. + // If it is the case and the node is already published, that means some + // node URLs are not valid anymore, and their references in the Varnish index + // must be purged. + $updated_node_paths = _nexteuropa_varnish_get_updated_node_paths($node); + + _nexteuropa_varnish_process_paths_sending($node, $updated_node_paths); +} + +/** + * Implements hook_node_presave(). + * + * It adds the old node paths (I.E. aliases) in the node object in order to + * make further control later in the node saving process. + * + * @see nexteuropa_varnish_workbench_moderation_transition() + * @see nexteuropa_varnish_node_update() + */ +function nexteuropa_varnish_node_presave($node) { + $node->nexteuropa_varnish_old_paths = array(); + // Even if the node has just been created it will receive an "old" path that + // corresponds to the creation form path. + // With this, old_paths must be set with values. + if (empty($node->is_new)) { + $node->nexteuropa_varnish_old_paths = _nexteuropa_varnish_get_node_paths($node); + } +} + +/** + * Implements hook_node_delete(). + */ +function nexteuropa_varnish_node_delete($node) { + // No rule defined for the node: Ignore the rest of the process. + if (!nexteuropa_varnish_node_has_rules($node)) { + return; + } + + $paths = array(); + + $entity_uri = entity_uri('node', $node); + + if (NULL !== $entity_uri) { + $url_locale = language_negotiation_get('language', 'locale-url'); + $url_suffix = language_negotiation_get('language', 'nexteuropa_multilingual_url_suffix'); + // Add logic to send a language prefix/suffix regex for non aliased paths. + $lgregex = ''; + + if ($url_locale) { + $lgregex = '[a-z]{2}\/'; + } + elseif ($url_suffix) { + $lgregex = '_[a-z]{2}'; + } + + $original_path = $entity_uri['path']; + $alias = drupal_get_path_alias($original_path); + // Get the node\/nid. + $paths['unaliased'] = preg_quote($original_path, '/'); + // Add regex prefix or suffix if needed. + // Example node\/nid_[a-z]{2}. + $paths['full_unaliased'] = $url_suffix ? $paths['unaliased'] . $lgregex : $lgregex . $paths['unaliased']; + // Add escaped alias. + // Example content\/my\-page. + $paths['escaped_alias'] = preg_quote($alias, '/'); + // Add regex prefix or suffix if needed to alias. + // Example content\/my\-page_[a-z]{2}. + $paths['alias_prefix'] = $url_suffix ? $paths['escaped_alias'] . $lgregex : $lgregex . $paths['escaped_alias']; + } + + // Add custom purge rules. + $purge_rules = nexteuropa_varnish_get_node_purge_rules($node); + foreach ($purge_rules as $purge_rule) { + if ($purge_rule->type() == PurgeRuleType::PATHS) { + $paths = array_merge($paths, $purge_rule->paths()); + } + } + + if (!empty($paths)) { + $paths = array_unique($paths); + $paths = array_filter($paths); + _nexteuropa_varnish_purge_paths($paths); + } +} + +/** + * Implements hook_path_delete(). + */ +function nexteuropa_varnish_path_delete($path) { + if (empty($path)) { + return; + } + + $node = menu_get_object('node', 1, $path['source']); + + // Its a node path. + if (empty($node)) { + return; + } + + // No rule defined for the node: Ignore the rest of the process. + if (!nexteuropa_varnish_node_has_rules($node)) { + return; + } + + $url_locale = language_negotiation_get('language', 'locale-url'); + $url_suffix = language_negotiation_get('language', 'nexteuropa_multilingual_url_suffix'); + // Add logic to send a language prefix/suffix regex for non aliased paths. + $lgregex = ''; + + if ($url_locale) { + $lgregex = '[a-z]{2}\/'; + } + elseif ($url_suffix) { + $lgregex = '_[a-z]{2}'; + } + + // Escaped alias. + // Example content\/my\-page. + $escaped_alias = preg_quote($path['alias'], '/'); + // Add regex prefix or suffix if needed. + $paths['alias_prefix'] = $url_suffix ? $escaped_alias . $lgregex : $lgregex . $escaped_alias; + $paths['alias'] = $escaped_alias; + + if (!empty($paths)) { + $paths = array_unique($paths); + $paths = array_filter($paths); + _nexteuropa_varnish_purge_paths($paths); + } +} + +/** + * Gets purge rules to apply for a specific node. + * + * @param object $node + * The node for which the rules are to be retrieved. + * + * @return array|bool + * An array of nexteuropa_varnish_cache_purge_rule objects indexed by their + * ids. When no results are found, an empty array is returned. + */ +function _nexteuropa_varnish_get_paths_to_purge($node) { + $paths = array(); + + $exist_node_path_rule = FALSE; + + $purge_rules = nexteuropa_varnish_get_node_purge_rules($node); + foreach ($purge_rules as $purge_rule) { + if ($purge_rule->type() == PurgeRuleType::PATHS) { + $paths = array_merge($paths, $purge_rule->paths()); + } + else { + $exist_node_path_rule = TRUE; + } + } + + $is_default_rule = variable_get('nexteuropa_varnish_default_purge_rule', FALSE); + + if (($exist_node_path_rule || $is_default_rule)) { + $node_paths = _nexteuropa_varnish_get_node_paths($node); + $paths = array_merge($paths, $node_paths); + } + + // NEPT-1163: Removing base path from the collected paths. + $paths = array_map('_nexteuropa_varnish_trim_base_path', $paths); + return $paths; +} + +/** + * Get the paths to purge for a specific node. + * + * @param object $node + * The node. + * + * @return string[] + * The paths to purge for this node. + */ +function nexteuropa_varnish_get_node_purge_rules($node) { + $cache_id = __FUNCTION__ . '_' . $node->type; + $rules = &drupal_static($cache_id, array()); + + if (!empty($rules)) { + return $rules; + } + + if ($cache = cache_get($cache_id, NEXTEUROPA_VARNISH_CACHE_TABLE)) { + $rules = $cache->data; + return $rules; + } + $type = $node->type; + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'nexteuropa_varnish_cache_purge_rule') + ->addTag('allow_all_entity') + ->addMetaData('type', $type); + $result = $query->execute(); + + if (isset($result['nexteuropa_varnish_cache_purge_rule'])) { + $rules = entity_load( + 'nexteuropa_varnish_cache_purge_rule', + array_keys($result['nexteuropa_varnish_cache_purge_rule']) + ); + + cache_set($cache_id, $rules, NEXTEUROPA_VARNISH_CACHE_TABLE, CACHE_TEMPORARY); + } + + return $rules; +} + +/** + * Implements hook_query_TAG_alter(). + * + * Alters the query for finding rules applying to all. + */ +function nexteuropa_varnish_query_allow_all_entity_alter(QueryAlterableInterface $query) { + $type = $query->getMetaData('type'); + $or = db_or()->condition('content_type', 'all')->condition('content_type', $type); + $query->condition($or); +} + +/** + * Purges certain paths on the web frontend cache. + */ +function _nexteuropa_varnish_purge_paths($paths) { + // First clear the actual backend page cache (usually DrupalDatabaseCache). + // Otherwise the web frontend cache will receive again + // outdated cached versions of pages. + // Be aware that this clears the full page cache instead of clearing only + // certain pages. To make it more precise, we need to have full knowledge of + // all possible base URLs. This can be improved later on. + cache_clear_all('*', 'cache_page', TRUE); + + // Escape the paths for usage in a regular expression. + $escaped_paths = array_map( + function ($regex) { + // Check if the regular expression is valid. + if (@preg_match('/' . $regex . '/', NULL) !== FALSE) { + return $regex; + } + else { + drupal_set_message( + t( + 'Please !check_config, the regex %regex you are trying to flush is not valid. We suggest you review and save your regex rules again using the documentation available and the "Check Scope" button. In case of doubt, please contact your site administrator or the devops team.', + array( + '!check_config' => l( + t('check your varnish rules'), + 'admin/config/system/nexteuropa-varnish/purge_rules'), + '%regex' => $regex, + '@doc' => "https://github.com/ec-europa/platform-dev/tree/nept-1545-2.5/profiles/common/modules/custom/nexteuropa_varnish#purge-nodes-that-match-this-regex", + ) + ), + 'error'); + } + }, + $paths + ); + watchdog( + 'nexteuropa_varnish', + 'Clearing paths: @paths', + array( + '@paths' => implode(', ', $escaped_paths), + ), + WATCHDOG_INFO + ); + _nexteuropa_varnish_varnish_requests_send($escaped_paths); +} + +/** + * Sends the purge HTTP requests to the different Varnish servers. + * + * The servers that must receive purging request are defined in the module's + * settings. + * + * @param array $path_regexp_rules + * The purge URL purge rule(s) (X-Invalidate-Regexp) sent to Varnish. + * Rules must contain escaped paths. + * + * @return bool + * TRUE if all requests have been sent successfully; otherwise FALSE. + */ +function _nexteuropa_varnish_varnish_requests_send($path_regexp_rules = array()) { + try { + $settings = _nexteuropa_varnish_get_varnish_settings(); + + $request_options = _nexteuropa_varnish_prepare_request_options($path_regexp_rules, $settings); + + // Sending the HTTP requests to the Varnish server(s) + $http_targets = $settings['varnish_http_targets']; + $request_path = '/invalidate'; + $requests_ok = TRUE; + foreach ($http_targets as $target) { + $url = sprintf('%s%s', $target, $request_path); + $response = drupal_http_request( + $url, + $request_options + ); + + if (isset($response->error)) { + watchdog( + 'nexteuropa_varnish', + 'Clear operation failed for target %target: %code %error', + array( + '%target' => $target, + '%error' => $response->error, + '%code' => $response->code, + ), + WATCHDOG_ERROR + ); + $requests_ok = FALSE; + } + } + + return $requests_ok; + } + catch (InvalidArgumentException $iae) { + $log_link = l(t('status report page'), 'admin/reports/status_en'); + watchdog( + 'nexteuropa_varnish', + 'No path has been sent for clearing because all module settings are not set.', + array(), + WATCHDOG_CRITICAL, + $log_link + ); + return FALSE; + } +} + +/** + * Prepares Options use of the HTTP requests sent to the Varnish servers. + * + * @param array $path_regexp_rules + * The purge URL purge rule(s) (X-Invalidate-Regexp) sent to Varnish. + * Rules must contain escaped paths. + * @param array $settings + * The authentication attributes to inject in the request header; I.E.: + * - varnish_request_user: the Varnish authentication user; + * - varnish_request_password:the Varnish authentication password; + * - varnish_tag: Site's Varnish tag; + * - varnish_request_method: The HTTP method to use with the request; + * - varnish_http_timeout: The timeout to set for the request. + * If empty, the values set in the module configuration are used. + * + * @return array + * The HTTP request options have the following elements: + * - headers: An array containing request headers with the Varnish + * "X-Invalidate" parameters. + * - method: A string containing the request method as define in the module + * configuration. + * - timeout: A float representing the request time out expressed in seconds, + * as define in the module configuration. + * + * @throws \InvalidArgumentException + * If the settings are incomplete. + * + * @see drupal_http_request() + */ +function _nexteuropa_varnish_prepare_request_options($path_regexp_rules = array(), $settings = array()) { + if (!$settings) { + $settings = _nexteuropa_varnish_get_varnish_settings(); + } + $additional_headers = array(); + + // Preparing the requests for Varnish server(s). + $base_request_headers = _nexteuropa_varnish_base_request_headers($settings); + + // Prepare values for 'X-Invalidate-Type' and 'X-Invalidate-Regexp' on the + // basis of $path_regexp_rules. + if (!empty($path_regexp_rules)) { + $invalidate_type = 'regexp'; + $sent_regexp = "^(" . implode('|', $path_regexp_rules) . ")$"; + $additional_headers = array( + 'X-Invalidate-Regexp' => $sent_regexp, + ); + } + else { + $invalidate_type = 'full'; + }; + + $request_headers = $base_request_headers + $additional_headers + array( + 'X-Invalidate-Tag' => $settings['varnish_tag'], + 'X-Invalidate-Type' => $invalidate_type, + ); + + $request_method = $settings['varnish_request_method']; + + return array( + 'headers' => $request_headers, + 'method' => $request_method, + 'timeout' => $settings['varnish_http_timeout'], + ); +} + +/** + * Get the base headers for the web frontend cache purge requests. + * + * @param array $settings + * The authentication attributes to inject in the request header; I.E.: + * - varnish_request_user: the Varnish authentication user; + * - varnish_request_password:the Varnish authentication password; + * If empty, the values set in the module configuration are used. + * + * @return array + * Key value pairs of request headers. + * + * @throws \InvalidArgumentException + * If the settings are incomplete. + */ +function _nexteuropa_varnish_base_request_headers($settings = array()) { + $headers = array(); + + if (!$settings) { + $settings = _nexteuropa_varnish_get_varnish_settings(); + } + + $request_user = $settings['varnish_request_user']; + $request_password = $settings['varnish_request_password']; + + if ($request_user && $request_password) { + $basic_auth = base64_encode("{$request_user}:{$request_password}"); + + $headers['Authorization'] = 'Basic ' . $basic_auth; + } + + return $headers; +} + +/** + * Returns paths for the node including translations. + * + * @param object $node + * Node object. + * + * @return array + * An array with the paths of the node. + */ +function _nexteuropa_varnish_get_node_paths($node) { + $paths = array(); + $original_alias = ''; + + $entity_uri = entity_uri('node', $node); + + if (NULL !== $entity_uri) { + $url_locale = language_negotiation_get('language', 'locale-url'); + $url_suffix = language_negotiation_get('language', 'nexteuropa_multilingual_url_suffix'); + // Add logic to send a language prefix/suffix regex for non aliased paths. + $lgregex = ''; + + if ($url_locale) { + $lgregex = '[a-z]{2}\/'; + } + elseif ($url_suffix) { + $lgregex = '_[a-z]{2}'; + } + + // Get the languages the node is translated into (entity translation). + if (module_exists('entity_translation') && entity_translation_enabled('node', $node)) { + $handler = entity_translation_get_handler('node', $node); + $translations = $handler->getTranslations(); + $lang_codes = array_keys($translations->data); + $all_languages = entity_translation_languages('node', $node); + // Small workaround for undefined language. + if (count($lang_codes) === 1 && LANGUAGE_NONE == $lang_codes[0]) { + // Push all the languages. + $lang_codes = array_keys($all_languages); + } + foreach ($lang_codes as $code) { + if (!isset($all_languages[$code])) { + continue; + } + $alias = drupal_get_path_alias($entity_uri['path'], $code); + $original_alias = $alias; + $lang = $all_languages[$code]; + // Add the prefix or suffix. + $paths[$code] = _nexteuropa_varnish_build_path($url_locale, $lang, $alias, $entity_uri); + + $alias = drupal_get_path_alias($entity_uri['path'], $code); + $original_alias = $alias; + // Add the prefix or suffix. + $paths[$code] = _nexteuropa_varnish_build_path($url_locale, $all_languages[$code], $alias, $entity_uri); + } + } + else { + // Content translation. + $lang_code = entity_language('node', $node); + $all_languages = language_list(); + $alias = drupal_get_path_alias($entity_uri['path'], $lang_code); + $original_alias = $alias; + + // Language is undefined, the content will show on all languages. + if (LANGUAGE_NONE == $lang_code) { + foreach ($all_languages as $code => $data) { + $lang = $all_languages[$code]; + // Add the prefix or suffix. + $paths[$code] = _nexteuropa_varnish_build_path($url_locale, $lang, $alias, $entity_uri); + } + } + else { + $lang = $all_languages[$lang_code]; + // Add the prefix or suffix. + $paths[$lang_code] = _nexteuropa_varnish_build_path($url_locale, $lang, $alias, $entity_uri); + } + } + // Get the node\/nid. + $paths['unaliased'] = $entity_uri['path']; + + // Add escaped alias. + // Example content\/my\-page. + $paths['escaped_alias'] = $original_alias; + $paths = array_map('_nexteuropa_varnish_escape', $paths); + + // Add regex prefix or suffix if needed. + // Example node\/nid_[a-z]{2}. + $paths['full_unaliased'] = $url_suffix ? $paths['unaliased'] . $lgregex : $lgregex . $paths['unaliased']; + } + + return $paths; +} + +/** + * Custom callback to escape all path at the end of the collection. + * + * @param string $path + * The iterative path to escape. + * + * @return string + * The escaped string. + */ +function _nexteuropa_varnish_escape($path) { + return preg_quote($path, '/'); +} + +/** + * Custom function returns alias depending of language & translation mechanism. + * + * @param string|bool $url_locale + * FALSE if locale-url language negotiation is not enabled, + * 'language-url' otherwise. + * @param object $lang + * The current node's language object. + * @param string $alias + * The current node's path alias. + * @param array $entity_uri + * The array containing the 'path' and 'options' keys used to build the URI of + * the entity. + * + * @return string + * The path incuding the prefix or suffix. + */ +function _nexteuropa_varnish_build_path($url_locale, $lang, $alias, $entity_uri) { + $options = $entity_uri['options']; + $altered_options = $options + array('language' => $lang) + array('external' => FALSE); + // Add the suffix/prefix depending negociation setup. + drupal_alter('url_outbound', $alias, $altered_options, $entity_uri['path']); + + // If we don't use prefix, outbound did the job, if not push prefix. + if (!$url_locale) { + $path = $alias; + } + else { + $path = $altered_options['prefix'] . $alias; + } + return $path; +} + +/** + * Gets node's paths that have changed since last time. + * + * @param object $node + * The node object. + * + * @return array + * The list of changed paths. + */ +function _nexteuropa_varnish_get_updated_node_paths($node) { + // Node old_paths are injected in nexteuropa_varnish_node_presave(). + $old_paths = array(); + if (isset($node->nexteuropa_varnish_old_paths)) { + $old_paths = $node->nexteuropa_varnish_old_paths; + } + $old_paths = array_map('_nexteuropa_varnish_trim_base_path', $old_paths); + $new_paths = _nexteuropa_varnish_get_node_paths($node); + $new_paths = array_map('_nexteuropa_varnish_trim_base_path', $new_paths); + return _nexteuropa_varnish_compare_node_path_revisions($old_paths, $new_paths); +} + +/** + * Compare the old node paths with the new ones. + * + * @param array $old_paths + * The list of previous node paths. + * @param array $new_paths + * The list of new node paths. + * + * @return array + * The list of old paths that have been changed; otherwise, it returns an + * empty array. + */ +function _nexteuropa_varnish_compare_node_path_revisions($old_paths, $new_paths) { + if (empty($old_paths)) { + return array(); + } + $diff_paths = array(); + foreach ($old_paths as $lang => $old_path) { + if (!isset($new_paths[$lang])) { + $diff_paths[] = $old_path; + continue; + } + + if ($old_path !== $new_paths[$lang]) { + $diff_paths[] = $old_path; + } + } + + return $diff_paths; +} + +/** + * Prepares the list of paths and sends them to Varnish. + * + * @param object $node + * The node object for which paths are processed. + * @param array $updated_node_paths + * The list of node's paths set in the previous revision. + */ +function _nexteuropa_varnish_process_paths_sending($node, $updated_node_paths = array()) { + $paths = _nexteuropa_varnish_get_paths_to_purge($node); + $updated_node_paths = array_merge($updated_node_paths, $paths); + + if (!empty($updated_node_paths)) { + $updated_node_paths = array_unique($updated_node_paths); + $updated_node_paths = array_filter($updated_node_paths); + + _nexteuropa_varnish_purge_paths($updated_node_paths); + } +} + +/** + * Determines if purge rules are set for a specific node. + * + * @param object $node + * The node object. + * + * @return bool + * True, if at least a rule is set. + */ +function nexteuropa_varnish_node_has_rules($node) { + if (variable_get('nexteuropa_varnish_default_purge_rule', FALSE)) { + // If the default rule is set, then the node has a defined rule! + return TRUE; + } + + $rules = nexteuropa_varnish_get_node_purge_rules($node); + + return !empty($rules); +} + +/** + * Determines if the purge mechanism disabling must be forced temporarily. + * + * It is based on the "nexteuropa_varnish_prevent_purge" variable. + * If it is set and equals to TRUE, the mechanism is disabled. + * + * @return bool + * TRUE if the mechanism must be disabled; ortherwise FALSE. + */ +function _nexteuropa_varnish_prevent_purge() { + return variable_get('nexteuropa_varnish_prevent_purge', FALSE); +} + +/** + * Determine if the full varnish purge flush can be accessible or not by a user. + * + * @return bool + * TRUE, the user can access; otherwise FALSE. + */ +function _nexteuropa_varnish_purge_all_access() { + return (!(_nexteuropa_varnish_prevent_purge()) && user_access('administer frontend cache purge rules')); +} + +/** + * Sets the temporary message when the purge mechanism is disabled by force. + * + * @return bool + * TRUE if the message is set and then that means the mechanism is disabled by + * force; otherwise FALSE. + */ +function _nexteuropa_varnish_temporary_message() { + if (_nexteuropa_varnish_prevent_purge()) { + drupal_set_message( + t('The purge mechanism is temporary disabled.
Purge rules are still manageable but they will not be executed until it is enabled again.'), + 'warning', + FALSE + ); + return TRUE; + } + + return FALSE; +} + +/** + * Trims the base path from the given path. + * + * @param string $path + * Path which is going to be trimmed. + * @param string $base_path + * The base path of the current Drupal site. If NULL, the function will + * use the value given by the function base_path(). + * + * @return string + * Trimmed path. + */ +function _nexteuropa_varnish_trim_base_path($path, $base_path = NULL) { + if (is_null($base_path)) { + $base_path = base_path(); + } + + if (drupal_substr($path, 0, drupal_strlen($base_path)) === $base_path) { + $path = drupal_substr($path, drupal_strlen($base_path)); + } + + return $path; +} diff --git a/nexteuropa_varnish.rules.admin.inc b/nexteuropa_varnish.rules.admin.inc new file mode 100644 index 0000000..f833c00 --- /dev/null +++ b/nexteuropa_varnish.rules.admin.inc @@ -0,0 +1,210 @@ + 'All'); + $form['content_type'] = array( + '#title' => t('Content Type'), + '#type' => 'select', + '#empty_option' => '', + '#options' => $all + node_type_get_names(), + '#default_value' => isset($purge_rule->content_type) ? $purge_rule->content_type : '', + '#required' => TRUE, + ); + + $type_default_value = isset($purge_rule->is_new) ? PurgeRuleType::PATHS : (string) $purge_rule->type(); + + $form['rule_type'] = array( + '#title' => t('What should be purged'), + '#type' => 'radios', + '#options' => _nexteuropa_varnish_get_rule_types(), + '#limit_validation_errors' => array(), + '#default_value' => $type_default_value, + '#required' => TRUE, + '#ajax' => array( + 'callback' => 'nexteuropa_varnish_cache_purge_rule_type_selection', + 'wrapper' => 'specifics-for-cache-purge-type', + ), + ); + + // This fieldset just serves as a container for the part of the form. + // that gets rebuilt. + $form['specifics'] = array( + '#type' => 'item', + '#prefix' => '
', + '#suffix' => '
', + ); + // Code here to populate the scope container. + // @TODO views paths? + if (isset($form_state['scope'])) { + $warning = '

Here is the 100 first results matching your regex

'; + $warning .= '

Your rule was not saved, to save it click "Save" button

'; + $form['scope']['#prefix'] = '
'; + $form['scope']['#suffix'] = '
'; + $form['scope']['#markup'] = $warning . $form_state['scope']; + } + + $current_rule_type = isset($form_state['values']['rule_type']) ? $form_state['values']['rule_type'] : $type_default_value; + + $form['actions'] = array('#type' => 'actions'); + + if ($current_rule_type === PurgeRuleType::PATHS) { + $form['specifics']['paths'] = array( + '#title' => t('Paths'), + '#type' => 'textarea', + '#default_value' => isset($purge_rule->paths) ? $purge_rule->paths : '', + '#attributes' => array( + 'placeholder' => t('Enter one regular expression per line'), + ), + '#required' => TRUE, + '#description' => _nexteuropa_varnish_paths_description(), + ); + $form['specifics']['actions']['check_scope'] = array( + '#type' => 'submit', + '#value' => t('Check scope'), + '#submit' => array('_nexteuropa_varnish_check_scope'), + '#weight' => 41, + ); + $form['#validate'][] = '_nexteuropa_varnish_regex_validate'; + } + + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Save'), + '#weight' => 40, + ); + + $form['actions']['cancel'] = array( + '#type' => 'item', + '#markup' => l(t('Cancel'), 'admin/config/system/nexteuropa-varnish/purge_rules'), + '#weight' => 42, + ); + return $form; +} + +/** + * Custom validate function, checks if regex is valid. + */ +function _nexteuropa_varnish_regex_validate($form, &$form_state) { + $regex = _nexteuropa_varnish_gather_expressions($form_state['values']['paths']); + if (@preg_match('/' . $regex . '/', NULL) === FALSE) { + form_set_error('specifics', t('Regex is invalid.
Please check your expression at the + Regex101 page.', + array('@regex101' => 'https://regex101.com') + )); + } +} + +/** + * Custom function checks which site URLs match the regex pattern. + */ +function _nexteuropa_varnish_check_scope(&$form, &$form_state) { + $output = ''; + $regex = _nexteuropa_varnish_gather_expressions($form_state['values']['paths']); + $query = db_select('url_alias', 'u') + ->fields('u'); + $or = db_or(); + $or->condition('alias', $regex, 'REGEXP'); + $query->condition($or); + $query = $query->extend('PagerDefault')->limit(100); + $results = $query->execute(); + + $headers = array('URL alias'); + $rows = array(); + foreach ($results as $u) { + $rows[] = array($u->alias); + } + if (!empty($rows)) { + $output = theme('table', array('header' => $headers, 'rows' => $rows)) . theme('pager'); + } + + $form_state['rebuild'] = TRUE; + $form_state['scope'] = $output; +} + +/** + * Custom function that gather the regex entered into one single expression. + */ +function _nexteuropa_varnish_gather_expressions($paths) { + $regex = preg_split("/[\r\n]+/", $paths); + $regex = implode('|', $regex); + return $regex; +} + +/** + * Ajax callback triggered when the cache purge type is changed. + */ +function nexteuropa_varnish_cache_purge_rule_type_selection($form, $form_state) { + return $form['specifics']; +} + +/** + * Get the description for the purge paths field. + * + * @return string + * Description for the field. + */ +function _nexteuropa_varnish_paths_description() { + $regex_descriptions = array( + t('Add ^ at the begining of each path, unless you want to match using part of the path.
Example : ^content\/article\/(how-to-.*|faqs\/.*) will match content/article/how-to-use-regex and content/article/faqs/using-regex but not my-content/article/how-to-use-regex'), + t('You can test your site URLs matching the regex by clicking the Check scope button.'), + t('Regex validation is done at save. For example, if you try saving * alone, you will get an error.'), + ); + + $description = '

' . t('You can check your expression at the + Regex101 page.', + array('@regex101' => 'https://regex101.com') + ) . '

'; + $wildcard_description = array( + '#theme' => 'item_list', + '#type' => 'ul', + '#items' => $regex_descriptions, + ); + + $description .= drupal_render( + $wildcard_description + ); + + return $description; +} + +/** + * Form API submit callback for the cache purge rule editing form. + */ +function nexteuropa_varnish_cache_purge_rule_form_submit(&$form, &$form_state) { + if ($form_state['values']['rule_type'] !== PurgeRuleType::PATHS) { + $form_state['values']['paths'] = ''; + } + $purge_rule = entity_ui_form_submit_build_entity($form, $form_state); + entity_save('nexteuropa_varnish_cache_purge_rule', $purge_rule); + + $form_state['redirect'] = 'admin/config/system/nexteuropa-varnish/purge_rules'; +} + +/** + * Returns the options array with the purge rule types. + */ +function _nexteuropa_varnish_get_rule_types() { + // Remove option if the default purge rule is enabled to prevent an + // overlapping rules. There is no need of the 'NODE' type because default + // rule works for all of the content types. + if (variable_get('nexteuropa_varnish_default_purge_rule', FALSE)) { + return array( + PurgeRuleType::PATHS => t('A specific list of regex'), + ); + } + + return array( + PurgeRuleType::NODE => t('Paths of the node the action is performed on'), + PurgeRuleType::PATHS => t('A specific list of regex'), + ); +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..85c5e8b --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,8 @@ + + + + + ./tests + + + diff --git a/src/Entity/PurgeRule.php b/src/Entity/PurgeRule.php new file mode 100644 index 0000000..56c59ab --- /dev/null +++ b/src/Entity/PurgeRule.php @@ -0,0 +1,58 @@ +paths) && $this->paths !== '') { + $type = PurgeRuleType::PATHS; + } + + return new PurgeRuleType($type); + } + + /** + * Get the paths associated with the purge rule. + * + * @return array + * The paths, as an array. + */ + public function paths() { + return preg_split("/[\r\n]+/", $this->paths); + } + + /** + * {@inheritdoc} + */ + public function save() { + $content_type = $this->content_type; + cache_clear_all('nexteuropa_varnish_get_node_purge_rules_' . $content_type, 'cache_nexteuropa_varnish'); + return parent::save(); + } + +} diff --git a/src/PurgeRuleController.php b/src/PurgeRuleController.php new file mode 100644 index 0000000..391e75b --- /dev/null +++ b/src/PurgeRuleController.php @@ -0,0 +1,50 @@ +load($ids) : FALSE; + if (!$entities) { + // Do nothing, in case invalid or no ids have been passed. + return; + } + + // We clear the content type rules cache. + $implied_entity_types = array(); + foreach ($entities as $entity) { + $content_type = $entity->content_type; + + if (!in_array($content_type, $implied_entity_types)) { + cache_clear_all('nexteuropa_varnish_get_node_purge_rules_' . $content_type, 'cache_nexteuropa_varnish'); + + $implied_entity_types[] = $content_type; + } + } + + parent::delete($ids, $transaction); + } + +} diff --git a/src/PurgeRuleType.php b/src/PurgeRuleType.php new file mode 100644 index 0000000..8c337b9 --- /dev/null +++ b/src/PurgeRuleType.php @@ -0,0 +1,54 @@ +type = $type; + } + + /** + * Get the possible purge rule types. + * + * @return array + * Purge rule types, as strings. + */ + public static function getConstList() { + return array( + self::NODE, + self::PATHS, + ); + } + + /** + * Get the type as a string. + */ + public function __toString() { + return $this->type; + } + +} diff --git a/src/Tests/.htaccess b/src/Tests/.htaccess new file mode 100644 index 0000000..3a42882 --- /dev/null +++ b/src/Tests/.htaccess @@ -0,0 +1 @@ +Deny from all diff --git a/src/Tests/HelperMethodsTest.php b/src/Tests/HelperMethodsTest.php new file mode 100644 index 0000000..e37396b --- /dev/null +++ b/src/Tests/HelperMethodsTest.php @@ -0,0 +1,73 @@ +assertEquals($expectedPath, $resultPath, sprintf('The %s case fails. it receives %s instead of %s!', $testedCase, $resultPath, $expectedPath)); + } + + /** + * Provides test data for testTrimmingPath(). + * + * @return array + * The test data. + */ + public function trimmingPathDataProvider() { + return array( + array( + 'nexteuropa/varnish/de/press/news/170925', + 'de/press/news/170925', + 'nexteuropa/varnish/', + 'with slashes', + ), + array( + '/de/press/news/170925', + 'de/press/news/170925', + '/', + 'with a simple slash', + ), + array( + 'de/press/news/170925', + 'de/press/news/170925', + '', + 'without slash', + ), + array( + 'de/press/news/170925', + 'de/press/news/170925', + 'nexteuropa/varnish/', + 'without the base path', + ), + ); + } + +} diff --git a/src/Tests/bootstrap.php b/src/Tests/bootstrap.php new file mode 100644 index 0000000..6c641d6 --- /dev/null +++ b/src/Tests/bootstrap.php @@ -0,0 +1,12 @@ +