Taxonaut is a Drupal module that provides a visual, interactive taxonomy management interface. It replaces the default flat taxonomy term listing with a hierarchical tree view, drag-and-drop reorganization, and powerful bulk operations — all within a single-page application experience.
Key features:
- Interactive tree view with expand/collapse for hierarchical vocabularies
- Drag-and-drop term reordering and re-parenting
- Inline term creation, editing, and deletion
- Snapshot and restore system for taxonomy versioning
- Import/Export support (JSON and CSV formats)
- CSRF-protected AJAX endpoints
- Works with Drupal 10.3+ and PHP 8.1+
How it differs from similar projects:
Unlike the default Drupal taxonomy admin or modules like Taxonomy Manager, Taxonaut provides a fully interactive single-page interface with real-time tree manipulation, undo capability via snapshots, and import/export functionality — all without page reloads.
Reviews of other applications:
(Will be added after reviewing 3 other applications in this queue)
Comments
Comment #2
vishal.kadamComment #3
vishal.kadamComment #4
avpadernoThank 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.
Keep in mind that once the project is opted into security advisory coverage, only Security Team members may change coverage.
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.
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.
Comment #5
zeeshan_khan commentedThank you for submitting Taxonaut for review. After going through the module
based on current Drupal coding standards, here are the issues that need to be
addressed before approval.
validateCsrfToken()is declared with avoidreturntype but is called in a boolean context:
. Since
voidalwaysreturns
null,!nullevaluates totrueon every call, meaning all write operations (add, update, delete, move, merge,
bulk) will always return an error response. The return type must be changed
to
booland the method must returntrueon a validtoken.
AccessDeniedHttpExceptionis used insidevalidateCsrfToken()but is not imported. Add the missing usestatement:
logOperation()uses->condition('vid', $vid)toclear undone revisions, but the column name in the
taxonaut_revisionstable isvocabulary_id. Thedelete query never matches any rows, so the redo stack is never cleared.
saveSnapshot()queries taxonomy_term entities using->condition('vocabulary_id', $vid), but the correct entity fieldname is
vid. All saved snapshots will have an empty term countand empty hierarchy data.
listSnapshots()reads$snapshot->label, but thedatabase column defined in the schema is
name. Snapshot nameswill always be empty in the listing.
src/Controller/RevisionController.php,
src/Controller/SnapshotController.php
validateCsrfToken()is copied identically across all threecontrollers. Extract it to a shared trait at
src/Traits/CsrfValidationTrait.php.src/Controller/TermController.php
getAncestryPath()is duplicated across both controllers. Extractit to a shared trait or service.
\Drupal::static calls must not be used inside OOP classes. Alldependencies must be injected via the constructor. Affected files:
src/Controller/TreeController.php—\Drupal::database()src/Controller/TermController.php—\Drupal::csrfToken()src/Controller/RevisionController.php—\Drupal::csrfToken()src/Controller/SnapshotController.php—\Drupal::database(),\Drupal::time(),\Drupal::csrfToken()src/Form/ImportForm.php—\Drupal::service('file_system')Constructor property promotion is not used in any of the injectable classes.
Drupal 10+ projects should use PHP 8 constructor property promotion instead of
separate property declarations and manual
$this->x = $xassignments. Affected files:
src/Service/RevisionManager.phpsrc/Controller/TermController.phpsrc/Controller/TreeController.phpsrc/Controller/RevisionController.phpsrc/Controller/SnapshotController.phpgetAncestryPath()andisDescendant()have an untyped$storageparameter. Add the appropriate type hint(
TermStorageInterface).Vocabulary::loadMultiple()is called as a static entity method ina non-static class. Inject
entity_type.managervia theconstructor instead.
Hooks should be moved to an OOP hook class at
src/Hook/TaxonautHooks.phpusing the#[Hook]attribute. Procedural
#[LegacyHook]shims should remain in.module. See changerecord: OOP hooks.
Services use manual
arguments:injection. Add a_defaultsblock withautowire: trueandautoconfigure: trueand remove the explicitarguments:arrays.strpos($real_path, $upload_dir) !== 0should be replaced with!str_starts_with($real_path, $upload_dir).attach: function (context) {violates theobject-shorthandESLint rule. Change to the method shorthandform:
attach(context) {.The
authorssection is missing. Add the maintainer name, email,role, and homepage.
The following required files are not present:
.cspell.json— required for Drupal.org security coverageclearance. Must include a
flagWordsblock per href="https://www.drupal.org/node/3524446">drupal.org/node/3524446 andproject-specific words in the
wordsarray..gitlab-ci.yml— required for the CI pipeline onDrupal.org.
config/schema/taxonaut.schema.yml— required configurationschema file.
config/install/taxonaut.settings.yml— required defaultconfiguration file.
Comment #6
vishal.kadam1. FILE: README.md
The README file is missing the required section - Configuration.
2. FILE: composer.json
There is no need to add the required Drupal version, since that is already added by the Drupal.org Composer façade.
3. FILE: taxonaut.libraries.yml
version: VERSIONVERSION 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.
4. FILE: templates/taxonaut-tree.html.twig
Strings shown in the user interface must be translatable. That holds true also for strings used in template files.
5. FILE: src/Controller/RevisionController.php
FILE: src/Controller/TermController.php
FILE: src/Service/RevisionManager.php
New modules, which are compatible with Drupal 10 and higher versions are expected to include type declarations in property definitions, and use constructor property promotion.
6. Enable Gitlab CI
I would suggest enabling GitLab CI for the project, follow the Drupal Association .gitlab-ci.yml template and fix the PHP_CodeSniffer errors/warnings it reports.
Comment #7
justinkjohnson commentedThank you @avpaderno for the guidance and @zeeshan_khan and @vishal.kadam for the detailed review. All issues from both rounds of feedback have been addressed in the latest commits on the 1.0.x branch.
**Round 1 fixes (commit 6cac49c):**
Bug fixes:
- Fixed `validateCsrfToken()` calling pattern in TermController — the void method was used in a boolean context (`if (!$this->validateCsrfToken(...))`), causing all write operations to always return an error response. Now calls directly and lets the exception propagate.
- Fixed `RevisionManager::logOperation()` delete condition from `vid` to `vocabulary_id` to match the actual DB column.
- Fixed `SnapshotController::saveSnapshot()` entity query from `vocabulary_id` to `vid` (correct entity field name).
- Fixed `SnapshotController::listSnapshots()` from `$snapshot->label` to `$snapshot->name` to match DB column.
Code quality:
- Extracted `validateCsrfToken()` into shared `src/Traits/CsrfValidationTrait.php`, used by all three controllers.
- Extracted `getAncestryPath()` into shared `src/Traits/AncestryPathTrait.php`, used by TreeController and TermController.
- Replaced all `\Drupal::` static calls with constructor dependency injection across TermController, RevisionController, SnapshotController, TreeController, ImportForm, and TaxonautPermissions.
- Applied PHP 8 constructor property promotion to all injectable classes.
- Added `TermStorageInterface` type hint to `isDescendant()` parameter.
- TaxonautPermissions now implements `ContainerInjectionInterface` with injected `EntityTypeManagerInterface`.
- Replaced `strpos()` with `str_starts_with()` in ImportForm.
- Added `autowire: true` and `autoconfigure: true` defaults to `taxonaut.services.yml`.
- JS `attach: function(context)` changed to method shorthand `attach(context)`.
- Added authors section to `composer.json`.
- Added `AccessDeniedHttpException` use statement to TermController.
New files:
- `.cspell.json` with flagWords per drupal.org requirements
- `.gitlab-ci.yml` with standard Drupal Association template includes
- `config/schema/taxonaut.schema.yml`
- `config/install/taxonaut.settings.yml`
**Round 2 fixes (commit 10b1a4d):**
- Added required Configuration section to `README.md`.
- Removed `drupal/core` from `composer.json` require (handled by Drupal.org Composer façade), kept `php: >=8.1` only.
- Changed library version from `VERSION` to literal `1.0.x` (VERSION is only for core modules).
- Made all template strings translatable using `{{ '...'|t }}` filter in `taxonaut-tree.html.twig`.
Will enable GitLab CI on the project settings. Setting back to Needs review.