Content Expiry Tracker helps site administrators manage content lifecycle by tracking expiry dates, automating notifications.

This module adds expiry date tracking to nodes, processes expired content via cron, and provides a centralized administrative interface for content review management.

Project Link

https://www.drupal.org/project/content_expiry_tracker

Comments

akasake created an issue. See original summary.

akasake’s picture

Status: Needs work » Needs review
circuitcipher’s picture

Issue summary: View changes
circuitcipher’s picture

Status: Needs review » Needs work

FILE: content_expiry_tracker.module

<?php
/**
 * @file
 * Primary module hooks for Content Expiry Tracker module.
 */

The usual description for a .module file is “Hook implementations for the [module name] module”, where [module name] is the module name given in the .info.yml file.

Use #config_target in ConfigFormBase forms

File: SettingsForm.php

With Drupal 10 and Drupal 11, there is no longer need to use #default_value for each form element, when the parent class is ConfigFormBase: It is sufficient to use #config_target, as in the following code.

    $form['image_toolkit'] = [
      '#type' => 'radios',
      '#title' => $this->t('Select an image processing toolkit'),
      '#config_target' => 'system.image:toolkit',
      '#options' => [],
    ];

Using that code, it is no longer needed to save the configuration values in the form submission handler: The parent class will take care of that.
For this change, it is necessary to require at least Drupal 10.3, but that is not an issue, since Drupal 10.2.x is no longer supported.

Use Constructor Property Promotion

File: ContentExpiryEvent.php

- New modules, which are compatible with Drupal 10 and higher versions are expected to include type declarations in property definitions, and use property promotion. Applies to all classes.
Example:

  public function __construct(
    protected EntityTypeManagerInterface $entityTypeManager,
    protected EntityRepositoryInterface $entityRepository,
    protected DateFormatterInterface $dateFormatter,
    TranslationInterface $translation,
  ) {
    $this->setStringTranslation($translation);
  }

Add tests

Id like to recommend adding some tests to the code. Adding tests is a great way to make sure edge cases work as expected and that you don’t introduce regressions as you update your module on the future.

vishal.kadam’s picture

@circuitcipher

It is preferable to wait for a project moderator before posting the first comment on newly created applications. Project moderators will do some preliminary checks that are necessary before any change on the project files is suggested.

akasake’s picture

Status: Needs work » Needs review

Thanks for the detailed feedback! I've addressed your concerns.
I still need to add tests, but understood these aren't strictly required.

Putting this back to "needs-review" for a project moderator to review.

avpaderno’s picture

Thank you for applying!

Please read Review process for security advisory coverage: What to expect for more details and Security advisory coverage application checklist to understand what reviewers look for. Tips for ensuring a smooth review gives some hints for a smoother review.

The important notes are the following.

  • New releases are not necessary for these applications, which could require changes that are not backward-compatible. Not creating new releases avoids any possible issue.
  • Please do not change the branch to review once reviews started, except in the case the used branch needs to be deleted.
  • If you have not done it yet, enable GitLab CI for the project, and fix what reported from the phpcs job. This help to fix most of what reviewers would report.
  • For the time this application is open, only your commits are allowed. No other people, including other maintainers/co-maintainers can make commits.
  • The purpose of this application is giving you a new drupal.org role that allows you to opt projects into security advisory coverage, either projects you already created, or projects you will create. The project status won't be changed by this application; when the application status is changed to Fixed, you will be able to change it and opt it into security advisory coverage.
  • Nobody else will get the permission to opt projects into security advisory policy. If there are other maintainers/co-maintainers who need to get that permission, they need to apply with a different module.
  • We only accept an application per user. If you change your mind about the project to use for this application, or it is necessary to use a different project for the application, please update the issue summary with the link to the correct project and the issue title with the project name and the branch to review.

To the reviewers

Please read How to review security advisory coverage applications, Application workflow, What to cover in an application review, and Tools to use for reviews.

The important notes are the following.

  • It is preferable to wait for a Project moderator before commenting on newly created applications. Project moderators will do some preliminary checks that are necessary before any change on the project files is suggested.
  • Reviewers should show the output of a CLI tool only once per application. The configuration used for these tools needs to be the same configuration used by GitLab CI, stored in the GitLab Templates repository.
  • It may be best to have the applicant fix things before further review.

For new reviewers, I would also suggest to first read In which way the issue queue for coverage applications is different from other project queues.

vishal.kadam’s picture

Status: Needs review » Needs work

1. The README file is missing the required sections - Requirements and Configuration.

2. FILE: composer.json

    "require": {
        "drupal/core": "^11.0"
    },

It is not necessary to add the Drupal core requirements in the composer.json file: The Drupal.org Composer Façade will add them.

3. FILE: content_expiry_tracker.libraries.yml

version: VERSION

VERSION is only used by Drupal core modules. Contributed modules should use a literal string that does not change with the Drupal core version a site is using.

akasake’s picture

Status: Needs work » Needs review

Thank you for the incredibly fast and detailed feedback. I appreciate the guidance.
I have implemented all the suggested changes:

  • README.md: Expanded the README to include the required Requirements and Configuration sections
  • composer.json: Removed drupal/core from composer.json.
  • libraries.yml: Replaced the VERSION constant with a literal string (1.0.x).

Thank you again for your time!

oivanov’s picture

Status: Needs review » Needs work

I reviewed the 1.0.x branch of content_expiry_tracker (commit e0591b3).

### Previous issues status (vishal.kadam, Comment #8)

All three resolved:
1. README missing sections -- fixed, now includes Requirements, Installation, Configuration, and Usage.
2. `drupal/core` in composer.json -- fixed, removed.
3. `version: VERSION` in libraries.yml -- fixed, now uses `1.0.x`.

### Security

No security issues found. All routes have proper permission-based access control. The custom access check on the mark-reviewed route uses both module permission and entity-level update access. Entity queries use parameterized conditions (no SQL injection). Output uses `$this->t()` with `@` placeholders (XSS-safe). Forms go through Form API (CSRF protected). No raw database queries, no debug code, no bundled third-party libraries, no `\Drupal::` static calls in service classes.

### Issues found

1. **Missing leading backslash in `.install` file** (lines 23, 29): `Drupal::entityDefinitionUpdateManager()` should be `\Drupal::entityDefinitionUpdateManager()`. There is no `use` statement for the `Drupal` class in this procedural file.

2. **Loose comparison** in `ContentExpiryEntityHooks.php` line 188: `$calculatedExpiry != $originalExpiry` should use `!==` (both values are strings).

3. **Missing cache metadata on `AccessResult` objects** in `ContentExpiryEntityHooks::controlExpiryFieldAccess()`: The forbidden result on line 121 depends on the `enable_email_recipients` config value but does not call `->addCacheableDependency($config)`. The permission-based forbidden result should call `->cachePerPermissions()`. Same for `ExpiryController::checkReviewAccess()` -- should add `->cachePerPermissions()`.

4. **Stale docblock** in `CronHooks.php` line 34: Says "between 2am and 3am server time" but this restriction was removed.

5. **Incorrect PHPDoc** in `ExpiryManager.php` line 172: `/** @var \Drupal\node\NodeInterface $nodes[] */` should be `/** @var \Drupal\node\NodeInterface[] $nodes */`.

6. **Dead code** in `ExpiryManager::getExpiredNodes()` line 274: `$currentTime` is assigned but never used.

7. **README.md**: Missing "Maintainers" section per the [Drupal README template](https://www.drupal.org/docs/develop/managing-a-drupalorg-theme-module-or...).

The module is well-structured overall -- good use of OOP hooks with `#[Hook]` attributes, constructor property promotion, dependency injection, proper config schema, and a clean event-driven architecture. All issues above are quick fixes. Setting to **Needs work**.

bbu23’s picture

@oivanov since u are using AI to help drafting your feedback, u could at least make an effort to format the output from Markdown to html or plain text so that people can easily read it...

oivanov’s picture

me personally - I prefer Markdown to everything else, so I make an effort not to have to deal with HTML (definitely) or plain text (when I can).
If markdown is not acceptable - we can add that to the guidance. Let me know if you want me to do that

akasake’s picture

Status: Needs work » Needs review

Thank you for the thorough review, @oivanov! All issues have been addressed:

  1. Missing leading backslash in .install file (lines 23, 29) — Fixed. Both calls to Drupal::entityDefinitionUpdateManager() now use \Drupal::entityDefinitionUpdateManager().
  2. Loose comparison in ContentExpiryEntityHooks.php line 188 — Fixed. Changed != to !== for strict string comparison.
  3. Missing cache metadata on AccessResult objects — Fixed. In controlExpiryFieldAccess(), the config-dependent forbidden result now calls ->addCacheableDependency($config) and the permission-based forbidden result calls ->cachePerPermissions(). In ExpiryController::checkReviewAccess(), ->cachePerPermissions() has been added alongside the existing ->addCacheableDependency($node).

    I even went to see kristiaanvandeneynde's talk last year about exactly this at Drupal Dev Day, and yet I overlooked it. Thanks again for the refresher!
  4. Stale docblock in CronHooks.php line 34 — Fixed. Removed the outdated "between 2am and 3am server time" clause from the docblock.
  5. Incorrect PHPDoc in ExpiryManager.php line 172 — Fixed. Changed /** @var \Drupal\node\NodeInterface $nodes[] */ to /** @var \Drupal\node\NodeInterface[] $nodes */.
  6. Dead code in ExpiryManager::getExpiredNodes() line 274 — Fixed. Removed the unused $currentTime assignment.
  7. README.md missing "Maintainers" section — Fixed. Added a "Maintainers" section at the end of the file.
oivanov’s picture

Status: Needs review » Reviewed & tested by the community

All 7 fixes look good to me, everything has been properly addressed.

avpaderno’s picture

Status: Reviewed & tested by the community » Needs work
  • The following points are just a start and don't necessarily encompass all of the changes that may be necessary
  • A specific point may just be an example and may apply in other places
  • A review is about code that doesn't follow the coding standards, contains possible security issue, or does not correctly use the Drupal API; the single points are not ordered, not even by importance

src/Controller/ExpiryController.php

Since that class does not use methods from the parent class, or it uses a single method from the parent class, it does not need to use ControllerBase as parent class.

Controllers do not need to have a parent class; as long as they implement \Drupal\Core\DependencyInjection\ContainerInjectionInterface, they are fine.

akasake’s picture

Status: Needs work » Needs review

Thank you for the feedback!
I've refactored the code to remove unnecessary base class inheritance.

ExpiryController now implements ContainerInjectionInterface.
Added StringTranslationTrait and injected FormBuilderInterface explicitly.

avpaderno’s picture

Assigned: Unassigned » avpaderno
Status: Needs review » Reviewed & tested by the community

Thank you for your contribution and for your patience with the review process!

I am going to update your account so you can opt into security advisory coverage any project you create, including the projects you already created.

These are some recommended readings to help you with maintainership:

You can find more contributors chatting on Slack or IRC in #drupal-contribute. So, come hang out and stay involved!
Anyone is welcome to participate in the review process. Please consider reviewing other projects that are pending review. I encourage you to learn more about that process and join the group of reviewers.

I thank also all the reviewers for helping with these applications.

avpaderno’s picture

Status: Reviewed & tested by the community » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.