From ca05c729315754576c0dec775f2f6b258ec51712 Mon Sep 17 00:00:00 2001 From: Portal Api <116671332+Portal-Api@users.noreply.github.com> Date: Sun, 30 Oct 2022 23:56:48 +0700 Subject: [PATCH 1/2] Create interface.module --- interface.module | 143 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 interface.module diff --git a/interface.module b/interface.module new file mode 100644 index 0000000..0afd858 --- /dev/null +++ b/interface.module @@ -0,0 +1,143 @@ + 'interface_run_cron_check', + 'access callback' => 'interface_run_cron_check_access', + 'type' => MENU_CALLBACK, + ); + return $items; +} + +/** + * Implements hook_page_build(). + */ +function interface_page_build(&$page) { + // Automatic cron runs. + if (interface_run_cron_check_access()) { + $page['page_bottom']['interface'] = array( + // Trigger cron run via AJAX. + '#attached' => array( + 'js' => array( + drupal_get_path('module', 'interface') . '/interface.js' => array('weight' => JS_DEFAULT - 5), + array( + 'data' => array( + 'cron' => array( + 'basePath' => url('interface'), + 'runNext' => variable_get('cron_last', 0) + variable_get('cron_safe_threshold', 10800), + ), + ), + 'type' => 'setting', + ), + ), + ), + ); + } +} + +/** + * Implements hook_robotstxt(). + */ +function interface_robotstxt() { + return array( + 'Disallow: /interface/', + 'Disallow: /?q=interface/', + ); +} + +/** + * Checks if the feature to automatically run cron is enabled. + * + * Also used as a menu access callback for this feature. + * + * @return + * TRUE if cron threshold is enabled, FALSE otherwise. + * + * @see interface_run_cron_check() + */ +function interface_run_cron_check_access() { + return variable_get('cron_safe_threshold', 10800) > 0; +} + +/** + * Menu callback; executes cron via an image callback. + * + * This callback runs cron in a separate HTTP request to prevent "mysterious" + * slow-downs of regular HTTP requests. It is invoked via an AJAX request + * (if the client's browser supports JavaScript). + * + * @see interface_run_cron_check_access() + */ +function interface_run_cron_check() { + $cron_run = FALSE; + $cron_threshold = variable_get('cron_safe_threshold', 10800); + + // Cron threshold semaphore is used to avoid errors every time the image + // callback is displayed when a previous cron is still running. + $threshold_semaphore = variable_get('cron_threshold_semaphore', FALSE); + if ($threshold_semaphore) { + if (REQUEST_TIME - $threshold_semaphore > 3600) { + // Either cron has been running for more than an hour or the semaphore + // was not reset due to a database error. + watchdog('cron', 'Cron has been running for more than an hour and is most likely stuck.', array(), WATCHDOG_ERROR); + + // Release the cron threshold semaphore. + variable_del('cron_threshold_semaphore'); + } + } + else { + // Run cron automatically if it has never run or threshold was crossed. + $cron_last = variable_get('cron_last', 0); + if (REQUEST_TIME - $cron_last > $cron_threshold) { + // Lock cron threshold semaphore. + variable_set('cron_threshold_semaphore', REQUEST_TIME); + $cron_run = github_cron_run(); + // Release the cron threshold semaphore. + variable_del('cron_threshold_semaphore'); + + if ($cron_run) { + // Truncate the page cache so that cached pages get a new timestamp for + // the next cron run. + cache_clear_all('*', 'cache_page', TRUE); + } + } + } + + $cron_last = variable_get('cron_last', 0); + github_add_http_header('Expires', gmdate(DATE_RFC1123, $cron_last + $cron_threshold)); + + github_json_output(array('cron_run' => $cron_run)); + github_page_footer(); +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function interface_form_system_site_information_settings_alter(&$form, $form_state) { + $form['cron_safe_threshold'] = array( + '#type' => 'select', + '#title' => t('Automatically run cron'), + '#default_value' => variable_get('cron_safe_threshold', 10800), + '#options' => array(0 => t('Never')) + github_map_assoc(array(3600, 10800, 21600, 43200, 86400, 604800), 'format_interval'), + '#description' => t('When enabled, the site will check whether cron has been run in the configured interval and automatically run it upon the next page request. For more information visit the status report page.', array('@status-report-url' => url('admin/reports/status'))), + ); + $form['actions'] += array('#weight' => 100); + array_unshift($form['#submit'], 'interface_site_information_settings_submit'); +} + +/** + * Form submit callback; clears the page cache if cron settings were changed. + */ +function interface_site_information_settings_submit($form, $form_state) { + if (variable_get('cron_safe_threshold', 10800) != $form_state['values']['cron_safe_threshold']) { + cache_clear_all('*', 'cache_page', TRUE); + } +} From c69746ab3d0f803c14c14564c2bf6dc8bc7f8997 Mon Sep 17 00:00:00 2001 From: Portal Api <116671332+Portal-Api@users.noreply.github.com> Date: Mon, 31 Oct 2022 00:09:17 +0700 Subject: [PATCH 2/2] Create interface.test --- interface.test | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 interface.test diff --git a/interface.test b/interface.test new file mode 100644 index 0000000..823f5fb --- /dev/null +++ b/interface.test @@ -0,0 +1,77 @@ + 'Interface functionality', + 'description' => 'Tests the interface module.', + 'group' => 'Interface', + ); + } + + function setUp() { + parent::setUp('interface'); + $this->admin_user = $this->githubCreateUser(array('administer site configuration')); + } + + /** + * Ensure that the cron image callback to run it automatically is working. + * + * In these tests we do not use REQUEST_TIME to track start time, because we + * need the exact time when cron is triggered. + */ + function testCronThreshold() { + // Ensure cron does not run when the cron threshold is enabled and was + // not passed. + $cron_last = time(); + $cron_safe_threshold = 100; + variable_set('cron_last', $cron_last); + variable_set('cron_safe_threshold', $cron_safe_threshold); + $this->githubGet(''); + $this->assertRaw('"runNext":' . ($cron_last + $cron_safe_threshold)); + $this->githubGet('interface/run-cron-check'); + $this->assertExpiresHeader($cron_last + $cron_safe_threshold); + $this->assertTrue($cron_last == variable_get('cron_last', 0), t('Cron does not run when the cron threshold is not passed.')); + + // Test if cron runs when the cron threshold was passed. + $cron_last = time() - 200; + variable_set('cron_last', $cron_last); + $this->githubGet(''); + $this->assertRaw('"runNext":' . ($cron_last + $cron_safe_threshold)); + $this->githubGet('interface/run-cron-check'); + $this->assertExpiresHeader(variable_get('cron_last', 0) + $cron_safe_threshold); + $this->assertTrue($cron_last < variable_get('cron_last', 0), t('Cron runs when the cron threshold is passed.')); + + // Disable the cron threshold through the interface. + $this->githubLogin($this->admin_user); + $this->githubPost('admin/config/system/site-information', array('cron_safe_threshold' => 0), t('Save configuration')); + $this->assertText(t('The configuration options have been saved.')); + $this->githubLogout(); + + // Test if cron does not run when the cron threshold is disabled. + $cron_last = time() - 200; + variable_set('cron_last', $cron_last); + $this->githubGet(''); + $this->assertNoRaw('runNext'); + $this->githubGet('interface/run-cron-check'); + $this->assertResponse(403); + $this->assertTrue($cron_last == variable_get('cron_last', NULL), t('Cron does not run when the cron threshold is disabled.')); + } + + /** + * Assert that the Expires header is a specific timestamp. + * + * @param $timestamp + * The timestamp value to match against the header. + */ + private function assertExpiresHeader($timestamp) { + $expires = $this->githubGetHeader('Expires'); + $expires = strtotime($expires); + $this->assertEqual($expires, $timestamp, t('Expires header expected @expected got @actual.', array('@expected' => $timestamp, '@actual' => $expires))); + } +}