The module provides a Views display plugin ("Leaflet Dynamic Attachment") that syncs a table with a Leaflet map viewport. As users pan and zoom the map, the table updates via AJAX to show only entities visible on the map, with infinite scroll and marker-click row highlighting.
Manual reviews of other projects
- https://www.drupal.org/project/projectapplications/issues/3573378#commen...
- https://www.drupal.org/project/projectapplications/issues/3572290#commen...
- https://www.drupal.org/project/projectapplications/issues/3569572#commen...
Project link
Issue fork projectapplications-3574872
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
vishal.kadamComment #3
rushikesh raval commentedThank 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 #4
vishal.kadam1.
masteris a wrong name for a branch and should be removed. Release branch names always end with the literal .x as described in Release branches.2. FILE: composer.json
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: leaflet_dynamic_table.info.yml
core_version_requirement: ^9 || ^10 || ^11A new project should not declare itself compatible with a Drupal release that is no longer supported. No site should be using Drupal 8 nor Drupal 9, and people should not be encouraged to use those Drupal releases.
4. FILE: leaflet_dynamic_table.module
For a new module that aims to be compatible with Drupal 10/11, it is expected it implements hooks as class methods as described in Support for object oriented hook implementations using autowired services.
5. FILE: src/Controller/LeafletDynamicTableController.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.
Comment #5
oivanov commentedAll five issues from Comment #4 have been fixed in release 1.0.4:
1. Default branch changed to 1.0.x, master branch deleted.
2. Removed drupal/core from composer.json.
3. Updated core_version_requirement to ^10.3 || ^11.
4. All hooks converted to OOP class methods with #[Hook] attributes in src/Hook/LeafletDynamicTableHooks.php, with #[LegacyHook] shims in .module for Drupal 10.3 backward
compatibility.
5. Controller uses constructor property promotion (protected readonly) and has return type declarations on all methods.
Review bonus — six applications reviewed:
1. Dropdown Pager: [https://www.drupal.org/project/projectapplications/issues/3573378#commen...
2. Antibot Redirect: [https://www.drupal.org/project/projectapplications/issues/3572290#commen...
3. Status Block: [https://www.drupal.org/project/projectapplications/issues/3569572#commen...
4. Trace Mail Log [https://www.drupal.org/project/projectapplications/issues/3565393#commen...
5. Secure Password Reset Log [https://www.drupal.org/project/projectapplications/issues/3560157#commen...
6. Content expiry tracker [https://www.drupal.org/project/projectapplications/issues/3570901#commen...
Comment #6
vishal.kadamIt is better not to create new releases during these applications, since a review could ask for a change that is not backward compatible with the existing releases. Just using a development version avoids those BC issues.
Comment #7
vishal.kadamRest seems fine to me.
Please wait for other reviewers and Project Moderator to take a look and if everything goes fine, you will get the role.
Comment #8
bbu23Comment #9
bbu23Below u have my feedback:
- Missing schema for the Views Style plugin
- The controller's parent class uses the
AutowireTrait, which means that the child staticcreatemethod is redundant when the constructor uses property promotion. On the other hand, it looks like the parent controller is not really needed here since none of its functionality is being used.- Plugins should be implemented as PHP Attributes instead of PHP Annotations
- The access on the route is way too open, not even CSRF, or custom access. Even though there's a bit of an access check at view level inside the controller, unauthorised requests should not hit the controller.
- You're manually building responses like 403, 404 that are not even checked in the JS. It would be recommended to use exceptions like
NotFoundHttpExceptionif responses like that are still needed after route adjustments.- Potential DoS through resource exhaustion caused by the following line in the controller
$view->setItemsPerPage(0). The controller fetches all results into memory with no rate limit.- Why the use of
&drupal_static('leaflet_dynamic_table_entity_ids', NULL);(which btw the second argument is redundant) when the Views object can be used to pass data to the hook?Comment #10
oivanov commentedThanks for your feedback. All 7 issues have been fixed on the 1.0.x branch:
1. Config schema added — Created config/schema/leaflet_dynamic_table.views.schema.yml with schema for all custom display plugin options (leaflet_map_display, update_on_zoom,
update_on_pan, debounce_delay, items_per_page, show_count, count_message, highlight_color), extending views_display base type.
2. Redundant create() removed — Controller now implements ContainerInjectionInterface with AutowireTrait instead of extending ControllerBase. The create() method and unused
ContainerInterface import are removed.
3. Annotation converted to PHP attribute — @ViewsDisplay annotation replaced with #[ViewsDisplay(...)] attribute using TranslatableMarkup.
4. Route secured — _access: "TRUE" replaced with _permission: 'access content'. Added isXmlHttpRequest() check in the controller to reject non-AJAX requests (CSRF mitigation).
5. Manual JSON error responses replaced with HTTP exceptions — Now throws BadRequestHttpException, NotFoundHttpException, and AccessDeniedHttpException instead of returning manual
JsonResponse objects.
6. DoS fix: database-level pagination — Removed setItemsPerPage(0) which loaded all results into memory. Now uses setItemsPerPage()/setOffset() with get_total_rows = TRUE for
DB-level pagination. Entity IDs array capped at 10,000 to prevent oversized IN clauses.
7. drupal_static() replaced with view object property — Entity IDs are now passed via $view->leaflet_dynamic_entity_ids instead of drupal_static(), read directly from the
ViewExecutable object in hook_views_query_alter.
However, I must point out that while #4 (Route _access: "TRUE" + CSRF ) and #6 (DoS via setItemsPerPage(0) ) were genuine security/performance concerns, and #1 (Config schema) and #3 (Annotation → PHP attribute) were legit best practice / Drupal standards issues, the remaining 3 out of 7 were rather questions of coding style and/or opinion:
#: 2
Item: Remove create(), use ContainerInjectionInterface
My opinion: Pure style. ControllerBase with create() is a perfectly valid, documented pattern. The reviewer prefers leaner classes, but neither is wrong
#: 5
Item: HTTP exceptions vs manual JsonResponse
My opinion: Style preference. The manual JsonResponse errors returned correct status codes and messages. HTTP exceptions are "more Drupal" but the existing code was not insecure or broken
#: 7
Item: drupal_static → view object property
My opinion: Code smell / style. drupal_static() is a legitimate Drupal API. View object properties are cleaner but drupal_static was not insecure — just old-school
Once again, all items were fixed. Thanks for your time and effort to review my code and safeguard the Drupal ecosystem, I appreciate it.
Comment #11
oivanov commentedre-designing the DB pagination, item #6, as it doesn't work well with the intended functionality. I'll let you know when it is fixed
Comment #12
oivanov commentedpushed DB pagination fixes, the pipeline is green.
@rushikesh-raval, @vishal.kadam, @bbu23 when any of you have a moment please kindly re-review
Comment #13
zeeshan_khan commentedReview of leaflet_dynamic_table - 1.0.x branch
Thank you for addressing the feedback from the previous reviews. The module is well-structured overall, and the switch to OOP hooks with #[Hook] attributes, the PHP attribute-based #[ViewsDisplay], the schema file, and the HTTP exception usage are all correct. Below are the remaining issues I found, grouped by severity.
Critical
Hardcoded AJAX URL breaks subdirectory
installations
In
LeafletDynamicAttachment::attachTo()(line 376):A leading
/is site-root-relative. If Drupal is installedin a subdirectory (e.g.,
https://example.com/drupal/), theJavaScript will send requests to
https://example.com/leaflet-dynamic-table/updateinstead ofhttps://example.com/drupal/leaflet-dynamic-table/update. This isa functional bug for any non-root installation.
Fix: Import the
Urlclass and generatethe URL properly:
items_per_pageis accepted from the client withoutserver-side validation
In
LeafletDynamicTableController::update()(line 70):max(0, ...)allows the client to senditems_per_page=0, which inhandleViewportChange()skips the PHP slice entirely and returns all matching results in a single
response, bypassing the cap. A client could also send an arbitrarily large
value such as
items_per_page=99999.The
items_per_pageis a display-level configuration value.The server already has it available via the loaded view display. It should
not be trusted from client input.
Fix: Read it from the display configuration
server-side:
If reading from the display is not straightforward at that point in the
call stack, at minimum clamp it to a safe range:
Major
declare(strict_types=1)is missing from all PHPfiles
None of the PHP files —
LeafletDynamicAttachment.php,LeafletDynamicTableController.php,LeafletDynamicTableHooks.php, orleaflet_dynamic_table.module— havedeclare(strict_types=1). This is expected for all PHP files inDrupal 11-targeting modules and is flagged by PHPCS on recent Drupal coding
standard rulesets.
Each PHP file should start with:
For the
.modulefile, the@filedocblock goesbefore
declare():Class constants are missing explicit
publicvisibility
In
LeafletDynamicTableController(lines 32–37):PHP 7.1+ supports visibility on class constants. Drupal coding
standards require explicit visibility:
Missing type hints on protected method parameters
handleViewportChange()(line 128) andhandleScroll()(line 199) both have an untyped$viewparameter:
These should be typed:
Similarly,
viewsQueryAlter()inLeafletDynamicTableHooks(line 94) has an untyped$queryparameter:Should be:
Dynamic property set on
ViewExecutableisdeprecated in PHP 8.2+
In both
handleViewportChange()andhandleScroll():Setting an undeclared dynamic property on an object triggers a PHP 8.2
deprecation warning when the class does not declare
#[AllowDynamicProperties]. WhileViewExecutablemaycurrently tolerate this, it is fragile and will break under stricter PHP
versions.
Recommended fix: Pass this data via a request-scoped
service or use a static property on your own class keyed by view ID and
display ID, then access it in
viewsQueryAlter().Direct manipulation of
$view->build_info['query']is fragile
In
handleScroll()(lines 229–231):build_info['query']is an internal implementation detailof Views' Sql query plugin and is not part of any stable API. It may change or
be removed in future Drupal releases. This should at minimum have a
notice, and the fragility should be documented in the README or
inline comments.
Minor
Classes should be
finalLeafletDynamicTableHooksandLeafletDynamicAttachmentare not declaredfinal. Forhook classes and Views plugins in Drupal 11,
finalis therecommended modifier to prevent unintended subclassing:
highlight_coloris not validatedserver-side
The
highlight_coloroption is passed directly from viewconfig to
drupalSettings. The#type => 'color'form element enforces the
#RRGGBBformat in the browser, butnothing validates the stored value server-side. A malicious actor with
sufficient permissions to edit views could craft a raw config save to inject
an arbitrary string which would then be passed to
jQuery.css().Add validation in
submitOptionsForm():JavaScript only supports a single map instance per
page
Drupal.leafletDynamicAttachmentis a single shared objectwith instance state (
currentPage,cacheKey,map, etc.). If two views each with a Leaflet map and a dynamicattachment appear on the same page, they will share this state and break each
other. This should be documented in the README as a known limitation.
README heading case inconsistency and unnecessary
LicensesectionThe
How It Worksheading should be sentence case:How it works, per the drupal.org README template. TheLicensesection is not part of the standard README template andis redundant with the
LICENSE.txtfile. It should be removed.Use a dedicated logger channel
In
services.yml, the module uses@logger.channel.default. This logs to the default channel, makingit harder for site builders to filter module-specific log entries. Use a
dedicated channel instead:
Summary
The module has made substantial progress since the initial reviews. The two
critical issues (hardcoded URL and unvalidated
items_per_page)must be addressed before approval. The major issues — particularly
declare(strict_types=1), missing type hints, and the dynamicproperty concern — should also be resolved. The minor items are improvements
but less urgent.
Comment #14
oivanov commentedThank you for the thorough review @zeeshan_khan! All 13 issues have been addressed in the latest commit on the 1.0.x branch.
Critical:
1. Hardcoded AJAX URL — Replaced '/leaflet-dynamic-table/update' with Url::fromRoute('leaflet_dynamic_table.update')->toString() in LeafletDynamicAttachment.php. This ensures the URL works correctly in subdirectory installs.
2. Client-supplied items_per_page — Removed items_per_page from the POST payload entirely. The controller now reads it server-side from the display plugin configuration via $view->getDisplay()->getOption('items_per_page'), with a clamp to the 5–200 range.
Major:
3. declare(strict_types=1) — Added to all .php files. Omitted from the .module file because phpcs requires the @file docblock as the first element after <?php.
4. Explicit constant visibility — Changed bare const to public const on ENTITY_IDS_CAP and TEMPSTORE_COLLECTION.
5. Untyped $view parameter — Added ViewExecutable type hint to handleViewportChange() and handleScroll().
6. Untyped $query parameter — Added QueryPluginBase type hint to viewsQueryAlter() in both the hooks class and the .module legacy shim.
7. Dynamic property on ViewExecutable — Replaced $view->leaflet_dynamic_entity_ids with a static $entityIds array on the controller, keyed by "view_id:display_id". Accessed via setEntityIds()/getEntityIds() static methods. No dynamic properties are set on core objects.
8. build_info['query'] manipulation — Added a detailed comment explaining why direct SelectQuery::range() is necessary: the None pager (used by Attachment displays) resets setLimit()/setOffset() during the build/execute cycle, so DB-level pagination must be applied after $view->build().
Minor:
9. final keyword — Made LeafletDynamicTableHooks final and changed hasAttachment() from protected to private. LeafletDynamicAttachment is not final because it extends the core Attachment plugin.
10. highlight_color validation — Added server-side regex validation (/^#[0-9a-fA-F]{6}$/) in submitOptionsForm(), falling back to the default #ffeb3b if the value doesn't match.
11. Single map per page limitation — Documented as a "Known limitations" section in README.md.
12. README cleanup — Changed "How It Works" to "How it works" per Drupal documentation standards. Removed the redundant License section.
13. Dedicated logger channel — Created a leaflet_dynamic_table logger channel in services.yml and wired it to the hooks class constructor.
All changes are in commit be790bb on the 1.0.x branch. The CI pipeline is green.
Comment #15
zeeshan_khan commentedVerified all 13 fixes against the actual code in commit
be790bb. 11 of 13 are confirmed fixed. 2 have remainingissues.
Confirmed fixed (11/13)
Url::fromRoute('leaflet_dynamic_table.update')->toString()correctly used in
attachTo().items_per_pagefrom client — No longerread from POST. Now read server-side via
$view->getDisplay()->getOption('items_per_page')with a 5–200clamp.
declare(strict_types=1)— Present inLeafletDynamicAttachment.php,LeafletDynamicTableController.php, andLeafletDynamicTableHooks.php. (see remaining issue below for.module)and
public const TEMPSTORE_COLLECTIONconfirmed.
$viewparameter - BothhandleViewportChange()andhandleScroll()now typedas
ViewExecutable.$queryin hooks class -QueryPluginBase $queryconfirmed on line 98 ofLeafletDynamicTableHooks.php. (see remaining issue below for.module)ViewExecutable-Replaced with
protected static array $entityIdson thecontroller, accessed via
setEntityIds()/getEntityIds()static methods.viewsQueryAlter()now callsLeafletDynamicTableController::getEntityIds().build_info['query']comment - Detailedinline comment added referencing
Drupal\views\Plugin\views\pager\None::query()to explain whydirect
SelectQuery::range()is necessary.final+hasAttachment()visibility -
LeafletDynamicTableHooksis nowfinalandhasAttachment()changed toprivate.highlight_colorserver-side validation -preg_match('/^#[0-9a-fA-F]{6}$/', ...)with#ffeb3bfallback confirmed in
submitOptionsForm().logger.channel.leaflet_dynamic_tableregistered inservices.ymland wired to the hooks class constructor.section added documenting the single map per page
constraint.
(sentence case) confirmed,
Licensesection removed.Two remaining issues
.modulefile is still missingdeclare(strict_types=1)The explanation given - that PHPCS prevents it - is not correct. PHPCS
requires the
@filedocblock to appear beforedeclare(), not instead of it. The correct order is:The
@filedocblock is already present in the file. Addingdeclare(strict_types=1);on the line after it is all that isneeded.
Legacy shim in
.modulestill has an untyped$queryparameterThe fix was confirmed in
LeafletDynamicTableHooks.php, butline 44 of
leaflet_dynamic_table.modulestill reads:$queryremains untyped in the legacy shim. It shouldbe:
Both are one-line fixes in the
.modulefile. Everything elseis correctly and thoroughly addressed - great work on the round 3 fixes
overall.
Comment #16
oivanov commentedThank you for the quick follow-up! Both remaining issues are now fixed in commit d65ad0f:
1. declare(strict_types=1) in .module file — Added after the @file docblock, in the correct order: <?php -> @file docblock -> declare(strict_types=1).
2. QueryPluginBase type hint on $query — Added to the leaflet_dynamic_table_views_query_alter() legacy shim (line 47).
The pipeline is green.
Comment #17
zeeshan_khan commentedI reviewed it again and I can confirm all the issues are fixed now.
Thankyou
Moving this to RTBC!
Comment #18
avpadernoThank 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.
Comment #19
avpadernoComment #21
oivanov commented@avpaderno Thank you so much for your help!
Big thanks to all the reviewers - @vishal.kadam @bbu23 @zeeshan_khan !