diff --git a/README.md b/README.md index 7e13506..a2d609e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # OIT Module -Module that has all misc functions that don't make sense under any specific modules. +Module that has all misc functions that don't make sense under any +specific modules. ![Axolotl Keyboard](https://giphy.com/embed/RE5iREBNhI0Ok) diff --git a/config/install/ultimate_cron.job.delete_news.yml b/config/install/ultimate_cron.job.delete_news.yml new file mode 100644 index 0000000..2ed89c9 --- /dev/null +++ b/config/install/ultimate_cron.job.delete_news.yml @@ -0,0 +1,29 @@ +langcode: en +status: true +dependencies: + module: + - oit +title: 'Delete Old News' +id: delete_news +weight: -10 +module: oit +callback: '\Drupal\oit\Services\CronManager::deleteNews' +scheduler: + id: crontab + configuration: + rules: + - '0 0 1 */6 *' + catch_up: 14400 +launcher: + id: serial + configuration: + timeouts: + lock_timeout: 3600 + launcher: + thread: 0 +logger: + id: database + configuration: + method: '3' + expire: 1209600 + retain: 1000 diff --git a/drush.services.yml b/drush.services.yml index 0cd15ad..e1011b3 100644 --- a/drush.services.yml +++ b/drush.services.yml @@ -7,5 +7,6 @@ services: - '@database' - '@oit.userclean' - '@oit.toppages' + - '@oit.deletenews' tags: - { name: drush.command } diff --git a/oit.install b/oit.install index f6598cd..f2ef471 100644 --- a/oit.install +++ b/oit.install @@ -2,7 +2,7 @@ /** * @file - * Install, update and uninstall functions for the sympa_initialize module. + * Install, update and uninstall functions for the OIT module. */ use Drupal\Component\Utility\Random; @@ -13,6 +13,13 @@ use Drupal\user\Entity\User; /** * Helper function to update node body text. + * + * @param int $nid + * The node ID. + * @param string $body + * The body text value. + * @param string $format + * The text format machine name. */ function update_node_body($nid, $body, $format) { $query = \Drupal::entityQuery('node') @@ -134,7 +141,7 @@ function oit_update_9005() { } /** - * Implements hook_update(). + * Enable show_login_form setting. */ function oit_update_9006() { $config = \Drupal::configFactory()->getEditable('oit.settings'); @@ -299,7 +306,8 @@ function oit_update_10006() { function oit_update_10007() { \Drupal::database()->truncate('node__field_domain_source')->execute(); - // Sql query to 'node__field_domain_access' table where delta = 0 pulling in all columns. + // Sql query to 'node__field_domain_access' table where delta = 0 pulling + // in all columns. $query = \Drupal::database()->select('node__field_domain_access', 'nfa'); $query->fields('nfa', [ 'bundle', @@ -319,7 +327,7 @@ function oit_update_10007() { $lang = $row->langcode; $bundle = $row->bundle; $target_id = $row->field_domain_access_target_id; - $insert = \Drupal::database()->insert('node__field_domain_source') + \Drupal::database()->insert('node__field_domain_source') ->fields([ 'bundle', 'deleted', @@ -687,7 +695,7 @@ function oit_update_10018() { } /** - * Clear field_news_front_image values from news nodes and delete associated files. + * Remove field_news_front_image values and associated files from news nodes. */ function oit_update_10019() { $database = \Drupal::database(); @@ -696,4 +704,3 @@ function oit_update_10019() { $database->truncate('node__field_news_front_image')->execute(); $database->truncate('node_revision__field_news_front_image')->execute(); } - diff --git a/oit.libraries.yml b/oit.libraries.yml index 866e283..dfdfc41 100644 --- a/oit.libraries.yml +++ b/oit.libraries.yml @@ -150,4 +150,3 @@ oit_env: license: name: 'GPL-2.0-or-later' url: 'https://www.drupal.org/licensing/faq' - diff --git a/oit.links.task.yml b/oit.links.task.yml index d9acd3b..b6e17c9 100644 --- a/oit.links.task.yml +++ b/oit.links.task.yml @@ -2,4 +2,3 @@ oit.abusetable: route_name: oit.abusetable title: 'Low Confidence Banned ips' base_route: entity.autoban.list - diff --git a/oit.module b/oit.module index 0ce4d86..b0f6942 100644 --- a/oit.module +++ b/oit.module @@ -12,10 +12,8 @@ use Drupal\Core\Cache\Cache; use Drupal\Core\Entity\Display\EntityViewDisplayInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Link; use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Session\AccountInterface; -use Drupal\Core\Url; use Drupal\node\NodeInterface; use Drupal\oit\Plugin\OitImageStyled; use Drupal\user\Entity\User; @@ -30,7 +28,7 @@ function oit_preprocess_html(&$variables) { $title[0] = $variables['head_title']['name'] ?? ''; $title[1] = ''; if (isset($variables['head_title']['title'])) { - $title[1] = is_string($variables['head_title']['title'])?$variables['head_title']['title']:$variables['head_title']['title']->__toString(); + $title[1] = is_string($variables['head_title']['title']) ? $variables['head_title']['title'] : $variables['head_title']['title']->__toString(); } $getEnv = \Drupal::service('oit.environment.icon'); $variables['head_title'] = $getEnv->getEnv() . ' ' . $title[1] . ' | ' . $title[0]; @@ -55,7 +53,7 @@ function oit_webform_access_rules_alter(array &$access_rules) { } /** - * Implements oit_webform_access_rules_alter(). + * Implements hook_entity_create(). */ function oit_entity_create(EntityInterface $entity) { if ($entity->getEntityType()->getBundleEntityType() == 'node_type' && $entity->getType() == 'webform') { @@ -65,7 +63,7 @@ function oit_entity_create(EntityInterface $entity) { } /** - * hook_preprocess(). + * Implements hook_preprocess(). */ function oit_preprocess(&$variables, $hook) { $theme = \Drupal::service('theme.manager')->getActiveTheme()->getName(); @@ -190,7 +188,22 @@ function oit_form_alter(&$form, FormStateInterface $form_state, $form_id) { \Drupal::messenger()->addMessage($message, 'warning'); $form['#access'] = FALSE; } - _oit_form_set_domain('content', 'oit_colorado_edu'); + // Get query string. + $query = \Drupal::request()->query->all(); + $domain_set = FALSE; + foreach ($query as $key => $value) { + if ($key == 'field_domain_access_target_id') { + $domain_set = TRUE; + } + } + if (!$domain_set) { + // Redirect to query string + // "field_domain_access_target_id=oit_colorado_edu". + $query['field_domain_access_target_id'] = 'oit_colorado_edu'; + $query = http_build_query($query); + $response = new RedirectResponse('/admin/content?' . $query); + $response->send(); + } } } @@ -199,11 +212,6 @@ function oit_form_alter(&$form, FormStateInterface $form_state, $form_id) { // Get query string. $query = \Drupal::request()->query->all(); $search_key = isset($query['keys']) ? Xss::filter(strtolower($query['keys'])) : 0; - $space = [ - 'space monkey', - 'space+monkey', - 'spacemonkey', - ]; if ($search_key == 'space monkey' || $search_key == 'space+monkey' || $search_key == 'spacemonkey') { $form['#attached']['library'][] = 'oit/spacemonkey'; } @@ -385,7 +393,7 @@ function oit_form_alter(&$form, FormStateInterface $form_state, $form_id) { $form['#attached']['library'][] = 'oit/oit_node_news_form'; $form['#validate'][] = 'oit_news_types_categories'; // Add submit handler. - $form['actions']['submit']['#submit'][] = 'news_node_form_submit'; + $form['actions']['submit']['#submit'][] = 'oit_news_node_form_submit'; break; case "user_login_form": @@ -398,8 +406,8 @@ function oit_form_alter(&$form, FormStateInterface $form_state, $form_id) { if (!$show_login_form) { // Cache is breaking the redirect, so kill it. \Drupal::service('page_cache_kill_switch')->trigger(); - $dest_get = \Drupal::request()->get('dest') != null ? Xss::filter(\Drupal::request()->get('dest')) : ''; - $destination_get = \Drupal::request()->get('destination') != null ? Xss::filter(\Drupal::request()->get('destination')) : ''; + $dest_get = \Drupal::request()->get('dest') != NULL ? Xss::filter(\Drupal::request()->get('dest')) : ''; + $destination_get = \Drupal::request()->get('destination') != NULL ? Xss::filter(\Drupal::request()->get('destination')) : ''; $destination = ""; if (!empty($dest_get)) { $destination = '?destination=' . preg_replace('/https:\/\/[^\/]+/', '', $dest_get); @@ -456,7 +464,7 @@ function oit_form_alter(&$form, FormStateInterface $form_state, $form_id) { } /** - * Set domain on content to match site domain + * Set domain on content to match site domain. */ function _oit_set_oit_domain(&$form, FormStateInterface $form_state) { _oit_set_domain_submit($form, $form_state, 'oit', 'oit_colorado_edu'); @@ -511,7 +519,8 @@ function oit_set_domain_defaults(array &$form, $domain_name, $domain_id) { // Force access and source if not set. array_unshift($form['actions']['submit']['#submit'], '_' . $domain_name . '_set_' . $domain_name . '_domain'); - } else { + } + else { // Disable source locally. $form['field_domain_source']['widget']['#default_value'] = []; $form['field_domain_source']['widget']['#default_value'][] = '_none'; @@ -540,10 +549,11 @@ function _oit_set_domain_submit(array &$form, FormStateInterface $form_state, $d } /** - * News node form submit handler to un-promote other news nodes when a news - * node is promoted. + * News node form submit handler. + * + * Un-promotes other news nodes when a news node is promoted. */ -function news_node_form_submit(&$form, FormStateInterface $form_state) { +function oit_news_node_form_submit(&$form, FormStateInterface $form_state) { if ($form_state->getValues()['promote']['value']) { $node = $form_state->getFormObject()->getEntity(); $node_id = $node->id(); @@ -578,7 +588,7 @@ function oit_servicealert_dashboard_category_add($form, FormStateInterface $form $dashboard_state = json_decode($dashboard_state, TRUE); } - $dashboard_state[]= $name; + $dashboard_state[] = $name; // Add term_name to a drupal state for future GH action processing. \Drupal::state()->set('oit_dashboard_add', json_encode($dashboard_state)); @@ -597,7 +607,7 @@ function oit_servicealert_dashboard_category_delete($form, FormStateInterface $f $dashboard_state = json_decode($dashboard_state, TRUE); } - $dashboard_state[]= $term_name; + $dashboard_state[] = $term_name; // Add term_name to a drupal state for future GH action processing. \Drupal::state()->set('oit_dashboard_delete', json_encode($dashboard_state)); @@ -1075,8 +1085,10 @@ function _oit_security_window_warning(array &$form, string $form_id): void { // Check if it's Wednesday between 1600 and 2200 UTC. $now = new \DateTime('now', new \DateTimeZone('UTC')); - $day_of_week = (int) $now->format('N'); // ISO: 3 = Wednesday. - $hour = (int) $now->format('G'); // 0–23. + // ISO: 3 = Wednesday. + $day_of_week = (int) $now->format('N'); + // 0–23. + $hour = (int) $now->format('G'); if ($day_of_week !== 3 || $hour < 16 || $hour >= 22) { return; } diff --git a/oit.services.yml b/oit.services.yml index bfd3dc5..dac4da2 100644 --- a/oit.services.yml +++ b/oit.services.yml @@ -35,6 +35,14 @@ services: oit.userclean: class: Drupal\oit\Plugin\Util\UserClean arguments: ["@entity_type.manager", "@oit.teamsalert"] + oit.deletenews: + class: Drupal\oit\Plugin\Util\DeleteNews + arguments: + - "@database" + - "@entity_type.manager" + - "@entity_field.manager" + - "@path_alias.manager" + - "@logger.factory" oit.servicehealth: class: Drupal\oit\Plugin\ServiceHealth arguments: ["@config.factory", "@date.formatter", "@entity_type.manager"] diff --git a/src/Commands/OitCommands.php b/src/Commands/OitCommands.php index 19dc608..5754caf 100644 --- a/src/Commands/OitCommands.php +++ b/src/Commands/OitCommands.php @@ -7,6 +7,7 @@ use Drupal\oit\Plugin\TeamsAlert; use Drupal\oit\Plugin\TopPages; use Drupal\Core\Database\Connection; +use Drupal\oit\Plugin\Util\DeleteNews; use Drupal\oit\Plugin\Util\UserClean; /** @@ -50,7 +51,27 @@ class OitCommands extends DrushCommands { protected $topPages; /** - * Construct object. + * Delete News. + * + * @var \Drupal\oit\Plugin\Util\DeleteNews + */ + protected $deleteNews; + + /** + * Constructs a new OitCommands object. + * + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger service. + * @param \Drupal\oit\Plugin\TeamsAlert $teams_alert + * The Teams alert service. + * @param \Drupal\Core\Database\Connection $database + * The database connection. + * @param \Drupal\oit\Plugin\Util\UserClean $user_clean + * The user clean service. + * @param \Drupal\oit\Plugin\TopPages $top_pages + * The top pages service. + * @param \Drupal\oit\Plugin\Util\DeleteNews $delete_news + * The delete news service. */ public function __construct( MessengerInterface $messenger, @@ -58,6 +79,7 @@ public function __construct( Connection $database, UserClean $user_clean, TopPages $top_pages, + DeleteNews $delete_news, ) { parent::__construct(); $this->messenger = $messenger; @@ -65,11 +87,15 @@ public function __construct( $this->database = $database; $this->userClean = $user_clean; $this->topPages = $top_pages; + $this->deleteNews = $delete_news; } /** * Send Teams Alert. * + * @param string $userMessage + * The message to send. + * * @command oit:send-teams-alert * @aliases oit:sta */ @@ -82,6 +108,9 @@ public function sendTeamsAlert($userMessage) { /** * Clean banned ip's. * + * @param int $keep + * Number of banned IPs to keep (default: 300). + * * @command oit:ban-ip-clean * @aliases oit:bic */ @@ -120,6 +149,9 @@ public function bannedIpClean($keep = 300) { /** * Clean users that haven't accessed the site in over a year. * + * @param int $limit + * Maximum number of users to remove (0 = no limit). + * * @command oit:clean-users * @aliases oit:cu */ @@ -147,4 +179,47 @@ public function topTutorialPages() { $this->topPages->getTopTutorials(); } + /** + * Delete news nodes older than N years that are not linked anywhere. + * + * @command oit:news-delete + * @aliases oit:nd + * @argument years Number of years back to use as the deletion cutoff (default: 5). + * @option dry-run Preview the kill list without deleting anything. + * @usage drush oit:news-delete + * Delete news older than 5 years that are not referenced. + * @usage drush oit:news-delete 3 --dry-run + * Preview what would be deleted with a 3-year cutoff. + */ + public function newsDelete(int $years = 5, array $options = ['dry-run' => FALSE]): void { + $dry_run = (bool) $options['dry-run']; + + if ($dry_run) { + $result = $this->deleteNews->findDeletable($years); + $this->output()->writeln('DRY RUN — no changes made.'); + } + else { + $result = $this->deleteNews->deleteNews($years); + } + + $deleted_count = count($result['deleted']); + $skipped_count = count($result['skipped']); + $verb = $dry_run ? 'Would delete' : 'Deleted'; + + $this->output()->writeln("{$verb} {$deleted_count} news node(s) older than {$years} year(s)."); + + if ($deleted_count > 0) { + foreach ($result['deleted'] as $nid => $title) { + $this->output()->writeln(" - [{$nid}] {$title}"); + } + } + + if ($skipped_count > 0) { + $this->output()->writeln("Skipped {$skipped_count} node(s) due to existing links:"); + foreach ($result['skipped'] as $nid => $info) { + $this->output()->writeln(" - [{$nid}] {$info['title']} — {$info['reason']}"); + } + } + } + } diff --git a/src/Controller/AbuseController.php b/src/Controller/AbuseController.php index 320aa90..0ebeb31 100644 --- a/src/Controller/AbuseController.php +++ b/src/Controller/AbuseController.php @@ -118,6 +118,27 @@ class AbuseController extends ControllerBase { /** * Constructs a new AbuseController object. + * + * @param \Drupal\Core\Session\AccountInterface $account + * The current user account. + * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack + * The request stack. + * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory + * The logger factory. + * @param \Drupal\Core\Config\ConfigFactory $config_factory + * The config factory. + * @param \Drupal\Core\Extension\ModuleExtensionList $module_extension_list + * The module extension list. + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer service. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\shortcode_svg\Plugin\ShortcodeIcon $shortcode_svg_icon + * The shortcode SVG icon service. + * @param \Drupal\Core\State\State $state + * The state service. + * @param \Drupal\Core\Database\Connection $database + * The database connection. */ public function __construct( AccountInterface $account, diff --git a/src/Controller/OitController.php b/src/Controller/OitController.php index 0853806..e8f2d0a 100644 --- a/src/Controller/OitController.php +++ b/src/Controller/OitController.php @@ -351,7 +351,10 @@ public function oitDenied() { } /** - * Build content to display on page. + * Build content to display on the access-denied page. + * + * @return string + * HTML string for the denied page body. */ private function deniedContent() { if ($_SERVER["REQUEST_URI"]) { @@ -471,6 +474,18 @@ public function requestPortal() { /** * Pull request portal block with contextual filter. + * + * @param string $view + * The view machine name. + * @param string $display + * The view display ID. + * @param array $arg + * Contextual filter arguments. + * @param string $title + * The block title. + * + * @return array + * Render array for the item list. */ private function requestPortalView($view, $display, $arg, $title) { // Firstly, get the view in question. diff --git a/src/Form/AbuseConfirmForm.php b/src/Form/AbuseConfirmForm.php index 646025c..5df348a 100644 --- a/src/Form/AbuseConfirmForm.php +++ b/src/Form/AbuseConfirmForm.php @@ -174,6 +174,9 @@ public function submitForm(array &$form, FormStateInterface $form_state) { /** * Remove IP from the questionable-ban state list. + * + * @param string $ip + * The IP address to remove. */ private function abuseIpRemove($ip) { $abuse = json_decode($this->state->get('ban_ip_questionable'), TRUE) ?: []; @@ -185,6 +188,9 @@ private function abuseIpRemove($ip) { /** * Remove IP from the ban_ip table. + * + * @param string $ip + * The IP address to remove. */ private function banIpRemove($ip) { $this->database->delete('ban_ip') @@ -194,6 +200,9 @@ private function banIpRemove($ip) { /** * Append IP to the autoban whitelist config. + * + * @param string $ip + * The IP address to whitelist. */ private function ipWhitelist($ip) { $autoban_settings = $this->configFactory->getEditable('autoban.settings'); @@ -204,6 +213,9 @@ private function ipWhitelist($ip) { /** * Human-readable verb for the pending action. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup + * The translated verb for the action. */ private function actionVerb() { return match ($this->action) { diff --git a/src/Plugin/Block/AbsoluteLinkShame.php b/src/Plugin/Block/AbsoluteLinkShame.php index 3451154..c1cdd61 100644 --- a/src/Plugin/Block/AbsoluteLinkShame.php +++ b/src/Plugin/Block/AbsoluteLinkShame.php @@ -161,7 +161,7 @@ public function build() { } /** - * Return no cache. + * {@inheritdoc} */ public function getCacheMaxAge() { return 0; diff --git a/src/Plugin/Block/CuHeader.php b/src/Plugin/Block/CuHeader.php index 7528cd2..9825c05 100644 --- a/src/Plugin/Block/CuHeader.php +++ b/src/Plugin/Block/CuHeader.php @@ -57,7 +57,7 @@ public static function create(ContainerInterface $container, array $configuratio * Plugin id string. * @param mixed $plugin_definition * Plugin Definition mixed. - * @param \\Drupal\Core\Extension\ThemeHandler $theme_handler + * @param \Drupal\Core\Extension\ThemeHandler $theme_handler * The theme manager. */ public function __construct(array $configuration, $plugin_id, $plugin_definition, ThemeHandler $theme_handler) { diff --git a/src/Plugin/Block/FrontServiceHealth.php b/src/Plugin/Block/FrontServiceHealth.php index dd51193..1d1428f 100644 --- a/src/Plugin/Block/FrontServiceHealth.php +++ b/src/Plugin/Block/FrontServiceHealth.php @@ -57,8 +57,8 @@ public static function create(ContainerInterface $container, array $configuratio * Plugin id string. * @param mixed $plugin_definition * Plugin Definition mixed. - * @param \Drupal\Core\Entity\ServiceHealth $service_health - * Invokes renderer. + * @param \Drupal\oit\Plugin\ServiceHealth $service_health + * The service health plugin. */ public function __construct(array $configuration, $plugin_id, $plugin_definition, ServiceHealth $service_health) { parent::__construct($configuration, $plugin_id, $plugin_definition); diff --git a/src/Plugin/Block/MenuAnchor.php b/src/Plugin/Block/MenuAnchor.php index f30f7c6..4799ec1 100644 --- a/src/Plugin/Block/MenuAnchor.php +++ b/src/Plugin/Block/MenuAnchor.php @@ -57,7 +57,7 @@ public static function create(ContainerInterface $container, array $configuratio * Plugin id string. * @param mixed $plugin_definition * Plugin Definition mixed. - * @param \\Drupal\Core\Extension\ThemeHandler $theme_handler + * @param \Drupal\Core\Extension\ThemeHandler $theme_handler * The theme manager. */ public function __construct(array $configuration, $plugin_id, $plugin_definition, ThemeHandler $theme_handler) { diff --git a/src/Plugin/Block/PageOverview.php b/src/Plugin/Block/PageOverview.php index 9572433..0b6f676 100644 --- a/src/Plugin/Block/PageOverview.php +++ b/src/Plugin/Block/PageOverview.php @@ -117,7 +117,7 @@ public function build() { $os = check_markup($set_comp_type, 'rich_text'); $summary .= "
$os
"; - // Download link if set + // Download link if set. // Return full external url or /node/# for internal links. if ($node->field_software_download_link->get(0) !== NULL) { if ($node->field_software_download_link->get(0)->isExternal()) { @@ -160,10 +160,10 @@ public function build() { } /** - * Set cache tag by node id. + * {@inheritdoc} */ public function getCacheTags() { - // With this when your node change your block will rebuild. + // With this, when your node changes, your block will rebuild. if ($node = $this->routMatchInterface->getParameter('node')) { // If there is node add its cachetag. return Cache::mergeTags(parent::getCacheTags(), ['node:' . $node->id()]); @@ -175,10 +175,10 @@ public function getCacheTags() { } /** - * Return cache contexts. + * {@inheritdoc} */ public function getCacheContexts() { - // If you depends on \Drupal::routeMatch() + // If you depend on \Drupal::routeMatch() // you must set context of this block with 'route' context tag. // Every new route this block will rebuild. return Cache::mergeContexts(parent::getCacheContexts(), ['route']); diff --git a/src/Plugin/Block/PortfolioBlock.php b/src/Plugin/Block/PortfolioBlock.php index 6304a34..6fbb3a1 100644 --- a/src/Plugin/Block/PortfolioBlock.php +++ b/src/Plugin/Block/PortfolioBlock.php @@ -28,7 +28,10 @@ public function build() { } /** - * Fetch portfolio from firebase. + * Fetch portfolio data from Google Sheets and build a render array. + * + * @return array + * Render array for the portfolio table. */ public function fetchPortfolio() { $fetchData = new GoogleSheetsFetch('1k4-Csp29uLZbh_g2nhuhpq3dVBgZ6zWFK20BXP1rL_s', 0, 0); diff --git a/src/Plugin/Block/ServiceAlertTweet.php b/src/Plugin/Block/ServiceAlertTweet.php index d4d559b..7fff5ff 100644 --- a/src/Plugin/Block/ServiceAlertTweet.php +++ b/src/Plugin/Block/ServiceAlertTweet.php @@ -28,28 +28,28 @@ class ServiceAlertTweet extends BlockBase implements /** * Current path injected. * - * @var current_path\Drupal\Core\Path\CurrentPathStack + * @var \Drupal\Core\Path\CurrentPathStack */ protected $currentPathStack; /** * Request injected. * - * @var request\Symfony\Component\HttpFoundation\RequestStack + * @var \Symfony\Component\HttpFoundation\RequestStack */ protected $request; /** * Route Match injected. * - * @var routeMatchInterface\Drupal\Core\Routing\RouteMatchInterface + * @var \Drupal\Core\Routing\RouteMatchInterface */ protected $routeMatchInterface; /** * Entity Manager injected. * - * @var entityTypeManager\Drupal\Core\Entity\EntityTypeManager + * @var \Drupal\Core\Entity\EntityTypeManager */ protected $entityTypeManager; @@ -142,10 +142,10 @@ public function build() { } /** - * Set cache tag by node id. + * {@inheritdoc} */ public function getCacheTags() { - // With this when your node change your block will rebuild. + // With this, when your node changes, your block will rebuild. if ($node = $this->routeMatchInterface->getParameter('node')) { // If there is node add its cachetag. return Cache::mergeTags(parent::getCacheTags(), ['node:' . $node->id()]); @@ -157,7 +157,7 @@ public function getCacheTags() { } /** - * Return cache contexts. + * {@inheritdoc} */ public function getCacheContexts() { // If you depend on \Drupal::routeMatch() diff --git a/src/Plugin/Block/TutorialBlock.php b/src/Plugin/Block/TutorialBlock.php index aa948f8..44598a7 100644 --- a/src/Plugin/Block/TutorialBlock.php +++ b/src/Plugin/Block/TutorialBlock.php @@ -139,10 +139,10 @@ public function build() { } /** - * Set cache tag by node id. + * {@inheritdoc} */ public function getCacheTags() { - // With this when your node change your block will rebuild. + // With this, when your node changes, your block will rebuild. if ($node = $this->routMatchInterface->getParameter('node')) { // If there is node add its cachetag. return Cache::mergeTags(parent::getCacheTags(), ['node:' . $node->id()]); @@ -154,10 +154,10 @@ public function getCacheTags() { } /** - * Return cache contexts. + * {@inheritdoc} */ public function getCacheContexts() { - // If you depends on \Drupal::routeMatch() + // If you depend on \Drupal::routeMatch() // you must set context of this block with 'route' context tag. // Every new route this block will rebuild. return Cache::mergeContexts(parent::getCacheContexts(), ['route']); diff --git a/src/Plugin/BlockUuidQuery.php b/src/Plugin/BlockUuidQuery.php index 054fdf4..695a97a 100644 --- a/src/Plugin/BlockUuidQuery.php +++ b/src/Plugin/BlockUuidQuery.php @@ -38,7 +38,12 @@ class BlockUuidQuery { protected $entityTypeManager; /** - * Construct object. + * Constructs a new BlockUuidQuery object. + * + * @param \Drupal\Core\Database\Connection $connection + * The database connection. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. */ public function __construct(Connection $connection, EntityTypeManagerInterface $entity_type_manager) { $this->connection = $connection; @@ -47,6 +52,9 @@ public function __construct(Connection $connection, EntityTypeManagerInterface $ /** * Query block and pull bid via uuid. + * + * @param string $uuid + * The UUID of the block content entity. */ public function getBidByUuid($uuid) { $query = $this->connection->select('block_content', 'bc'); @@ -58,7 +66,10 @@ public function getBidByUuid($uuid) { } /** - * Return block. + * Load and return a rendered block entity. + * + * @return array + * Render array for the block content entity. */ public function loadBlock() { $block = $this->entityTypeManager->getStorage('block_content')->load($this->bid); diff --git a/src/Plugin/CvsToArray.php b/src/Plugin/CvsToArray.php index a263981..aebcdc5 100644 --- a/src/Plugin/CvsToArray.php +++ b/src/Plugin/CvsToArray.php @@ -20,7 +20,12 @@ class CvsToArray { private $arrayCvs; /** - * Function to convert CSV into associative array. + * Constructs a new CvsToArray object and parses the CSV file. + * + * @param string $file + * The path or URL of the CSV file to open. + * @param string $delimiter + * The field delimiter character used in the CSV file. */ public function __construct($file, $delimiter) { if (($handle = fopen($file, 'r')) !== FALSE) { @@ -37,7 +42,10 @@ public function __construct($file, $delimiter) { } /** - * Return array. + * Return the parsed CSV data as an array. + * + * @return array + * The parsed CSV data. */ public function getBuiltArray() { return $this->arrayCvs; diff --git a/src/Plugin/Domain.php b/src/Plugin/Domain.php index 128d1ea..1e0adbc 100644 --- a/src/Plugin/Domain.php +++ b/src/Plugin/Domain.php @@ -23,14 +23,20 @@ class Domain { protected $token; /** - * Construct object. + * Constructs a new Domain object. + * + * @param \Drupal\Core\Utility\Token $token + * The token service. */ public function __construct(Token $token) { $this->token = $token; } /** - * Get current domain. + * Get current domain machine name. + * + * @return string + * The domain identifier: 'oit', 'oda', or 'na'. */ public function getDomain() { $domainName = $this->token->replace('[domain:name]'); diff --git a/src/Plugin/EnvironmentIcon.php b/src/Plugin/EnvironmentIcon.php index 6461adc..8f65593 100644 --- a/src/Plugin/EnvironmentIcon.php +++ b/src/Plugin/EnvironmentIcon.php @@ -30,7 +30,10 @@ class EnvironmentIcon { private $env; /** - * Check environment and give icon accordingly. + * Constructs a new EnvironmentIcon object. + * + * @param \Drupal\Core\Session\AccountProxyInterface $account + * The current user account proxy. */ public function __construct(AccountProxyInterface $account) { $this->account = $account; @@ -54,7 +57,10 @@ public function __construct(AccountProxyInterface $account) { } /** - * Return icon. + * Return the environment icon string. + * + * @return string + * The environment icon emoji string. */ public function getEnv() { return $this->env; diff --git a/src/Plugin/Filter/FilterCu.php b/src/Plugin/Filter/FilterCu.php index e9dbd9c..006502f 100644 --- a/src/Plugin/Filter/FilterCu.php +++ b/src/Plugin/Filter/FilterCu.php @@ -18,7 +18,7 @@ class FilterCu extends FilterBase { /** - * Process filter to replace dashes. + * {@inheritdoc} */ public function process($text, $langcode) { $patterns = []; diff --git a/src/Plugin/Filter/FilterEmail.php b/src/Plugin/Filter/FilterEmail.php index d6f1372..47c5cb9 100644 --- a/src/Plugin/Filter/FilterEmail.php +++ b/src/Plugin/Filter/FilterEmail.php @@ -18,7 +18,7 @@ class FilterEmail extends FilterBase { /** - * Search and replace all dashes in Email. + * {@inheritdoc} */ public function process($text, $langcode) { $patterns = []; diff --git a/src/Plugin/Filter/FilterNoopener.php b/src/Plugin/Filter/FilterNoopener.php index 4a67614..df49133 100644 --- a/src/Plugin/Filter/FilterNoopener.php +++ b/src/Plugin/Filter/FilterNoopener.php @@ -18,7 +18,7 @@ class FilterNoopener extends FilterBase { /** - * Find and replace target=_blank links. + * {@inheritdoc} */ public function process($text, $langcode) { $patterns = []; diff --git a/src/Plugin/GoogleSheetsApi.php b/src/Plugin/GoogleSheetsApi.php index 549c539..187a405 100644 --- a/src/Plugin/GoogleSheetsApi.php +++ b/src/Plugin/GoogleSheetsApi.php @@ -26,7 +26,21 @@ class GoogleSheetsApi { private $cvsSheet; /** - * Grabbing the sheet data. + * Fetch and process a Google Sheet by key and column letters. + * + * @param string $key + * The Google Sheets document key. + * @param string $sheet_letters + * Comma-separated column letters to include. + * @param int $gid + * The sheet GID (tab index). + * @param int $shift + * Number of header rows to skip. + * @param bool $shentity + * Whether to treat the key as a full URL. + * + * @return array + * The processed sheet data. */ public function sheetDefined($key, $sheet_letters, $gid = 0, $shift = 0, $shentity = FALSE) { $fetchData = new GoogleSheetsFetch($key, $gid, $shift, $shentity); @@ -39,7 +53,10 @@ public function sheetDefined($key, $sheet_letters, $gid = 0, $shift = 0, $shenti } /** - * Return sheet data. + * Return the processed sheet data. + * + * @return array + * The processed sheet data. */ public function getSheetData() { return $this->sheetData; diff --git a/src/Plugin/GoogleSheetsFetch.php b/src/Plugin/GoogleSheetsFetch.php index e91f3ea..b4987b6 100644 --- a/src/Plugin/GoogleSheetsFetch.php +++ b/src/Plugin/GoogleSheetsFetch.php @@ -36,7 +36,16 @@ class GoogleSheetsFetch { private $cvsSheet; /** - * Fetch google sheet. + * Constructs a new GoogleSheetsFetch object and fetches the sheet data. + * + * @param string $key + * The Google Sheets document key or full URL when $shentity is TRUE. + * @param int $gid + * The sheet GID (tab index). + * @param int $shift + * Number of leading rows to remove from the data. + * @param bool $shentity + * When TRUE, treat $key as a full Google Sheets URL. */ public function __construct($key, $gid, $shift = 0, $shentity = FALSE) { if ($shentity) { @@ -70,6 +79,9 @@ public function __construct($key, $gid, $shift = 0, $shentity = FALSE) { /** * Get sheet that was fetched. + * + * @return array + * The fetched sheet data as a two-dimensional array. */ public function getFetchedSheet() { return $this->fetchData; @@ -77,6 +89,9 @@ public function getFetchedSheet() { /** * Get the result count. + * + * @return int + * The number of rows in the fetched sheet. */ public function getCount() { return $this->sheetCount; diff --git a/src/Plugin/GoogleSheetsProcess.php b/src/Plugin/GoogleSheetsProcess.php index 8590862..2e4e93b 100644 --- a/src/Plugin/GoogleSheetsProcess.php +++ b/src/Plugin/GoogleSheetsProcess.php @@ -22,7 +22,14 @@ class GoogleSheetsProcess { private $processedData; /** - * Process google sheet. + * Constructs a new GoogleSheetsProcess object and processes the sheet data. + * + * @param array $gsheet_returned_data + * The raw two-dimensional array from GoogleSheetsFetch. + * @param string $sheet_letters + * Comma-separated column letters specifying which columns to include. + * @param string $process + * Processing mode: 'ss' for standard or 'custom' for custom headers. */ public function __construct($gsheet_returned_data, $sheet_letters, $process = 'ss') { // Validate input data. @@ -149,7 +156,10 @@ public function __construct($gsheet_returned_data, $sheet_letters, $process = 's } /** - * Return processed google sheet. + * Return the processed Google Sheets data. + * + * @return array + * Associative array with 'rows' and 'header' keys. */ public function getProcessedData() { return $this->processedData; diff --git a/src/Plugin/OitImageStyled.php b/src/Plugin/OitImageStyled.php index 54979f9..48f62ba 100644 --- a/src/Plugin/OitImageStyled.php +++ b/src/Plugin/OitImageStyled.php @@ -6,7 +6,7 @@ use Drupal\image\Entity\ImageStyle; /** - * Environment icon to be used on header title. + * Plugin to generate a styled image URL from a file entity ID. * * @OitImageStyled( * id = "imagestyle", @@ -23,7 +23,16 @@ class OitImageStyled { private $imageUrl; /** - * Take image id and style wanted and return url. + * Constructs a new OitImageStyled object and generates the styled image URL. + * + * @param int $image_id + * The file entity ID of the image. + * @param string $style + * The image style machine name. + * @param string $filefield_replace + * Optional path replacement for filefield_paths URIs. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface|null $entityTypeManager + * The entity type manager. */ public function __construct($image_id, $style = "max_325x325", $filefield_replace = '', ?EntityTypeManagerInterface $entityTypeManager = NULL) { $style = ImageStyle::load($style); @@ -42,7 +51,10 @@ public function __construct($image_id, $style = "max_325x325", $filefield_replac } /** - * Return icon. + * Return the styled image URL. + * + * @return string + * The absolute URL of the styled image. */ public function getImageUrl() { return $this->imageUrl; diff --git a/src/Plugin/RedirectAddAnalytics.php b/src/Plugin/RedirectAddAnalytics.php index ecfd6d6..5444d12 100644 --- a/src/Plugin/RedirectAddAnalytics.php +++ b/src/Plugin/RedirectAddAnalytics.php @@ -6,7 +6,7 @@ use Drupal\Core\Logger\LoggerChannelFactoryInterface; /** - * Environment icon to be used on header title. + * Plugin to query redirects and append UTM analytics parameters. * * @RedirectAddAnalytics( * id = "redirect_add_analytics", @@ -32,7 +32,12 @@ class RedirectAddAnalytics { protected $logger; /** - * Look for redirects missing utm code and add it. + * Constructs a new RedirectAddAnalytics object and processes redirects. + * + * @param \Drupal\Core\Database\Connection $connection + * The database connection. + * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $channelFactory + * The logger channel factory. */ public function __construct( Connection $connection, diff --git a/src/Plugin/ServiceHealth.php b/src/Plugin/ServiceHealth.php index 254e51d..5b8d8ec 100644 --- a/src/Plugin/ServiceHealth.php +++ b/src/Plugin/ServiceHealth.php @@ -44,7 +44,14 @@ class ServiceHealth { protected $entityTypeManager; /** - * Constructs request stuff. + * Constructs a new ServiceHealth object. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory. + * @param \Drupal\Core\Datetime\DateFormatterInterface $dateFormatter + * The date formatter service. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. */ public function __construct( ConfigFactoryInterface $config_factory, @@ -256,7 +263,13 @@ public function statusCircle($status) { } /** - * Make translate work maybe. + * Wraps the global t() function for use inside this class. + * + * @param string $text + * The string to translate. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup + * The translated string. */ private function t($text) { // @codingStandardsIgnoreStart diff --git a/src/Plugin/TeamsAlert.php b/src/Plugin/TeamsAlert.php index 05fc314..90d0ea7 100644 --- a/src/Plugin/TeamsAlert.php +++ b/src/Plugin/TeamsAlert.php @@ -8,7 +8,7 @@ use Drupal\key\KeyRepositoryInterface; /** - * Environment icon to be used on header title. + * Plugin to send alert messages into a Microsoft Teams channel via webhook. * * @TeamsAlert ( * id = "teams_alert", @@ -60,7 +60,14 @@ class TeamsAlert { protected $logger; /** - * Sets up to send message to Teams. + * Constructs a new TeamsAlert object. + * + * @param \Drupal\key\KeyRepositoryInterface $key_repository + * The key repository service. + * @param \Drupal\encrypt\EncryptServiceInterface $encrypt_service + * The encrypt service. + * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $channelFactory + * The logger channel factory. */ public function __construct( KeyRepositoryInterface $key_repository, @@ -77,7 +84,12 @@ public function __construct( } /** - * Send message. + * Send a message to the configured Microsoft Teams channel. + * + * @param string $message + * The message text to send. + * @param array $environment + * List of environments in which to send the alert. */ public function sendMessage( $message, @@ -111,7 +123,10 @@ public function sendMessage( } /** - * Setup MS Teams card. + * Build the MS Teams adaptive card payload array. + * + * @return array + * The adaptive card message array for the Teams webhook. */ public function getMessage() { $message = $this->message; diff --git a/src/Plugin/TopPages.php b/src/Plugin/TopPages.php index 8b53fdb..7299ed8 100644 --- a/src/Plugin/TopPages.php +++ b/src/Plugin/TopPages.php @@ -9,7 +9,7 @@ use GuzzleHttp\ClientInterface; /** - * Environment icon to be used on header title. + * Plugin to retrieve top pages data from a GitHub-hosted JSON file. * * @TopPages ( * id = "top_pages", @@ -83,7 +83,20 @@ class TopPages { protected $entityTypeManager; /** - * Sets up to send message to Teams. + * Constructs a new TopPages object. + * + * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $channelFactory + * The logger channel factory. + * @param \Drupal\Core\Config\ConfigFactory $config_factory + * The config factory. + * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack + * The request stack. + * @param \GuzzleHttp\ClientInterface $http_client + * The HTTP client. + * @param \Drupal\oit\Plugin\TeamsAlert $teams_alert + * The Teams alert service. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. */ public function __construct( LoggerChannelFactoryInterface $channelFactory, @@ -106,7 +119,7 @@ public function __construct( } /** - * Get data. + * Fetch top pages JSON data from the remote source. */ private function fetchData() { // Get yesterdays date in the format YYYYMMDD. @@ -252,7 +265,20 @@ public function getTopTutorials() { } /** - * Build and save block. + * Build an HTML list and save it to a block content entity. + * + * @param int $count + * The expected number of items in the list. + * @param array $top_pages + * Array of page data with 'title' and 'url' keys. + * @param int $block_id + * The block content entity ID to update. + * @param string $block_title + * The heading text for the block. + * @param string $block_url + * The URL the block heading should link to. + * @param string $block_class + * CSS class to apply to the list element. */ private function buildSaveBlock($count, $top_pages, $block_id, $block_title, $block_url, $block_class) { if ($top_pages === NULL) { @@ -286,7 +312,13 @@ private function buildSaveBlock($count, $top_pages, $block_id, $block_title, $bl } /** - * Need to parse html to grab titles. + * Fetch a page and parse its HTML title element. + * + * @param string $url + * The relative URL of the page to fetch. + * + * @return string + * The page title, or 'Log in' if the page is inaccessible. */ private function titleLookup($url) { $response = $this->httpClient->request('GET', $this->host . $url); diff --git a/src/Plugin/Util/ArchiveNews.php b/src/Plugin/Util/ArchiveNews.php index 31241e2..709745d 100644 --- a/src/Plugin/Util/ArchiveNews.php +++ b/src/Plugin/Util/ArchiveNews.php @@ -39,7 +39,14 @@ class ArchiveNews { protected $teamsAlert; /** - * Construct object. + * Constructs a new ArchiveNews object. + * + * @param \Drupal\Core\Database\Connection $connection + * The database connection. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\oit\Plugin\TeamsAlert $teams_alert + * The Teams alert service. */ public function __construct( Connection $connection, @@ -52,7 +59,10 @@ public function __construct( } /** - * Function to archive news after cut off date. $cut_off is a unix timestamp. + * Archive news nodes whose changed date is before the cutoff timestamp. + * + * @param int $cut_off + * Unix timestamp; news changed before this date will be archived. */ public function archive($cut_off) { $query = $this->connection->select('node__field_news_archive', 'na'); diff --git a/src/Plugin/Util/DeleteNews.php b/src/Plugin/Util/DeleteNews.php new file mode 100644 index 0000000..4c6ece2 --- /dev/null +++ b/src/Plugin/Util/DeleteNews.php @@ -0,0 +1,378 @@ +connection = $connection; + $this->entityTypeManager = $entity_type_manager; + $this->entityFieldManager = $entity_field_manager; + $this->aliasManager = $alias_manager; + $this->loggerFactory = $logger_factory; + } + + /** + * Build the deletion plan without performing any deletes. + * + * @param int $years + * News nodes whose created timestamp is older than this many years + * are candidates for deletion. + * + * @return array + * [ + * 'deleted' => [nid => title, ...], + * 'skipped' => [nid => ['title' => '...', 'reason' => '...'], ...], + * ] + */ + public function findDeletable(int $years): array { + $candidates = $this->getCandidates($years); + if (empty($candidates)) { + return ['deleted' => [], 'skipped' => []]; + } + + $alias_map = $this->buildAliasMap($candidates); + $referenced = $this->buildReferencedSet($candidates, $alias_map); + + $deleted = array_diff_key($candidates, $referenced); + $skipped = []; + foreach ($referenced as $nid => $reason) { + $skipped[$nid] = [ + 'title' => $candidates[$nid], + 'reason' => $reason, + ]; + } + + return ['deleted' => $deleted, 'skipped' => $skipped]; + } + + /** + * Build the plan and execute it. + * + * Calls findDeletable() internally, then deletes every node in the + * 'deleted' list and writes a watchdog notice per node. + * + * @param int $years + * News nodes whose created timestamp is older than this many years + * are candidates for deletion. + * + * @return array + * Same shape as findDeletable(). + */ + public function deleteNews(int $years): array { + $result = $this->findDeletable($years); + $logger = $this->loggerFactory->get('oit'); + + if (!empty($result['deleted'])) { + $storage = $this->entityTypeManager->getStorage('node'); + $chunks = array_chunk($result['deleted'], 50, TRUE); + foreach ($chunks as $chunk) { + foreach ($chunk as $nid => $title) { + $logger->notice('Deleted news nid:@nid title:@title', [ + '@nid' => $nid, + '@title' => $title, + ]); + } + $entities = $storage->loadMultiple(array_keys($chunk)); + $storage->delete($entities); + } + } + + $logger->notice('News deletion: Deleted @deleted / Skipped @skipped', [ + '@deleted' => count($result['deleted']), + '@skipped' => count($result['skipped']), + ]); + + return $result; + } + + /** + * Query candidate news nodes older than $years years. + */ + protected function getCandidates(int $years): array { + $cutoff = strtotime("-{$years} years"); + $query = $this->connection->select('node_field_data', 'n'); + $query->fields('n', ['nid', 'title']); + $query->condition('n.type', 'news'); + $query->condition('n.created', $cutoff, '<'); + $candidates = []; + foreach ($query->execute() as $row) { + $candidates[(int) $row->nid] = $row->title; + } + return $candidates; + } + + /** + * Build a map of nid => alias for candidates that have a custom alias. + */ + protected function buildAliasMap(array $candidates): array { + $alias_map = []; + foreach (array_keys($candidates) as $nid) { + $alias = $this->aliasManager->getAliasByPath('/node/' . $nid); + if ($alias !== '/node/' . $nid) { + $alias_map[$nid] = $alias; + } + } + return $alias_map; + } + + /** + * Run all reference checks and return [nid => reason] for referenced nodes. + * + * Checks run in order A -> B -> C -> D; first match wins per nid. + */ + protected function buildReferencedSet(array $candidates, array $alias_map): array { + $referenced = []; + $this->checkLongTextFields($candidates, $alias_map, $referenced); + $this->checkEntityReferenceFields($candidates, $referenced); + $this->checkMenuLinks($candidates, $alias_map, $referenced); + $this->checkRedirects($candidates, $referenced); + return $referenced; + } + + /** + * Check A: Scan text_long, text_with_summary, and string_long fields. + */ + protected function checkLongTextFields(array $candidates, array $alias_map, array &$referenced): void { + $unique_fields = []; + $seen = []; + foreach (['text_long', 'text_with_summary', 'string_long'] as $type) { + foreach ($this->entityFieldManager->getFieldMapByFieldType($type) as $entity_type => $fields) { + foreach (array_keys($fields) as $field_name) { + $key = $entity_type . ':' . $field_name; + if (!isset($seen[$key])) { + $seen[$key] = TRUE; + $unique_fields[] = [$entity_type, $field_name]; + } + } + } + } + + $nids = array_keys($candidates); + foreach ($unique_fields as [$entity_type, $field_name]) { + $table = $entity_type . '__' . $field_name; + $value_col = $field_name . '_value'; + if (!$this->connection->schema()->tableExists($table)) { + continue; + } + + foreach (array_chunk($nids, 200) as $chunk_nids) { + $unreferenced_nids = array_diff($chunk_nids, array_keys($referenced)); + if (empty($unreferenced_nids)) { + continue; + } + + $nid_pattern = implode('|', $unreferenced_nids); + $alias_parts = []; + foreach ($unreferenced_nids as $nid) { + if (isset($alias_map[$nid])) { + $alias_parts[] = $this->mysqlRegexpEscape(ltrim($alias_map[$nid], '/')); + } + } + + $query = $this->connection->select($table, 'f'); + $query->fields('f', ['entity_id', $value_col]); + $or = $query->orConditionGroup() + ->where("f.`{$value_col}` REGEXP :node_pat", [ + ':node_pat' => '/node/(' . $nid_pattern . ')([^0-9]|$)', + ]); + if (!empty($alias_parts)) { + $or->where("f.`{$value_col}` REGEXP :alias_pat", [ + ':alias_pat' => '(' . implode('|', $alias_parts) . ')([^a-zA-Z0-9/_-]|$)', + ]); + } + $query->condition($or); + + foreach ($query->execute() as $row) { + $value = $row->$value_col; + foreach ($unreferenced_nids as $nid) { + if (isset($referenced[$nid])) { + continue; + } + if (preg_match('#/node/' . $nid . '([^0-9]|$)#', $value)) { + $referenced[$nid] = "Referenced in {$field_name} of {$entity_type}/{$row->entity_id}"; + continue; + } + if (isset($alias_map[$nid])) { + $alias = ltrim($alias_map[$nid], '/'); + if (preg_match('#' . preg_quote($alias, '#') . '([^a-zA-Z0-9/_-]|$)#', $value)) { + $referenced[$nid] = "Referenced via alias in {$field_name} of {$entity_type}/{$row->entity_id}"; + } + } + } + } + } + } + } + + /** + * Check B: Entity reference fields whose target_type is node. + */ + protected function checkEntityReferenceFields(array $candidates, array &$referenced): void { + $nids = array_keys($candidates); + foreach ($this->entityFieldManager->getFieldMapByFieldType('entity_reference') as $entity_type => $fields) { + try { + $storage_defs = $this->entityFieldManager->getFieldStorageDefinitions($entity_type); + } + catch (\Exception $e) { + continue; + } + + foreach (array_keys($fields) as $field_name) { + if (!isset($storage_defs[$field_name])) { + continue; + } + if ($storage_defs[$field_name]->getSetting('target_type') !== 'node') { + continue; + } + + $table = $entity_type . '__' . $field_name; + $target_col = $field_name . '_target_id'; + if (!$this->connection->schema()->tableExists($table)) { + continue; + } + + foreach (array_chunk($nids, 200) as $chunk_nids) { + $unreferenced = array_diff($chunk_nids, array_keys($referenced)); + if (empty($unreferenced)) { + continue; + } + + $query = $this->connection->select($table, 'er'); + $query->fields('er', ['entity_id', $target_col]); + $query->condition($target_col, $unreferenced, 'IN'); + foreach ($query->execute() as $row) { + $nid = (int) $row->$target_col; + if (!isset($referenced[$nid])) { + $referenced[$nid] = "Referenced via {$field_name} on {$entity_type}/{$row->entity_id}"; + } + } + } + } + } + } + + /** + * Check C: Menu links pointing at candidate nodes. + */ + protected function checkMenuLinks(array $candidates, array $alias_map, array &$referenced): void { + if (!$this->connection->schema()->tableExists('menu_link_content_data')) { + return; + } + + $uri_to_nid = []; + foreach (array_keys($candidates) as $nid) { + if (isset($referenced[$nid])) { + continue; + } + foreach (['entity:node/' . $nid, 'internal:/node/' . $nid] as $uri) { + $uri_to_nid[$uri] = $nid; + } + if (isset($alias_map[$nid])) { + $uri = 'internal:' . $alias_map[$nid]; + $uri_to_nid[$uri] = $nid; + } + } + + foreach (array_chunk(array_keys($uri_to_nid), 200) as $chunk) { + $query = $this->connection->select('menu_link_content_data', 'm'); + $query->fields('m', ['id', 'link__uri', 'menu_name']); + $query->condition('link__uri', $chunk, 'IN'); + foreach ($query->execute() as $row) { + $nid = $uri_to_nid[$row->link__uri] ?? NULL; + if ($nid !== NULL && !isset($referenced[$nid])) { + $referenced[$nid] = "Linked from menu '{$row->menu_name}' (menu_link_content/{$row->id})"; + } + } + } + } + + /** + * Check D: Redirects whose destination is a candidate node. + */ + protected function checkRedirects(array $candidates, array &$referenced): void { + if (!$this->connection->schema()->tableExists('redirect')) { + return; + } + + $uri_to_nid = []; + foreach (array_keys($candidates) as $nid) { + if (!isset($referenced[$nid])) { + $uri_to_nid['internal:/node/' . $nid] = $nid; + } + } + + foreach (array_chunk(array_keys($uri_to_nid), 200) as $chunk) { + $query = $this->connection->select('redirect', 'r'); + $query->fields('r', ['rid', 'redirect_source__path', 'redirect_redirect__uri']); + $query->condition('redirect_redirect__uri', $chunk, 'IN'); + foreach ($query->execute() as $row) { + $nid = $uri_to_nid[$row->redirect_redirect__uri] ?? NULL; + if ($nid !== NULL && !isset($referenced[$nid])) { + $referenced[$nid] = "Redirect /{$row->redirect_source__path} points to this node (redirect/{$row->rid})"; + } + } + } + } + + /** + * Escape special characters for MySQL REGEXP patterns. + */ + protected function mysqlRegexpEscape(string $str): string { + return preg_replace('/([.^$*+?{}()\[\]\\\\|])/', '\\\\$1', $str); + } + +} diff --git a/src/Plugin/Util/DeleteOldTermNode.php b/src/Plugin/Util/DeleteOldTermNode.php index a147c52..0b140f3 100644 --- a/src/Plugin/Util/DeleteOldTermNode.php +++ b/src/Plugin/Util/DeleteOldTermNode.php @@ -52,7 +52,12 @@ public function __construct( } /** - * Function to delete old node by term id. + * Delete nodes associated with a term created before the given date. + * + * @param int $term_id + * The taxonomy term ID used to find candidate nodes. + * @param int $date + * Unix timestamp; nodes created before this date will be deleted. */ public function update($term_id, $date) { $query = $this->connection->select('taxonomy_index', 'ti'); @@ -64,7 +69,6 @@ public function update($term_id, $date) { $updated_nid = ''; foreach ($fetch as $nid) { $node = $this->entityTypeManager->getStorage('node')->load($nid); - // $updated_date = $node->getChangedTime(); $updated_date = $node->getCreatedTime(); if ($date > $updated_date) { $node->delete(); diff --git a/src/Plugin/Util/LatestAutoBan.php b/src/Plugin/Util/LatestAutoBan.php index 3e38e7b..6e84a42 100644 --- a/src/Plugin/Util/LatestAutoBan.php +++ b/src/Plugin/Util/LatestAutoBan.php @@ -70,7 +70,18 @@ class LatestAutoBan { public $lastBanId; /** - * Construct object. + * Constructs a new LatestAutoBan object. + * + * @param \Drupal\encrypt\EncryptServiceInterface $encrypt_service + * The encrypt service. + * @param \Drupal\key\KeyRepositoryInterface $key_repository + * The key repository service. + * @param \Drupal\Core\Database\Connection $connection + * The database connection. + * @param \Drupal\oit\Plugin\TeamsAlert $teams_alert + * The Teams alert service. + * @param \Drupal\Core\State\State $state + * The state service. */ public function __construct( EncryptServiceInterface $encrypt_service, @@ -96,7 +107,7 @@ public function __construct( } /** - * Send message to teams listing new banned ip's. + * Send a Teams message listing newly banned IPs since the last check. */ public function messageLatestIps() { $query = $this->connection->select('ban_ip', 'bi'); @@ -131,7 +142,16 @@ public function messageLatestIps() { } /** - * Curl abuseipdb api. + * Query the AbuseIPDB API for information about an IP address. + * + * @param string $ip + * The IP address to look up. + * + * @return string + * The raw JSON response from the AbuseIPDB API. + * + * @throws \Exception + * Thrown if the cURL request fails. */ public function abuseApi($ip) { $key = trim($this->keyRepository->getKey('abuseipdb_crypt')->getKeyValue()); @@ -152,7 +172,7 @@ public function abuseApi($ip) { curl_setopt($ch, CURLOPT_URL, $url); - // Set request to GET method (default) + // Set request to GET method (default). curl_setopt($ch, CURLOPT_HTTPGET, TRUE); curl_setopt($ch, CURLOPT_HTTPHEADER, [ @@ -179,7 +199,7 @@ public function abuseApi($ip) { } /** - * Set last id after teams message. + * Persist the latest ban ID to state after sending the Teams message. */ public function setLastBanId() { $this->state->set('ban_ip_last_id', $this->latestBanId); diff --git a/src/Plugin/Util/ServiceMaintenanceCompletion.php b/src/Plugin/Util/ServiceMaintenanceCompletion.php index 26d810c..6e9b914 100644 --- a/src/Plugin/Util/ServiceMaintenanceCompletion.php +++ b/src/Plugin/Util/ServiceMaintenanceCompletion.php @@ -39,7 +39,14 @@ class ServiceMaintenanceCompletion { protected $teamsAlert; /** - * Function to set to Service maintenance completed once past end date. + * Constructs a new ServiceMaintenanceCompletion object. + * + * @param \Drupal\Core\Database\Connection $connection + * The database connection. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\oit\Plugin\TeamsAlert $teams_alert + * The Teams alert service. */ public function __construct( Connection $connection, @@ -60,7 +67,12 @@ public function __construct( } /** - * Change status if past end date. + * Update service alert nodes from one status to another when past end date. + * + * @param string $from_status + * The current status value to look for. + * @param string $to_status + * The status value to set when the end date has passed. */ public function statusChange($from_status, $to_status) { $query = $this->connection->select('node__field_service_alert_status', 'sa'); diff --git a/src/Plugin/Util/UserClean.php b/src/Plugin/Util/UserClean.php index 65215d6..85cea68 100644 --- a/src/Plugin/Util/UserClean.php +++ b/src/Plugin/Util/UserClean.php @@ -6,12 +6,12 @@ use Drupal\oit\Plugin\TeamsAlert; /** - * Set Serv Maint Completed when past end date. + * Removes user accounts that have not logged in for over a year. * - * @smc( - * id = "service_maintenance_completion", - * title = @Translation("Service Maintenance Completion"), - * description = @Translation("Set service maint complete when past now") + * @UserClean( + * id = "user_clean", + * title = @Translation("User Clean"), + * description = @Translation("Remove inactive user accounts") * ) */ class UserClean { @@ -31,7 +31,12 @@ class UserClean { protected $teamsAlert; /** - * Function to set to Service maintenance completed once past end date. + * Constructs a new UserClean object. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\oit\Plugin\TeamsAlert $teams_alert + * The Teams alert service. */ public function __construct( EntityTypeManagerInterface $entity_type_manager, @@ -43,6 +48,9 @@ public function __construct( /** * Remove users that have not logged in for over a year. + * + * @param int $limit + * Maximum number of users to delete (0 = no limit). */ public function removeUsers($limit = 0) { $user_storage = $this->entityTypeManager->getStorage('user'); diff --git a/src/Services/CronManager.php b/src/Services/CronManager.php index daf5205..405e111 100644 --- a/src/Services/CronManager.php +++ b/src/Services/CronManager.php @@ -74,4 +74,45 @@ public static function topTutorials() { } } + /** + * Delete old news nodes that are not linked anywhere on the site. + * + * Live environment only. Sends Teams report and writes watchdog log. + */ + public static function deleteNews() { + $env = getenv('PANTHEON_ENVIRONMENT'); + if ($env !== 'live') { + \Drupal::logger('oit')->notice('News deletion skipped. This is the @env environment.', ['@env' => $env]); + return; + } + + $result = \Drupal::service('oit.deletenews')->deleteNews(5); + + $deleted_count = count($result['deleted']); + $skipped_count = count($result['skipped']); + + $message = "**News Deletion Report**\n\n"; + $message .= "Deleted **{$deleted_count}** news node(s) older than 5 years.\n\n"; + + if ($deleted_count > 0) { + $message .= "**Deleted nodes:**\n"; + foreach ($result['deleted'] as $nid => $title) { + $message .= "- [{$nid}] {$title}\n"; + } + $message .= "\n"; + } + + if ($skipped_count > 0) { + $message .= "**Skipped {$skipped_count} node(s) due to existing links:**\n"; + foreach ($result['skipped'] as $nid => $info) { + $message .= "- [{$nid}] {$info['title']} — {$info['reason']}\n"; + } + } + else { + $message .= "No nodes were skipped."; + } + + \Drupal::service('oit.teamsalert')->sendMessage($message, ['live']); + } + }