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.

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']);
+ }
+
}