WP Manifestindependent plugin directory
manifest / builders / modularity

Modularity WP Registry grade D. High-severity findings · 9 findingsWP RegistryHigh-severity findings · 9 findingsOpen the reportD

[Deprecated] Widget plugin for WordPress based on post-type's

★ 9stars
26kcomposer installs
6.90.41latest · 11mo ago

Install

Download source zip No release zip yet

This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.

Install straight from GitHub with WP-CLI:

wp plugin install https://github.com/helsingborg-stad/modularity/archive/refs/heads/main.zip

On Packagist as helsingborg-stad/modularity:

composer require helsingborg-stad/modularity
No built-in updater. New versions won't appear under Dashboard → Updates. Install new releases yourself, or let Git Updater track the repository.

⚠️ Deprecation Notice: Modularity Plugin

The Modularity plugin has been deprecated.

All features have been moved into the Municipio Theme, where ongoing updates and improvements will continue.

This plugin will no longer receive updates or maintenance.

Modularity

Modular component system plugin for WordPress. Drag and drop the bundled modules or your custom modules to your page layout.

Download plugin.

To download a complete out of the box working plugin without the need to compile or fetch dependencies.
Go here and download the full-release.zip from the latest version.

Creating modules

To create your very own Modularity module you simply create a plugin with a class that extends our Modularity\Module class.

A module actually is the same as a custom post type. However we've added a few details to enable you to use them as modules.

Use the $this->register() method to create a very basic module.

Here's a very basic example module for you:

/*
 * Plugin Name: Modularity Article Module
 * Plugin URI: -
 * Description: Article module for Modularity
 * Version: 1.0
 * Author: Modularity
 */

namespace MyArticleModule;

class Article extends \Modularity\Module
{
    public function __construct()
    {
        $id = 'article';
        $nameSingular = 'Article';
        $namePlural = 'Articles';
        $description = 'Outputs a full article with title and content';
        $supports = array('editor'); // All modules automatically supports title
        $icon = '[BASE-64 encoded svg data-uri]';
        $plugin = '/path/to/include-file.php' // CAn also be an array of paths to include 
        $cacheTTL = 60*60*24 //Time to live for fragment cache (stored in persistent object store redis / memcached). 

        $this->register(
            $id,
            $nameSingular,
            $namePlural,
            $description,
            $supports,
            $icon,
            $plugin,
            $cacheTTL
        );
    }
}

new \MyArticleModule\Article;

Module templates

You can easily create your own module templates by placing them in: /wp-content/themes/[my-theme]/templates/module/.

Name your template file with the following pattern: modularity-[module-id].php. You can get your module's id from the Modularity options page.

Module boilerplate

You can download our module boilerplate. It will be a good starting point for any custom module that you would like to build.

Download it here (NOT AVAILABLE YET)

Action reference

Modularity

Runs when Modularity core is loaded. Typically used to add custom modules.

Example:

add_action('Modularity', function () {
    // Do your thing
});

Modularity/Module/[MODULE SLUG]/enqueue

Enqueue js or css only for the add and edit page of the specified module.

Example:

add_action('Modularity/Module/mod-article/enqueue', function () {
    // Do your thing
});

Modularity/Options/Module

Action to use for adding option fields to modularity options page. Use "Modularity/Options/Save" action to handle save of the option field added

Example:

add_action('Modularity/Options/Module', function () {
    echo '<input type="text">';
});

Modularity/save_block

Action triggered whenever a post or page is created or updated.

Example:

add_action('Modularity/save_block', function ($block, $post) {
    // Your code here
});

Filter reference

Modularity/Module/TemplateVersion3

Enable preview of the upcoming version 3 views with BEM formatting. This may be used already when progressing towards BEM.

Example:

add_filter('Modularity/Module/TemplateVersion3', function(){return true;});

Modularity/Editor/WidthOptions

Filter module width options

Params:

$options      The default width options array ('value' => 'label')

Example:

add_filter('Modularity/Editor/WidthOptions', function ($options) {
    // Do your thing
    return $filteredValue;
});

Modularity/Editor/SidebarIncompability

Enables the theme to add incompability indicators of specific module to an sidebar area. The user will not be able to drag and drop to unsupported areas. This filter may simplify the theme developers work by ruling out some cases.

Params:

$moduleSpecification      The default options for the module post object. 

Example:

add_filter('Modularity/Editor/WidthOptions', function ($moduleSpecification) {

    $moduleSpecification['sidebar_compability'] = array("content-area-top"); 

    return $moduleSpecification;
});

Modularity/Display/BeforeModule

Filter module sidebar wrapper (before)

Params:

$beforeModule     The value to filter
$args             Arguments of the sidebar (ex: before_widget)
$moduleType       The module's type
$moduleId         The ID of the module

Example:

add_filter('Modularity/Display/BeforeModule', function ($beforeModule, $args, $moduleType, $moduleId) {
    // Do your thing
    return $filteredValue;
});

Modularity/Display/AfterModule

Filter module sidebar wrapper (after)

Params:

$afterModule      The value to filter
$args             Arguments of the sidebar (ex: before_widget)
$moduleType       The module's type
$moduleId         The ID of the module

Example:

add_filter('Modularity/Display/AfterModule', function ($afterModule, $args, $moduleType, $moduleId) {
    // Do your thing
    return $filteredValue;
});

Modularity/Module/Container/Sidebars

Container wrapper: Filter what sidebars that should support a containing wrapper on some modules

Params:

$sidebars      A array of sidebar id's

Modularity/Module/Container/Modules

Container wrapper: Filter what modules that should support a containing wrapper

Params:

$modules      A array of module ids (post-type names)

Modularity/Module/Container/Template

Container wrapper: Filter the template with html that should be wrapped around each module

Params:

$markup      A string with markup containing {{module-markup}} replacement key

Modularity/Module/TemplatePath & Modularity/Theme/TemplatePath

Modify (add/edit) paths where to look for module/theme templates Typically used for adding search path's for finding custom modules/theme templates.

Attention: Unsetting paths may cause issues displaying modules. Plase do not do this unless you know exacly what you are doing.

Params:

$paths      The value to filter

Example:

add_filter('Modularity/Module/TemplatePath', function ($paths) {
    return $paths;
});

add_filter('Modularity/Theme/TemplatePath', function ($paths) {
    return $paths;
});

Modularity/Module/Classes

Modify the list of classes added to a module's main element

Params:

$classes      The classes (array)
$moduleType   The module type
$sidebarArgs  The sidebar's args

Example:

add_filter('Modularity/Module/Classes', function ($classes, $moduleType, $sidebarArgs) {
    $classes[] = 'example-class';
    return $classes;
});

Modularity/Display/Markup

Module display markup

Params:

$markup      The markup
$module      The module post

Example:

add_filter('Modularity/Display/Markup', function ($markup, $module) {
    return $markup;
});

Modularity/Display/[MODULE SLUG]/Markup

Params:

$markup      The markup
$module      The module post

Example:

add_filter('Modularity/Display/Markup', function ($markup, $module) {
    return $markup;
});

Modularity/CoreTemplatesSearchTemplates

What template files to look for

Params:

$templates

Example:

add_filter('Modularity/CoreTemplatesSearchTemplates', function ($templates) {
    $templates[] = 'my-custom-template';
    return $templates;
});

Modularity/Module/Posts/Date

Modify the displayed publish date in Post Modules

Params:

$date
$postId
$postType

Example:

add_filter('Modularity/Module/Posts/Date', function ($date, $postId, $postType) {
    return $date;
});

Modularity/Module/Posts/Slider/Arguments

Modify the slider arguments in Post Modules

Params:

$args['slider']['slidesPerPage']
$args['slider']['autoSlide']
$args['slider']['showStepper']
$args['slider']['repeatSlide']

Example:

add_filter('Modularity/Module/Posts/Slider/Arguments', function ($args) {
    return $args;
});

Modularity/Editor/ModuleCssScope

Allow editors to select a unique appeance (provided by a theme etc) for a module. Adds a single class to the module wrapper, to allow scoping of css styles.

Params:

$scopes - Previously declared scopes. 

Example:

add_filter('Modularity/Editor/ModuleCssScope',function($scopes) {
        return array(
            'mod-posts' => array(
                's-buy-card' => __("Make this module sparkle!", 'modularity'),
                's-user-list' => __("A boring user list is what i see", 'modularity')
            )
        );
    });

Modularity/Display/viewData

Params:

$viewData - array

Example:

add_filter('Modularity/Display/viewData',function($data) {
        //do something
        return $data;
    });

Modularity/Display/{modulePostTypeSlug}/viewData

Params:

$viewData - array

Example:

add_filter("Modularity/Display/mod-posts/viewData", function($data) {
        //do something
        return $data;
    });

Module Attributes API

Some module features are available by setting certain attributes on the module's outmost element.

data-module-refresh-interval

Creates an interval on which the module is refreshed via XHR by calling the REST API. This attribute also requires that the data-module-id attribute is set on the same element.

The value of the attribute should be the number of seconds on which to run the refresh interval.

Example:

<div data-module-id="123" data-module-refresh-interval="60">
    This content will get refreshed every 60 seconds.
</div>

Rest API

The WordPress REST API is extended with the following endpoints.

modularity/v1/modules/{id}

This endpoint returns the markup for a specific module.

  • Method: GET
  • Params:
    • id: The ID of the module to retrieve.
  • Response: The html markup of the module.

Upgrade

In some updates, database migrations might need to be run (will be in the release docs). To run the upgrade action on all sites in a multisite network, run the following command in the document root of your site.

wp site list --field=url --public=1 --archived=0 --deleted=0 --allow-root | xargs -n1 -I % wp modularity upgrade --url=% --allow-root

Or for a single site installation:

wp modularity upgrade

Constants

MODULARITY_DISABLE_FRAGMENT_CACHE

Disabling the built-in fragment cache means that each module's output will not be stored as an HTML cache. As a result, every time a visitor reloads the page, each module will have to be rendered again. However, it is important to consider this option only if you do not have any object cache enabled, such as Redis or Memcached. We strongly advise enabling the fragment cache feature in your application to enhance performance and caching efficiency, allowing modules to be rendered more quickly and reducing the need for repetitive rendering upon page reloads.

By defining this as true, cache will be turned off. Default: Undefined.

If enabled, each cached module will be rendered with a timestamp and cache ID in the following format:

Tested with support from BrowserStack

This software is tested with the awesome tools from Browserstack.

Releases

Newest 25 of 60 recorded releases.

Tag
Published
6.90.41 latest
Oct 28, 2025 11mo ago
Oct 28, 2025 11mo ago
Oct 24, 2025 12mo ago
Oct 16, 2025 1 year ago
Oct 16, 2025 1 year ago
Oct 15, 2025 1 year ago
Oct 15, 2025 1 year ago
Oct 10, 2025 1 year ago
Oct 9, 2025 1 year ago
Oct 9, 2025 1 year ago
Oct 9, 2025 1 year ago
Oct 8, 2025 1 year ago
Oct 8, 2025 1 year ago
Oct 1, 2025 1 year ago
Sep 30, 2025 1 year ago
Sep 29, 2025 1 year ago
Sep 23, 2025 1 year ago
Sep 23, 2025 1 year ago
Sep 22, 2025 1 year ago
Sep 18, 2025 1 year ago
Sep 18, 2025 1 year ago
Sep 18, 2025 1 year ago
Sep 18, 2025 1 year ago
Sep 10, 2025 1 year ago
Sep 9, 2025 1 year ago
All releases on GitHub →

These releases are tags only. The author does not attach a packaged zip, so there are no download counts to report.

D grade

Security

Modularity 6.90.41 · audited by WP Registry

High-severity findings.

6 high 2 medium 1 low
Audited release
6.90.41
Findings
9
Worst severity
high
Content hash
275186e81882977ed1316404…

Findings

  • high Unauthenticated REST module renderer ignores post status and capabilities

    missing_permission_callback

    register_rest_route for GET modularity/v1/modules/(?P<id>[\d]+) sets permission_callback to __return_true. get_item() loads the post with get_post() and, as long as the post_type is a registered mod-* module, instantiates the module class and returns Display::getModuleMarkup() with no current_user_can('read_post'), no post_status allow-list, and no check that the module is actually placed on a public page. Editor::getModule() used by the shortcode does restrict to publish (and private only when logged in); the REST path does not, so drafts, private, pending, and trash modules are rendered for anyone who can guess or enumerate IDs. Observable: unpublished Script embed codes, Contacts PII, Markdown/RSS remote fetches, and other ACF-backed module HTML. Frontend ModuleRefresher inserts this HTML via template.innerHTML + replaceChild for modules marked data-module-refresh-interval.

    source/php/Api/V1/Modules.php:24-59

    Recommendation

    Replace __return_true with a callback that requires current_user_can('read_post', $id) (or equivalent publish-only for anonymous). Refuse non-publish statuses for unauthenticated requests, and do not instantiate/render modules the requester could not view via the shortcode path.

  • high wp_ajax_get_post returns any WP_Post to every logged-in user

    information_disclosure

    Ajax::getPost is hooked only to wp_ajax_get_post (any authenticated role, including Subscriber). It echoes json_encode(get_post($_POST['id'])) with no nonce, no current_user_can('read_post'), and no post_status check. get_post() returns drafts, private pages, pending review, trash, and password-protected posts, including the post_password column. A Subscriber can therefore read unpublished page content and the plaintext password of protected posts, then open those posts. No nopriv hook, so unauthenticated visitors are not in scope.

    source/php/Ajax.php:9-26

    Recommendation

    Require a capability check (current_user_can('read_post', $id) or edit_post) and a valid nonce. Return only fields the role is allowed to see; never include post_password. Consider using the REST posts controller instead of a custom dump.

  • high wp_ajax_get_post_modules leaks module layout and ACF meta for any post

    information_disclosure

    Ajax::getPostModules is reachable by every logged-in user. It calls Editor::getPostModules($_POST['id']) with no nonce or capability check. getPostModules loads modularity-modules (or the archive option) and Editor::getModule(), which attaches get_post_custom() as $module->meta. The $includeMeta flag is inverted: meta is unset only when includeMeta is true, and the AJAX caller never passes that argument, so full ACF/custom-field blobs ship in the JSON. A Subscriber who knows a page ID learns every module on that page plus unpublished field values (embed codes, API identifiers, contact fields).

    source/php/Ajax.php:32-61

    Recommendation

    Require current_user_can('edit_post', $id) (this endpoint is an editor helper) and a nonce. Do not attach get_post_custom() for callers that should not see ACF internals; fix the inverted includeMeta check.

  • high save_modules updates any post's layout with only a nonce, no object capability

    missing_capability

    Editor::save (wp_ajax_save_modules and the editor load-* hook) gates on Options::isValidPostSave(), which only checks modularity-action=modularity-options and wp_verify_nonce(..., 'modularity-options'). There is no current_user_can('edit_post', $id) or edit_module check. getPostId() takes any numeric $_REQUEST['id'] and saveAsPostMeta() writes modularity-modules and unsanitized modularity-sidebar-options. The nonce is printed on the editor screen registered with capability edit_posts, so Contributor and Author can load options.php?page=modularity-editor, grab the nonce, and rewrite the module sidebar of the front page or any other post they cannot otherwise edit. App::addCaps grants Author edit_modules/publish_modules, and Script::disableHTMLFiltering forces acf/allow_unfiltered_html on mod-script saves, so an Author can create a Script module (raw JS, {!! $embedContent !!} on the frontend) and attach it to pages they do not own. Subscriber cannot obtain the nonce.

    source/php/Editor.php:576-647

    Recommendation

    After verifying the nonce, require current_user_can('edit_post', $postId) for numeric IDs and manage_options (or a dedicated cap) for archive option keys. Do not let Author/Contributor attach modules to posts they cannot edit. Keep unfiltered Script-module HTML behind unfiltered_html or manage_options.

  • high Unauthenticated Curator load-more renders attacker JSON as unsanitized HTML

    xss_reflected

    wp_ajax_nopriv_mod_curator_load_more accepts attacker-controlled posts JSON after a nonce that is localized on any public page displaying a Curator module (wp_create_nonce('mod-posts-load-more'); uid-0 nonces are shared among logged-out users). parseSocialMediaPosts() only overwrites oembed for YouTube/Vimeo; any other network_name leaves attacker-supplied oembed intact. The Blade partial prints {!! $post->oembed !!}. Bundled JS insertAdjacentHTML's the response into the page (event-handler XSS). A cross-origin form POST to admin-ajax.php navigates the browser to the raw HTML response, so a <script> payload executes in the site origin for logged-out victims who share that nonce. layout is also taken from POST and concatenated into the view name (partials/$layout) with no allow-list.

    source/php/Module/Curator/Curator.php:32-68

    Recommendation

    Do not accept client-supplied post objects for rendering. Re-fetch the Curator feed server-side using a server-known embed code. Escape oembed/HTML (or allow-list wp_kses). Allow-list layout to card|block. Treat the nonce as CSRF-only, not as authorization of arbitrary HTML.

  • high Reflected XSS in Modularity editor via unsanitized id query parameter

    xss_reflected

    On options.php?page=modularity-editor the Editor adminBar copies a non-numeric $_GET['id'] into $archive and self::$isEditing['title'], then echoes it unescaped as the options page title suffix. The same $archive is concatenated into an inline script in App::enqueueAdmin as var modularity_post_id = '...' without json_encode/esc_js. The editor screen is registered with capability edit_posts, so any Contributor/Author/Editor/Administrator who opens a crafted URL such as options.php?page=modularity-editor&id=';alert(1);// executes attacker JavaScript in wp-admin (session takeover via plugin install, user create, etc.). Subscriber cannot load the page.

    source/php/App.php:310-325

    Recommendation

    Allow-list archive ids (archive-{post_type}, search, author, numeric post IDs). Escape the title with esc_html and emit the JS variable with wp_add_inline_script + wp_json_encode.

  • medium Posts module AJAX helpers have no nonce or capability check

    missing_auth

    PostsAjax registers wp_ajax_get_taxonomy_types_v2, wp_ajax_get_taxonomy_values_v2, wp_ajax_mod_posts_get_date_source, and wp_ajax_get_sortable_meta_keys_v2 with no nopriv but also no nonce and no current_user_can. Any logged-in Subscriber can pass an arbitrary posttype/tax/post id and receive object taxonomies, get_terms(..., hide_empty=false), and get_field() values (posts_taxonomy_type, posts_taxonomy_value, posts_sort_by, posts_meta_key). These are editor-only helpers. Impact is information disclosure of taxonomies and selected ACF values, not a write.

    source/php/Module/Posts/PostsAjax.php:14-103

    Recommendation

    Restrict these callbacks to users who can edit the relevant module (edit_module / edit_posts), verify a nonce created on the module edit screen, and sanitize posttype/taxonomy names against registered objects.

  • medium Unauthenticated Curator feed proxy with attacker-controlled path segment

    missing_auth

    wp_ajax_nopriv_mod_curator_get_feed has no nonce. On AJAX it copies $_POST['embed-code'] into https://api.curator.io/restricted/feeds/{$embedCode}/posts and returns the body via wp_die(). The host is fixed so this is not arbitrary-URL SSRF (wp_http_validate_url still applies), but any visitor can force the site to fetch arbitrary Curator feed IDs, fill 12-hour transients, and use the site as an unauthenticated proxy. Combined with load-more it is the first step of the XSS chain.

    source/php/Module/Curator/Curator.php:256-287

    Recommendation

    Bind feed fetches to a known module ID (server-side embed_code ACF field) rather than a client-supplied embed-code. Require a nonce. Sanitize the embed id to a strict charset. Rate-limit.

  • low Plugin is explicitly deprecated and unmaintained

    unmaintained

    readme.md states the Modularity plugin has been deprecated, all features moved into the Municipio theme, and that this plugin will no longer receive updates or maintenance. Version 6.90.41 is therefore a frozen attack surface (REST, AJAX, Script module) with no upstream patch path.

    readme.md:1-7

    Recommendation

    Migrate to Municipio's built-in module system and remove this plugin from production sites.

WP Registry hashes the installable build and reports on that exact bytes-for-bytes copy. Embargoed findings are withheld until they are disclosed, so a clean verdict means nothing public is outstanding. WP Manifest does not audit code itself.