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

Project link

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

Command icon 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

    oivanov created an issue. See original summary.

    vishal.kadam’s picture

    Title: Security coverage application for leaflet_dynamic_table » [1.0.x] Leaflet Dynamic Table
    Issue summary: View changes
    rushikesh raval’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.

    • If you have not done it yet, you should enable GitLab CI for the project and fix the PHP_CodeSniffer errors/warnings it reports.
    • For the time this application is open, only your commits are allowed.
    • 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 will not be changed by this application; once this application is closed, you will be able to change the project status from Not covered to Opt into security advisory coverage. This is possible only 14 days after the project is created.


      Keep in mind that once the project is opted into security advisory coverage, only Security Team members may change coverage.
    • Only the person who created the application will get the permission to opt projects into security advisory coverage. No other person will get the same permission from the same application; that applies also to co-maintainers/maintainers of the project used for the application.
    • 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 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.
    • Reviewers should show the output of a CLI tool only once per application.
    • 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. master is 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 || ^11

    A 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

      /**
       * The renderer service.
       *
       * @var \Drupal\Core\Render\RendererInterface
       */
      protected $renderer;
    
      /**
       * Constructs a LeafletDynamicTableController object.
       *
       * @param \Drupal\Core\Render\RendererInterface $renderer
       *   The renderer service.
       */
      public function __construct(RendererInterface $renderer) {
        $this->renderer = $renderer;
      }

    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.

    oivanov’s picture

    Status: Needs work » Needs review

    All 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...

    vishal.kadam’s picture

    It 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.

    vishal.kadam’s picture

    Rest 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.

    bbu23’s picture

    Assigned: Unassigned » bbu23
    bbu23’s picture

    Assigned: bbu23 » Unassigned
    Status: Needs review » Needs work
    Issue tags: +PAreview: security

    Below u have my feedback:
    - Missing schema for the Views Style plugin
    - The controller's parent class uses the AutowireTrait, which means that the child static create method 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
    NotFoundHttpException if 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?

    oivanov’s picture

    Status: Needs work » Needs review

    Thanks 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.

    oivanov’s picture

    Status: Needs review » Needs work

    re-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

    oivanov’s picture

    Status: Needs work » Needs review

    pushed DB pagination fixes, the pipeline is green.
    @rushikesh-raval, @vishal.kadam, @bbu23 when any of you have a moment please kindly re-review

    zeeshan_khan’s picture

    Status: Needs review » Needs work

    Review 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

    1. Hardcoded AJAX URL breaks subdirectory
      installations

      In LeafletDynamicAttachment::attachTo() (line 376):

      'ajaxUrl' => '/leaflet-dynamic-table/update',

      A leading / is site-root-relative. If Drupal is installed
      in a subdirectory (e.g., https://example.com/drupal/), the
      JavaScript will send requests to
      https://example.com/leaflet-dynamic-table/update instead of
      https://example.com/drupal/leaflet-dynamic-table/update. This is
      a functional bug for any non-root installation.

      Fix: Import the Url class and generate
      the URL properly:

      use Drupal\Core\Url;
        // ...
        'ajaxUrl' =>
        Url::fromRoute('leaflet_dynamic_table.update')->toString(),
    2. items_per_page is accepted from the client without
      server-side validation

      In LeafletDynamicTableController::update() (line 70):

      $items_per_page = max(0, (int)
        $request->request->get('items_per_page', 0));

      max(0, ...) allows the client to send
      items_per_page=0, which in handleViewportChange()
      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_page is 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:

      $display = $view->getDisplay();
        $items_per_page = (int) $display->getOption('items_per_page') ?:
        25;

      If reading from the display is not straightforward at that point in the
      call stack, at minimum clamp it to a safe range:

      $items_per_page = max(5, min(200, (int)
        $request->request->get('items_per_page', 25)));

    Major

    1. declare(strict_types=1) is missing from all PHP
      files

      None of the PHP files — LeafletDynamicAttachment.php,
      LeafletDynamicTableController.php,
      LeafletDynamicTableHooks.php, or
      leaflet_dynamic_table.module — have
      declare(strict_types=1). This is expected for all PHP files in
      Drupal 11-targeting modules and is flagged by PHPCS on recent Drupal coding
      standard rulesets.

      Each PHP file should start with:

      <?php
      
        declare(strict_types=1);

      For the .module file, the @file docblock goes
      before declare():

      <?php
      
        /**
         * @file
         * ...
         */
      
        declare(strict_types=1);
    2. Class constants are missing explicit public
      visibility

      In LeafletDynamicTableController (lines 32–37):

      const ENTITY_IDS_CAP = 10000;
        const TEMPSTORE_COLLECTION = 'leaflet_dynamic_table';

      PHP 7.1+ supports visibility on class constants. Drupal coding
      standards require explicit visibility:

      public const ENTITY_IDS_CAP = 10000;
        public const TEMPSTORE_COLLECTION = 'leaflet_dynamic_table';
    3. Missing type hints on protected method parameters

      handleViewportChange() (line 128) and
      handleScroll() (line 199) both have an untyped $view
      parameter:

      protected function handleViewportChange(Request $request,
        $view, ...)
        protected function handleScroll($view, ...)

      These should be typed:

      use Drupal\views\ViewExecutable;
        // ...
        protected function handleViewportChange(Request $request, ViewExecutable
        $view, ...)
        protected function handleScroll(ViewExecutable $view, ...)

      Similarly, viewsQueryAlter() in
      LeafletDynamicTableHooks (line 94) has an untyped
      $query parameter:

      public function viewsQueryAlter(ViewExecutable $view, $query):
        void {

      Should be:

      use Drupal\views\Plugin\views\query\QueryPluginBase;
        // ...
        public function viewsQueryAlter(ViewExecutable $view, QueryPluginBase $query):
         void {
    4. Dynamic property set on ViewExecutable is
      deprecated in PHP 8.2+

      In both handleViewportChange() and
      handleScroll():

      $view->leaflet_dynamic_entity_ids = [...];

      Setting an undeclared dynamic property on an object triggers a PHP 8.2
      deprecation warning when the class does not declare
      #[AllowDynamicProperties]. While ViewExecutable may
      currently 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().

    5. Direct manipulation of $view->build_info['query']
      is fragile

      In handleScroll() (lines 229–231):

      if (isset($view->build_info['query'])) {
          $view->build_info['query']->range($offset, $limit);
        }

      build_info['query'] is an internal implementation detail
      of 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

      //
        @todo

      notice, and the fragility should be documented in the README or
      inline comments.

    Minor

    1. Classes should be final

      LeafletDynamicTableHooks and
      LeafletDynamicAttachment are not declared final. For
      hook classes and Views plugins in Drupal 11, final is the
      recommended modifier to prevent unintended subclassing:

      final class LeafletDynamicTableHooks { ... }
        final class LeafletDynamicAttachment extends Attachment { ... }
    2. highlight_color is not validated
      server-side

      The highlight_color option is passed directly from view
      config to drupalSettings. The #type => 'color'
      form element enforces the #RRGGBB format in the browser, but
      nothing 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():

      $color = $form_state->getValue('highlight_color');
        if (!preg_match('/^#[0-9a-fA-F]{6}$/', $color)) {
          $color = '#ffeb3b';
        }
        $this->setOption('highlight_color', $color);
    3. JavaScript only supports a single map instance per
      page

      Drupal.leafletDynamicAttachment is a single shared object
      with instance state (currentPage, cacheKey,
      map, etc.). If two views each with a Leaflet map and a dynamic
      attachment 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.

    4. README heading case inconsistency and unnecessary
      License section

      The How It Works heading should be sentence case:
      How it works, per the drupal.org README template. The
      License section is not part of the standard README template and
      is redundant with the LICENSE.txt file. It should be removed.

    5. Use a dedicated logger channel

      In services.yml, the module uses
      @logger.channel.default. This logs to the default channel, making
      it harder for site builders to filter module-specific log entries. Use a
      dedicated channel instead:

      $logger: '@logger.channel.leaflet_dynamic_table'

    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 dynamic
    property concern — should also be resolved. The minor items are improvements
    but less urgent.

    oivanov’s picture

    Status: Needs work » Needs review

    Thank 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.

    zeeshan_khan’s picture

    Verified all 13 fixes against the actual code in commit
    be790bb. 11 of 13 are confirmed fixed. 2 have remaining
    issues.

    Confirmed fixed (11/13)

    1. Hardcoded URL -
      Url::fromRoute('leaflet_dynamic_table.update')->toString()
      correctly used in attachTo().
    2. items_per_page from client — No longer
      read from POST. Now read server-side via
      $view->getDisplay()->getOption('items_per_page') with a 5–200
      clamp.
    3. declare(strict_types=1) — Present in
      LeafletDynamicAttachment.php,
      LeafletDynamicTableController.php, and
      LeafletDynamicTableHooks.php. (see remaining issue below for
      .module)
    4. Constant visibility -
      public const
        ENTITY_IDS_CAP

      and public const TEMPSTORE_COLLECTION
      confirmed.

    5. Untyped $view parameter - Both
      handleViewportChange() and handleScroll() now typed
      as ViewExecutable.
    6. Untyped $query in hooks class -
      QueryPluginBase $query confirmed on line 98 of
      LeafletDynamicTableHooks.php. (see remaining issue below for
      .module)
    7. Dynamic property on ViewExecutable -
      Replaced with protected static array $entityIds on the
      controller, accessed via
      setEntityIds()/getEntityIds() static methods.
      viewsQueryAlter() now calls
      LeafletDynamicTableController::getEntityIds().
    8. build_info['query'] comment - Detailed
      inline comment added referencing
      Drupal\views\Plugin\views\pager\None::query() to explain why
      direct SelectQuery::range() is necessary.
    9. final + hasAttachment()
      visibility
      - LeafletDynamicTableHooks is now
      final and hasAttachment() changed to
      private.
    10. highlight_color server-side validation -
      preg_match('/^#[0-9a-fA-F]{6}$/', ...) with #ffeb3b
      fallback confirmed in submitOptionsForm().
    11. Dedicated logger channel -
      logger.channel.leaflet_dynamic_table registered in
      services.yml and wired to the hooks class constructor.
    12. Known limitations in README -
      Known
        limitations

      section added documenting the single map per page
      constraint.

    13. README heading case and License section -
      How it
        works

      (sentence case) confirmed, License section removed.

    Two remaining issues

    1. .module file is still missing
      declare(strict_types=1)

      The explanation given - that PHPCS prevents it - is not correct. PHPCS
      requires the @file docblock to appear before
      declare(), not instead of it. The correct order is:

      &lt;?php
      
        /**
         * @file
         * Legacy hook implementations...
         */
      
        declare(strict_types=1);

      The @file docblock is already present in the file. Adding
      declare(strict_types=1); on the line after it is all that is
      needed.

    2. Legacy shim in .module still has an untyped
      $query parameter

      The fix was confirmed in LeafletDynamicTableHooks.php, but
      line 44 of leaflet_dynamic_table.module still reads:

      function leaflet_dynamic_table_views_query_alter(ViewExecutable
         $view, $query): void {

      $query remains untyped in the legacy shim. It should
      be:

      use Drupal\views\Plugin\views\query\QueryPluginBase;
      
        function leaflet_dynamic_table_views_query_alter(ViewExecutable $view,
        QueryPluginBase $query): void {

    Both are one-line fixes in the .module file. Everything else
    is correctly and thoroughly addressed - great work on the round 3 fixes
    overall.

    oivanov’s picture

    Thank 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.

    zeeshan_khan’s picture

    Status: Needs review » Reviewed & tested by the community

    I reviewed it again and I can confirm all the issues are fixed now.

    Thankyou

    Moving this to RTBC!

    avpaderno’s picture

    Assigned: Unassigned » avpaderno
    Issue summary: View changes

    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.

    oivanov’s picture

    @avpaderno Thank you so much for your help!

    Big thanks to all the reviewers - @vishal.kadam @bbu23 @zeeshan_khan !

    Status: Fixed » Closed (fixed)

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