From 2af8c8da1b2c063f1ff9fd1abfbbad4b2587e820 Mon Sep 17 00:00:00 2001
From: Gaurav Tiwari
Date: Wed, 22 Jul 2026 08:41:35 +0530
Subject: [PATCH 1/4] Fix module defaults and publish roadmap
---
README.md | 31 ++++++++-------
ROADMAP.md | 56 +++++++++++++++++++++++++++
includes/features/class-svg-icons.php | 4 +-
3 files changed, 76 insertions(+), 15 deletions(-)
create mode 100644 ROADMAP.md
diff --git a/README.md b/README.md
index 348e262..4f70dd3 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Dynamic Functionalities
-All-in-one WordPress optimization toolkit with 15+ modules for performance, security, SEO, and content management. Built with modern WordPress coding standards and a clean module-based dashboard. Optimized for performance with lazy-loading, static property caching, and intelligent transients.
+All-in-one WordPress optimization toolkit with 16 modules for performance, security, SEO, and content management. Built with modern WordPress coding standards and a clean module-based dashboard. Optimized for performance with modular initialization, static property caching, and intelligent transients.
**Version:** 1.4.7
**License:** GPL-2.0-or-later
@@ -19,18 +19,8 @@ All-in-one WordPress optimization toolkit with 15+ modules for performance, secu
All modules are accessed through a unified dashboard at `wp-admin/admin.php?page=functionalities`. Click any module card to configure its settings.
Learn more on [Functionalities Site](https://functionalities.dev)
----
-## Performance & Footprint
-
-This plugin is built with a "Performance First" philosophy. Unlike many all-in-one plugins that slow down your site, Functionalities is designed to be as lightweight as possible:
-
-- **Modular & Lazy Loaded:** Using a custom autoloader, the plugin only loads the code required for active modules. If a feature is disabled, its code is never even included in memory.
-- **Minimized Database Load:** All module settings are cached in static properties. This ensures that `get_option()` is called at most once per module per request, regardless of how many times a feature is accessed.
-- **Zero Frontend Bloat:** Most modules are "Zero Footprint" on the frontend, meaning they load no CSS or JS unless explicitly required (like the Components or Fonts modules).
-- **Intelligent Filtering:** Content filters (`the_content`, etc.) use `strpos()` fast-exit checks. If the specific markers or tags for a feature aren't present in your content, the plugin exits immediately without running expensive regular expressions or DOM parsing.
-- **Efficient HTML Processing:** We use targeted regex for lightweight tasks (like Schema injection) and only resort to `DOMDocument` when structural manipulation is strictly necessary, ensuring maximum speed.
-- **Aggressive Caching:** Heavy operations—such as reading JSON exception lists, calculating file hashes, or managing redirects—are cached using WordPress Transients or versioned options to minimize Disk I/O.
+See the [public roadmap](ROADMAP.md) for planned fixes and features.
---
@@ -38,7 +28,7 @@ This plugin is built with a "Performance First" philosophy. Unlike many all-in-o
This plugin is built with a "Performance First" philosophy. Unlike many all-in-one plugins that slow down your site, Functionalities is designed to be as lightweight as possible:
-- **Modular & Lazy Loaded:** Using a custom autoloader, the plugin only loads the code required for active modules. If a feature is disabled, its code is never even included in memory.
+- **Modular Initialization:** Each module checks its enabled state before registering its feature hooks. Disabled modules add no frontend assets or feature behavior.
- **Minimized Database Load:** All module settings are cached in static properties. This ensures that `get_option()` is called at most once per module per request, regardless of how many times a feature is accessed.
- **Zero Frontend Bloat:** Most modules are "Zero Footprint" on the frontend, meaning they load no CSS or JS unless explicitly required (like the Components or Fonts modules).
- **Intelligent Filtering:** Content filters (`the_content`, etc.) use `strpos()` fast-exit checks. If the specific markers or tags for a feature aren't present in your content, the plugin exits immediately without running expensive regular expressions or DOM parsing.
@@ -274,6 +264,21 @@ Enhanced login protection and security measures for your WordPress site.
---
+### Progressive Web App
+
+Make the site installable with a web app manifest, service worker, offline fallback, and optional install prompt.
+
+**Features:**
+- Configurable app name, colors, icons, display mode, and orientation
+- Offline page and versioned runtime caching
+- App shortcuts, screenshots, and advanced manifest fields
+- Optional install prompt and Web Share Target support
+- Root-level manifest and service worker endpoints
+
+**Navigate to:** `?page=functionalities&module=pwa`
+
+---
+
## Link Management: Developer Reference
### JSON Preset Format
diff --git a/ROADMAP.md b/ROADMAP.md
new file mode 100644
index 0000000..946ffe5
--- /dev/null
+++ b/ROADMAP.md
@@ -0,0 +1,56 @@
+# Dynamic Functionalities Roadmap
+
+This roadmap favors reliability and measurable performance before adding more
+surface area. GitHub issues are the source of truth for scope and acceptance
+criteria; this file explains the intended release sequence.
+
+## v1.4.8: Stability and quality gates
+
+- Fix SVG Icons so a fresh install leaves the module disabled until the user
+ enables it, matching the explicit-activation policy introduced in v1.2.0.
+- Correct README duplication, module coverage, and performance claims.
+- Make Task Manager and Redirect Manager JSON updates atomic and concurrency-safe
+ ([#41](https://github.com/wpgaurav/functionalities/issues/41)).
+- Add pull-request CI, supported-PHP checks, distribution validation, coding
+ standards, and a minimal test foundation
+ ([#42](https://github.com/wpgaurav/functionalities/issues/42)).
+
+Release gate: automated checks pass across the supported PHP range, concurrent
+file updates cannot lose data, and the manual block-editor iframe smoke test in
+`CLAUDE.md` passes in a real WordPress install.
+
+## v1.5.0: Architecture and portability
+
+- Introduce a true lazy module registry so disabled feature classes are not
+ loaded on frontend requests
+ ([#43](https://github.com/wpgaurav/functionalities/issues/43)).
+- Split the monolithic Admin class into module controllers and move remaining
+ inline assets into versioned files
+ ([#44](https://github.com/wpgaurav/functionalities/issues/44)).
+- Add validated, versioned settings export/import plus a redacted diagnostics
+ bundle ([#45](https://github.com/wpgaurav/functionalities/issues/45)).
+
+Release gate: existing settings and admin URLs remain backward compatible, a
+configuration round-trips between clean sites, and benchmarks show the lazy
+registry reduces included files without increasing option queries.
+
+## v1.6.0: Operational workflows
+
+- Add CSV redirect migration and a bounded, privacy-conscious 404 monitor
+ ([#46](https://github.com/wpgaurav/functionalities/issues/46)).
+- Integrate Assumption Detection and Content Integrity with Site Health,
+ scheduled scans, deduplicated notifications, and useful snapshot diffs
+ ([#47](https://github.com/wpgaurav/functionalities/issues/47)).
+
+Release gate: monitoring is opt-in, capped, and cheap on frontend requests;
+notifications are rate-limited; imports are validated and all-or-nothing.
+
+## Planning rules
+
+- Fix security, data loss, and compatibility regressions before feature work.
+- Every feature needs a disabled-by-default path with no frontend assets.
+- Persisted-data changes need migration, rollback, concurrency, and uninstall
+ coverage.
+- New admin UI must reuse WordPress patterns and remain keyboard accessible.
+- A release is not complete until the GitHub artifact, WordPress.org package,
+ version metadata, and manual editor smoke test agree.
diff --git a/includes/features/class-svg-icons.php b/includes/features/class-svg-icons.php
index bb4065b..8ea37a9 100644
--- a/includes/features/class-svg-icons.php
+++ b/includes/features/class-svg-icons.php
@@ -276,8 +276,8 @@ public static function get_options(): array
}
$defaults = array(
- 'enabled' => true, // Enabled by default
- 'icons' => array(),
+ 'enabled' => false,
+ 'icons' => array(),
);
$opts = (array) \get_option('functionalities_svg_icons', $defaults);
self::$options = array_merge($defaults, $opts);
From 1a7fd50323c1847a5085ae38e025e30178b9eef1 Mon Sep 17 00:00:00 2001
From: Gaurav Tiwari
Date: Wed, 22 Jul 2026 08:45:55 +0530
Subject: [PATCH 2/4] Consolidate roadmap into v1.4.8
---
ROADMAP.md | 33 +++++++++++++++++----------------
1 file changed, 17 insertions(+), 16 deletions(-)
diff --git a/ROADMAP.md b/ROADMAP.md
index 946ffe5..dbaa9a9 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -1,10 +1,10 @@
# Dynamic Functionalities Roadmap
-This roadmap favors reliability and measurable performance before adding more
-surface area. GitHub issues are the source of truth for scope and acceptance
-criteria; this file explains the intended release sequence.
+All planned fixes and features below target **v1.4.8**. GitHub issues are the
+source of truth for scope and acceptance criteria. Work can land in phases, but
+the v1.4.8 release should not be tagged until every listed issue is complete.
-## v1.4.8: Stability and quality gates
+## Phase 1: Stabilize the foundation
- Fix SVG Icons so a fresh install leaves the module disabled until the user
enables it, matching the explicit-activation policy introduced in v1.2.0.
@@ -15,11 +15,7 @@ criteria; this file explains the intended release sequence.
standards, and a minimal test foundation
([#42](https://github.com/wpgaurav/functionalities/issues/42)).
-Release gate: automated checks pass across the supported PHP range, concurrent
-file updates cannot lose data, and the manual block-editor iframe smoke test in
-`CLAUDE.md` passes in a real WordPress install.
-
-## v1.5.0: Architecture and portability
+## Phase 2: Improve architecture and portability
- Introduce a true lazy module registry so disabled feature classes are not
loaded on frontend requests
@@ -30,11 +26,7 @@ file updates cannot lose data, and the manual block-editor iframe smoke test in
- Add validated, versioned settings export/import plus a redacted diagnostics
bundle ([#45](https://github.com/wpgaurav/functionalities/issues/45)).
-Release gate: existing settings and admin URLs remain backward compatible, a
-configuration round-trips between clean sites, and benchmarks show the lazy
-registry reduces included files without increasing option queries.
-
-## v1.6.0: Operational workflows
+## Phase 3: Add operational workflows
- Add CSV redirect migration and a bounded, privacy-conscious 404 monitor
([#46](https://github.com/wpgaurav/functionalities/issues/46)).
@@ -42,8 +34,17 @@ registry reduces included files without increasing option queries.
scheduled scans, deduplicated notifications, and useful snapshot diffs
([#47](https://github.com/wpgaurav/functionalities/issues/47)).
-Release gate: monitoring is opt-in, capped, and cheap on frontend requests;
-notifications are rate-limited; imports are validated and all-or-nothing.
+## v1.4.8 release gate
+
+- All seven roadmap issues are closed through reviewed changes.
+- Automated checks pass across the supported PHP and WordPress range.
+- Concurrent file updates cannot lose redirects, tasks, or hit counts.
+- Existing settings, hooks, admin URLs, and stored data remain compatible.
+- Settings can round-trip between clean sites without exposing sensitive data.
+- Monitoring is opt-in, capped, rate-limited, and cheap on frontend requests.
+- Redirect imports are validated, previewable, and all-or-nothing.
+- The manual block-editor iframe smoke test in `CLAUDE.md` passes in a real
+ WordPress install.
## Planning rules
From 1f088051c093b4f7044fe55f86f0dc770164678f Mon Sep 17 00:00:00 2001
From: Gaurav Tiwari
Date: Wed, 22 Jul 2026 09:44:51 +0530
Subject: [PATCH 3/4] Ship Dynamic Functionalities 1.4.8
---
.distignore | 4 +-
.github/workflows/quality.yml | 74 +
README.md | 39 +-
ROADMAP.md | 6 +-
assets/css/admin.css | 39 +
assets/js/admin-redirects.js | 104 +
assets/js/admin-tools.js | 78 +
assets/js/content-regression.js | 25 +-
composer.json | 28 +
composer.lock | 2427 ++++++
docs/performance-baseline.md | 22 +
functionalities.php | 152 +-
includes/admin/class-admin-ui.php | 14 +-
includes/admin/class-admin.php | 7192 +---------------
includes/admin/class-module-controller.php | 7232 +++++++++++++++++
includes/admin/class-module-docs.php | 284 +-
.../class-redirect-manager-controller.php | 27 +
.../class-settings-portability-controller.php | 451 +
.../admin/class-site-health-controller.php | 262 +
includes/admin/class-svg-icons-controller.php | 27 +
.../admin/class-task-manager-controller.php | 27 +
includes/admin/trait-admin-ajax.php | 8 +-
includes/admin/trait-admin-options.php | 76 +-
includes/admin/trait-admin-sanitizers.php | 57 +-
includes/core/class-module-registry.php | 210 +
.../features/class-assumption-detection.php | 381 +-
includes/features/class-block-cleanup.php | 14 +-
includes/features/class-components.php | 33 +-
.../features/class-content-regression.php | 551 +-
includes/features/class-editor-links.php | 16 +-
includes/features/class-fonts.php | 50 +-
includes/features/class-link-management.php | 150 +-
includes/features/class-login-security.php | 86 +-
includes/features/class-meta.php | 199 +-
includes/features/class-misc.php | 22 +-
includes/features/class-pwa.php | 149 +-
includes/features/class-redirect-manager.php | 800 +-
includes/features/class-schema.php | 42 +-
includes/features/class-snippets.php | 45 +-
includes/features/class-svg-icons.php | 314 +-
includes/features/class-task-manager.php | 564 +-
includes/storage/class-atomic-json-store.php | 259 +
includes/traits/trait-css-sanitizer.php | 2 +-
includes/traits/trait-has-dom-parser.php | 2 +-
phpcs.xml.dist | 40 +
phpunit.xml.dist | 11 +
readme.txt | 19 +-
tests/AtomicJsonStoreTest.php | 159 +
tests/ContentDiffTest.php | 26 +
tests/ModuleDefaultsTest.php | 23 +
tests/ModuleRegistryTest.php | 77 +
tests/RedirectImportTest.php | 49 +
tests/SettingsPortabilityTest.php | 59 +
tests/VersionConsistencyTest.php | 34 +
tests/bootstrap.php | 81 +
tests/fixtures/atomic-worker.php | 27 +
tests/fixtures/module-registry-worker.php | 75 +
tests/php-lint.php | 42 +
uninstall.php | 6 +
59 files changed, 14546 insertions(+), 8726 deletions(-)
create mode 100644 .github/workflows/quality.yml
create mode 100644 assets/js/admin-redirects.js
create mode 100644 assets/js/admin-tools.js
create mode 100644 composer.json
create mode 100644 composer.lock
create mode 100644 docs/performance-baseline.md
create mode 100644 includes/admin/class-module-controller.php
create mode 100644 includes/admin/class-redirect-manager-controller.php
create mode 100644 includes/admin/class-settings-portability-controller.php
create mode 100644 includes/admin/class-site-health-controller.php
create mode 100644 includes/admin/class-svg-icons-controller.php
create mode 100644 includes/admin/class-task-manager-controller.php
create mode 100644 includes/core/class-module-registry.php
create mode 100644 includes/storage/class-atomic-json-store.php
create mode 100644 phpcs.xml.dist
create mode 100644 phpunit.xml.dist
create mode 100644 tests/AtomicJsonStoreTest.php
create mode 100644 tests/ContentDiffTest.php
create mode 100644 tests/ModuleDefaultsTest.php
create mode 100644 tests/ModuleRegistryTest.php
create mode 100644 tests/RedirectImportTest.php
create mode 100644 tests/SettingsPortabilityTest.php
create mode 100644 tests/VersionConsistencyTest.php
create mode 100644 tests/bootstrap.php
create mode 100644 tests/fixtures/atomic-worker.php
create mode 100644 tests/fixtures/module-registry-worker.php
create mode 100644 tests/php-lint.php
diff --git a/.distignore b/.distignore
index 3407609..8257854 100644
--- a/.distignore
+++ b/.distignore
@@ -12,8 +12,8 @@ coverage
.editorconfig
.eslintrc.js
.prettierrc
-phpunit.xml
-phpcs.xml
+phpunit.xml*
+phpcs.xml*
phpstan.neon
# Build tools
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
new file mode 100644
index 0000000..4753a35
--- /dev/null
+++ b/.github/workflows/quality.yml
@@ -0,0 +1,74 @@
+name: Quality
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ php-syntax:
+ name: PHP ${{ matrix.php }} syntax
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ php: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']
+ steps:
+ - uses: actions/checkout@v4
+ - uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ coverage: none
+ - run: php tests/php-lint.php
+
+ quality:
+ name: Coding standards and tests
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.3'
+ coverage: none
+ tools: composer:v2
+ - run: composer install --no-interaction --prefer-dist
+ - run: composer phpcs
+ - run: composer test
+ - run: |
+ for file in $(find assets -name '*.js' -type f); do
+ node --check "$file"
+ done
+ - run: bash -n build.sh
+
+ package:
+ name: Distribution package
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - run: ./build.sh
+ - run: |
+ version=$(grep -m1 'Version:' functionalities.php | awk '{print $NF}')
+ zip="functionalities-${version}.zip"
+ test -s "$zip"
+ unzip -t "$zip"
+ for required in \
+ functionalities/functionalities.php \
+ functionalities/readme.txt \
+ functionalities/uninstall.php \
+ functionalities/includes/admin/class-admin.php \
+ functionalities/includes/admin/class-module-controller.php \
+ functionalities/includes/admin/class-settings-portability-controller.php \
+ functionalities/includes/admin/class-site-health-controller.php \
+ functionalities/includes/core/class-module-registry.php \
+ functionalities/includes/storage/class-atomic-json-store.php \
+ functionalities/assets/js/admin-redirects.js \
+ functionalities/assets/js/admin-tools.js; do
+ unzip -Z1 "$zip" | grep -qx "$required"
+ done
+ if unzip -Z1 "$zip" | grep -Eq '(^|/)(tests|vendor|node_modules|\.git|\.github)/|composer\.(json|lock)$|\.md$'; then
+ echo 'Development files leaked into the distribution.' >&2
+ exit 1
+ fi
diff --git a/README.md b/README.md
index 4f70dd3..5bee44d 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
All-in-one WordPress optimization toolkit with 16 modules for performance, security, SEO, and content management. Built with modern WordPress coding standards and a clean module-based dashboard. Optimized for performance with modular initialization, static property caching, and intelligent transients.
-**Version:** 1.4.7
+**Version:** 1.4.8
**License:** GPL-2.0-or-later
**Text Domain:** `functionalities`
**Pricing:** Free
@@ -244,6 +244,8 @@ Manage URL redirects directly from WordPress admin with high-performance file-ba
- File-based JSON storage for zero database overhead during redirects
- Integrated hit counter for tracking redirect usage
- Normalized path matching
+- CSV import/export with a validated dry-run preview
+- Optional bounded 404 aggregation without visitor identifiers or full referrers
**Navigate to:** `?page=functionalities&module=redirect-manager`
@@ -378,6 +380,8 @@ functionalities/
│ │ └── svg-icons-editor.css
│ └── js/
│ ├── admin.js
+│ ├── admin-redirects.js
+│ ├── admin-tools.js
│ ├── admin-ui.js
│ ├── content-regression.js
│ └── svg-icons-editor.js
@@ -385,7 +389,15 @@ functionalities/
│ ├── admin/
│ │ ├── class-admin.php
│ │ ├── class-admin-ui.php
-│ │ └── class-module-docs.php
+│ │ ├── class-module-controller.php
+│ │ ├── class-module-docs.php
+│ │ ├── class-redirect-manager-controller.php
+│ │ ├── class-settings-portability-controller.php
+│ │ ├── class-site-health-controller.php
+│ │ ├── class-svg-icons-controller.php
+│ │ └── class-task-manager-controller.php
+│ ├── core/
+│ │ └── class-module-registry.php
│ ├── features/
│ │ ├── class-assumption-detection.php
│ │ ├── class-block-cleanup.php
@@ -397,11 +409,14 @@ functionalities/
│ │ ├── class-login-security.php
│ │ ├── class-meta.php
│ │ ├── class-misc.php
+│ │ ├── class-pwa.php
│ │ ├── class-redirect-manager.php
│ │ ├── class-schema.php
│ │ ├── class-snippets.php
│ │ ├── class-svg-icons.php
│ │ └── class-task-manager.php
+│ └── storage/
+│ └── class-atomic-json-store.php
├── languages/
├── exception-urls.json.sample
├── functionalities.php
@@ -414,9 +429,23 @@ functionalities/
## Adding New Modules
1. Create a feature class in `includes/features/class-your-module.php`
-2. Add module definition in `Admin::define_modules()`
-3. Register settings in `Admin::register_settings()`
-4. Initialize in `functionalities.php`
+2. Add its definition to `Core\Module_Registry::get_definitions()`
+3. Register its settings in the module controller
+4. Add focused tests for defaults and any pure helpers
+
+## Local development checks
+
+```bash
+composer install
+composer lint
+composer phpcs
+composer test
+node --check assets/js/admin.js
+bash -n build.sh
+./build.sh
+```
+
+The pull-request workflow runs PHP syntax checks on PHP 7.4 through 8.5, coding standards, PHPUnit, JavaScript and shell syntax, version consistency, and distribution ZIP assertions.
Example module definition:
diff --git a/ROADMAP.md b/ROADMAP.md
index dbaa9a9..5b1e706 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -1,8 +1,8 @@
# Dynamic Functionalities Roadmap
-All planned fixes and features below target **v1.4.8**. GitHub issues are the
-source of truth for scope and acceptance criteria. Work can land in phases, but
-the v1.4.8 release should not be tagged until every listed issue is complete.
+All planned fixes and features below ship in **v1.4.8**. GitHub issues remain the
+source of truth for scope and acceptance criteria. The release is tagged only
+after automated checks and the real-WordPress smoke gate pass.
## Phase 1: Stabilize the foundation
diff --git a/assets/css/admin.css b/assets/css/admin.css
index 84afb28..c398a37 100644
--- a/assets/css/admin.css
+++ b/assets/css/admin.css
@@ -1543,3 +1543,42 @@
grid-template-columns: repeat(2, 1fr);
}
}
+
+.functionalities-tools {
+ margin-top: 30px;
+ padding: 20px;
+ background: #fff;
+ border: 1px solid #c3c4c7;
+ border-radius: 4px;
+}
+
+.functionalities-tools h2 {
+ margin-top: 0;
+}
+
+.functionalities-tools details {
+ margin: 14px 0;
+}
+
+.functionalities-tools__modules {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
+ gap: 8px 16px;
+ margin: 12px 0;
+}
+
+.functionalities-tools__actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-top: 16px;
+}
+
+.functionalities-tools__result {
+ max-height: 320px;
+ overflow: auto;
+ margin: 16px 0 0;
+ padding: 12px;
+ background: #f6f7f7;
+ white-space: pre-wrap;
+}
diff --git a/assets/js/admin-redirects.js b/assets/js/admin-redirects.js
new file mode 100644
index 0000000..e43d4ff
--- /dev/null
+++ b/assets/js/admin-redirects.js
@@ -0,0 +1,104 @@
+(function ($) {
+ 'use strict';
+
+ if (typeof functionalitiesRedirectAdmin === 'undefined') {
+ return;
+ }
+
+ var config = functionalitiesRedirectAdmin;
+
+ function message(response) {
+ return response && response.data && response.data.message ? response.data.message : 'Error';
+ }
+
+ function download(filename, content, type) {
+ var link = document.createElement('a');
+ link.href = URL.createObjectURL(new Blob([content], { type: type }));
+ link.download = filename;
+ link.click();
+ URL.revokeObjectURL(link.href);
+ }
+
+ $(function () {
+ $('#redirect-search').on('input', function () {
+ var term = $(this).val().toLowerCase();
+ $('#redirects-list tr:not(.no-items)').each(function () {
+ var row = $(this);
+ var matches = row.find('td:eq(1)').text().toLowerCase().indexOf(term) !== -1 || row.find('td:eq(2)').text().toLowerCase().indexOf(term) !== -1;
+ row.toggle(matches);
+ });
+ });
+
+ $('#add-redirect-btn').on('click', function () {
+ var from = $('#redirect-from').val().trim();
+ var to = $('#redirect-to').val().trim();
+ if (!from || !to) {
+ window.alert(config.bothRequired);
+ return;
+ }
+ $.post(config.ajaxUrl, { action: 'functionalities_redirect_add', nonce: config.nonce, from: from, to: to, type: $('#redirect-type').val() }, function (response) {
+ if (response.success) { window.location.reload(); } else { window.alert(message(response)); }
+ });
+ });
+
+ $(document).on('change', '.toggle-redirect', function () {
+ $.post(config.ajaxUrl, { action: 'functionalities_redirect_toggle', nonce: config.nonce, id: $(this).closest('tr').data('id') });
+ });
+
+ $(document).on('click', '.delete-redirect', function () {
+ if (!window.confirm(config.deletePrompt)) { return; }
+ var row = $(this).closest('tr');
+ $.post(config.ajaxUrl, { action: 'functionalities_redirect_delete', nonce: config.nonce, id: row.data('id') }, function (response) {
+ if (response.success) { row.fadeOut(function () { row.remove(); }); }
+ });
+ });
+
+ $('#export-redirects-btn, #export-redirects-csv-btn').on('click', function () {
+ var format = this.id.indexOf('csv') !== -1 ? 'csv' : 'json';
+ $.post(config.ajaxUrl, { action: 'functionalities_redirect_export', nonce: config.nonce, format: format }, function (response) {
+ if (response.success) { download(response.data.filename, response.data.content, format === 'csv' ? 'text/csv' : 'application/json'); }
+ });
+ });
+
+ $('#import-redirects-btn, #import-redirects-csv-btn').on('click', function () {
+ $('#import-format').val(this.id.indexOf('csv') !== -1 ? 'csv' : 'json');
+ $('#import-preview').prop('hidden', true).text('');
+ $('#confirm-import').prop('disabled', true);
+ $('#import-modal').css('display', 'flex');
+ });
+ $('#cancel-import').on('click', function () { $('#import-modal').hide(); });
+
+ $('#preview-import').on('click', function () {
+ $.post(config.ajaxUrl, { action: 'functionalities_redirect_import', nonce: config.nonce, document: $('#import-json').val(), format: $('#import-format').val(), dry_run: 1 }, function (response) {
+ $('#import-preview').prop('hidden', false).text(JSON.stringify(response.data, null, 2));
+ $('#confirm-import').prop('disabled', !response.success);
+ });
+ });
+
+ $('#confirm-import').on('click', function () {
+ $.post(config.ajaxUrl, { action: 'functionalities_redirect_import', nonce: config.nonce, document: $('#import-json').val(), format: $('#import-format').val() }, function (response) {
+ if (response.success) { window.location.reload(); } else { window.alert(message(response)); }
+ });
+ });
+
+ $('#purge-404-btn').on('click', function () {
+ if (!window.confirm(config.purgePrompt)) { return; }
+ $.post(config.ajaxUrl, { action: 'functionalities_redirect_404_purge', nonce: config.nonce }, function (response) {
+ if (response.success) { window.location.reload(); }
+ });
+ });
+
+ $(document).on('click', '.ignore-404', function () {
+ var row = $(this).closest('tr');
+ $.post(config.ajaxUrl, { action: 'functionalities_redirect_404_ignore', nonce: config.nonce, path: row.data('path') }, function (response) {
+ if (response.success) { row.remove(); }
+ });
+ });
+
+ $(document).on('click', '.create-from-404', function () {
+ $('#redirect-from').val($(this).closest('tr').data('path'));
+ $('#redirect-to').trigger('focus');
+ window.scrollTo({ top: $('#redirect-from').offset().top - 80, behavior: 'smooth' });
+ });
+ });
+}(jQuery));
diff --git a/assets/js/admin-tools.js b/assets/js/admin-tools.js
new file mode 100644
index 0000000..d528c55
--- /dev/null
+++ b/assets/js/admin-tools.js
@@ -0,0 +1,78 @@
+(function () {
+ 'use strict';
+
+ var root = document.querySelector('[data-functionalities-tools]');
+ if (!root || typeof functionalitiesTools === 'undefined') {
+ return;
+ }
+
+ var fileInput = root.querySelector('[data-tools-file]');
+ var previewButton = root.querySelector('[data-tools-preview]');
+ var applyButton = root.querySelector('[data-tools-apply]');
+ var result = root.querySelector('[data-tools-result]');
+ var documentText = '';
+
+ function request(action, data) {
+ var body = new FormData();
+ body.append('action', action);
+ body.append('nonce', functionalitiesTools.nonce);
+ Object.keys(data || {}).forEach(function (key) {
+ var value = data[key];
+ if (Array.isArray(value)) {
+ value.forEach(function (item) { body.append(key + '[]', item); });
+ } else {
+ body.append(key, value);
+ }
+ });
+ return fetch(functionalitiesTools.ajaxUrl, { method: 'POST', credentials: 'same-origin', body: body }).then(function (response) { return response.json(); });
+ }
+
+ function show(data) {
+ result.hidden = false;
+ result.textContent = JSON.stringify(data, null, 2);
+ }
+
+ function download(filename, content) {
+ var link = document.createElement('a');
+ link.href = URL.createObjectURL(new Blob([content], { type: 'application/json' }));
+ link.download = filename;
+ link.click();
+ URL.revokeObjectURL(link.href);
+ }
+
+ root.querySelector('[data-tools-export]').addEventListener('click', function () {
+ var modules = Array.prototype.map.call(root.querySelectorAll('[data-tools-module]:checked'), function (input) { return input.value; });
+ request('functionalities_settings_export', { modules: modules, include_code: root.querySelector('[data-tools-include-code]').checked ? '1' : '' }).then(function (response) {
+ if (response.success) { download(response.data.filename, response.data.content); } else { show(response.data); }
+ });
+ });
+
+ fileInput.addEventListener('change', function () {
+ if (!fileInput.files.length) { return; }
+ fileInput.files[0].text().then(function (text) {
+ documentText = text;
+ previewButton.disabled = false;
+ applyButton.disabled = true;
+ });
+ });
+
+ previewButton.addEventListener('click', function () {
+ request('functionalities_settings_preview', { document: documentText, include_code: root.querySelector('[data-tools-include-code]').checked ? '1' : '' }).then(function (response) {
+ show(response.data);
+ applyButton.disabled = !response.success;
+ });
+ });
+
+ applyButton.addEventListener('click', function () {
+ request('functionalities_settings_import', { document: documentText, include_code: root.querySelector('[data-tools-include-code]').checked ? '1' : '' }).then(function (response) {
+ show(response.data);
+ applyButton.disabled = response.success;
+ });
+ });
+
+ root.querySelector('[data-tools-diagnostics]').addEventListener('click', function () {
+ request('functionalities_diagnostics').then(function (response) {
+ if (response.success) { download(response.data.filename, response.data.content); } else { show(response.data); }
+ });
+ });
+}());
diff --git a/assets/js/content-regression.js b/assets/js/content-regression.js
index 61d8f7a..4b6b372 100644
--- a/assets/js/content-regression.js
+++ b/assets/js/content-regression.js
@@ -276,6 +276,28 @@
);
}
+ /**
+ * Explain heading-level additions and removals from the snapshot diff.
+ *
+ * @param {Object} props Component props.
+ * @return {JSX.Element|null} Heading change details.
+ */
+ function HeadingChanges( { diff } ) {
+ if ( ! diff || ( ! diff.headings_added.length && ! diff.headings_removed.length ) ) {
+ return null;
+ }
+
+ const added = diff.headings_added.map( ( level ) => 'H' + level ).join( ', ' );
+ const removed = diff.headings_removed.map( ( level ) => 'H' + level ).join( ', ' );
+
+ return wp.element.createElement(
+ 'div',
+ { style: { marginTop: '10px', fontSize: '12px', color: '#50575e' } },
+ added && wp.element.createElement( 'p', { style: { margin: '0 0 4px' } }, i18n.headingsAdded + ' ' + added ),
+ removed && wp.element.createElement( 'p', { style: { margin: 0 } }, i18n.headingsRemoved + ' ' + removed )
+ );
+ }
+
/**
* Main regression panel content component.
*
@@ -411,7 +433,7 @@
return wp.element.createElement( NoBaseline );
}
- const { warnings, has_baseline: hasBaseline, post_settings: postSettings, current, baseline } = status;
+ const { warnings, has_baseline: hasBaseline, post_settings: postSettings, current, baseline, diff } = status;
const hasWarnings = warnings && warnings.length > 0;
return wp.element.createElement(
@@ -439,6 +461,7 @@
current: current,
baseline: baseline
} ),
+ hasBaseline && showActions && wp.element.createElement( HeadingChanges, { diff: diff } ),
// Actions (only shown if there are warnings and showActions is true).
showActions && hasWarnings && ! ignoreWarnings && wp.element.createElement(
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..c41e756
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,28 @@
+{
+ "name": "wpgaurav/functionalities",
+ "description": "Development tooling for the Dynamic Functionalities WordPress plugin.",
+ "type": "wordpress-plugin",
+ "license": "GPL-2.0-or-later",
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^1.1",
+ "phpcompatibility/phpcompatibility-wp": "^2.1.8",
+ "phpunit/phpunit": "^9.6.31",
+ "wp-coding-standards/wpcs": "^3.4"
+ },
+ "config": {
+ "allow-plugins": {
+ "dealerdirect/phpcodesniffer-composer-installer": true
+ },
+ "sort-packages": true
+ },
+ "scripts": {
+ "lint": "@php tests/php-lint.php",
+ "phpcs": "@php -d error_reporting=8191 vendor/bin/phpcs",
+ "test": "phpunit",
+ "check": [
+ "@lint",
+ "@phpcs",
+ "@test"
+ ]
+ }
+}
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..99249c6
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,2427 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "cb986f864b0875c8b9975629640b00e8",
+ "packages": [],
+ "packages-dev": [
+ {
+ "name": "dealerdirect/phpcodesniffer-composer-installer",
+ "version": "v1.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPCSStandards/composer-installer.git",
+ "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/963f0c67bffde0eac41b56be71ac0e8ba132f0bd",
+ "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^2.2",
+ "php": ">=5.4",
+ "squizlabs/php_codesniffer": "^3.1.0 || ^4.0"
+ },
+ "require-dev": {
+ "composer/composer": "^2.2",
+ "ext-json": "*",
+ "ext-zip": "*",
+ "php-parallel-lint/php-parallel-lint": "^1.4.0",
+ "phpcompatibility/php-compatibility": "^9.0 || ^10.0.0@dev",
+ "yoast/phpunit-polyfills": "^1.0"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin"
+ },
+ "autoload": {
+ "psr-4": {
+ "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Franck Nijhof",
+ "email": "opensource@frenck.dev",
+ "homepage": "https://frenck.dev",
+ "role": "Open source developer"
+ },
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors"
+ }
+ ],
+ "description": "PHP_CodeSniffer Standards Composer Installer Plugin",
+ "keywords": [
+ "PHPCodeSniffer",
+ "PHP_CodeSniffer",
+ "code quality",
+ "codesniffer",
+ "composer",
+ "installer",
+ "phpcbf",
+ "phpcs",
+ "plugin",
+ "qa",
+ "quality",
+ "standard",
+ "standards",
+ "style guide",
+ "stylecheck",
+ "tests"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPCSStandards/composer-installer/issues",
+ "security": "https://github.com/PHPCSStandards/composer-installer/security/policy",
+ "source": "https://github.com/PHPCSStandards/composer-installer"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/PHPCSStandards",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/jrfnl",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/php_codesniffer",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/phpcsstandards",
+ "type": "thanks_dev"
+ }
+ ],
+ "time": "2026-05-06T08:26:05+00:00"
+ },
+ {
+ "name": "doctrine/instantiator",
+ "version": "2.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/instantiator.git",
+ "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/23da848e1a2308728fe5fdddabf4be17ff9720c7",
+ "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.4"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^14",
+ "ext-pdo": "*",
+ "ext-phar": "*",
+ "phpbench/phpbench": "^1.2",
+ "phpstan/phpstan": "^2.1",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpunit/phpunit": "^10.5.58"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com",
+ "homepage": "https://ocramius.github.io/"
+ }
+ ],
+ "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
+ "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
+ "keywords": [
+ "constructor",
+ "instantiate"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/instantiator/issues",
+ "source": "https://github.com/doctrine/instantiator/tree/2.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-05T06:47:08+00:00"
+ },
+ {
+ "name": "myclabs/deep-copy",
+ "version": "1.13.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/DeepCopy.git",
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "conflict": {
+ "doctrine/collections": "<1.6.8",
+ "doctrine/common": "<2.13.3 || >=3 <3.2.2"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.6.8",
+ "doctrine/common": "^2.13.3 || ^3.2.2",
+ "phpspec/prophecy": "^1.10",
+ "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/DeepCopy/deep_copy.php"
+ ],
+ "psr-4": {
+ "DeepCopy\\": "src/DeepCopy/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Create deep copies (clones) of your objects",
+ "keywords": [
+ "clone",
+ "copy",
+ "duplicate",
+ "object",
+ "object graph"
+ ],
+ "support": {
+ "issues": "https://github.com/myclabs/DeepCopy/issues",
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-01T08:46:24+00:00"
+ },
+ {
+ "name": "nikic/php-parser",
+ "version": "v5.8.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nikic/PHP-Parser.git",
+ "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/044a6a392ff8ad0d61f14370a5fbbd0a0107152f",
+ "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "ext-tokenizer": "*",
+ "php": ">=7.4"
+ },
+ "require-dev": {
+ "ircmaxell/php-yacc": "^0.0.7",
+ "phpunit/phpunit": "^9.0"
+ },
+ "bin": [
+ "bin/php-parse"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PhpParser\\": "lib/PhpParser"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Nikita Popov"
+ }
+ ],
+ "description": "A PHP parser written in PHP",
+ "keywords": [
+ "parser",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/nikic/PHP-Parser/issues",
+ "source": "https://github.com/nikic/PHP-Parser/tree/v5.8.0"
+ },
+ "time": "2026-07-04T14:30:18+00:00"
+ },
+ {
+ "name": "phar-io/manifest",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/manifest.git",
+ "reference": "54750ef60c58e43759730615a392c31c80e23176"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176",
+ "reference": "54750ef60c58e43759730615a392c31c80e23176",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-phar": "*",
+ "ext-xmlwriter": "*",
+ "phar-io/version": "^3.0.1",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
+ "support": {
+ "issues": "https://github.com/phar-io/manifest/issues",
+ "source": "https://github.com/phar-io/manifest/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-03T12:33:53+00:00"
+ },
+ {
+ "name": "phar-io/version",
+ "version": "3.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/version.git",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Library for handling version information and constraints",
+ "support": {
+ "issues": "https://github.com/phar-io/version/issues",
+ "source": "https://github.com/phar-io/version/tree/3.2.1"
+ },
+ "time": "2022-02-21T01:04:05+00:00"
+ },
+ {
+ "name": "phpcompatibility/php-compatibility",
+ "version": "9.3.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPCompatibility/PHPCompatibility.git",
+ "reference": "9fb324479acf6f39452e0655d2429cc0d3914243"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243",
+ "reference": "9fb324479acf6f39452e0655d2429cc0d3914243",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3",
+ "squizlabs/php_codesniffer": "^2.3 || ^3.0.2"
+ },
+ "conflict": {
+ "squizlabs/php_codesniffer": "2.6.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0"
+ },
+ "suggest": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.",
+ "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues."
+ },
+ "type": "phpcodesniffer-standard",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-3.0-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Wim Godden",
+ "homepage": "https://github.com/wimg",
+ "role": "lead"
+ },
+ {
+ "name": "Juliette Reinders Folmer",
+ "homepage": "https://github.com/jrfnl",
+ "role": "lead"
+ },
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors"
+ }
+ ],
+ "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.",
+ "homepage": "http://techblog.wimgodden.be/tag/codesniffer/",
+ "keywords": [
+ "compatibility",
+ "phpcs",
+ "standards"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues",
+ "source": "https://github.com/PHPCompatibility/PHPCompatibility"
+ },
+ "time": "2019-12-27T09:44:58+00:00"
+ },
+ {
+ "name": "phpcompatibility/phpcompatibility-paragonie",
+ "version": "1.3.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git",
+ "reference": "244d7b04fc4bc2117c15f5abe23eb933b5f02bbf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/244d7b04fc4bc2117c15f5abe23eb933b5f02bbf",
+ "reference": "244d7b04fc4bc2117c15f5abe23eb933b5f02bbf",
+ "shasum": ""
+ },
+ "require": {
+ "phpcompatibility/php-compatibility": "^9.0"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
+ "paragonie/random_compat": "dev-master",
+ "paragonie/sodium_compat": "dev-master"
+ },
+ "suggest": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.",
+ "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues."
+ },
+ "type": "phpcodesniffer-standard",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-3.0-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Wim Godden",
+ "role": "lead"
+ },
+ {
+ "name": "Juliette Reinders Folmer",
+ "role": "lead"
+ }
+ ],
+ "description": "A set of rulesets for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by the Paragonie polyfill libraries.",
+ "homepage": "http://phpcompatibility.com/",
+ "keywords": [
+ "compatibility",
+ "paragonie",
+ "phpcs",
+ "polyfill",
+ "standards",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues",
+ "security": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/security/policy",
+ "source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/PHPCompatibility",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/jrfnl",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/php_codesniffer",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/phpcompatibility",
+ "type": "thanks_dev"
+ }
+ ],
+ "time": "2025-09-19T17:43:28+00:00"
+ },
+ {
+ "name": "phpcompatibility/phpcompatibility-wp",
+ "version": "2.1.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git",
+ "reference": "7c8d18b4d90dac9e86b0869a608fa09158e168fa"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/7c8d18b4d90dac9e86b0869a608fa09158e168fa",
+ "reference": "7c8d18b4d90dac9e86b0869a608fa09158e168fa",
+ "shasum": ""
+ },
+ "require": {
+ "phpcompatibility/php-compatibility": "^9.0",
+ "phpcompatibility/phpcompatibility-paragonie": "^1.0",
+ "squizlabs/php_codesniffer": "^3.3"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^1.0"
+ },
+ "suggest": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.",
+ "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues."
+ },
+ "type": "phpcodesniffer-standard",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-3.0-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Wim Godden",
+ "role": "lead"
+ },
+ {
+ "name": "Juliette Reinders Folmer",
+ "role": "lead"
+ }
+ ],
+ "description": "A ruleset for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by WordPress.",
+ "homepage": "http://phpcompatibility.com/",
+ "keywords": [
+ "compatibility",
+ "phpcs",
+ "standards",
+ "static analysis",
+ "wordpress"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues",
+ "security": "https://github.com/PHPCompatibility/PHPCompatibilityWP/security/policy",
+ "source": "https://github.com/PHPCompatibility/PHPCompatibilityWP"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/PHPCompatibility",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/jrfnl",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/php_codesniffer",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/phpcompatibility",
+ "type": "thanks_dev"
+ }
+ ],
+ "time": "2025-10-18T00:05:59+00:00"
+ },
+ {
+ "name": "phpcsstandards/phpcsextra",
+ "version": "1.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPCSStandards/PHPCSExtra.git",
+ "reference": "b598aa890815b8df16363271b659d73280129101"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/b598aa890815b8df16363271b659d73280129101",
+ "reference": "b598aa890815b8df16363271b659d73280129101",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.4",
+ "phpcsstandards/phpcsutils": "^1.2.0",
+ "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1"
+ },
+ "require-dev": {
+ "php-parallel-lint/php-console-highlighter": "^1.0",
+ "php-parallel-lint/php-parallel-lint": "^1.4.0",
+ "phpcsstandards/phpcsdevcs": "^1.2.0",
+ "phpcsstandards/phpcsdevtools": "^1.2.1",
+ "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4"
+ },
+ "type": "phpcodesniffer-standard",
+ "extra": {
+ "branch-alias": {
+ "dev-stable": "1.x-dev",
+ "dev-develop": "1.x-dev"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-3.0-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Juliette Reinders Folmer",
+ "homepage": "https://github.com/jrfnl",
+ "role": "lead"
+ },
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/PHPCSStandards/PHPCSExtra/graphs/contributors"
+ }
+ ],
+ "description": "A collection of sniffs and standards for use with PHP_CodeSniffer.",
+ "keywords": [
+ "PHP_CodeSniffer",
+ "phpcbf",
+ "phpcodesniffer-standard",
+ "phpcs",
+ "standards",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPCSStandards/PHPCSExtra/issues",
+ "security": "https://github.com/PHPCSStandards/PHPCSExtra/security/policy",
+ "source": "https://github.com/PHPCSStandards/PHPCSExtra"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/PHPCSStandards",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/jrfnl",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/php_codesniffer",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/phpcsstandards",
+ "type": "thanks_dev"
+ }
+ ],
+ "time": "2025-11-12T23:06:57+00:00"
+ },
+ {
+ "name": "phpcsstandards/phpcsutils",
+ "version": "1.2.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPCSStandards/PHPCSUtils.git",
+ "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/c216317e96c8b3f5932808f9b0f1f7a14e3bbf55",
+ "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55",
+ "shasum": ""
+ },
+ "require": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0",
+ "php": ">=5.4",
+ "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1"
+ },
+ "require-dev": {
+ "ext-filter": "*",
+ "php-parallel-lint/php-console-highlighter": "^1.0",
+ "php-parallel-lint/php-parallel-lint": "^1.4.0",
+ "phpcsstandards/phpcsdevcs": "^1.2.0",
+ "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0"
+ },
+ "type": "phpcodesniffer-standard",
+ "extra": {
+ "branch-alias": {
+ "dev-stable": "1.x-dev",
+ "dev-develop": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "PHPCSUtils/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-3.0-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Juliette Reinders Folmer",
+ "homepage": "https://github.com/jrfnl",
+ "role": "lead"
+ },
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/PHPCSStandards/PHPCSUtils/graphs/contributors"
+ }
+ ],
+ "description": "A suite of utility functions for use with PHP_CodeSniffer",
+ "homepage": "https://phpcsutils.com/",
+ "keywords": [
+ "PHP_CodeSniffer",
+ "phpcbf",
+ "phpcodesniffer-standard",
+ "phpcs",
+ "phpcs3",
+ "phpcs4",
+ "standards",
+ "static analysis",
+ "tokens",
+ "utility"
+ ],
+ "support": {
+ "docs": "https://phpcsutils.com/",
+ "issues": "https://github.com/PHPCSStandards/PHPCSUtils/issues",
+ "security": "https://github.com/PHPCSStandards/PHPCSUtils/security/policy",
+ "source": "https://github.com/PHPCSStandards/PHPCSUtils"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/PHPCSStandards",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/jrfnl",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/php_codesniffer",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/phpcsstandards",
+ "type": "thanks_dev"
+ }
+ ],
+ "time": "2025-12-08T14:27:58+00:00"
+ },
+ {
+ "name": "phpunit/php-code-coverage",
+ "version": "9.2.32",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+ "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5",
+ "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-xmlwriter": "*",
+ "nikic/php-parser": "^4.19.1 || ^5.1.0",
+ "php": ">=7.3",
+ "phpunit/php-file-iterator": "^3.0.6",
+ "phpunit/php-text-template": "^2.0.4",
+ "sebastian/code-unit-reverse-lookup": "^2.0.3",
+ "sebastian/complexity": "^2.0.3",
+ "sebastian/environment": "^5.1.5",
+ "sebastian/lines-of-code": "^1.0.4",
+ "sebastian/version": "^3.0.2",
+ "theseer/tokenizer": "^1.2.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.6"
+ },
+ "suggest": {
+ "ext-pcov": "PHP extension that provides line coverage",
+ "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "9.2.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
+ "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
+ "keywords": [
+ "coverage",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
+ "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-08-22T04:23:01+00:00"
+ },
+ {
+ "name": "phpunit/php-file-iterator",
+ "version": "3.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
+ "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
+ "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "FilterIterator implementation that filters files based on a list of suffixes.",
+ "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
+ "keywords": [
+ "filesystem",
+ "iterator"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
+ "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2021-12-02T12:48:52+00:00"
+ },
+ {
+ "name": "phpunit/php-invoker",
+ "version": "3.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-invoker.git",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "ext-pcntl": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-pcntl": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Invoke callables with a timeout",
+ "homepage": "https://github.com/sebastianbergmann/php-invoker/",
+ "keywords": [
+ "process"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-invoker/issues",
+ "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:58:55+00:00"
+ },
+ {
+ "name": "phpunit/php-text-template",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-text-template.git",
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Simple template engine.",
+ "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+ "keywords": [
+ "template"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
+ "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T05:33:50+00:00"
+ },
+ {
+ "name": "phpunit/php-timer",
+ "version": "5.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-timer.git",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Utility class for timing",
+ "homepage": "https://github.com/sebastianbergmann/php-timer/",
+ "keywords": [
+ "timer"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-timer/issues",
+ "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:16:10+00:00"
+ },
+ {
+ "name": "phpunit/phpunit",
+ "version": "9.6.35",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit.git",
+ "reference": "0edba2f3a0c48df3553cb9b640810b30df60302b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0edba2f3a0c48df3553cb9b640810b30df60302b",
+ "reference": "0edba2f3a0c48df3553cb9b640810b30df60302b",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/instantiator": "^1.5.0 || ^2",
+ "ext-dom": "*",
+ "ext-filter": "*",
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-xmlwriter": "*",
+ "myclabs/deep-copy": "^1.13.4",
+ "phar-io/manifest": "^2.0.4",
+ "phar-io/version": "^3.2.1",
+ "php": ">=7.3",
+ "phpunit/php-code-coverage": "^9.2.32",
+ "phpunit/php-file-iterator": "^3.0.6",
+ "phpunit/php-invoker": "^3.1.1",
+ "phpunit/php-text-template": "^2.0.4",
+ "phpunit/php-timer": "^5.0.3",
+ "sebastian/cli-parser": "^1.0.2",
+ "sebastian/code-unit": "^1.0.8",
+ "sebastian/comparator": "^4.0.10",
+ "sebastian/diff": "^4.0.6",
+ "sebastian/environment": "^5.1.5",
+ "sebastian/exporter": "^4.0.8",
+ "sebastian/global-state": "^5.0.8",
+ "sebastian/object-enumerator": "^4.0.4",
+ "sebastian/resource-operations": "^3.0.4",
+ "sebastian/type": "^3.2.1",
+ "sebastian/version": "^3.0.2"
+ },
+ "suggest": {
+ "ext-soap": "To be able to generate mocks based on WSDL files",
+ "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+ },
+ "bin": [
+ "phpunit"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "9.6-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/Framework/Assert/Functions.php"
+ ],
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "The PHP Unit Testing framework.",
+ "homepage": "https://phpunit.de/",
+ "keywords": [
+ "phpunit",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/phpunit/issues",
+ "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.35"
+ },
+ "funding": [
+ {
+ "url": "https://phpunit.de/sponsoring.html",
+ "type": "other"
+ }
+ ],
+ "time": "2026-07-06T14:48:07+00:00"
+ },
+ {
+ "name": "sebastian/cli-parser",
+ "version": "1.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/cli-parser.git",
+ "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
+ "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for parsing CLI options",
+ "homepage": "https://github.com/sebastianbergmann/cli-parser",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/cli-parser/issues",
+ "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-02T06:27:43+00:00"
+ },
+ {
+ "name": "sebastian/code-unit",
+ "version": "1.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit.git",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/code-unit",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:08:54+00:00"
+ },
+ {
+ "name": "sebastian/code-unit-reverse-lookup",
+ "version": "2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Looks up which function or method a line of code belongs to",
+ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:30:19+00:00"
+ },
+ {
+ "name": "sebastian/comparator",
+ "version": "4.0.10",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/comparator.git",
+ "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d",
+ "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/diff": "^4.0",
+ "sebastian/exporter": "^4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@2bepublished.at"
+ }
+ ],
+ "description": "Provides the functionality to compare PHP values for equality",
+ "homepage": "https://github.com/sebastianbergmann/comparator",
+ "keywords": [
+ "comparator",
+ "compare",
+ "equality"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/comparator/issues",
+ "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-24T09:22:56+00:00"
+ },
+ {
+ "name": "sebastian/complexity",
+ "version": "2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/complexity.git",
+ "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a",
+ "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.18 || ^5.0",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for calculating the complexity of PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/complexity",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/complexity/issues",
+ "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-12-22T06:19:30+00:00"
+ },
+ {
+ "name": "sebastian/diff",
+ "version": "4.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/diff.git",
+ "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc",
+ "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3",
+ "symfony/process": "^4.2 || ^5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Kore Nordmann",
+ "email": "mail@kore-nordmann.de"
+ }
+ ],
+ "description": "Diff implementation",
+ "homepage": "https://github.com/sebastianbergmann/diff",
+ "keywords": [
+ "diff",
+ "udiff",
+ "unidiff",
+ "unified diff"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/diff/issues",
+ "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-02T06:30:58+00:00"
+ },
+ {
+ "name": "sebastian/environment",
+ "version": "5.1.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/environment.git",
+ "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
+ "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-posix": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides functionality to handle HHVM/PHP environments",
+ "homepage": "http://www.github.com/sebastianbergmann/environment",
+ "keywords": [
+ "Xdebug",
+ "environment",
+ "hhvm"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/environment/issues",
+ "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:03:51+00:00"
+ },
+ {
+ "name": "sebastian/exporter",
+ "version": "4.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/exporter.git",
+ "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c",
+ "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "ext-mbstring": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Provides the functionality to export PHP variables for visualization",
+ "homepage": "https://www.github.com/sebastianbergmann/exporter",
+ "keywords": [
+ "export",
+ "exporter"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/exporter/issues",
+ "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-09-24T06:03:27+00:00"
+ },
+ {
+ "name": "sebastian/global-state",
+ "version": "5.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/global-state.git",
+ "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6",
+ "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "ext-dom": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-uopz": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Snapshotting of global state",
+ "homepage": "http://www.github.com/sebastianbergmann/global-state",
+ "keywords": [
+ "global state"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/global-state/issues",
+ "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-10T07:10:35+00:00"
+ },
+ {
+ "name": "sebastian/lines-of-code",
+ "version": "1.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/lines-of-code.git",
+ "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5",
+ "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.18 || ^5.0",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for counting the lines of code in PHP source code",
+ "homepage": "https://github.com/sebastianbergmann/lines-of-code",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
+ "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-12-22T06:20:34+00:00"
+ },
+ {
+ "name": "sebastian/object-enumerator",
+ "version": "4.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-enumerator.git",
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71",
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Traverses array structures and object graphs to enumerate all referenced objects",
+ "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
+ "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:12:34+00:00"
+ },
+ {
+ "name": "sebastian/object-reflector",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-reflector.git",
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Allows reflection of object attributes, including inherited and non-public ones",
+ "homepage": "https://github.com/sebastianbergmann/object-reflector/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
+ "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:14:26+00:00"
+ },
+ {
+ "name": "sebastian/recursion-context",
+ "version": "4.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/recursion-context.git",
+ "reference": "539c6691e0623af6dc6f9c20384c120f963465a0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0",
+ "reference": "539c6691e0623af6dc6f9c20384c120f963465a0",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ }
+ ],
+ "description": "Provides functionality to recursively process PHP variables",
+ "homepage": "https://github.com/sebastianbergmann/recursion-context",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-10T06:57:39+00:00"
+ },
+ {
+ "name": "sebastian/resource-operations",
+ "version": "3.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/resource-operations.git",
+ "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
+ "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides a list of PHP built-in functions that operate on resources",
+ "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
+ "support": {
+ "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-14T16:00:52+00:00"
+ },
+ {
+ "name": "sebastian/type",
+ "version": "3.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/type.git",
+ "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
+ "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.2-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the types of the PHP type system",
+ "homepage": "https://github.com/sebastianbergmann/type",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/type/issues",
+ "source": "https://github.com/sebastianbergmann/type/tree/3.2.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:13:03+00:00"
+ },
+ {
+ "name": "sebastian/version",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/version.git",
+ "reference": "c6c1022351a901512170118436c764e473f6de8c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c",
+ "reference": "c6c1022351a901512170118436c764e473f6de8c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+ "homepage": "https://github.com/sebastianbergmann/version",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/version/issues",
+ "source": "https://github.com/sebastianbergmann/version/tree/3.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T06:39:44+00:00"
+ },
+ {
+ "name": "squizlabs/php_codesniffer",
+ "version": "3.13.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
+ "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4",
+ "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4",
+ "shasum": ""
+ },
+ "require": {
+ "ext-simplexml": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4"
+ },
+ "bin": [
+ "bin/phpcbf",
+ "bin/phpcs"
+ ],
+ "type": "library",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Greg Sherwood",
+ "role": "Former lead"
+ },
+ {
+ "name": "Juliette Reinders Folmer",
+ "role": "Current lead"
+ },
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors"
+ }
+ ],
+ "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
+ "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
+ "keywords": [
+ "phpcs",
+ "standards",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues",
+ "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy",
+ "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
+ "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/PHPCSStandards",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/jrfnl",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/php_codesniffer",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/phpcsstandards",
+ "type": "thanks_dev"
+ }
+ ],
+ "time": "2025-11-04T16:30:35+00:00"
+ },
+ {
+ "name": "theseer/tokenizer",
+ "version": "1.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/theseer/tokenizer.git",
+ "reference": "b7489ce515e168639d17feec34b8847c326b0b3c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c",
+ "reference": "b7489ce515e168639d17feec34b8847c326b0b3c",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+ "support": {
+ "issues": "https://github.com/theseer/tokenizer/issues",
+ "source": "https://github.com/theseer/tokenizer/tree/1.3.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2025-11-17T20:03:58+00:00"
+ },
+ {
+ "name": "wp-coding-standards/wpcs",
+ "version": "3.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/WordPress/WordPress-Coding-Standards.git",
+ "reference": "469c18ceab4d642b15bad4c65ebf3b307bfd55ab"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/469c18ceab4d642b15bad4c65ebf3b307bfd55ab",
+ "reference": "469c18ceab4d642b15bad4c65ebf3b307bfd55ab",
+ "shasum": ""
+ },
+ "require": {
+ "ext-filter": "*",
+ "ext-libxml": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlreader": "*",
+ "php": ">=7.2",
+ "phpcsstandards/phpcsextra": "^1.5.0",
+ "phpcsstandards/phpcsutils": "^1.2.2",
+ "squizlabs/php_codesniffer": "^3.13.5"
+ },
+ "require-dev": {
+ "php-parallel-lint/php-console-highlighter": "^1.0.0",
+ "php-parallel-lint/php-parallel-lint": "^1.4.0",
+ "phpcompatibility/php-compatibility": "^10.0.0@dev",
+ "phpcsstandards/phpcsdevtools": "^1.2.0",
+ "phpunit/phpunit": "^8.0 || ^9.0"
+ },
+ "suggest": {
+ "ext-iconv": "For improved results",
+ "ext-mbstring": "For improved results"
+ },
+ "type": "phpcodesniffer-standard",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors"
+ }
+ ],
+ "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions",
+ "keywords": [
+ "phpcs",
+ "standards",
+ "static analysis",
+ "wordpress"
+ ],
+ "support": {
+ "issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues",
+ "source": "https://github.com/WordPress/WordPress-Coding-Standards",
+ "wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/php_codesniffer",
+ "type": "custom"
+ }
+ ],
+ "time": "2026-07-16T13:05:29+00:00"
+ }
+ ],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": {},
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {},
+ "platform-dev": {},
+ "plugin-api-version": "2.9.0"
+}
diff --git a/docs/performance-baseline.md b/docs/performance-baseline.md
new file mode 100644
index 0000000..4eb04c8
--- /dev/null
+++ b/docs/performance-baseline.md
@@ -0,0 +1,22 @@
+# Module loading baseline
+
+The v1.4.8 registry reads each module's lightweight option before referencing its feature class.
+
+| Frontend request | v1.4.7 feature files | v1.4.8 feature files |
+| --- | ---: | ---: |
+| All modules disabled | 16 | 0 |
+| Only Performance & Cleanup enabled | 16 | 1 |
+
+The isolated registry test records included files for both v1.4.8 scenarios. Shared bootstrap and storage classes remain autoloaded only when their callers need them. Existing option names, enable filters, admin URLs, and module classes remain compatible.
+
+## WordPress runtime measurement
+
+The all-disabled scenario was also measured on July 22, 2026 in the same local WordPress Studio site on PHP 8.3. Each result was reproduced across three requests after swapping only the plugin version.
+
+| Version | Feature files | Memory usage | Peak memory | Database queries |
+| --- | ---: | ---: | ---: | ---: |
+| v1.4.7 | 16 | 59,171,832 bytes | 59,361,480 bytes | 4 |
+| v1.4.8 | 0 | 56,842,472 bytes | 57,032,464 bytes | 4 |
+| Difference | -16 | -2,329,360 bytes | -2,329,016 bytes | 0 |
+
+With all modules disabled, v1.4.8 avoids loading every feature implementation and reduces request memory by about 2.22 MiB in this environment. The database query count is unchanged because WordPress loads these small module flags from its autoloaded options cache.
diff --git a/functionalities.php b/functionalities.php
index 9f132ae..c1ef07c 100644
--- a/functionalities.php
+++ b/functionalities.php
@@ -3,7 +3,7 @@
* Plugin Name: Dynamic Functionalities
* Plugin URI: https://functionalities.dev
* Description: All-in-one WordPress optimization toolkit. 15+ modules for performance, security, SEO, and content management.
- * Version: 1.4.7
+ * Version: 1.4.8
* Author: Gaurav Tiwari
* Author URI: https://gauravtiwari.org
* License: GPL-2.0-or-later
@@ -13,96 +13,110 @@
* Requires PHP: 7.4
*/
-if (!defined('ABSPATH')) {
+if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
// Define constants.
-if (!defined('FUNCTIONALITIES_VERSION')) {
- define('FUNCTIONALITIES_VERSION', '1.4.7');
+if ( ! defined( 'FUNCTIONALITIES_VERSION' ) ) {
+ define( 'FUNCTIONALITIES_VERSION', '1.4.8' );
}
-if (!defined('FUNCTIONALITIES_FILE')) {
- define('FUNCTIONALITIES_FILE', __FILE__);
+if ( ! defined( 'FUNCTIONALITIES_FILE' ) ) {
+ define( 'FUNCTIONALITIES_FILE', __FILE__ );
}
-if (!defined('FUNCTIONALITIES_DIR')) {
- define('FUNCTIONALITIES_DIR', plugin_dir_path(__FILE__));
+if ( ! defined( 'FUNCTIONALITIES_DIR' ) ) {
+ define( 'FUNCTIONALITIES_DIR', plugin_dir_path( __FILE__ ) );
}
-if (!defined('FUNCTIONALITIES_URL')) {
- define('FUNCTIONALITIES_URL', plugin_dir_url(__FILE__));
+if ( ! defined( 'FUNCTIONALITIES_URL' ) ) {
+ define( 'FUNCTIONALITIES_URL', plugin_dir_url( __FILE__ ) );
}
// Simple autoloader for plugin classes.
-spl_autoload_register(function (string $class) {
- if (strpos($class, 'Functionalities\\') !== 0) {
- return;
- }
+spl_autoload_register(
+ function ( string $class ) {
+ if ( strpos( $class, 'Functionalities\\' ) !== 0 ) {
+ return;
+ }
- $parts = explode('\\', $class);
- array_shift($parts); // Remove Functionalities
+ $parts = explode( '\\', $class );
+ array_shift( $parts ); // Remove Functionalities
- $subpath = '';
- if (count($parts) > 1) {
- $subpath = strtolower((string) $parts[0]) . '/';
- }
+ $subpath = '';
+ if ( count( $parts ) > 1 ) {
+ $subpath = strtolower( (string) $parts[0] ) . '/';
+ }
- $basename = strtolower(str_replace('_', '-', (string) end($parts)));
+ $basename = strtolower( str_replace( '_', '-', (string) end( $parts ) ) );
- // Try class- prefix first, then trait- prefix.
- $file = FUNCTIONALITIES_DIR . 'includes/' . $subpath . 'class-' . $basename . '.php';
- if (!file_exists($file)) {
- $file = FUNCTIONALITIES_DIR . 'includes/' . $subpath . 'trait-' . $basename . '.php';
- }
+ // Try class- prefix first, then trait- prefix.
+ $file = FUNCTIONALITIES_DIR . 'includes/' . $subpath . 'class-' . $basename . '.php';
+ if ( ! file_exists( $file ) ) {
+ $file = FUNCTIONALITIES_DIR . 'includes/' . $subpath . 'trait-' . $basename . '.php';
+ }
- if (file_exists($file)) {
- require_once $file;
+ if ( file_exists( $file ) ) {
+ require_once $file;
+ }
}
-});
+);
-// Initialize plugin on init hook.
-\add_action('init', function () {
- \Functionalities\Admin\Admin::init();
- \Functionalities\Features\Link_Management::init();
- \Functionalities\Features\Block_Cleanup::init();
- \Functionalities\Features\Editor_Links::init();
- \Functionalities\Features\Misc::init();
- \Functionalities\Features\Snippets::init();
- \Functionalities\Features\Schema::init();
- \Functionalities\Features\Components::init();
- \Functionalities\Features\Fonts::init();
- \Functionalities\Features\Meta::init();
- \Functionalities\Features\Content_Regression::init();
- \Functionalities\Features\Assumption_Detection::init();
- \Functionalities\Features\Task_Manager::init();
- \Functionalities\Features\Redirect_Manager::init();
- \Functionalities\Features\Login_Security::init();
- \Functionalities\Features\SVG_Icons::init();
- \Functionalities\Features\PWA::init();
-}, 10);
+// Initialize admin and only the modules required for this request.
+\add_action(
+ 'plugins_loaded',
+ function () {
+ if ( \is_admin() ) {
+ \Functionalities\Admin\Admin::init();
+ }
+ \Functionalities\Admin\Site_Health_Controller::init();
+ \add_action( 'init', array( '\Functionalities\Core\Module_Registry', 'boot' ), 5 );
+ },
+ 10
+);
+
+\add_action( 'updated_option', array( '\Functionalities\Core\Module_Registry', 'handle_option_update' ), 10, 3 );
// Activation hook.
-\register_activation_hook(__FILE__, function () {
- if (function_exists('flush_rewrite_rules')) {
- \flush_rewrite_rules();
+\register_activation_hook(
+ __FILE__,
+ function () {
+ if ( \Functionalities\Core\Module_Registry::is_enabled( 'pwa' ) ) {
+ \Functionalities\Core\Module_Registry::boot_module( 'pwa' );
+ }
+ if ( function_exists( 'flush_rewrite_rules' ) ) {
+ \flush_rewrite_rules();
+ }
}
-});
+);
// Quick Settings link on the Plugins screen.
-\add_filter('plugin_action_links_' . \plugin_basename(__FILE__), function (array $links): array {
- $url = \admin_url('admin.php?page=functionalities');
- $links[] = '' . \esc_html__('Settings', 'functionalities') . ' ';
- return $links;
-});
+\add_filter(
+ 'plugin_action_links_' . \plugin_basename( __FILE__ ),
+ function ( array $links ): array {
+ $url = \admin_url( 'admin.php?page=functionalities' );
+ $links[] = '' . \esc_html__( 'Settings', 'functionalities' ) . ' ';
+ return $links;
+ }
+);
// Add meta links on the Plugins screen (row meta).
-\add_filter('plugin_row_meta', function (array $links, string $file): array {
- if (\plugin_basename(__FILE__) === $file) {
- $links[] = '' . \esc_html__('Support', 'functionalities') . ' ';
- $links[] = '' . \esc_html__('Report Issues', 'functionalities') . ' ';
- }
- return $links;
-}, 10, 2);
+\add_filter(
+ 'plugin_row_meta',
+ function ( array $links, string $file ): array {
+ if ( \plugin_basename( __FILE__ ) === $file ) {
+ $links[] = '' . \esc_html__( 'Support', 'functionalities' ) . ' ';
+ $links[] = '' . \esc_html__( 'Report Issues', 'functionalities' ) . ' ';
+ }
+ return $links;
+ },
+ 10,
+ 2
+);
-\register_deactivation_hook(__FILE__, function () {
- if (function_exists('flush_rewrite_rules')) {
- \flush_rewrite_rules();
+\register_deactivation_hook(
+ __FILE__,
+ function () {
+ \wp_clear_scheduled_hook( 'functionalities_assumption_background_scan' );
+ if ( function_exists( 'flush_rewrite_rules' ) ) {
+ \flush_rewrite_rules();
+ }
}
-});
+);
diff --git a/includes/admin/class-admin-ui.php b/includes/admin/class-admin-ui.php
index dc4c91c..84fa2b7 100644
--- a/includes/admin/class-admin-ui.php
+++ b/includes/admin/class-admin-ui.php
@@ -27,9 +27,9 @@ class Admin_UI {
* @param bool $open Whether to show open by default. Default false.
* @return void
*/
- public static function render_docs_section( string $title, string $content, string $type = 'info', bool $open = false ) : void {
+ public static function render_docs_section( string $title, string $content, string $type = 'info', bool $open = false ): void {
$open_attr = $open ? ' open' : '';
- $class = 'functionalities-docs-accordion functionalities-docs-' . esc_attr( $type );
+ $class = 'functionalities-docs-accordion functionalities-docs-' . esc_attr( $type );
echo '';
echo '' . esc_html( $title ) . ' ';
@@ -44,7 +44,7 @@ public static function render_docs_section( string $title, string $content, stri
* @param array $items List of feature descriptions.
* @return void
*/
- public static function render_features_docs( array $items ) : void {
+ public static function render_features_docs( array $items ): void {
$content = '';
foreach ( $items as $item ) {
$content .= '' . esc_html( $item ) . ' ';
@@ -64,7 +64,7 @@ public static function render_features_docs( array $items ) : void {
* @param string $description Usage description.
* @return void
*/
- public static function render_usage_docs( string $description ) : void {
+ public static function render_usage_docs( string $description ): void {
self::render_docs_section(
\__( 'How to Use', 'functionalities' ),
'' . esc_html( $description ) . '
',
@@ -78,7 +78,7 @@ public static function render_usage_docs( string $description ) : void {
* @param array $hooks Array of hooks with 'name' and 'description' keys.
* @return void
*/
- public static function render_developer_docs( array $hooks ) : void {
+ public static function render_developer_docs( array $hooks ): void {
$content = '';
foreach ( $hooks as $hook ) {
$content .= '' . esc_html( $hook['name'] ) . ' ';
@@ -99,7 +99,7 @@ public static function render_developer_docs( array $hooks ) : void {
* @param string $message Warning message.
* @return void
*/
- public static function render_caution_docs( string $message ) : void {
+ public static function render_caution_docs( string $message ): void {
self::render_docs_section(
\__( 'Caution', 'functionalities' ),
'' . esc_html( $message ) . '
',
@@ -113,7 +113,7 @@ public static function render_caution_docs( string $message ) : void {
* @param array $config Configuration array with 'features', 'usage', 'caution', 'hooks' keys.
* @return void
*/
- public static function render_module_docs( array $config ) : void {
+ public static function render_module_docs( array $config ): void {
echo '';
if ( ! empty( $config['features'] ) ) {
diff --git a/includes/admin/class-admin.php b/includes/admin/class-admin.php
index b358b92..1098623 100644
--- a/includes/admin/class-admin.php
+++ b/includes/admin/class-admin.php
@@ -1,6 +1,6 @@
array(
- 'title' => \__( 'Task Manager', 'functionalities' ),
- 'description' => \__( 'File-based project task management with JSON storage.', 'functionalities' ),
- 'icon' => 'dashicons-yes-alt',
- 'custom_page' => true,
- ),
- 'misc' => array(
- 'title' => \__( 'Performance & Cleanup', 'functionalities' ),
- 'description' => \__( 'Disable bloat, emojis, embeds, heartbeat, and more.', 'functionalities' ),
- 'icon' => 'dashicons-performance',
- ),
- 'snippets' => array(
- 'title' => \__( 'Header & Footer', 'functionalities' ),
- 'description' => \__( 'Add GA4, custom header and footer code.', 'functionalities' ),
- 'icon' => 'dashicons-editor-code',
- ),
- 'link-management' => array(
- 'title' => \__( 'Link Management', 'functionalities' ),
- 'description' => \__( 'Control nofollow, new tabs, and link behavior.', 'functionalities' ),
- 'icon' => 'dashicons-admin-links',
- ),
- 'redirect-manager' => array(
- 'title' => \__( 'Redirect Manager', 'functionalities' ),
- 'description' => \__( 'Create and manage 301/302 URL redirects.', 'functionalities' ),
- 'icon' => 'dashicons-randomize',
- 'custom_page' => true,
- ),
- 'block-cleanup' => array(
- 'title' => \__( 'Block Cleanup', 'functionalities' ),
- 'description' => \__( 'Strip block classes from frontend output.', 'functionalities' ),
- 'icon' => 'dashicons-block-default',
- ),
- 'schema' => array(
- 'title' => \__( 'Schema Settings', 'functionalities' ),
- 'description' => \__( 'Add microdata to key areas and content.', 'functionalities' ),
- 'icon' => 'dashicons-networking',
- ),
- 'content-regression' => array(
- 'title' => \__( 'Content Integrity', 'functionalities' ),
- 'description' => \__( 'Detect structural regressions when posts are updated.', 'functionalities' ),
- 'icon' => 'dashicons-shield',
- ),
- 'assumption-detection' => array(
- 'title' => \__( 'Assumption Detection', 'functionalities' ),
- 'description' => \__( 'Notice when implicit site assumptions stop being true.', 'functionalities' ),
- 'icon' => 'dashicons-visibility',
- ),
- 'login-security' => array(
- 'title' => \__( 'Login Security', 'functionalities' ),
- 'description' => \__( 'Limit login attempts, customize login page, block XML-RPC.', 'functionalities' ),
- 'icon' => 'dashicons-lock',
- ),
- 'meta' => array(
- 'title' => \__( 'Meta & Copyright', 'functionalities' ),
- 'description' => \__( 'Copyright, Dublin Core, licensing, and SEO plugin integration.', 'functionalities' ),
- 'icon' => 'dashicons-media-text',
- ),
- 'components' => array(
- 'title' => \__( 'Components', 'functionalities' ),
- 'description' => \__( 'Define reusable CSS components.', 'functionalities' ),
- 'icon' => 'dashicons-layout',
- ),
- 'fonts' => array(
- 'title' => \__( 'Fonts', 'functionalities' ),
- 'description' => \__( 'Register custom font families.', 'functionalities' ),
- 'icon' => 'dashicons-editor-textcolor',
- ),
- 'editor-links' => array(
- 'title' => \__( 'Editor Link Suggestions', 'functionalities' ),
- 'description' => \__( 'Limit link suggestions to selected post types.', 'functionalities' ),
- 'icon' => 'dashicons-editor-unlink',
- ),
- 'svg-icons' => array(
- 'title' => \__( 'SVG Icons', 'functionalities' ),
- 'description' => \__( 'Upload custom SVG icons and insert them inline in the block editor.', 'functionalities' ),
- 'icon' => 'dashicons-flag',
- 'custom_page' => true,
- ),
- 'pwa' => array(
- 'title' => \__( 'Progressive Web App', 'functionalities' ),
- 'description' => \__( 'Make your site installable and work offline.', 'functionalities' ),
- 'icon' => 'dashicons-smartphone',
- ),
- );
- }
-
- /**
- * Register admin menu.
- *
- * @return void
- */
- public static function register_menu() : void {
- $parent_slug = 'functionalities';
- \add_menu_page(
- \__( 'Functionalities', 'functionalities' ),
- \__( 'Functionalities', 'functionalities' ),
- 'manage_options',
- $parent_slug,
- array( __CLASS__, 'render_main_page' ),
- 'dashicons-admin-generic',
- 65
- );
-
- // Add Dashboard as the first submenu.
- \add_submenu_page(
- $parent_slug,
- \__( 'Dashboard', 'functionalities' ),
- \__( 'Dashboard', 'functionalities' ),
- 'manage_options',
- $parent_slug,
- array( __CLASS__, 'render_main_page' )
- );
-
- // Add submenus for top modules.
- $skip_submenus = array( 'misc', 'assumption-detection', 'login-security', 'block-cleanup' );
-
- foreach ( self::$modules as $slug => $module ) {
- if ( in_array( $slug, $skip_submenus, true ) ) {
- continue;
- }
-
- \add_submenu_page(
- $parent_slug,
- $module['title'] . ' ‹ ' . \__( 'Functionalities', 'functionalities' ),
- $module['title'],
- 'manage_options',
- 'functionalities-' . $slug,
- array( __CLASS__, 'render_main_page' )
- );
- }
- }
-
- /**
- * Enqueue admin assets.
- *
- * @param string $hook Current admin page hook.
- * @return void
- */
- public static function enqueue_admin_assets( $hook ) : void {
- if ( strpos( $hook, 'functionalities' ) === false ) {
- return;
- }
-
- $deps = array( 'jquery' );
- // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Page detection doesn't require nonce.
- $page = isset( $_GET['page'] ) ? \sanitize_key( $_GET['page'] ) : '';
- // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Module detection doesn't require nonce.
- $module = isset( $_GET['module'] ) ? \sanitize_key( $_GET['module'] ) : '';
-
- if ( 'task-manager' === $module || 'functionalities-task-manager' === $page ) {
- $deps[] = 'jquery-ui-sortable';
- }
-
- if ( 'pwa' === $module ) {
- \wp_enqueue_media();
- \wp_enqueue_style( 'wp-color-picker' );
- $deps[] = 'wp-color-picker';
- }
-
- \wp_enqueue_style(
- 'functionalities-admin',
- FUNCTIONALITIES_URL . 'assets/css/admin.css',
- array( 'dashicons' ),
- FUNCTIONALITIES_VERSION
- );
-
- \wp_enqueue_script(
- 'functionalities-admin',
- FUNCTIONALITIES_URL . 'assets/js/admin.js',
- $deps,
- FUNCTIONALITIES_VERSION,
- true
- );
-
- // Localize script with AJAX data.
- \wp_localize_script(
- 'functionalities-admin',
- 'functionalitiesAdmin',
- array(
- 'ajaxUrl' => \admin_url( 'admin-ajax.php' ),
- 'runDetectionNonce' => \wp_create_nonce( 'functionalities_run_detection' ),
- 'runningText' => \__( 'Running...', 'functionalities' ),
- 'runDetectionText' => \__( 'Run Detection Now', 'functionalities' ),
- )
- );
- }
-
- /**
- * Render main admin page with module navigation.
- *
- * @return void
- */
- public static function render_main_page() : void {
- if ( ! \current_user_can( 'manage_options' ) ) {
- \wp_die( \esc_html__( 'Insufficient permissions', 'functionalities' ) );
- }
-
- // Get current module from URL parameter or page slug.
- // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Page/module detection doesn't require nonce.
- $current_module = isset( $_GET['module'] ) ? \sanitize_key( $_GET['module'] ) : '';
- // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Page detection doesn't require nonce.
- $page = isset( $_GET['page'] ) ? \sanitize_key( $_GET['page'] ) : '';
-
- if ( empty( $current_module ) && strpos( $page, 'functionalities-' ) === 0 ) {
- $current_module = str_replace( 'functionalities-', '', $page );
- }
-
- // If no module or invalid module, show dashboard.
- if ( empty( $current_module ) || ! isset( self::$modules[ $current_module ] ) ) {
- self::render_dashboard();
- return;
- }
-
- // Render specific module.
- self::render_module( $current_module );
- }
-
- /**
- * Render dashboard with module cards.
- *
- * @return void
- */
- /**
- * Check if a module is enabled.
- *
- * @param string $slug Module slug (hyphenated).
- * @return bool
- */
- private static function is_module_enabled( string $slug ) : bool {
- $option_name = 'functionalities_' . str_replace( '-', '_', $slug );
- $opts = (array) \get_option( $option_name, array() );
-
- return ! empty( $opts['enabled'] );
- }
-
- private static function render_dashboard() : void {
- ?>
-
-
-
-
-
-
-
- $module ) :
- $is_active = self::is_module_enabled( $slug );
- ?>
-
-
-
-
-
-
-
-
-
-
-
- style="margin-top: 2px;"
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- functionalities.dev'
- );
- ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ›
-
-
-
-
-
- [ __CLASS__, 'sanitize_link_management' ],
- 'default' => [
- 'enabled' => false,
- 'nofollow_external' => false,
- 'exceptions' => '',
- 'open_external_new_tab' => false,
- 'open_internal_new_tab' => false,
- 'internal_new_tab_exceptions' => '',
- ],
- ]
- );
-
- \add_settings_section(
- 'functionalities_link_management_section',
- \__( 'Link Management Settings', 'functionalities' ),
- array( __CLASS__, 'section_link_management' ),
- 'functionalities_link_management'
- );
-
- \add_settings_field(
- 'enabled',
- \__( 'Enable Link Management', 'functionalities' ),
- function() {
- $o = self::get_link_management_options();
- $checked = ! empty( $o['enabled'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Enable link management features', 'functionalities' ) . '';
- },
- 'functionalities_link_management',
- 'functionalities_link_management_section'
- );
-
- \add_settings_field(
- 'nofollow_external',
- \__( 'Add nofollow to external links', 'functionalities' ),
- [ __CLASS__, 'field_nofollow_external' ],
- 'functionalities_link_management',
- 'functionalities_link_management_section'
- );
-
- \add_settings_field(
- 'exceptions',
- \__( 'Exceptions', 'functionalities' ),
- [ __CLASS__, 'field_exceptions' ],
- 'functionalities_link_management',
- 'functionalities_link_management_section'
- );
-
- // New tab options.
- \add_settings_field(
- 'open_external_new_tab',
- \__( 'Open external links in new tab', 'functionalities' ),
- array( __CLASS__, 'field_open_external_new_tab' ),
- 'functionalities_link_management',
- 'functionalities_link_management_section'
- );
- \add_settings_field(
- 'open_internal_new_tab',
- \__( 'Open internal links in new tab', 'functionalities' ),
- array( __CLASS__, 'field_open_internal_new_tab' ),
- 'functionalities_link_management',
- 'functionalities_link_management_section'
- );
- \add_settings_field(
- 'internal_new_tab_exceptions',
- \__( 'Internal new-tab exceptions (domains)', 'functionalities' ),
- array( __CLASS__, 'field_internal_new_tab_exceptions' ),
- 'functionalities_link_management',
- 'functionalities_link_management_section'
- );
-
- // Advanced features.
- \add_settings_field(
- 'json_preset_url',
- \__( 'JSON Preset File Path', 'functionalities' ),
- array( __CLASS__, 'field_json_preset_url' ),
- 'functionalities_link_management',
- 'functionalities_link_management_section'
- );
- \add_settings_field(
- 'enable_developer_filters',
- \__( 'Enable Developer Filters', 'functionalities' ),
- array( __CLASS__, 'field_enable_developer_filters' ),
- 'functionalities_link_management',
- 'functionalities_link_management_section'
- );
- \add_settings_field(
- 'database_update_tool',
- \__( 'Database Update Tool', 'functionalities' ),
- array( __CLASS__, 'field_database_update_tool' ),
- 'functionalities_link_management',
- 'functionalities_link_management_section'
- );
-
- // Block Cleanup settings
- \register_setting(
- 'functionalities_block_cleanup',
- 'functionalities_block_cleanup',
- [
- 'sanitize_callback' => [ __CLASS__, 'sanitize_block_cleanup' ],
- 'default' => [
- 'enabled' => false,
- 'remove_heading_block_class' => false,
- 'remove_list_block_class' => false,
- 'remove_image_block_class' => false,
- 'remove_paragraph_block_class' => false,
- 'remove_quote_block_class' => false,
- 'remove_table_block_class' => false,
- 'remove_separator_block_class' => false,
- 'remove_group_block_class' => false,
- 'remove_columns_block_class' => false,
- 'remove_button_block_class' => false,
- 'remove_cover_block_class' => false,
- 'remove_media_text_block_class' => false,
- 'custom_classes_to_remove' => '',
- ],
- ]
- );
-
- \add_settings_section(
- 'functionalities_block_cleanup_section',
- \__( 'Frontend Block Class Cleanup', 'functionalities' ),
- array( __CLASS__, 'section_block_cleanup' ),
- 'functionalities_block_cleanup'
- );
-
- \add_settings_field(
- 'enabled',
- \__( 'Enable Block Cleanup', 'functionalities' ),
- function() {
- $o = self::get_block_cleanup_options();
- $checked = ! empty( $o['enabled'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Enable block class cleanup on frontend', 'functionalities' ) . '';
- },
- 'functionalities_block_cleanup',
- 'functionalities_block_cleanup_section'
- );
-
- \add_settings_field(
- 'remove_heading_block_class',
- \__( 'Headings', 'functionalities' ),
- [ __CLASS__, 'field_bc_remove_heading' ],
- 'functionalities_block_cleanup',
- 'functionalities_block_cleanup_section'
- );
- \add_settings_field(
- 'remove_list_block_class',
- \__( 'Lists', 'functionalities' ),
- [ __CLASS__, 'field_bc_remove_list' ],
- 'functionalities_block_cleanup',
- 'functionalities_block_cleanup_section'
- );
- \add_settings_field(
- 'remove_image_block_class',
- \__( 'Images', 'functionalities' ),
- [ __CLASS__, 'field_bc_remove_image' ],
- 'functionalities_block_cleanup',
- 'functionalities_block_cleanup_section'
- );
- \add_settings_field(
- 'remove_paragraph_block_class',
- \__( 'Paragraphs', 'functionalities' ),
- function() {
- $opts = self::get_block_cleanup_options();
- $checked = ! empty( $opts['remove_paragraph_block_class'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Remove "wp-block-paragraph" from paragraph elements', 'functionalities' ) . '';
- },
- 'functionalities_block_cleanup',
- 'functionalities_block_cleanup_section'
- );
- \add_settings_field(
- 'remove_quote_block_class',
- \__( 'Quotes', 'functionalities' ),
- function() {
- $opts = self::get_block_cleanup_options();
- $checked = ! empty( $opts['remove_quote_block_class'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Remove "wp-block-quote" from blockquote elements', 'functionalities' ) . '';
- },
- 'functionalities_block_cleanup',
- 'functionalities_block_cleanup_section'
- );
- \add_settings_field(
- 'remove_table_block_class',
- \__( 'Tables', 'functionalities' ),
- function() {
- $opts = self::get_block_cleanup_options();
- $checked = ! empty( $opts['remove_table_block_class'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Remove "wp-block-table" from table elements', 'functionalities' ) . '';
- },
- 'functionalities_block_cleanup',
- 'functionalities_block_cleanup_section'
- );
- \add_settings_field(
- 'remove_separator_block_class',
- \__( 'Separators', 'functionalities' ),
- function() {
- $opts = self::get_block_cleanup_options();
- $checked = ! empty( $opts['remove_separator_block_class'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Remove "wp-block-separator" from hr/separator elements', 'functionalities' ) . '';
- },
- 'functionalities_block_cleanup',
- 'functionalities_block_cleanup_section'
- );
- \add_settings_field(
- 'remove_group_block_class',
- \__( 'Groups', 'functionalities' ),
- function() {
- $opts = self::get_block_cleanup_options();
- $checked = ! empty( $opts['remove_group_block_class'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Remove "wp-block-group" from group containers', 'functionalities' ) . '';
- },
- 'functionalities_block_cleanup',
- 'functionalities_block_cleanup_section'
- );
- \add_settings_field(
- 'remove_columns_block_class',
- \__( 'Columns', 'functionalities' ),
- function() {
- $opts = self::get_block_cleanup_options();
- $checked = ! empty( $opts['remove_columns_block_class'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Remove "wp-block-columns" and "wp-block-column" from column layouts', 'functionalities' ) . '';
- },
- 'functionalities_block_cleanup',
- 'functionalities_block_cleanup_section'
- );
- \add_settings_field(
- 'remove_button_block_class',
- \__( 'Buttons', 'functionalities' ),
- function() {
- $opts = self::get_block_cleanup_options();
- $checked = ! empty( $opts['remove_button_block_class'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Remove "wp-block-button(s)" from button elements', 'functionalities' ) . '';
- },
- 'functionalities_block_cleanup',
- 'functionalities_block_cleanup_section'
- );
- \add_settings_field(
- 'remove_cover_block_class',
- \__( 'Covers', 'functionalities' ),
- function() {
- $opts = self::get_block_cleanup_options();
- $checked = ! empty( $opts['remove_cover_block_class'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Remove "wp-block-cover" from cover blocks', 'functionalities' ) . '';
- },
- 'functionalities_block_cleanup',
- 'functionalities_block_cleanup_section'
- );
- \add_settings_field(
- 'remove_media_text_block_class',
- \__( 'Media & Text', 'functionalities' ),
- function() {
- $opts = self::get_block_cleanup_options();
- $checked = ! empty( $opts['remove_media_text_block_class'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Remove "wp-block-media-text" from media-text blocks', 'functionalities' ) . '';
- },
- 'functionalities_block_cleanup',
- 'functionalities_block_cleanup_section'
- );
- \add_settings_field(
- 'custom_classes_to_remove',
- \__( 'Custom Classes', 'functionalities' ),
- function() {
- $opts = self::get_block_cleanup_options();
- $val = isset( $opts['custom_classes_to_remove'] ) ? $opts['custom_classes_to_remove'] : '';
- echo '
';
- echo '
' . \esc_html__( 'Enter additional CSS classes to remove from content output (one per line). Example: my-plugin-class', 'functionalities' ) . '
';
- },
- 'functionalities_block_cleanup',
- 'functionalities_block_cleanup_section'
- );
-
- // Editor Link Suggestions settings
- \register_setting(
- 'functionalities_editor_links',
- 'functionalities_editor_links',
- [
- 'sanitize_callback' => [ __CLASS__, 'sanitize_editor_links' ],
- 'default' => [
- 'enabled' => false,
- 'enable_limit' => false,
- 'post_types' => self::default_editor_link_post_types(),
- ],
- ]
- );
-
- \add_settings_section(
- 'functionalities_editor_links_section',
- \__( 'Editor Link Suggestions', 'functionalities' ),
- function() {
- echo '
' . \esc_html__( 'Control which post types appear in the block editor link search suggestions.', 'functionalities' ) . '
';
-
- echo '
';
- echo '
' . \esc_html__( 'What This Module Does', 'functionalities' ) . ' ';
- echo '
';
- echo '' . \esc_html__( 'Limits link search results to specific post types when inserting links in Gutenberg', 'functionalities' ) . ' ';
- echo '' . \esc_html__( 'Reduces clutter by hiding unwanted content types from search results', 'functionalities' ) . ' ';
- echo '' . \esc_html__( 'Works with posts, pages, and custom post types', 'functionalities' ) . ' ';
- echo ' ';
- echo '
';
-
- echo '
';
- echo '
' . \esc_html__( 'How to Use', 'functionalities' ) . ' ';
- echo '
' . \esc_html__( 'Enable limitation, then check only the post types you want to appear when searching for links in the editor. Unchecked post types will be hidden from link search results.', 'functionalities' ) . '
';
- echo '
';
-
- echo '
';
- echo '
' . \esc_html__( 'For Developers', 'functionalities' ) . ' ';
- echo '
';
- echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_editor_links_enabled — ' . \esc_html__( 'toggle feature', 'functionalities' ) . ' ';
- echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_editor_links_post_types — ' . \esc_html__( 'modify allowed post types', 'functionalities' );
- echo '
';
- echo '
';
- },
- 'functionalities_editor_links'
- );
-
- \add_settings_field(
- 'enabled',
- \__( 'Enable Editor Link Suggestions', 'functionalities' ),
- function() {
- $o = self::get_editor_links_options();
- $checked = ! empty( $o['enabled'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Enable editor link suggestion filtering', 'functionalities' ) . '';
- },
- 'functionalities_editor_links',
- 'functionalities_editor_links_section'
- );
-
- \add_settings_field(
- 'enable_limit',
- \__( 'Enable limitation', 'functionalities' ),
- [ __CLASS__, 'field_el_enable' ],
- 'functionalities_editor_links',
- 'functionalities_editor_links_section'
- );
- \add_settings_field(
- 'post_types',
- \__( 'Allowed post types', 'functionalities' ),
- [ __CLASS__, 'field_el_post_types' ],
- 'functionalities_editor_links',
- 'functionalities_editor_links_section'
- );
-
- // Header & Footer (Snippets) settings
- \register_setting(
- 'functionalities_snippets',
- 'functionalities_snippets',
- [
- 'sanitize_callback' => [ __CLASS__, 'sanitize_snippets' ],
- 'default' => [
- 'enabled' => false,
- 'enable_ga4' => false,
- 'ga4_id' => '',
- 'header' => array(),
- 'body_open' => array(),
- 'footer' => array(),
- ],
- ]
- );
-
- \add_settings_section(
- 'functionalities_snippets_section',
- \__( 'Header & Footer Code', 'functionalities' ),
- function() {
- echo '
' . \esc_html__( 'Insert custom code snippets into your site header and footer without editing theme files.', 'functionalities' ) . '
';
-
- echo '
';
- echo '
' . \esc_html__( 'What This Module Does', 'functionalities' ) . ' ';
- echo '
';
- echo '' . \esc_html__( 'Native Google Analytics 4 integration - just enter your Measurement ID', 'functionalities' ) . ' ';
- echo '' . \esc_html__( 'Multiple snippets per location — each independently toggleable', 'functionalities' ) . ' ';
- echo '' . \esc_html__( 'Custom code for meta tags, scripts, styles, and tracking codes', 'functionalities' ) . ' ';
- echo '' . \esc_html__( 'Automatically skips admin pages, feeds, and REST API requests', 'functionalities' ) . ' ';
- echo ' ';
- echo '
';
-
- echo '
';
- echo '
' . \esc_html__( 'Allowed Tags', 'functionalities' ) . ' ';
- echo '
';
- echo '<script>, <style>, <link>, <meta>, <noscript>';
- echo '
';
- echo '
';
-
- echo '
';
- echo '
' . \esc_html__( 'For Developers', 'functionalities' ) . ' ';
- echo '
';
- echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_snippets_output_enabled — ' . \esc_html__( 'disable on specific pages', 'functionalities' ) . ' ';
- echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_snippets_ga4_enabled — ' . \esc_html__( 'control GA4 per user/page', 'functionalities' ) . ' ';
- echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_snippets_header_code — ' . \esc_html__( 'modify header code', 'functionalities' ) . ' ';
- echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_snippets_footer_code — ' . \esc_html__( 'modify footer code', 'functionalities' ) . ' ';
- echo \esc_html__( 'Actions:', 'functionalities' ) . ' functionalities_before/after_header/footer_snippets';
- echo '
';
- echo '
';
- },
- 'functionalities_snippets'
- );
-
- \add_settings_field(
- 'enabled',
- \__( 'Enable Header & Footer', 'functionalities' ),
- function() {
- $o = self::get_snippets_options();
- $checked = ! empty( $o['enabled'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Enable header and footer code injection', 'functionalities' ) . '';
- },
- 'functionalities_snippets',
- 'functionalities_snippets_section'
- );
-
- \add_settings_field(
- 'enable_ga4',
- \__( 'Enable Google Analytics 4', 'functionalities' ),
- function() {
- $o = self::get_snippets_options();
- $checked = ! empty( $o['enable_ga4'] ) ? 'checked' : '';
- echo '
' . \esc_html__( 'Insert GA4 gtag in head', 'functionalities' ) . '';
- },
- 'functionalities_snippets',
- 'functionalities_snippets_section'
- );
- \add_settings_field(
- 'ga4_id',
- \__( 'GA4 Measurement ID', 'functionalities' ),
- function() {
- $o = self::get_snippets_options();
- $val = isset( $o['ga4_id'] ) ? (string) $o['ga4_id'] : '';
- echo '
';
- },
- 'functionalities_snippets',
- 'functionalities_snippets_section'
- );
- \add_settings_field(
- 'header_snippets',
- \__( 'Header Snippets', 'functionalities' ),
- function() {
- self::field_snippets_repeater( 'header', 'wp_head' );
- },
- 'functionalities_snippets',
- 'functionalities_snippets_section'
- );
- \add_settings_field(
- 'body_open_snippets',
- \__( 'Body Open Snippets', 'functionalities' ),
- function() {
- self::field_snippets_repeater( 'body_open', 'wp_body_open' );
- },
- 'functionalities_snippets',
- 'functionalities_snippets_section'
- );
- \add_settings_field(
- 'footer_snippets',
- \__( 'Footer Snippets', 'functionalities' ),
- function() {
- self::field_snippets_repeater( 'footer', 'wp_footer' );
- },
- 'functionalities_snippets',
- 'functionalities_snippets_section'
- );
-
- // Schema settings
- \register_setting(
- 'functionalities_schema',
- 'functionalities_schema',
- [
- 'sanitize_callback' => [ __CLASS__, 'sanitize_schema' ],
- 'default' => [
- 'enabled' => false,
- 'enable_site_schema' => true,
- 'site_itemtype' => 'WebPage',
- 'enable_header_part' => true,
- 'enable_footer_part' => true,
- 'enable_article' => true,
- 'article_itemtype' => 'Article',
- 'add_headline' => true,
- 'add_dates' => true,
- 'add_author' => true,
- 'enable_breadcrumbs' => false,
- ],
- ]
- );
-
- \add_settings_section(
- 'functionalities_schema_section',
- \__( 'Schema Settings', 'functionalities' ),
- function() {
- echo '
' . \esc_html__( 'Add Schema.org microdata attributes to improve search engine understanding of your content.', 'functionalities' ) . '
';
-
- echo '
';
- echo '
' . \esc_html__( 'What This Module Does', 'functionalities' ) . ' ';
- echo '
';
- echo '' . \esc_html__( 'Adds itemscope/itemtype to the HTML element for page-level schema', 'functionalities' ) . ' ';
- echo '' . \esc_html__( 'Wraps article content with Article/BlogPosting microdata', 'functionalities' ) . ' ';
- echo '' . \esc_html__( 'Adds structured data for headlines, dates, and authors', 'functionalities' ) . ' ';
- echo '' . \esc_html__( 'Marks header and footer regions with WPHeader/WPFooter types', 'functionalities' ) . ' ';
- echo ' ';
- echo '
';
-
- echo '
';
- echo '
' . \esc_html__( 'Supported Types', 'functionalities' ) . ' ';
- echo '
';
- echo '' . \esc_html__( 'Site:', 'functionalities' ) . ' WebPage, AboutPage, ContactPage, Blog, SearchResultsPage ';
- echo '' . \esc_html__( 'Article:', 'functionalities' ) . ' Article, BlogPosting, NewsArticle';
- echo '
';
- echo '
';
-
- echo '
';
- echo '
' . \esc_html__( 'For Developers', 'functionalities' ) . ' ';
- echo '
';
- echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_schema_enabled — ' . \esc_html__( 'toggle all schema output', 'functionalities' ) . ' ';
- echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_schema_site_type — ' . \esc_html__( 'modify site itemtype', 'functionalities' ) . ' ';
- echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_schema_article_type — ' . \esc_html__( 'modify article itemtype', 'functionalities' ) . ' ';
- echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_schema_content — ' . \esc_html__( 'modify wrapped content', 'functionalities' );
- echo '
';
- echo '
';
- },
- 'functionalities_schema'
- );
-
- \add_settings_field(
- 'enabled',
- \__( 'Enable Schema', 'functionalities' ),
- function() {
- $o = self::get_schema_options();
- $checked = ! empty( $o['enabled'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Enable schema microdata output', 'functionalities' ) . '';
- },
- 'functionalities_schema',
- 'functionalities_schema_section'
- );
-
- \add_settings_field(
- 'enable_site_schema',
- \__( 'Enable site schema (html tag)', 'functionalities' ),
- function() {
- $o = self::get_schema_options();
- $checked = ! empty( $o['enable_site_schema'] ) ? 'checked' : '';
- echo '
' . \esc_html__( 'Add itemscope/itemtype to ', 'functionalities' ) . '';
- },
- 'functionalities_schema',
- 'functionalities_schema_section'
- );
- \add_settings_field(
- 'site_itemtype',
- \__( 'Site itemtype', 'functionalities' ),
- function() {
- $o = self::get_schema_options();
- $val = $o['site_itemtype'] ?? 'WebPage';
- $opts = [ 'WebPage', 'AboutPage', 'ContactPage', 'Blog', 'SearchResultsPage' ];
- echo '
';
- foreach ( $opts as $opt ) {
- $sel = selected( $val, $opt, false );
- echo '' . esc_html( $opt ) . ' ';
- }
- echo ' ';
- },
- 'functionalities_schema',
- 'functionalities_schema_section'
- );
- \add_settings_field(
- 'enable_header_part',
- \__( 'Add WPHeader microdata', 'functionalities' ),
- function() {
- $o = self::get_schema_options();
- $checked = ! empty( $o['enable_header_part'] ) ? 'checked' : '';
- echo '
' . \esc_html__( 'Output a microdata hasPart for header', 'functionalities' ) . '';
- },
- 'functionalities_schema',
- 'functionalities_schema_section'
- );
- \add_settings_field(
- 'enable_footer_part',
- \__( 'Add WPFooter microdata', 'functionalities' ),
- function() {
- $o = self::get_schema_options();
- $checked = ! empty( $o['enable_footer_part'] ) ? 'checked' : '';
- echo '
' . \esc_html__( 'Output a microdata hasPart for footer', 'functionalities' ) . '';
- },
- 'functionalities_schema',
- 'functionalities_schema_section'
- );
- \add_settings_field(
- 'enable_article',
- \__( 'Enable Article microdata in content', 'functionalities' ),
- function() {
- $o = self::get_schema_options();
- $checked = ! empty( $o['enable_article'] ) ? 'checked' : '';
- echo '
' . \esc_html__( 'Wrap content with Article microdata on singular', 'functionalities' ) . '';
- },
- 'functionalities_schema',
- 'functionalities_schema_section'
- );
- \add_settings_field(
- 'article_itemtype',
- \__( 'Article itemtype', 'functionalities' ),
- function() {
- $o = self::get_schema_options();
- $val = $o['article_itemtype'] ?? 'Article';
- $opts = [ 'Article', 'BlogPosting', 'NewsArticle' ];
- echo '
';
- foreach ( $opts as $opt ) {
- $sel = selected( $val, $opt, false );
- echo '' . esc_html( $opt ) . ' ';
- }
- echo ' ';
- },
- 'functionalities_schema',
- 'functionalities_schema_section'
- );
- \add_settings_field(
- 'add_headline',
- \__( 'Add headline from first heading', 'functionalities' ),
- function() {
- $o = self::get_schema_options();
- $checked = ! empty( $o['add_headline'] ) ? 'checked' : '';
- echo '
' . \esc_html__( 'Add itemprop="headline"', 'functionalities' ) . '';
- },
- 'functionalities_schema',
- 'functionalities_schema_section'
- );
- \add_settings_field(
- 'add_dates',
- \__( 'Add published/modified dates', 'functionalities' ),
- function() {
- $o = self::get_schema_options();
- $checked = ! empty( $o['add_dates'] ) ? 'checked' : '';
- echo '
' . \esc_html__( 'Add itemprop dates to time tags', 'functionalities' ) . '';
- },
- 'functionalities_schema',
- 'functionalities_schema_section'
- );
- \add_settings_field(
- 'add_author',
- \__( 'Add author microdata', 'functionalities' ),
- function() {
- $o = self::get_schema_options();
- $checked = ! empty( $o['add_author'] ) ? 'checked' : '';
- echo '
' . \esc_html__( 'Add itemprop="author" where possible', 'functionalities' ) . '';
- },
- 'functionalities_schema',
- 'functionalities_schema_section'
- );
- \add_settings_field(
- 'enable_breadcrumbs',
- \__( 'Enable BreadcrumbList', 'functionalities' ),
- function() {
- $o = self::get_schema_options();
- $checked = ! empty( $o['enable_breadcrumbs'] ) ? 'checked' : '';
- echo '
' . \esc_html__( 'Add BreadcrumbList JSON-LD to singular pages', 'functionalities' ) . '';
- },
- 'functionalities_schema',
- 'functionalities_schema_section'
- );
-
- // Components settings
- \register_setting(
- 'functionalities_components',
- 'functionalities_components',
- [
- 'sanitize_callback' => [ __CLASS__, 'sanitize_components' ],
- 'default' => [
- 'enabled' => false,
- 'items' => self::default_components(),
- ],
- ]
- );
-
- \add_settings_section(
- 'functionalities_components_section',
- \__( 'Components', 'functionalities' ),
- function() {
- echo '
' . \esc_html__( 'Create reusable CSS components that are automatically loaded across your entire site.', 'functionalities' ) . '
';
-
- echo '
';
- echo '
' . \esc_html__( 'What This Module Does', 'functionalities' ) . ' ';
- echo '
';
- echo '' . \esc_html__( 'Define CSS class names and their style rules in one place', 'functionalities' ) . ' ';
- echo '' . \esc_html__( 'Components are compiled into a single CSS file for optimal caching', 'functionalities' ) . ' ';
- echo '' . \esc_html__( 'Available on both frontend and admin pages', 'functionalities' ) . ' ';
- echo '' . \esc_html__( 'Includes default utility components like visually-hidden, skip-link, and marquee', 'functionalities' ) . ' ';
- echo ' ';
- echo '
';
-
- echo '
';
- echo '
' . \esc_html__( 'How to Use', 'functionalities' ) . ' ';
- echo '
' . \esc_html__( 'Add components by entering a CSS selector (e.g., .my-button) and CSS rules (e.g., background: blue; color: white;). Use the grid below to manage your components.', 'functionalities' ) . '
';
- echo '
';
-
- echo '
';
- echo '
' . \esc_html__( 'For Developers', 'functionalities' ) . ' ';
- echo '
';
- echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_components_enabled — ' . \esc_html__( 'toggle output', 'functionalities' ) . ' ';
- echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_components_items — ' . \esc_html__( 'add components dynamically', 'functionalities' ) . ' ';
- echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_components_css — ' . \esc_html__( 'modify generated CSS', 'functionalities' ) . ' ';
- echo \esc_html__( 'Action:', 'functionalities' ) . ' functionalities_components_updated — ' . \esc_html__( 'fires when CSS file regenerates', 'functionalities' );
- echo '
';
- echo '
';
- },
- 'functionalities_components'
- );
- \add_settings_field(
- 'enabled',
- \__( 'Enable components CSS', 'functionalities' ),
- function() {
- $o = self::get_components_options();
- $checked = ! empty( $o['enabled'] ) ? 'checked' : '';
- echo '
' . \esc_html__( 'Output components CSS on frontend', 'functionalities' ) . '';
- },
- 'functionalities_components',
- 'functionalities_components_section'
- );
- \add_settings_field(
- 'items',
- \__( 'Component list', 'functionalities' ),
- [ __CLASS__, 'field_components_items' ],
- 'functionalities_components',
- 'functionalities_components_section'
- );
-
- // Miscellaneous (bloat control)
- \register_setting(
- 'functionalities_misc',
- 'functionalities_misc',
- [
- 'sanitize_callback' => [ __CLASS__, 'sanitize_misc' ],
- 'default' => [
- 'enabled' => false,
- 'disable_block_widgets' => false,
- 'load_separate_core_block_assets' => false,
- 'disable_emojis' => false,
- 'disable_embeds' => false,
- 'remove_rest_api_links_head' => false,
- 'remove_rsd_wlw_shortlink' => false,
- 'remove_generator_meta' => false,
- 'disable_xmlrpc' => false,
- 'disable_xmlrpc_pingbacks' => false,
- 'disable_feeds' => false,
- 'disable_gravatars' => false,
- 'disable_self_pingbacks' => false,
- 'remove_query_strings' => false,
- 'remove_dns_prefetch' => false,
- 'remove_recent_comments_css' => false,
- 'limit_revisions' => false,
- 'disable_dashicons_for_guests' => false,
- 'disable_heartbeat' => false,
- 'disable_admin_bar_front' => false,
- 'remove_jquery_migrate' => false,
- 'enable_prism_admin' => false,
- 'enable_textarea_fullscreen' => false,
- ],
- ]
- );
-
- \add_settings_section(
- 'functionalities_misc_section',
- \__( 'Miscellaneous (Bloat Control)', 'functionalities' ),
- function() {
- echo '
' . \esc_html__( 'Remove unnecessary WordPress features to improve performance and security.', 'functionalities' ) . '
';
-
- echo '
';
- echo '
' . \esc_html__( 'What This Module Does', 'functionalities' ) . ' ';
- echo '
';
- echo '' . \esc_html__( 'Remove bloat like emojis, oEmbeds, and unnecessary meta tags', 'functionalities' ) . ' ';
- echo '' . \esc_html__( 'Disable security concerns like XML-RPC and version disclosure', 'functionalities' ) . ' ';
- echo '' . \esc_html__( 'Improve performance by removing unused scripts and styles', 'functionalities' ) . ' ';
- echo '' . \esc_html__( 'Add useful enhancements like PrismJS and fullscreen textareas', 'functionalities' ) . ' ';
- echo ' ';
- echo '
';
-
- echo '
';
- echo '
' . \esc_html__( 'Caution', 'functionalities' ) . ' ';
- echo '
' . \esc_html__( 'Some options may break functionality if plugins depend on them. Test after enabling. Disable Heartbeat API with care if you use auto-save or real-time features.', 'functionalities' ) . '
';
- echo '
';
-
- echo '
';
- echo '
' . \esc_html__( 'For Developers', 'functionalities' ) . ' ';
- echo '
';
- echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_misc_options — ' . \esc_html__( 'modify options before application', 'functionalities' ) . ' ';
- echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_misc_disable_emojis — ' . \esc_html__( 'control emoji removal', 'functionalities' ) . ' ';
- echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_misc_disable_embeds — ' . \esc_html__( 'control embed removal', 'functionalities' );
- echo '
';
- echo '
';
- },
- 'functionalities_misc'
- );
-
- \add_settings_field(
- 'enabled',
- \__( 'Enable Performance & Cleanup', 'functionalities' ),
- function() {
- $o = self::get_misc_options();
- $checked = ! empty( $o['enabled'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Enable performance and cleanup features', 'functionalities' ) . '';
- },
- 'functionalities_misc',
- 'functionalities_misc_section'
- );
-
- self::add_misc_field( 'disable_block_widgets', \__( 'Disable block-based widget editor (use classic widgets)', 'functionalities' ) );
- self::add_misc_field( 'load_separate_core_block_assets', \__( 'Load core block styles separately (per-block CSS)', 'functionalities' ) );
- self::add_misc_field( 'disable_emojis', \__( 'Disable emojis scripts/styles', 'functionalities' ) );
- self::add_misc_field( 'disable_embeds', \__( 'Disable oEmbed scripts and endpoints', 'functionalities' ) );
- self::add_misc_field( 'remove_rest_api_links_head', \__( 'Remove REST API and oEmbed discovery links from ', 'functionalities' ) );
- self::add_misc_field( 'remove_rsd_wlw_shortlink', \__( 'Remove RSD, WLWManifest, and shortlink tags', 'functionalities' ) );
- self::add_misc_field( 'remove_generator_meta', \__( 'Remove WordPress version meta (generator)', 'functionalities' ) );
- self::add_misc_field( 'disable_xmlrpc', \__( 'Disable XML-RPC (complete)', 'functionalities' ) );
- self::add_misc_field( 'disable_xmlrpc_pingbacks', \__( 'Disable only XML-RPC Pingbacks', 'functionalities' ) );
- self::add_misc_field( 'disable_feeds', \__( 'Disable RSS/Atom feeds (redirect to homepage)', 'functionalities' ) );
- self::add_misc_field( 'disable_gravatars', \__( 'Disable Gravatars (site-wide)', 'functionalities' ) );
- self::add_misc_field( 'disable_self_pingbacks', \__( 'Disable self-pingbacks (pings to own site)', 'functionalities' ) );
- self::add_misc_field( 'remove_query_strings', \__( 'Remove query strings from static resources (?ver=)', 'functionalities' ) );
- self::add_misc_field( 'remove_dns_prefetch', \__( 'Remove DNS prefetch links from ', 'functionalities' ) );
- self::add_misc_field( 'remove_recent_comments_css', \__( 'Remove Recent Comments inline CSS', 'functionalities' ) );
- self::add_misc_field( 'limit_revisions', \__( 'Limit post revisions to 10', 'functionalities' ) );
- self::add_misc_field( 'disable_dashicons_for_guests', \__( 'Disable Dashicons on frontend for non-logged-in users', 'functionalities' ) );
- self::add_misc_field( 'disable_heartbeat', \__( 'Disable Heartbeat API', 'functionalities' ) );
- self::add_misc_field( 'disable_admin_bar_front', \__( 'Disable admin bar on the frontend', 'functionalities' ) );
- self::add_misc_field( 'remove_jquery_migrate', \__( 'Remove jQuery Migrate from frontend', 'functionalities' ) );
- self::add_misc_field( 'enable_prism_admin', \__( 'Load PrismJS on admin screens (code highlighting where applicable)', 'functionalities' ) );
- self::add_misc_field( 'enable_textarea_fullscreen', \__( 'Enable fullscreen toggle for all backend textareas', 'functionalities' ) );
-
- // Fonts settings
- \register_setting(
- 'functionalities_fonts',
- 'functionalities_fonts',
- [
- 'sanitize_callback' => [ __CLASS__, 'sanitize_fonts' ],
- 'default' => [
- 'enabled' => false,
- 'items' => [],
- ],
- ]
- );
- \add_settings_section(
- 'functionalities_fonts_section',
- \__( 'Font Families', 'functionalities' ),
- function() {
- echo '
' . \esc_html__( 'Self-host custom fonts with automatic @font-face CSS generation.', 'functionalities' ) . '
';
-
- echo '
';
- echo '
' . \esc_html__( 'What This Module Does', 'functionalities' ) . ' ';
- echo '
';
- echo '' . \esc_html__( 'Generate @font-face CSS rules for self-hosted fonts', 'functionalities' ) . ' ';
- echo '' . \esc_html__( 'Support for variable fonts with weight ranges (e.g., 100 900)', 'functionalities' ) . ' ';
- echo '' . \esc_html__( 'WOFF2 format for modern browsers, optional WOFF fallback', 'functionalities' ) . ' ';
- echo '' . \esc_html__( 'Configurable font-display strategy (swap, auto, block, etc.)', 'functionalities' ) . ' ';
- echo ' ';
- echo '
';
-
- echo '
';
- echo '
' . \esc_html__( 'How to Use', 'functionalities' ) . ' ';
- echo '
' . \esc_html__( 'Upload font files to your media library or server, then add font entries below with the family name and file URLs. Use the generated font-family name in your CSS.', 'functionalities' ) . '
';
- echo '
';
-
- echo '
';
- echo '
' . \esc_html__( 'For Developers', 'functionalities' ) . ' ';
- echo '
';
- echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_fonts_enabled — ' . \esc_html__( 'toggle output', 'functionalities' ) . ' ';
- echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_fonts_items — ' . \esc_html__( 'add fonts dynamically', 'functionalities' ) . ' ';
- echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_fonts_css — ' . \esc_html__( 'modify generated CSS', 'functionalities' ) . ' ';
- echo \esc_html__( 'Action:', 'functionalities' ) . ' functionalities_fonts_before_output';
- echo '
';
- echo '
';
- },
- 'functionalities_fonts'
- );
- \add_settings_field(
- 'fonts_enabled',
- \__( 'Enable fonts output', 'functionalities' ),
- function() {
- $o = self::get_fonts_options();
- $checked = ! empty( $o['enabled'] ) ? 'checked' : '';
- echo '
' . \esc_html__( 'Output @font-face CSS across site (front and admin)', 'functionalities' ) . '';
- },
- 'functionalities_fonts',
- 'functionalities_fonts_section'
- );
- \add_settings_field(
- 'fonts_items',
- \__( 'Families', 'functionalities' ),
- [ __CLASS__, 'field_fonts_items' ],
- 'functionalities_fonts',
- 'functionalities_fonts_section'
- );
- \add_settings_field(
- 'fonts_assignments',
- \__( 'Typography Assignments', 'functionalities' ),
- [ __CLASS__, 'field_fonts_assignments' ],
- 'functionalities_fonts',
- 'functionalities_fonts_section'
- );
-
- // Login Security settings.
- \register_setting(
- 'functionalities_login_security',
- 'functionalities_login_security',
- array(
- 'sanitize_callback' => array( __CLASS__, 'sanitize_login_security' ),
- 'default' => array(
- 'enabled' => false,
- 'limit_login_attempts' => true,
- 'max_attempts' => 5,
- 'lockout_duration' => 15,
- 'disable_xmlrpc_auth' => true,
- 'disable_application_passwords' => false,
- 'hide_login_errors' => true,
- 'trust_proxy_headers' => false,
- 'custom_logo_url' => '',
- 'custom_background_color' => '',
- 'custom_form_background' => '',
- ),
- )
- );
- \add_settings_section(
- 'functionalities_login_security_section',
- \__( 'Login Security Settings', 'functionalities' ),
- function() {
- echo '
' . \esc_html__( 'Protect your login page and customize its appearance.', 'functionalities' ) . '
';
- echo '
';
- echo '
' . \esc_html__( 'Security Features', 'functionalities' ) . ' ';
- echo '
';
- echo '' . \esc_html__( 'Limit failed login attempts to prevent brute force attacks', 'functionalities' ) . ' ';
- echo '' . \esc_html__( 'Disable XML-RPC authentication to block remote login attacks', 'functionalities' ) . ' ';
- echo '' . \esc_html__( 'Hide specific login errors to prevent username enumeration', 'functionalities' ) . ' ';
- echo '' . \esc_html__( 'Customize the login page with your logo and colors', 'functionalities' ) . ' ';
- echo ' ';
- echo '
';
- $logs = \Functionalities\Features\Login_Security::get_lockout_log( 5 );
- if ( ! empty( $logs ) ) {
- echo '
';
- echo '
' . \esc_html__( 'Recent Lockouts', 'functionalities' ) . ' ';
- echo '
';
- foreach ( $logs as $log ) {
- echo '' . \esc_html( $log['ip'] ) . ' — ' . \esc_html( $log['username'] ) . ' (' . \esc_html( $log['time'] ) . ') ';
- }
- echo ' ';
- echo '
';
- }
- },
- 'functionalities_login_security'
- );
- \add_settings_field( 'login_enabled', \__( 'Enable Login Security', 'functionalities' ), function() {
- $o = self::get_login_security_options();
- echo '
' . \esc_html__( 'Enable login security features', 'functionalities' ) . '';
- }, 'functionalities_login_security', 'functionalities_login_security_section' );
- \add_settings_field( 'limit_login_attempts', \__( 'Limit Login Attempts', 'functionalities' ), function() {
- $o = self::get_login_security_options();
- echo '
' . \esc_html__( 'Block IPs after too many failed attempts', 'functionalities' ) . '';
- }, 'functionalities_login_security', 'functionalities_login_security_section' );
- \add_settings_field( 'max_attempts', \__( 'Max Attempts', 'functionalities' ), function() {
- $o = self::get_login_security_options();
- echo '
' . \esc_html__( 'failed attempts before lockout', 'functionalities' );
- }, 'functionalities_login_security', 'functionalities_login_security_section' );
- \add_settings_field( 'lockout_duration', \__( 'Lockout Duration', 'functionalities' ), function() {
- $o = self::get_login_security_options();
- echo '
' . \esc_html__( 'minutes', 'functionalities' );
- }, 'functionalities_login_security', 'functionalities_login_security_section' );
- \add_settings_field( 'disable_xmlrpc_auth', \__( 'Disable XML-RPC', 'functionalities' ), function() {
- $o = self::get_login_security_options();
- echo '
' . \esc_html__( 'Disable XML-RPC authentication (recommended)', 'functionalities' ) . '';
- }, 'functionalities_login_security', 'functionalities_login_security_section' );
- \add_settings_field( 'disable_application_passwords', \__( 'Disable App Passwords', 'functionalities' ), function() {
- $o = self::get_login_security_options();
- echo '
' . \esc_html__( 'Disable WordPress Application Passwords', 'functionalities' ) . '';
- }, 'functionalities_login_security', 'functionalities_login_security_section' );
- \add_settings_field( 'hide_login_errors', \__( 'Hide Login Errors', 'functionalities' ), function() {
- $o = self::get_login_security_options();
- echo '
' . \esc_html__( 'Show generic error instead of specific username/password errors', 'functionalities' ) . '';
- }, 'functionalities_login_security', 'functionalities_login_security_section' );
- \add_settings_field( 'trust_proxy_headers', \__( 'Trust Proxy Headers', 'functionalities' ), function() {
- $o = self::get_login_security_options();
- echo '
' . \esc_html__( 'Read client IP from X-Forwarded-For / Client-IP headers', 'functionalities' ) . '';
- echo '
' . \esc_html__( 'Only enable when this site sits behind a trusted reverse proxy or CDN (Cloudflare, nginx, etc.). With this OFF, lockouts are keyed by REMOTE_ADDR and cannot be spoofed via headers — the secure default.', 'functionalities' ) . '
';
- }, 'functionalities_login_security', 'functionalities_login_security_section' );
- \add_settings_field( 'custom_logo_url', \__( 'Custom Logo URL', 'functionalities' ), function() {
- $o = self::get_login_security_options();
- echo '
';
- }, 'functionalities_login_security', 'functionalities_login_security_section' );
- \add_settings_field( 'custom_background_color', \__( 'Background Color', 'functionalities' ), function() {
- $o = self::get_login_security_options();
- echo '
';
- }, 'functionalities_login_security', 'functionalities_login_security_section' );
-
- // Meta & Copyright settings.
- \register_setting(
- 'functionalities_meta',
- 'functionalities_meta',
- array(
- 'sanitize_callback' => array( __CLASS__, 'sanitize_meta' ),
- 'default' => array(
- 'enabled' => false,
- 'enable_copyright_meta' => true,
- 'enable_dublin_core' => true,
- 'enable_license_metabox' => true,
- 'enable_schema_integration' => true,
- 'default_license' => 'all-rights-reserved',
- 'default_license_url' => '',
- 'post_types' => array( 'post' ),
- 'copyright_holder_type' => 'author',
- 'custom_copyright_holder' => '',
- 'dc_language' => '',
- ),
- )
- );
-
- \add_settings_section(
- 'functionalities_meta_section',
- \__( 'Meta & Copyright Settings', 'functionalities' ),
- array( __CLASS__, 'section_meta' ),
- 'functionalities_meta'
- );
-
- \add_settings_field(
- 'meta_enabled',
- \__( 'Enable Meta Module', 'functionalities' ),
- function() {
- $o = self::get_meta_options();
- $checked = ! empty( $o['enabled'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Enable copyright, Dublin Core, and licensing features', 'functionalities' ) . '';
- },
- 'functionalities_meta',
- 'functionalities_meta_section'
- );
-
- \add_settings_field(
- 'enable_copyright_meta',
- \__( 'Copyright Meta Tags', 'functionalities' ),
- function() {
- $o = self::get_meta_options();
- $checked = ! empty( $o['enable_copyright_meta'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Output copyright, author, owner, and rights meta tags', 'functionalities' ) . '';
- },
- 'functionalities_meta',
- 'functionalities_meta_section'
- );
-
- \add_settings_field(
- 'enable_dublin_core',
- \__( 'Dublin Core (DCMI) Metadata', 'functionalities' ),
- function() {
- $o = self::get_meta_options();
- $checked = ! empty( $o['enable_dublin_core'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Output Dublin Core metadata (DC.title, DC.creator, DC.rights, etc.)', 'functionalities' ) . '';
- },
- 'functionalities_meta',
- 'functionalities_meta_section'
- );
-
- \add_settings_field(
- 'enable_license_metabox',
- \__( 'License Metabox', 'functionalities' ),
- function() {
- $o = self::get_meta_options();
- $checked = ! empty( $o['enable_license_metabox'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Show license selection metabox in post editor', 'functionalities' ) . '';
- },
- 'functionalities_meta',
- 'functionalities_meta_section'
- );
-
- \add_settings_field(
- 'enable_schema_integration',
- \__( 'SEO Plugin Schema Integration', 'functionalities' ),
- function() {
- $o = self::get_meta_options();
- $checked = ! empty( $o['enable_schema_integration'] ) ? 'checked' : '';
- $detected = \Functionalities\Features\Meta::detect_seo_plugin();
- $plugin_names = array(
- 'rank-math' => 'Rank Math',
- 'yoast' => 'Yoast SEO',
- 'seo-framework' => 'The SEO Framework',
- 'seopress' => 'SEOPress',
- 'aioseo' => 'All in One SEO',
- 'none' => \__( 'None detected', 'functionalities' ),
- );
- echo '
';
- echo \esc_html__( 'Add copyright data to SEO plugin Schema.org output', 'functionalities' ) . '';
- echo '
';
- if ( $detected !== 'none' ) {
- echo '✓ ' . \esc_html__( 'Detected:', 'functionalities' ) . ' ' . \esc_html( $plugin_names[ $detected ] ) . ' ';
- } else {
- echo \esc_html__( 'Supports: Rank Math, Yoast SEO, The SEO Framework, SEOPress, AIOSEO', 'functionalities' );
- }
- echo '
';
- },
- 'functionalities_meta',
- 'functionalities_meta_section'
- );
-
- \add_settings_field(
- 'default_license',
- \__( 'Default License', 'functionalities' ),
- function() {
- $o = self::get_meta_options();
- $val = isset( $o['default_license'] ) ? (string) $o['default_license'] : 'all-rights-reserved';
- $licenses = array(
- 'all-rights-reserved' => \__( 'All Rights Reserved', 'functionalities' ),
- 'cc-by' => 'CC BY 4.0',
- 'cc-by-sa' => 'CC BY-SA 4.0',
- 'cc-by-nc' => 'CC BY-NC 4.0',
- 'cc-by-nc-sa' => 'CC BY-NC-SA 4.0',
- 'cc-by-nd' => 'CC BY-ND 4.0',
- 'cc-by-nc-nd' => 'CC BY-NC-ND 4.0',
- 'cc0' => 'CC0 1.0 (Public Domain)',
- );
- echo '
';
- foreach ( $licenses as $key => $label ) {
- $sel = selected( $val, $key, false );
- echo '' . \esc_html( $label ) . ' ';
- }
- echo ' ';
- echo '
' . \esc_html__( 'Default license for new posts (can be overridden per-post).', 'functionalities' ) . '
';
- },
- 'functionalities_meta',
- 'functionalities_meta_section'
- );
-
- \add_settings_field(
- 'default_license_url',
- \__( 'Custom License URL', 'functionalities' ),
- function() {
- $o = self::get_meta_options();
- $val = isset( $o['default_license_url'] ) ? (string) $o['default_license_url'] : '';
- echo '
';
- echo '
' . \esc_html__( 'Custom URL for "All Rights Reserved" license (e.g., your terms/disclaimer page).', 'functionalities' ) . '
';
- },
- 'functionalities_meta',
- 'functionalities_meta_section'
- );
-
- \add_settings_field(
- 'meta_post_types',
- \__( 'Post Types', 'functionalities' ),
- function() {
- $o = self::get_meta_options();
- $selected = isset( $o['post_types'] ) && is_array( $o['post_types'] ) ? $o['post_types'] : array( 'post' );
- $pts = \get_post_types( array( 'public' => true ), 'objects' );
- echo '
';
- foreach ( $pts as $name => $obj ) {
- $is_checked = in_array( $name, $selected, true ) ? 'checked' : '';
- $label = sprintf( '%s (%s)', $obj->labels->singular_name ?? $name, $name );
- echo ' ' . \esc_html( $label ) . ' ';
- }
- echo ' ';
- echo '
' . \esc_html__( 'Select post types where meta tags and license metabox should appear.', 'functionalities' ) . '
';
- },
- 'functionalities_meta',
- 'functionalities_meta_section'
- );
-
- \add_settings_field(
- 'copyright_holder_type',
- \__( 'Copyright Holder', 'functionalities' ),
- function() {
- $o = self::get_meta_options();
- $val = isset( $o['copyright_holder_type'] ) ? (string) $o['copyright_holder_type'] : 'author';
- $options = array(
- 'author' => \__( 'Post Author', 'functionalities' ),
- 'site' => \__( 'Site Name', 'functionalities' ),
- 'custom' => \__( 'Custom Name', 'functionalities' ),
- );
- echo '
';
- foreach ( $options as $key => $label ) {
- $sel = selected( $val, $key, false );
- echo '' . \esc_html( $label ) . ' ';
- }
- echo ' ';
- echo '
' . \esc_html__( 'Who should be listed as the copyright holder.', 'functionalities' ) . '
';
- },
- 'functionalities_meta',
- 'functionalities_meta_section'
- );
-
- \add_settings_field(
- 'custom_copyright_holder',
- \__( 'Custom Copyright Holder Name', 'functionalities' ),
- function() {
- $o = self::get_meta_options();
- $val = isset( $o['custom_copyright_holder'] ) ? (string) $o['custom_copyright_holder'] : '';
- echo '
';
- echo '
' . \esc_html__( 'Used when "Custom Name" is selected above.', 'functionalities' ) . '
';
- },
- 'functionalities_meta',
- 'functionalities_meta_section'
- );
-
- \add_settings_field(
- 'dc_language',
- \__( 'Dublin Core Language', 'functionalities' ),
- function() {
- $o = self::get_meta_options();
- $val = isset( $o['dc_language'] ) ? (string) $o['dc_language'] : '';
- $site_lang = \get_bloginfo( 'language' );
- echo '
';
- echo '
' . \esc_html__( 'Leave empty to use site language. Use ISO 639 codes (en, en-US, de, etc.).', 'functionalities' ) . '
';
- },
- 'functionalities_meta',
- 'functionalities_meta_section'
- );
-
- // Content Regression Detection settings.
- \register_setting(
- 'functionalities_content_regression',
- 'functionalities_content_regression',
- array(
- 'sanitize_callback' => array( __CLASS__, 'sanitize_content_regression' ),
- 'default' => array(
- 'enabled' => false,
- 'post_types' => array( 'post', 'page' ),
- 'link_drop_enabled' => true,
- 'link_drop_percent' => 30,
- 'link_drop_absolute' => 3,
- 'exclude_nofollow_links' => false,
- 'word_count_enabled' => true,
- 'word_count_drop_percent' => 35,
- 'word_count_min_age_days' => 30,
- 'word_count_compare_average' => false,
- 'exclude_shortcodes' => false,
- 'heading_enabled' => true,
- 'detect_missing_h1' => true,
- 'detect_multiple_h1' => true,
- 'detect_skipped_levels' => true,
- 'snapshot_rolling_count' => 5,
- 'show_post_column' => true,
- ),
- )
- );
-
- \add_settings_section(
- 'functionalities_content_regression_section',
- \__( 'Content Integrity Settings', 'functionalities' ),
- array( __CLASS__, 'section_content_regression' ),
- 'functionalities_content_regression'
- );
-
- // Enable module.
- \add_settings_field(
- 'regression_enabled',
- \__( 'Enable Content Integrity', 'functionalities' ),
- function() {
- $o = self::get_content_regression_options();
- $checked = ! empty( $o['enabled'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Detect structural regressions when posts are updated', 'functionalities' ) . '';
- },
- 'functionalities_content_regression',
- 'functionalities_content_regression_section'
- );
-
- // Post types.
- \add_settings_field(
- 'regression_post_types',
- \__( 'Post Types', 'functionalities' ),
- function() {
- $o = self::get_content_regression_options();
- $selected = isset( $o['post_types'] ) && is_array( $o['post_types'] ) ? $o['post_types'] : array( 'post', 'page' );
- $pts = \get_post_types( array( 'public' => true ), 'objects' );
- echo '
';
- foreach ( $pts as $name => $obj ) {
- if ( 'attachment' === $name ) {
- continue;
- }
- $is_checked = in_array( $name, $selected, true ) ? 'checked' : '';
- $label = sprintf( '%s (%s)', $obj->labels->singular_name ?? $name, $name );
- echo ' ' . \esc_html( $label ) . ' ';
- }
- echo ' ';
- },
- 'functionalities_content_regression',
- 'functionalities_content_regression_section'
- );
-
- // Link drop detection header.
- \add_settings_field(
- 'link_drop_header',
- '
' . \__( 'Internal Link Detection', 'functionalities' ) . ' ',
- function() {
- echo '
' . \esc_html__( 'Detect when internal links are removed from content.', 'functionalities' ) . '
';
- },
- 'functionalities_content_regression',
- 'functionalities_content_regression_section'
- );
-
- \add_settings_field(
- 'link_drop_enabled',
- \__( 'Enable Link Detection', 'functionalities' ),
- function() {
- $o = self::get_content_regression_options();
- $checked = ! empty( $o['link_drop_enabled'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Warn when internal links drop significantly', 'functionalities' ) . '';
- },
- 'functionalities_content_regression',
- 'functionalities_content_regression_section'
- );
-
- \add_settings_field(
- 'link_drop_percent',
- \__( 'Link Drop Threshold (%)', 'functionalities' ),
- function() {
- $o = self::get_content_regression_options();
- $val = isset( $o['link_drop_percent'] ) ? (int) $o['link_drop_percent'] : 30;
- echo '
%';
- echo '
' . \esc_html__( 'Warn if internal links drop by this percentage or more.', 'functionalities' ) . '
';
- },
- 'functionalities_content_regression',
- 'functionalities_content_regression_section'
- );
-
- \add_settings_field(
- 'link_drop_absolute',
- \__( 'Link Drop Absolute Threshold', 'functionalities' ),
- function() {
- $o = self::get_content_regression_options();
- $val = isset( $o['link_drop_absolute'] ) ? (int) $o['link_drop_absolute'] : 3;
- echo '
' . \esc_html__( 'links', 'functionalities' );
- echo '
' . \esc_html__( 'Also warn if this many links are removed (whichever triggers first).', 'functionalities' ) . '
';
- },
- 'functionalities_content_regression',
- 'functionalities_content_regression_section'
- );
-
- \add_settings_field(
- 'exclude_nofollow_links',
- \__( 'Exclude Nofollow Links', 'functionalities' ),
- function() {
- $o = self::get_content_regression_options();
- $checked = ! empty( $o['exclude_nofollow_links'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Do not count links with rel="nofollow"', 'functionalities' ) . '';
- },
- 'functionalities_content_regression',
- 'functionalities_content_regression_section'
- );
-
- // Word count detection header.
- \add_settings_field(
- 'word_count_header',
- '
' . \__( 'Word Count Detection', 'functionalities' ) . ' ',
- function() {
- echo '
' . \esc_html__( 'Detect when content is shortened significantly.', 'functionalities' ) . '
';
- },
- 'functionalities_content_regression',
- 'functionalities_content_regression_section'
- );
-
- \add_settings_field(
- 'word_count_enabled',
- \__( 'Enable Word Count Detection', 'functionalities' ),
- function() {
- $o = self::get_content_regression_options();
- $checked = ! empty( $o['word_count_enabled'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Warn when word count drops significantly', 'functionalities' ) . '';
- },
- 'functionalities_content_regression',
- 'functionalities_content_regression_section'
- );
-
- \add_settings_field(
- 'word_count_drop_percent',
- \__( 'Word Count Drop Threshold (%)', 'functionalities' ),
- function() {
- $o = self::get_content_regression_options();
- $val = isset( $o['word_count_drop_percent'] ) ? (int) $o['word_count_drop_percent'] : 35;
- echo '
%';
- echo '
' . \esc_html__( 'Warn if word count drops by this percentage.', 'functionalities' ) . '
';
- },
- 'functionalities_content_regression',
- 'functionalities_content_regression_section'
- );
-
- \add_settings_field(
- 'word_count_min_age_days',
- \__( 'Minimum Post Age (days)', 'functionalities' ),
- function() {
- $o = self::get_content_regression_options();
- $val = isset( $o['word_count_min_age_days'] ) ? (int) $o['word_count_min_age_days'] : 30;
- echo '
' . \esc_html__( 'days', 'functionalities' );
- echo '
' . \esc_html__( 'Only check word count for posts older than this (to avoid alerts on new posts).', 'functionalities' ) . '
';
- },
- 'functionalities_content_regression',
- 'functionalities_content_regression_section'
- );
-
- \add_settings_field(
- 'exclude_shortcodes',
- \__( 'Exclude Shortcodes', 'functionalities' ),
- function() {
- $o = self::get_content_regression_options();
- $checked = ! empty( $o['exclude_shortcodes'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Remove shortcode content from word count', 'functionalities' ) . '';
- },
- 'functionalities_content_regression',
- 'functionalities_content_regression_section'
- );
-
- // Heading detection header.
- \add_settings_field(
- 'heading_header',
- '
' . \__( 'Heading Structure Detection', 'functionalities' ) . ' ',
- function() {
- echo '
' . \esc_html__( 'Detect accessibility issues with heading hierarchy.', 'functionalities' ) . '
';
- },
- 'functionalities_content_regression',
- 'functionalities_content_regression_section'
- );
-
- \add_settings_field(
- 'heading_enabled',
- \__( 'Enable Heading Detection', 'functionalities' ),
- function() {
- $o = self::get_content_regression_options();
- $checked = ! empty( $o['heading_enabled'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Check heading structure for accessibility issues', 'functionalities' ) . '';
- },
- 'functionalities_content_regression',
- 'functionalities_content_regression_section'
- );
-
- \add_settings_field(
- 'detect_missing_h1',
- \__( 'Detect Missing H1', 'functionalities' ),
- function() {
- $o = self::get_content_regression_options();
- $checked = ! empty( $o['detect_missing_h1'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Warn when no H1 heading is present', 'functionalities' ) . '';
- },
- 'functionalities_content_regression',
- 'functionalities_content_regression_section'
- );
-
- \add_settings_field(
- 'detect_multiple_h1',
- \__( 'Detect Multiple H1s', 'functionalities' ),
- function() {
- $o = self::get_content_regression_options();
- $checked = ! empty( $o['detect_multiple_h1'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Warn when multiple H1 headings exist', 'functionalities' ) . '';
- },
- 'functionalities_content_regression',
- 'functionalities_content_regression_section'
- );
-
- \add_settings_field(
- 'detect_skipped_levels',
- \__( 'Detect Skipped Levels', 'functionalities' ),
- function() {
- $o = self::get_content_regression_options();
- $checked = ! empty( $o['detect_skipped_levels'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Warn when heading levels are skipped (e.g., H2 to H4)', 'functionalities' ) . '';
- },
- 'functionalities_content_regression',
- 'functionalities_content_regression_section'
- );
-
- // Advanced settings header.
- \add_settings_field(
- 'advanced_header',
- '
' . \__( 'Advanced Settings', 'functionalities' ) . ' ',
- function() {
- echo '
' . \esc_html__( 'Configure snapshot and display options.', 'functionalities' ) . '
';
- },
- 'functionalities_content_regression',
- 'functionalities_content_regression_section'
- );
-
- \add_settings_field(
- 'snapshot_rolling_count',
- \__( 'Snapshots to Keep', 'functionalities' ),
- function() {
- $o = self::get_content_regression_options();
- $val = isset( $o['snapshot_rolling_count'] ) ? (int) $o['snapshot_rolling_count'] : 5;
- echo '
';
- echo '
' . \esc_html__( 'Number of historical snapshots to retain per post.', 'functionalities' ) . '
';
- },
- 'functionalities_content_regression',
- 'functionalities_content_regression_section'
- );
-
- \add_settings_field(
- 'show_post_column',
- \__( 'Show Post List Column', 'functionalities' ),
- function() {
- $o = self::get_content_regression_options();
- $checked = ! empty( $o['show_post_column'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Display integrity status icon in post list table', 'functionalities' ) . '';
- },
- 'functionalities_content_regression',
- 'functionalities_content_regression_section'
- );
-
- // Assumption Detection settings.
- \register_setting(
- 'functionalities_assumption_detection',
- 'functionalities_assumption_detection',
- array(
- 'sanitize_callback' => array( __CLASS__, 'sanitize_assumption_detection' ),
- 'default' => array(
- 'enabled' => false,
- 'detect_schema_collision' => true,
- 'detect_analytics_dupe' => true,
- 'detect_font_redundancy' => true,
- 'detect_inline_css_growth' => true,
- 'inline_css_threshold_kb' => 50,
- 'detect_jquery_conflicts' => true,
- 'detect_meta_duplication' => true,
- 'detect_rest_exposure' => true,
- 'detect_lazy_load_conflict' => true,
- 'detect_mixed_content' => true,
- 'detect_missing_security_headers' => true,
- 'detect_debug_exposure' => true,
- 'detect_cron_issues' => true,
- ),
- )
- );
-
- \add_settings_section(
- 'functionalities_assumption_detection_section',
- \__( 'Assumption Detection Settings', 'functionalities' ),
- array( __CLASS__, 'section_assumption_detection' ),
- 'functionalities_assumption_detection'
- );
-
- \add_settings_field(
- 'assumption_enabled',
- \__( 'Enable Assumption Detection', 'functionalities' ),
- function() {
- $o = self::get_assumption_detection_options();
- $checked = ! empty( $o['enabled'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Detect when implicit site assumptions stop being true', 'functionalities' ) . '';
- },
- 'functionalities_assumption_detection',
- 'functionalities_assumption_detection_section'
- );
-
- \add_settings_field(
- 'detect_schema_collision',
- \__( 'Schema Collision Detection', 'functionalities' ),
- function() {
- $o = self::get_assumption_detection_options();
- $checked = ! empty( $o['detect_schema_collision'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Warn when multiple sources output the same schema type', 'functionalities' ) . '';
- },
- 'functionalities_assumption_detection',
- 'functionalities_assumption_detection_section'
- );
-
- \add_settings_field(
- 'detect_analytics_dupe',
- \__( 'Analytics Duplication Detection', 'functionalities' ),
- function() {
- $o = self::get_assumption_detection_options();
- $checked = ! empty( $o['detect_analytics_dupe'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Warn when analytics scripts load multiple times', 'functionalities' ) . '';
- },
- 'functionalities_assumption_detection',
- 'functionalities_assumption_detection_section'
- );
-
- \add_settings_field(
- 'detect_font_redundancy',
- \__( 'Font Redundancy Detection', 'functionalities' ),
- function() {
- $o = self::get_assumption_detection_options();
- $checked = ! empty( $o['detect_font_redundancy'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Warn when fonts load from multiple sources', 'functionalities' ) . '';
- },
- 'functionalities_assumption_detection',
- 'functionalities_assumption_detection_section'
- );
-
- \add_settings_field(
- 'detect_inline_css_growth',
- \__( 'Inline CSS Growth Detection', 'functionalities' ),
- function() {
- $o = self::get_assumption_detection_options();
- $checked = ! empty( $o['detect_inline_css_growth'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Track inline CSS size over time', 'functionalities' ) . '';
- },
- 'functionalities_assumption_detection',
- 'functionalities_assumption_detection_section'
- );
-
- \add_settings_field(
- 'inline_css_threshold_kb',
- \__( 'Inline CSS Threshold (KB)', 'functionalities' ),
- function() {
- $o = self::get_assumption_detection_options();
- $val = isset( $o['inline_css_threshold_kb'] ) ? (int) $o['inline_css_threshold_kb'] : 50;
- echo '
KB';
- echo '
' . \esc_html__( 'Warn when inline CSS exceeds this size.', 'functionalities' ) . '
';
- },
- 'functionalities_assumption_detection',
- 'functionalities_assumption_detection_section'
- );
-
- \add_settings_field(
- 'detect_jquery_conflicts',
- \__( 'jQuery Conflict Detection', 'functionalities' ),
- function() {
- $o = self::get_assumption_detection_options();
- $checked = ! empty( $o['detect_jquery_conflicts'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Warn when multiple jQuery versions or sources are loaded', 'functionalities' ) . '';
- },
- 'functionalities_assumption_detection',
- 'functionalities_assumption_detection_section'
- );
-
- \add_settings_field(
- 'detect_meta_duplication',
- \__( 'Meta Tag Duplication Detection', 'functionalities' ),
- function() {
- $o = self::get_assumption_detection_options();
- $checked = ! empty( $o['detect_meta_duplication'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Warn when duplicate meta tags are detected (viewport, robots, OG tags)', 'functionalities' ) . '';
- },
- 'functionalities_assumption_detection',
- 'functionalities_assumption_detection_section'
- );
-
- \add_settings_field(
- 'detect_rest_exposure',
- \__( 'REST API Exposure Detection', 'functionalities' ),
- function() {
- $o = self::get_assumption_detection_options();
- $checked = ! empty( $o['detect_rest_exposure'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Warn when REST API exposes user information publicly', 'functionalities' ) . '';
- },
- 'functionalities_assumption_detection',
- 'functionalities_assumption_detection_section'
- );
-
- \add_settings_field(
- 'detect_lazy_load_conflict',
- \__( 'Lazy Loading Conflict Detection', 'functionalities' ),
- function() {
- $o = self::get_assumption_detection_options();
- $checked = ! empty( $o['detect_lazy_load_conflict'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Warn when multiple lazy loading implementations are detected', 'functionalities' ) . '';
- },
- 'functionalities_assumption_detection',
- 'functionalities_assumption_detection_section'
- );
-
- \add_settings_field(
- 'detect_mixed_content',
- \__( 'Mixed Content Detection', 'functionalities' ),
- function() {
- $o = self::get_assumption_detection_options();
- $checked = ! empty( $o['detect_mixed_content'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Warn when HTTP resources are loaded on HTTPS pages', 'functionalities' ) . '';
- },
- 'functionalities_assumption_detection',
- 'functionalities_assumption_detection_section'
- );
-
- \add_settings_field(
- 'detect_missing_security_headers',
- \__( 'Security Headers Detection', 'functionalities' ),
- function() {
- $o = self::get_assumption_detection_options();
- $checked = ! empty( $o['detect_missing_security_headers'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Warn when critical security headers are missing', 'functionalities' ) . '';
- },
- 'functionalities_assumption_detection',
- 'functionalities_assumption_detection_section'
- );
-
- \add_settings_field(
- 'detect_debug_exposure',
- \__( 'Debug Mode Exposure', 'functionalities' ),
- function() {
- $o = self::get_assumption_detection_options();
- $checked = ! empty( $o['detect_debug_exposure'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Warn when WP_DEBUG or error display is enabled in production', 'functionalities' ) . '';
- },
- 'functionalities_assumption_detection',
- 'functionalities_assumption_detection_section'
- );
-
- \add_settings_field(
- 'detect_cron_issues',
- \__( 'Cron Issues Detection', 'functionalities' ),
- function() {
- $o = self::get_assumption_detection_options();
- $checked = ! empty( $o['detect_cron_issues'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Warn when WP-Cron is disabled or has stuck jobs', 'functionalities' ) . '';
- },
- 'functionalities_assumption_detection',
- 'functionalities_assumption_detection_section'
- );
-
- // Explicit save button field - ensures visibility.
- \add_settings_field(
- 'assumption_save_settings',
- '',
- function() {
- echo '
';
- \submit_button( \__( 'Save Settings', 'functionalities' ), 'primary', 'submit', false );
- echo ' ';
- echo '';
- echo \esc_html__( 'Run Detection Now', 'functionalities' );
- echo ' ';
- echo '
';
- },
- 'functionalities_assumption_detection',
- 'functionalities_assumption_detection_section'
- );
-
- \add_settings_field(
- 'detected_assumptions',
- \__( 'Detected Assumptions', 'functionalities' ),
- array( __CLASS__, 'field_detected_assumptions' ),
- 'functionalities_assumption_detection',
- 'functionalities_assumption_detection_section'
- );
-
- // PWA settings.
- \register_setting(
- 'functionalities_pwa',
- 'functionalities_pwa',
- array(
- 'sanitize_callback' => array( __CLASS__, 'sanitize_pwa' ),
- 'default' => array(
- 'enabled' => false,
- ),
- )
- );
-
- \add_settings_section(
- 'functionalities_pwa_identity',
- \__( 'App Identity', 'functionalities' ),
- function() {
- echo '
' . \esc_html__( 'Basic information about your progressive web app.', 'functionalities' ) . '
';
- },
- 'functionalities_pwa'
- );
-
- \add_settings_field(
- 'pwa_enabled',
- \__( 'Enable PWA', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- $checked = ! empty( $o['enabled'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Enable Progressive Web App features', 'functionalities' ) . '';
- },
- 'functionalities_pwa',
- 'functionalities_pwa_identity'
- );
-
- \add_settings_field(
- 'pwa_app_name',
- \__( 'App Name', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- echo '
';
- echo '
' . \esc_html__( 'Full name displayed on install. Leave blank to use site title.', 'functionalities' ) . '
';
- },
- 'functionalities_pwa',
- 'functionalities_pwa_identity'
- );
-
- \add_settings_field(
- 'pwa_short_name',
- \__( 'Short Name', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- echo '
';
- echo '
' . \esc_html__( 'Short name for the home screen (12 chars max).', 'functionalities' ) . '
';
- },
- 'functionalities_pwa',
- 'functionalities_pwa_identity'
- );
-
- \add_settings_field(
- 'pwa_description',
- \__( 'Description', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- echo '
';
- echo '
' . \esc_html__( 'Shown in app stores and install dialogs. Leave blank to use site tagline.', 'functionalities' ) . '
';
- },
- 'functionalities_pwa',
- 'functionalities_pwa_identity'
- );
-
- \add_settings_field(
- 'pwa_start_url',
- \__( 'Start URL', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- echo '
';
- },
- 'functionalities_pwa',
- 'functionalities_pwa_identity'
- );
-
- \add_settings_field(
- 'pwa_scope',
- \__( 'Scope', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- echo '
';
- echo '
' . \esc_html__( 'Navigation scope. "/" means the entire site.', 'functionalities' ) . '
';
- },
- 'functionalities_pwa',
- 'functionalities_pwa_identity'
- );
-
- \add_settings_field(
- 'pwa_categories',
- \__( 'Categories', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- echo '
';
- echo '
' . \esc_html__( 'Comma-separated W3C categories (e.g. news, education, business).', 'functionalities' ) . '
';
- },
- 'functionalities_pwa',
- 'functionalities_pwa_identity'
- );
-
- \add_settings_field(
- 'pwa_display',
- \__( 'Display Mode', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- $modes = array( 'standalone', 'fullscreen', 'minimal-ui', 'browser' );
- echo '
';
- foreach ( $modes as $mode ) {
- echo '' . \esc_html( $mode ) . ' ';
- }
- echo ' ';
- },
- 'functionalities_pwa',
- 'functionalities_pwa_identity'
- );
-
- \add_settings_field(
- 'pwa_orientation',
- \__( 'Orientation', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- $orientations = array( 'any', 'portrait', 'landscape', 'portrait-primary', 'landscape-primary' );
- echo '
';
- foreach ( $orientations as $orient ) {
- echo '' . \esc_html( $orient ) . ' ';
- }
- echo ' ';
- },
- 'functionalities_pwa',
- 'functionalities_pwa_identity'
- );
-
- // Appearance section.
- \add_settings_section(
- 'functionalities_pwa_appearance',
- \__( 'Appearance & Icons', 'functionalities' ),
- function() {
- echo '
' . \esc_html__( 'Theme colors and app icons. Icons must be square PNG files.', 'functionalities' ) . '
';
- },
- 'functionalities_pwa'
- );
-
- \add_settings_field(
- 'pwa_theme_color',
- \__( 'Theme Color', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- echo '
';
- },
- 'functionalities_pwa',
- 'functionalities_pwa_appearance'
- );
-
- \add_settings_field(
- 'pwa_background_color',
- \__( 'Background Color', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- echo '
';
- },
- 'functionalities_pwa',
- 'functionalities_pwa_appearance'
- );
-
- \add_settings_field(
- 'pwa_icon_512',
- \__( 'Icon 512x512', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- self::render_media_field( 'functionalities_pwa[icon_512]', $o['icon_512'], \__( 'Primary app icon (required for installability).', 'functionalities' ) );
- },
- 'functionalities_pwa',
- 'functionalities_pwa_appearance'
- );
-
- \add_settings_field(
- 'pwa_icon_192',
- \__( 'Icon 192x192', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- self::render_media_field( 'functionalities_pwa[icon_192]', $o['icon_192'], \__( 'Smaller icon for home screen and splash.', 'functionalities' ) );
- },
- 'functionalities_pwa',
- 'functionalities_pwa_appearance'
- );
-
- \add_settings_field(
- 'pwa_maskable_icon_512',
- \__( 'Maskable Icon 512x512', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- self::render_media_field( 'functionalities_pwa[maskable_icon_512]', $o['maskable_icon_512'], \__( 'Maskable icon for adaptive icon support (safe zone padding recommended).', 'functionalities' ) );
- },
- 'functionalities_pwa',
- 'functionalities_pwa_appearance'
- );
-
- \add_settings_field(
- 'pwa_maskable_icon_192',
- \__( 'Maskable Icon 192x192', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- self::render_media_field( 'functionalities_pwa[maskable_icon_192]', $o['maskable_icon_192'], \__( 'Smaller maskable icon.', 'functionalities' ) );
- },
- 'functionalities_pwa',
- 'functionalities_pwa_appearance'
- );
-
- // Install Prompt section.
- \add_settings_section(
- 'functionalities_pwa_prompt',
- \__( 'Install Prompt', 'functionalities' ),
- function() {
- echo '
' . \esc_html__( 'Customize the in-page install prompt shown to visitors.', 'functionalities' ) . '
';
- },
- 'functionalities_pwa'
- );
-
- \add_settings_field(
- 'pwa_install_prompt',
- \__( 'Show Install Prompt', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- $checked = ! empty( $o['install_prompt'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Display a custom install prompt to visitors', 'functionalities' ) . '';
- },
- 'functionalities_pwa',
- 'functionalities_pwa_prompt'
- );
-
- \add_settings_field(
- 'pwa_prompt_title',
- \__( 'Prompt Title', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- echo '
';
- },
- 'functionalities_pwa',
- 'functionalities_pwa_prompt'
- );
-
- \add_settings_field(
- 'pwa_prompt_text',
- \__( 'Prompt Text', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- echo '
';
- },
- 'functionalities_pwa',
- 'functionalities_pwa_prompt'
- );
-
- \add_settings_field(
- 'pwa_prompt_button',
- \__( 'Button Text', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- echo '
';
- },
- 'functionalities_pwa',
- 'functionalities_pwa_prompt'
- );
-
- \add_settings_field(
- 'pwa_prompt_dismiss',
- \__( 'Dismiss Text', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- echo '
';
- },
- 'functionalities_pwa',
- 'functionalities_pwa_prompt'
- );
-
- \add_settings_field(
- 'pwa_prompt_position',
- \__( 'Position', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- $positions = array(
- 'bottom' => \__( 'Bottom', 'functionalities' ),
- 'top' => \__( 'Top', 'functionalities' ),
- 'center' => \__( 'Center (modal)', 'functionalities' ),
- );
- echo '
';
- foreach ( $positions as $val => $label ) {
- echo '' . \esc_html( $label ) . ' ';
- }
- echo ' ';
- },
- 'functionalities_pwa',
- 'functionalities_pwa_prompt'
- );
-
- \add_settings_field(
- 'pwa_prompt_style',
- \__( 'Style', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- $styles = array(
- 'banner' => \__( 'Banner', 'functionalities' ),
- 'card' => \__( 'Card', 'functionalities' ),
- );
- echo '
';
- foreach ( $styles as $val => $label ) {
- echo '' . \esc_html( $label ) . ' ';
- }
- echo ' ';
- },
- 'functionalities_pwa',
- 'functionalities_pwa_prompt'
- );
-
- \add_settings_field(
- 'pwa_prompt_frequency',
- \__( 'Re-show After (days)', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- echo '
';
- echo '
' . \esc_html__( 'Days to wait before showing prompt again after dismissal.', 'functionalities' ) . '
';
- },
- 'functionalities_pwa',
- 'functionalities_pwa_prompt'
- );
-
- // Offline & Caching section.
- \add_settings_section(
- 'functionalities_pwa_caching',
- \__( 'Offline & Caching', 'functionalities' ),
- function() {
- echo '
' . \esc_html__( 'Service worker caching and offline behavior.', 'functionalities' ) . '
';
- },
- 'functionalities_pwa'
- );
-
- \add_settings_field(
- 'pwa_cache_version',
- \__( 'Cache Version', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- echo '
';
- echo '
' . \esc_html__( 'Increment to force cache refresh (e.g. v1, v2).', 'functionalities' ) . '
';
- },
- 'functionalities_pwa',
- 'functionalities_pwa_caching'
- );
-
- \add_settings_field(
- 'pwa_precache_urls',
- \__( 'Precache URLs', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- echo '
';
- echo '
' . \esc_html__( 'One URL per line. These pages will be cached immediately when the service worker installs.', 'functionalities' ) . '
';
- },
- 'functionalities_pwa',
- 'functionalities_pwa_caching'
- );
-
- // Shortcuts section.
- \add_settings_section(
- 'functionalities_pwa_shortcuts',
- \__( 'Shortcuts', 'functionalities' ),
- function() {
- echo '
' . \esc_html__( 'App shortcut links shown on long-press of the app icon. Up to 4 shortcuts.', 'functionalities' ) . '
';
- },
- 'functionalities_pwa'
- );
-
- \add_settings_field(
- 'pwa_shortcuts',
- \__( 'Shortcuts', 'functionalities' ),
- array( __CLASS__, 'field_pwa_shortcuts' ),
- 'functionalities_pwa',
- 'functionalities_pwa_shortcuts'
- );
-
- // Screenshots section.
- \add_settings_section(
- 'functionalities_pwa_screenshots',
- \__( 'Screenshots', 'functionalities' ),
- function() {
- echo '
' . \esc_html__( 'App screenshots shown in install dialogs and app stores. Provide wide and narrow sizes.', 'functionalities' ) . '
';
- },
- 'functionalities_pwa'
- );
-
- \add_settings_field(
- 'pwa_screenshots',
- \__( 'Screenshots', 'functionalities' ),
- array( __CLASS__, 'field_pwa_screenshots' ),
- 'functionalities_pwa',
- 'functionalities_pwa_screenshots'
- );
-
- // Advanced section.
- \add_settings_section(
- 'functionalities_pwa_advanced',
- \__( 'Advanced', 'functionalities' ),
- function() {
- echo '
' . \esc_html__( 'Advanced PWA features. Only change these if you know what you\'re doing.', 'functionalities' ) . '
';
- },
- 'functionalities_pwa'
- );
-
- \add_settings_field(
- 'pwa_display_override',
- \__( 'Display Override', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- $checked = ! empty( $o['display_override'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Enable display_override with window-controls-overlay fallback chain', 'functionalities' ) . '';
- },
- 'functionalities_pwa',
- 'functionalities_pwa_advanced'
- );
-
- \add_settings_field(
- 'pwa_edge_side_panel',
- \__( 'Edge Side Panel', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- $checked = ! empty( $o['edge_side_panel'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Allow opening in Microsoft Edge side panel', 'functionalities' ) . '';
- },
- 'functionalities_pwa',
- 'functionalities_pwa_advanced'
- );
-
- \add_settings_field(
- 'pwa_launch_handler',
- \__( 'Launch Handler', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- $handlers = array(
- '' => \__( 'Default', 'functionalities' ),
- 'focus-existing' => 'focus-existing',
- 'navigate-new' => 'navigate-new',
- 'navigate-existing' => 'navigate-existing',
- );
- echo '
';
- foreach ( $handlers as $val => $label ) {
- echo '' . \esc_html( $label ) . ' ';
- }
- echo ' ';
- echo '
' . \esc_html__( 'Controls behavior when the app is already open and a new navigation is triggered.', 'functionalities' ) . '
';
- },
- 'functionalities_pwa',
- 'functionalities_pwa_advanced'
- );
-
- \add_settings_field(
- 'pwa_share_target',
- \__( 'Share Target', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- $checked = ! empty( $o['share_target_enabled'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Register as a Web Share Target', 'functionalities' ) . '';
- echo '
';
- echo '' . \esc_html__( 'Action URL:', 'functionalities' ) . ' ';
- echo ' ';
- echo '
';
- $methods = array( 'GET' => 'GET', 'POST' => 'POST' );
- echo '
';
- echo '' . \esc_html__( 'Method:', 'functionalities' ) . ' ';
- echo '';
- foreach ( $methods as $val => $label ) {
- echo '' . \esc_html( $label ) . ' ';
- }
- echo ' ';
- echo '
';
- },
- 'functionalities_pwa',
- 'functionalities_pwa_advanced'
- );
-
- \add_settings_field(
- 'pwa_advanced_manifest',
- \__( 'Custom Manifest JSON', 'functionalities' ),
- function() {
- $o = self::get_pwa_options();
- echo '
';
- echo '
' . \esc_html__( 'Raw JSON to merge into the manifest. Must be a valid JSON object.', 'functionalities' ) . '
';
- },
- 'functionalities_pwa',
- 'functionalities_pwa_advanced'
- );
- }
-
- public static function field_nofollow_external() : void {
- $opts = self::get_link_management_options();
- $checked = ! empty( $opts['nofollow_external'] ) ? 'checked' : '';
- echo '
' . \esc_html__( 'Enable rel="nofollow" for all external links', 'functionalities' ) . '';
- }
-
- /**
- * Render exceptions field.
- *
- * @return void
- */
- public static function field_exceptions() : void {
- $opts = self::get_link_management_options();
- $value = isset( $opts['exceptions'] ) ? (string) $opts['exceptions'] : '';
- echo '
';
- echo '
' . \esc_html__( 'One per line. Supports full URLs (https://example.com/page), domains (example.com), or partial matches (e.g., /partner/).', 'functionalities' ) . '
';
- }
-
- /**
- * Render section description for link management.
- *
- * @return void
- */
- public static function section_link_management() : void {
- echo '
' . \esc_html__( 'Control how external and internal links are handled across your site.', 'functionalities' ) . '
';
-
- // Get module docs.
- $docs = Module_Docs::get( 'link-management' );
-
- // Render documentation accordions.
- echo '
';
-
- if ( ! empty( $docs['features'] ) ) {
- $list = '
';
- foreach ( $docs['features'] as $feature ) {
- $list .= '' . \esc_html( $feature ) . ' ';
- }
- $list .= ' ';
- Admin_UI::render_docs_section( \__( 'What This Module Does', 'functionalities' ), $list, 'info' );
- }
-
- if ( ! empty( $docs['hooks'] ) ) {
- $hooks_html = '
';
- foreach ( $docs['hooks'] as $hook ) {
- $hooks_html .= '' . \esc_html( $hook['name'] ) . ' ';
- $hooks_html .= '' . \esc_html( $hook['description'] ) . ' ';
- }
- $hooks_html .= ' ';
- Admin_UI::render_docs_section( \__( 'Developer Hooks', 'functionalities' ), $hooks_html, 'developer' );
- }
-
- echo '
';
- }
-
- /**
- * Render open external links in new tab field.
- *
- * @return void
- */
- public static function field_open_external_new_tab() : void {
- $opts = self::get_link_management_options();
- $checked = ! empty( $opts['open_external_new_tab'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Adds target="_blank" and rel="noopener" to external links', 'functionalities' ) . '';
- }
+class Admin extends Module_Controller {
/**
- * Render open internal links in new tab field.
+ * Initialize admin controllers.
*
* @return void
*/
- public static function field_open_internal_new_tab() : void {
- $opts = self::get_link_management_options();
- $checked = ! empty( $opts['open_internal_new_tab'] ) ? 'checked' : '';
- echo '
';
- echo \esc_html__( 'Adds target="_blank" to same-domain links (see exceptions)', 'functionalities' ) . '';
- }
-
- /**
- * Render internal new tab exceptions field.
- *
- * @return void
- */
- public static function field_internal_new_tab_exceptions() : void {
- $opts = self::get_link_management_options();
- $val = isset( $opts['internal_new_tab_exceptions'] ) ? (string) $opts['internal_new_tab_exceptions'] : '';
- echo '
';
- echo '
' . \esc_html__( 'One domain per line. Matching hosts will NOT be forced to open in a new tab when internal option is enabled.', 'functionalities' ) . '
';
- }
-
- /**
- * Render JSON preset URL field.
- *
- * @return void
- */
- public static function field_json_preset_url() : void {
- $opts = self::get_link_management_options();
- $val = isset( $opts['json_preset_url'] ) ? (string) $opts['json_preset_url'] : '';
-
- // Check for theme exception-urls.json.
- $theme_json_path = \get_stylesheet_directory() . '/exception-urls.json';
- $theme_json_exists = file_exists( $theme_json_path );
- $parent_json_path = \get_template_directory() . '/exception-urls.json';
- $parent_json_exists = \get_stylesheet_directory() !== \get_template_directory() && file_exists( $parent_json_path );
- ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- get( 'Name' ) )
- );
- ?>
-
-
-
-
-
- get( 'Name' ) )
- );
- ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
<?php
-/**
- * Add trusted domains to nofollow exceptions.
- * Links to these domains will NOT have nofollow added.
- */
-add_filter( 'functionalities_exception_domains', function( $domains ) {
- // Add your trusted domains here
- $trusted_domains = array(
- 'trusted-partner.com',
- 'another-trusted-site.org',
- 'your-other-website.net',
- );
-
- return array_merge( $domains, $trusted_domains );
-} );
-
-
-
-
-
-
-
-
-
-
-
<?php
-/**
- * Add specific URLs to nofollow exceptions.
- * These specific URLs will NOT have nofollow added.
- */
-add_filter( 'functionalities_exception_urls', function( $urls ) {
- // Add specific URLs or patterns
- $trusted_urls = array(
- 'https://example.com/specific-page',
- 'https://partner.com/affiliate/*',
- 'https://trusted.org/blog/*',
- );
-
- return array_merge( $urls, $trusted_urls );
-} );
-
-
-
-
-
-
-
-
-
-
-
<?php
-/**
- * Dynamically add exception domains based on conditions.
- * Example: Trust all domains for administrators.
- */
-add_filter( 'functionalities_exception_domains', function( $domains ) {
- // Skip nofollow for administrators
- if ( current_user_can( 'manage_options' ) ) {
- // Return wildcard to match all domains
- $domains[] = '*';
- }
-
- // Add domains from a custom option
- $custom_domains = get_option( 'my_trusted_domains', array() );
- if ( is_array( $custom_domains ) ) {
- $domains = array_merge( $domains, $custom_domains );
- }
-
- // Add sponsor domains on specific post types
- if ( is_singular( 'sponsored_post' ) ) {
- $domains[] = 'sponsor-website.com';
- }
-
- return $domains;
-} );
-
-
-
-
-
-
-
-
-
-
-
<?php
-/**
- * Use a custom JSON file path for exception URLs.
- */
-add_filter( 'functionalities_json_preset_path', function( $default_path ) {
- // Use a JSON file from your theme
- $theme_json = get_stylesheet_directory() . '/config/exceptions.json';
-
- if ( file_exists( $theme_json ) ) {
- return $theme_json;
- }
-
- // Or use an external URL (use with caution!)
- // return 'https://your-cdn.com/exception-urls.json';
-
- return $default_path;
-} );
-
-
-
-
-
-
-
-
-
-
- ' . \esc_html__( 'Remove Gutenberg block-specific CSS classes from your frontend HTML for cleaner markup.', 'functionalities' ) . '';
-
- // Get module docs.
- $docs = Module_Docs::get( 'block-cleanup' );
-
- // Render documentation accordions.
- echo '
';
-
- if ( ! empty( $docs['features'] ) ) {
- $list = '
';
- foreach ( $docs['features'] as $feature ) {
- $list .= '' . \esc_html( $feature ) . ' ';
- }
- $list .= ' ';
- Admin_UI::render_docs_section( \__( 'What This Module Does', 'functionalities' ), $list, 'info' );
- }
-
- // Classes removed info.
- $classes_html = '
';
- $classes_html .= 'wp-block-heading → ' . \esc_html__( 'from h1-h6 elements', 'functionalities' ) . ' ';
- $classes_html .= 'wp-block-list → ' . \esc_html__( 'from ul/ol elements', 'functionalities' ) . ' ';
- $classes_html .= 'wp-block-image → ' . \esc_html__( 'from figure/div elements', 'functionalities' ) . ' ';
- $classes_html .= ' ';
- Admin_UI::render_docs_section( \__( 'Classes Removed', 'functionalities' ), $classes_html, 'usage' );
-
- if ( ! empty( $docs['hooks'] ) ) {
- $hooks_html = '
';
- foreach ( $docs['hooks'] as $hook ) {
- $hooks_html .= '' . \esc_html( $hook['name'] ) . ' ';
- $hooks_html .= '' . \esc_html( $hook['description'] ) . ' ';
- }
- $hooks_html .= ' ';
- Admin_UI::render_docs_section( \__( 'Developer Hooks', 'functionalities' ), $hooks_html, 'developer' );
- }
-
- echo '
';
- }
-
- // Block Cleanup: fields & helpers
- public static function field_bc_remove_heading() : void {
- $opts = self::get_block_cleanup_options();
- $checked = ! empty( $opts['remove_heading_block_class'] ) ? 'checked' : '';
- echo '
' . \esc_html__( 'Remove class "wp-block-heading" from headings (h1–h6)', 'functionalities' ) . '';
- }
- public static function field_bc_remove_list() : void {
- $opts = self::get_block_cleanup_options();
- $checked = ! empty( $opts['remove_list_block_class'] ) ? 'checked' : '';
- echo '
' . \esc_html__( 'Remove class "wp-block-list" from ul/ol', 'functionalities' ) . '';
- }
- public static function field_bc_remove_image() : void {
- $opts = self::get_block_cleanup_options();
- $checked = ! empty( $opts['remove_image_block_class'] ) ? 'checked' : '';
- echo '
' . \esc_html__( 'Remove class ".wp-block-image" from frontend', 'functionalities' ) . '';
- }
-
- // Editor Links: fields & helpers
- public static function field_el_enable() : void {
- $opts = self::get_editor_links_options();
- $checked = ! empty( $opts['enable_limit'] ) ? 'checked' : '';
- echo '
' . \esc_html__( 'Limit editor link suggestions to selected post types', 'functionalities' ) . '';
- }
- public static function field_el_post_types() : void {
- $opts = self::get_editor_links_options();
- $selected = isset( $opts['post_types'] ) && is_array( $opts['post_types'] ) ? $opts['post_types'] : [];
- $pts = get_post_types( [ 'public' => true ], 'objects' );
- echo '
';
- foreach ( $pts as $name => $obj ) {
- $is_checked = in_array( $name, $selected, true ) ? 'checked' : '';
- $label = sprintf( '%s (%s)', $obj->labels->singular_name ?? $name, $name );
- echo ' ' . esc_html( $label ) . ' ';
- }
- echo ' ';
- }
-
- public static function field_components_items() : void {
- $o = self::get_components_options();
- $items = isset( $o['items'] ) && is_array( $o['items'] ) ? $o['items'] : [];
- $per_page = 24;
- $total_items = count( $items );
- $total_pages = (int) max( 1, ceil( $total_items / $per_page ) );
- ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 1 ) : ?>
-
-
-
-
-
-
-
-
-
-
.new-component
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
' . esc_html( $label ) . '';
- },
- 'functionalities_misc',
- 'functionalities_misc_section'
- );
- }
-
- public static function field_fonts_items() : void {
- $o = self::get_fonts_options();
- $items = isset( $o['items'] ) && is_array( $o['items'] ) ? $o['items'] : [];
- $total_items = count( $items );
-
- // Enqueue media uploader
- \wp_enqueue_media();
- ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- id="fc-assign-toggle">
-
-
-
-
-
-
-
-
- ' . \esc_html__( 'Add copyright metadata, Dublin Core (DCMI) tags, and per-post licensing options. Works standalone or integrates with major SEO plugins.', 'functionalities' ) . '';
-
- // Get module docs.
- $docs = Module_Docs::get( 'meta' );
-
- // Render documentation accordions.
- echo '
';
-
- if ( ! empty( $docs['features'] ) ) {
- $list = '
';
- foreach ( $docs['features'] as $feature ) {
- $list .= '' . \esc_html( $feature ) . ' ';
- }
- $list .= ' ';
- Admin_UI::render_docs_section( \__( 'What This Module Does', 'functionalities' ), $list, 'info' );
- }
-
- // Schema.org Support (dynamic based on detected plugin).
- $plugin_names = array(
- 'rank-math' => 'Rank Math',
- 'yoast' => 'Yoast SEO',
- 'seo-framework' => 'The SEO Framework',
- 'seopress' => 'SEOPress',
- 'aioseo' => 'All in One SEO',
- );
-
- if ( $detected !== 'none' ) {
- $schema_content = '
✓ ' . \esc_html__( 'Detected:', 'functionalities' ) . ' ' . \esc_html( $plugin_names[ $detected ] ?? $detected ) . '
';
- $schema_content .= '
' . \esc_html__( 'Copyright data will be added to your SEO plugin\'s existing schema output.', 'functionalities' ) . '
';
- } else {
- $schema_content = '
✓ ' . \esc_html__( 'Standalone Mode', 'functionalities' ) . '
';
- $schema_content .= '
' . \esc_html__( 'No SEO plugin detected. Complete Article schema with copyright will be output independently.', 'functionalities' ) . '
';
- }
- Admin_UI::render_docs_section( \__( 'Schema.org Support', 'functionalities' ), $schema_content, 'developer', true );
-
- // Compatible plugins.
- $plugins_html = '
';
- $plugins_html .= 'Rank Math ';
- $plugins_html .= 'Yoast SEO ';
- $plugins_html .= 'The SEO Framework ';
- $plugins_html .= 'SEOPress ';
- $plugins_html .= 'All in One SEO ';
- $plugins_html .= '' . \esc_html__( 'or Standalone', 'functionalities' ) . ' ';
- $plugins_html .= ' ';
- Admin_UI::render_docs_section( \__( 'Compatible SEO Plugins', 'functionalities' ), $plugins_html, 'usage' );
-
- if ( ! empty( $docs['hooks'] ) ) {
- $hooks_html = '
';
- foreach ( $docs['hooks'] as $hook ) {
- $hooks_html .= '' . \esc_html( $hook['name'] ) . ' ';
- $hooks_html .= '' . \esc_html( $hook['description'] ) . ' ';
- }
- $hooks_html .= ' ';
- Admin_UI::render_docs_section( \__( 'Developer Hooks', 'functionalities' ), $hooks_html, 'developer' );
- }
-
- echo '
';
- }
-
- /**
- * Render section description for content regression detection.
- *
- * @return void
- */
- public static function section_content_regression() : void {
- echo '
' . \esc_html__( 'Detect structural regressions when posts are updated. This module compares each post against its own historical baseline.', 'functionalities' ) . '
';
-
- // Get module docs.
- $docs = Module_Docs::get( 'content-regression' );
-
- // Render documentation accordions.
- echo '
';
-
- if ( ! empty( $docs['features'] ) ) {
- $list = '
';
- foreach ( $docs['features'] as $feature ) {
- $list .= '' . \esc_html( $feature ) . ' ';
- }
- $list .= ' ';
- Admin_UI::render_docs_section( \__( 'What This Module Does', 'functionalities' ), $list, 'info' );
- }
-
- if ( ! empty( $docs['usage'] ) ) {
- Admin_UI::render_docs_section( \__( 'Philosophy', 'functionalities' ), '
' . \esc_html( $docs['usage'] ) . '
', 'usage' );
- }
-
- if ( ! empty( $docs['hooks'] ) ) {
- $hooks_html = '
';
- foreach ( $docs['hooks'] as $hook ) {
- $hooks_html .= '' . \esc_html( $hook['name'] ) . ' ';
- $hooks_html .= '' . \esc_html( $hook['description'] ) . ' ';
- }
- $hooks_html .= ' ';
- Admin_UI::render_docs_section( \__( 'Developer Hooks', 'functionalities' ), $hooks_html, 'developer' );
- }
-
- echo '
';
- }
-
- /**
- * Assumption detection section callback.
- *
- * @return void
- */
- public static function section_assumption_detection() : void {
- echo '
' . \esc_html__( 'Detects when technical assumptions stop being true. This module notices changes, not problems.', 'functionalities' ) . '
';
-
- // Get module docs.
- $docs = Module_Docs::get( 'assumption-detection' );
-
- // Render documentation accordions.
- if ( ! empty( $docs['features'] ) ) {
- $list = '
';
- foreach ( $docs['features'] as $feature ) {
- $list .= '' . \esc_html( $feature ) . ' ';
- }
- $list .= ' ';
- Admin_UI::render_docs_section( \__( 'What This Module Does', 'functionalities' ), $list, 'info' );
- }
-
- if ( ! empty( $docs['usage'] ) ) {
- Admin_UI::render_docs_section( \__( 'How to Use', 'functionalities' ), '
' . \esc_html( $docs['usage'] ) . '
', 'usage' );
- }
-
- if ( ! empty( $docs['hooks'] ) ) {
- $hooks_html = '
';
- foreach ( $docs['hooks'] as $hook ) {
- $hooks_html .= '' . \esc_html( $hook['name'] ) . ' ';
- $hooks_html .= '' . \esc_html( $hook['description'] ) . ' ';
- }
- $hooks_html .= ' ';
- Admin_UI::render_docs_section( \__( 'Developer Hooks', 'functionalities' ), $hooks_html, 'developer' );
- }
- }
-
- /**
- * Detected assumptions field callback.
- *
- * @return void
- */
- public static function field_detected_assumptions() : void {
- $assumptions = \Functionalities\Features\Assumption_Detection::get_detected_assumptions();
- $ignored = \Functionalities\Features\Assumption_Detection::get_ignored_assumptions();
-
- // Filter out expired ignored items and already-ignored assumptions.
- $active_warnings = array();
- foreach ( $assumptions as $assumption ) {
- $hash = self::generate_warning_hash( $assumption );
- // Skip if ignored and not expired.
- if ( isset( $ignored[ $hash ] ) && $ignored[ $hash ]['expires'] > time() ) {
- continue;
- }
- $assumption['_hash'] = $hash;
- $active_warnings[] = $assumption;
- }
-
- if ( empty( $active_warnings ) ) {
- echo '
';
- echo ' ';
- echo '' . \esc_html__( 'No assumption changes detected. All monitored items are consistent.', 'functionalities' ) . ' ';
- echo '
';
- return;
- }
-
- echo '
';
-
- foreach ( $active_warnings as $assumption ) {
- $hash = $assumption['_hash'];
- $warning_type = $assumption['type'] ?? 'unknown';
- $type_class = 'warning';
- $icon = 'dashicons-warning';
-
- echo '
';
- echo '';
-
- // Actions.
- echo '
';
- echo '';
- echo \esc_html__( 'Dismiss', 'functionalities' );
- echo ' ';
- echo '';
- echo \esc_html__( 'Snooze 7 days', 'functionalities' );
- echo ' ';
- echo '';
- echo \esc_html__( 'Ignore permanently', 'functionalities' );
- echo ' ';
- echo '
';
-
- echo '
';
- }
-
- echo '
';
-
- // Inline script for AJAX actions.
- $nonce = \wp_create_nonce( 'functionalities_assumptions' );
- ?>
-
- false ) );
-
- // Handle enable/disable toggle.
- if ( isset( $_POST['functionalities_task_manager_toggle'] ) && \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'functionalities_task_manager_toggle' ) ) {
- $tm_opts['enabled'] = ! empty( $_POST['enabled'] );
- \update_option( 'functionalities_task_manager', $tm_opts );
- echo '
' . \esc_html__( 'Settings saved.', 'functionalities' ) . '
';
- }
- ?>
-
-
-
-
-
-
-
-
-
-
- ›
-
-
-
-
- ›
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ' . \esc_html( WP_CONTENT_DIR . '/functionalities/tasks/' ) . ''
- );
- ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- $project ) :
- $stats = \Functionalities\Features\Task_Manager::get_stats( $project );
- ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
>
-
-
-
-
-
- !
-
-
-
- #
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- false ) );
-
- // Handle enable/disable toggle.
- if ( isset( $_POST['functionalities_redirect_manager_toggle'] ) && \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'functionalities_redirect_manager_toggle' ) ) {
- $rm_opts['enabled'] = ! empty( $_POST['enabled'] );
- \update_option( 'functionalities_redirect_manager', $rm_opts );
- echo '
' . \esc_html__( 'Settings saved.', 'functionalities' ) . '
';
- }
- ?>
-
-
-
-
-
-
-
-
-
-
- ›
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 301
- 302
- 307
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
' . \esc_html__( 'Settings saved.', 'functionalities' ) . '
';
- }
- ?>
-
-
-
-
-
-
-
-
-
-
- ›
-
-
-
-
-
-
-
-
- ';
- foreach ( $docs['features'] as $feature ) {
- $list .= '
' . \esc_html( $feature ) . ' ';
- }
- $list .= '';
- Admin_UI::render_docs_section( \__( 'What This Module Does', 'functionalities' ), $list, 'info' );
- }
- if ( ! empty( $docs['usage'] ) ) {
- Admin_UI::render_docs_section( \__( 'How to Use', 'functionalities' ), '
' . \esc_html( $docs['usage'] ) . '
', 'usage' );
- }
- if ( ! empty( $docs['hooks'] ) ) {
- $hooks_html = '
';
- foreach ( $docs['hooks'] as $hook ) {
- $hooks_html .= '' . \esc_html( $hook['name'] ) . ' ';
- $hooks_html .= '' . \esc_html( $hook['description'] ) . ' ';
- }
- $hooks_html .= ' ';
- Admin_UI::render_docs_section( \__( 'Developer Hooks', 'functionalities' ), $hooks_html, 'developer' );
- }
- ?>
-
-
-
-
-
-
-
()
-
-
-
-
-
- $icon ) : ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- true,
- 'class' => true,
- 'style' => true,
- 'fill' => true,
- 'fill-opacity' => true,
- 'fill-rule' => true,
- 'stroke' => true,
- 'stroke-width' => true,
- 'stroke-linecap' => true,
- 'stroke-linejoin' => true,
- 'stroke-dasharray' => true,
- 'stroke-dashoffset' => true,
- 'stroke-opacity' => true,
- 'opacity' => true,
- 'transform' => true,
- 'clip-path' => true,
- 'clip-rule' => true,
- 'mask' => true,
- );
-
- return array(
- 'svg' => array_merge( $common_attrs, array(
- 'xmlns' => true,
- 'xmlns:xlink' => true,
- 'viewbox' => true,
- 'width' => true,
- 'height' => true,
- 'aria-hidden' => true,
- 'role' => true,
- 'focusable' => true,
- 'preserveaspectratio' => true,
- 'version' => true,
- 'xml:space' => true,
- 'enable-background' => true,
- ) ),
- 'g' => $common_attrs,
- 'path' => array_merge( $common_attrs, array( 'd' => true ) ),
- 'circle' => array_merge( $common_attrs, array(
- 'cx' => true,
- 'cy' => true,
- 'r' => true,
- ) ),
- 'ellipse' => array_merge( $common_attrs, array(
- 'cx' => true,
- 'cy' => true,
- 'rx' => true,
- 'ry' => true,
- ) ),
- 'rect' => array_merge( $common_attrs, array(
- 'x' => true,
- 'y' => true,
- 'width' => true,
- 'height' => true,
- 'rx' => true,
- 'ry' => true,
- ) ),
- 'line' => array_merge( $common_attrs, array(
- 'x1' => true,
- 'y1' => true,
- 'x2' => true,
- 'y2' => true,
- ) ),
- 'polyline' => array_merge( $common_attrs, array( 'points' => true ) ),
- 'polygon' => array_merge( $common_attrs, array( 'points' => true ) ),
- 'defs' => array( 'id' => true ),
- 'clippath' => array( 'id' => true ),
- 'mask' => array( 'id' => true ),
- 'use' => array(
- 'xlink:href' => true,
- 'href' => true,
- 'x' => true,
- 'y' => true,
- 'width' => true,
- 'height' => true,
- ),
- 'symbol' => array(
- 'id' => true,
- 'viewbox' => true,
- 'preserveaspectratio' => true,
- ),
- 'title' => array(),
- 'desc' => array(),
- 'lineargradient' => array(
- 'id' => true,
- 'gradientunits' => true,
- 'gradienttransform' => true,
- 'spreadmethod' => true,
- 'x1' => true,
- 'y1' => true,
- 'x2' => true,
- 'y2' => true,
- ),
- 'radialgradient' => array(
- 'id' => true,
- 'gradientunits' => true,
- 'gradienttransform' => true,
- 'spreadmethod' => true,
- 'cx' => true,
- 'cy' => true,
- 'r' => true,
- 'fx' => true,
- 'fy' => true,
- ),
- 'stop' => array(
- 'offset' => true,
- 'stop-color' => true,
- 'stop-opacity' => true,
- 'style' => true,
- ),
- );
- }
-
- // -------------------------------------------------------------------------
- // PWA Field Renderers
- // -------------------------------------------------------------------------
-
- /**
- * Render a media upload field.
- *
- * @param string $name Input name attribute.
- * @param string $value Current URL value.
- * @param string $desc Description text.
- * @return void
- */
- private static function render_media_field( string $name, string $value, string $desc = '' ) : void {
- $id = 'func-media-' . \sanitize_key( str_replace( array( '[', ']' ), '-', $name ) );
- echo '';
- }
-
- /**
- * Render snippets repeater field for a given location.
- *
- * @since 1.4.0
- *
- * @param string $location Location key: header, body_open, or footer.
- * @param string $hook_name WordPress hook name for description.
- * @return void
- */
- public static function field_snippets_repeater( string $location, string $hook_name ) : void {
- $o = self::get_snippets_options();
- $items = ! empty( $o[ $location ] ) && \is_array( $o[ $location ] ) ? $o[ $location ] : array();
- $cid = 'func-snippets-' . $location;
-
- echo '';
- $i = 0;
- foreach ( $items as $item ) {
- self::render_snippet_row( $location, $i, $item );
- $i++;
- }
- echo '
';
-
- echo '';
- echo \esc_html__( '+ Add Snippet', 'functionalities' ) . ' ';
- echo '' . \sprintf(
- /* translators: %s: WordPress hook name */
- \esc_html__( 'Output in %s. Each snippet can be individually toggled.', 'functionalities' ),
- '' . \esc_html( $hook_name ) . ''
- ) . '
';
-
- // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Inline JS for repeater template, encoded via wp_json_encode.
- echo '';
- }
-
- /**
- * Render a single snippet repeater row.
- *
- * @since 1.4.0
- *
- * @param string $location Location key.
- * @param int $index Row index.
- * @param array $data Snippet data (label, code, enabled).
- * @return void
- */
- private static function render_snippet_row( string $location, int $index, array $data ) : void {
- $label = $data['label'] ?? '';
- $code = $data['code'] ?? '';
- $enabled = ! empty( $data['enabled'] );
- $prefix = 'functionalities_snippets[' . $location . '][' . $index . ']';
- $has_code = '' !== $code;
-
- echo '';
-
- // Header bar — always visible.
- echo '';
-
- // Body — collapsible.
- echo '
';
- echo '';
- echo '
';
-
- echo '
';
- }
-
- /**
- * Detect snippet type from code for badge display.
- *
- * @since 1.4.0
- *
- * @param string $code Snippet code.
- * @return string Badge label.
- */
- private static function snippet_type_badge( string $code ) : string {
- $code_lower = strtolower( ltrim( $code ) );
- if ( 0 === strpos( $code_lower, '
+
+
+
+
+
+
+
+
+
+
+ functionalities.dev'
+ );
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ›
+
+
+
+
+
+ array( __CLASS__, 'sanitize_link_management' ),
+ 'default' => array(
+ 'enabled' => false,
+ 'nofollow_external' => false,
+ 'exceptions' => '',
+ 'open_external_new_tab' => false,
+ 'open_internal_new_tab' => false,
+ 'internal_new_tab_exceptions' => '',
+ ),
+ )
+ );
+
+ \add_settings_section(
+ 'functionalities_link_management_section',
+ \__( 'Link Management Settings', 'functionalities' ),
+ array( __CLASS__, 'section_link_management' ),
+ 'functionalities_link_management'
+ );
+
+ \add_settings_field(
+ 'enabled',
+ \__( 'Enable Link Management', 'functionalities' ),
+ function () {
+ $o = self::get_link_management_options();
+ $checked = ! empty( $o['enabled'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Enable link management features', 'functionalities' ) . ' ';
+ },
+ 'functionalities_link_management',
+ 'functionalities_link_management_section'
+ );
+
+ \add_settings_field(
+ 'nofollow_external',
+ \__( 'Add nofollow to external links', 'functionalities' ),
+ array( __CLASS__, 'field_nofollow_external' ),
+ 'functionalities_link_management',
+ 'functionalities_link_management_section'
+ );
+
+ \add_settings_field(
+ 'exceptions',
+ \__( 'Exceptions', 'functionalities' ),
+ array( __CLASS__, 'field_exceptions' ),
+ 'functionalities_link_management',
+ 'functionalities_link_management_section'
+ );
+
+ // New tab options.
+ \add_settings_field(
+ 'open_external_new_tab',
+ \__( 'Open external links in new tab', 'functionalities' ),
+ array( __CLASS__, 'field_open_external_new_tab' ),
+ 'functionalities_link_management',
+ 'functionalities_link_management_section'
+ );
+ \add_settings_field(
+ 'open_internal_new_tab',
+ \__( 'Open internal links in new tab', 'functionalities' ),
+ array( __CLASS__, 'field_open_internal_new_tab' ),
+ 'functionalities_link_management',
+ 'functionalities_link_management_section'
+ );
+ \add_settings_field(
+ 'internal_new_tab_exceptions',
+ \__( 'Internal new-tab exceptions (domains)', 'functionalities' ),
+ array( __CLASS__, 'field_internal_new_tab_exceptions' ),
+ 'functionalities_link_management',
+ 'functionalities_link_management_section'
+ );
+
+ // Advanced features.
+ \add_settings_field(
+ 'json_preset_url',
+ \__( 'JSON Preset File Path', 'functionalities' ),
+ array( __CLASS__, 'field_json_preset_url' ),
+ 'functionalities_link_management',
+ 'functionalities_link_management_section'
+ );
+ \add_settings_field(
+ 'enable_developer_filters',
+ \__( 'Enable Developer Filters', 'functionalities' ),
+ array( __CLASS__, 'field_enable_developer_filters' ),
+ 'functionalities_link_management',
+ 'functionalities_link_management_section'
+ );
+ \add_settings_field(
+ 'database_update_tool',
+ \__( 'Database Update Tool', 'functionalities' ),
+ array( __CLASS__, 'field_database_update_tool' ),
+ 'functionalities_link_management',
+ 'functionalities_link_management_section'
+ );
+
+ // Block Cleanup settings
+ \register_setting(
+ 'functionalities_block_cleanup',
+ 'functionalities_block_cleanup',
+ array(
+ 'sanitize_callback' => array( __CLASS__, 'sanitize_block_cleanup' ),
+ 'default' => array(
+ 'enabled' => false,
+ 'remove_heading_block_class' => false,
+ 'remove_list_block_class' => false,
+ 'remove_image_block_class' => false,
+ 'remove_paragraph_block_class' => false,
+ 'remove_quote_block_class' => false,
+ 'remove_table_block_class' => false,
+ 'remove_separator_block_class' => false,
+ 'remove_group_block_class' => false,
+ 'remove_columns_block_class' => false,
+ 'remove_button_block_class' => false,
+ 'remove_cover_block_class' => false,
+ 'remove_media_text_block_class' => false,
+ 'custom_classes_to_remove' => '',
+ ),
+ )
+ );
+
+ \add_settings_section(
+ 'functionalities_block_cleanup_section',
+ \__( 'Frontend Block Class Cleanup', 'functionalities' ),
+ array( __CLASS__, 'section_block_cleanup' ),
+ 'functionalities_block_cleanup'
+ );
+
+ \add_settings_field(
+ 'enabled',
+ \__( 'Enable Block Cleanup', 'functionalities' ),
+ function () {
+ $o = self::get_block_cleanup_options();
+ $checked = ! empty( $o['enabled'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Enable block class cleanup on frontend', 'functionalities' ) . ' ';
+ },
+ 'functionalities_block_cleanup',
+ 'functionalities_block_cleanup_section'
+ );
+
+ \add_settings_field(
+ 'remove_heading_block_class',
+ \__( 'Headings', 'functionalities' ),
+ array( __CLASS__, 'field_bc_remove_heading' ),
+ 'functionalities_block_cleanup',
+ 'functionalities_block_cleanup_section'
+ );
+ \add_settings_field(
+ 'remove_list_block_class',
+ \__( 'Lists', 'functionalities' ),
+ array( __CLASS__, 'field_bc_remove_list' ),
+ 'functionalities_block_cleanup',
+ 'functionalities_block_cleanup_section'
+ );
+ \add_settings_field(
+ 'remove_image_block_class',
+ \__( 'Images', 'functionalities' ),
+ array( __CLASS__, 'field_bc_remove_image' ),
+ 'functionalities_block_cleanup',
+ 'functionalities_block_cleanup_section'
+ );
+ \add_settings_field(
+ 'remove_paragraph_block_class',
+ \__( 'Paragraphs', 'functionalities' ),
+ function () {
+ $opts = self::get_block_cleanup_options();
+ $checked = ! empty( $opts['remove_paragraph_block_class'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Remove "wp-block-paragraph" from paragraph elements', 'functionalities' ) . ' ';
+ },
+ 'functionalities_block_cleanup',
+ 'functionalities_block_cleanup_section'
+ );
+ \add_settings_field(
+ 'remove_quote_block_class',
+ \__( 'Quotes', 'functionalities' ),
+ function () {
+ $opts = self::get_block_cleanup_options();
+ $checked = ! empty( $opts['remove_quote_block_class'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Remove "wp-block-quote" from blockquote elements', 'functionalities' ) . ' ';
+ },
+ 'functionalities_block_cleanup',
+ 'functionalities_block_cleanup_section'
+ );
+ \add_settings_field(
+ 'remove_table_block_class',
+ \__( 'Tables', 'functionalities' ),
+ function () {
+ $opts = self::get_block_cleanup_options();
+ $checked = ! empty( $opts['remove_table_block_class'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Remove "wp-block-table" from table elements', 'functionalities' ) . ' ';
+ },
+ 'functionalities_block_cleanup',
+ 'functionalities_block_cleanup_section'
+ );
+ \add_settings_field(
+ 'remove_separator_block_class',
+ \__( 'Separators', 'functionalities' ),
+ function () {
+ $opts = self::get_block_cleanup_options();
+ $checked = ! empty( $opts['remove_separator_block_class'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Remove "wp-block-separator" from hr/separator elements', 'functionalities' ) . ' ';
+ },
+ 'functionalities_block_cleanup',
+ 'functionalities_block_cleanup_section'
+ );
+ \add_settings_field(
+ 'remove_group_block_class',
+ \__( 'Groups', 'functionalities' ),
+ function () {
+ $opts = self::get_block_cleanup_options();
+ $checked = ! empty( $opts['remove_group_block_class'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Remove "wp-block-group" from group containers', 'functionalities' ) . ' ';
+ },
+ 'functionalities_block_cleanup',
+ 'functionalities_block_cleanup_section'
+ );
+ \add_settings_field(
+ 'remove_columns_block_class',
+ \__( 'Columns', 'functionalities' ),
+ function () {
+ $opts = self::get_block_cleanup_options();
+ $checked = ! empty( $opts['remove_columns_block_class'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Remove "wp-block-columns" and "wp-block-column" from column layouts', 'functionalities' ) . ' ';
+ },
+ 'functionalities_block_cleanup',
+ 'functionalities_block_cleanup_section'
+ );
+ \add_settings_field(
+ 'remove_button_block_class',
+ \__( 'Buttons', 'functionalities' ),
+ function () {
+ $opts = self::get_block_cleanup_options();
+ $checked = ! empty( $opts['remove_button_block_class'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Remove "wp-block-button(s)" from button elements', 'functionalities' ) . ' ';
+ },
+ 'functionalities_block_cleanup',
+ 'functionalities_block_cleanup_section'
+ );
+ \add_settings_field(
+ 'remove_cover_block_class',
+ \__( 'Covers', 'functionalities' ),
+ function () {
+ $opts = self::get_block_cleanup_options();
+ $checked = ! empty( $opts['remove_cover_block_class'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Remove "wp-block-cover" from cover blocks', 'functionalities' ) . ' ';
+ },
+ 'functionalities_block_cleanup',
+ 'functionalities_block_cleanup_section'
+ );
+ \add_settings_field(
+ 'remove_media_text_block_class',
+ \__( 'Media & Text', 'functionalities' ),
+ function () {
+ $opts = self::get_block_cleanup_options();
+ $checked = ! empty( $opts['remove_media_text_block_class'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Remove "wp-block-media-text" from media-text blocks', 'functionalities' ) . ' ';
+ },
+ 'functionalities_block_cleanup',
+ 'functionalities_block_cleanup_section'
+ );
+ \add_settings_field(
+ 'custom_classes_to_remove',
+ \__( 'Custom Classes', 'functionalities' ),
+ function () {
+ $opts = self::get_block_cleanup_options();
+ $val = isset( $opts['custom_classes_to_remove'] ) ? $opts['custom_classes_to_remove'] : '';
+ echo '';
+ echo '' . \esc_html__( 'Enter additional CSS classes to remove from content output (one per line). Example: my-plugin-class', 'functionalities' ) . '
';
+ },
+ 'functionalities_block_cleanup',
+ 'functionalities_block_cleanup_section'
+ );
+
+ // Editor Link Suggestions settings
+ \register_setting(
+ 'functionalities_editor_links',
+ 'functionalities_editor_links',
+ array(
+ 'sanitize_callback' => array( __CLASS__, 'sanitize_editor_links' ),
+ 'default' => array(
+ 'enabled' => false,
+ 'enable_limit' => false,
+ 'post_types' => self::default_editor_link_post_types(),
+ ),
+ )
+ );
+
+ \add_settings_section(
+ 'functionalities_editor_links_section',
+ \__( 'Editor Link Suggestions', 'functionalities' ),
+ function () {
+ echo '' . \esc_html__( 'Control which post types appear in the block editor link search suggestions.', 'functionalities' ) . '
';
+
+ echo '';
+ echo '
' . \esc_html__( 'What This Module Does', 'functionalities' ) . ' ';
+ echo '
';
+ echo '' . \esc_html__( 'Limits link search results to specific post types when inserting links in Gutenberg', 'functionalities' ) . ' ';
+ echo '' . \esc_html__( 'Reduces clutter by hiding unwanted content types from search results', 'functionalities' ) . ' ';
+ echo '' . \esc_html__( 'Works with posts, pages, and custom post types', 'functionalities' ) . ' ';
+ echo ' ';
+ echo '
';
+
+ echo '';
+ echo '
' . \esc_html__( 'How to Use', 'functionalities' ) . ' ';
+ echo '
' . \esc_html__( 'Enable limitation, then check only the post types you want to appear when searching for links in the editor. Unchecked post types will be hidden from link search results.', 'functionalities' ) . '
';
+ echo '
';
+
+ echo '';
+ echo '
' . \esc_html__( 'For Developers', 'functionalities' ) . ' ';
+ echo '
';
+ echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_editor_links_enabled — ' . \esc_html__( 'toggle feature', 'functionalities' ) . ' ';
+ echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_editor_links_post_types — ' . \esc_html__( 'modify allowed post types', 'functionalities' );
+ echo '
';
+ echo '
';
+ },
+ 'functionalities_editor_links'
+ );
+
+ \add_settings_field(
+ 'enabled',
+ \__( 'Enable Editor Link Suggestions', 'functionalities' ),
+ function () {
+ $o = self::get_editor_links_options();
+ $checked = ! empty( $o['enabled'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Enable editor link suggestion filtering', 'functionalities' ) . ' ';
+ },
+ 'functionalities_editor_links',
+ 'functionalities_editor_links_section'
+ );
+
+ \add_settings_field(
+ 'enable_limit',
+ \__( 'Enable limitation', 'functionalities' ),
+ array( __CLASS__, 'field_el_enable' ),
+ 'functionalities_editor_links',
+ 'functionalities_editor_links_section'
+ );
+ \add_settings_field(
+ 'post_types',
+ \__( 'Allowed post types', 'functionalities' ),
+ array( __CLASS__, 'field_el_post_types' ),
+ 'functionalities_editor_links',
+ 'functionalities_editor_links_section'
+ );
+
+ // Header & Footer (Snippets) settings
+ \register_setting(
+ 'functionalities_snippets',
+ 'functionalities_snippets',
+ array(
+ 'sanitize_callback' => array( __CLASS__, 'sanitize_snippets' ),
+ 'default' => array(
+ 'enabled' => false,
+ 'enable_ga4' => false,
+ 'ga4_id' => '',
+ 'header' => array(),
+ 'body_open' => array(),
+ 'footer' => array(),
+ ),
+ )
+ );
+
+ \add_settings_section(
+ 'functionalities_snippets_section',
+ \__( 'Header & Footer Code', 'functionalities' ),
+ function () {
+ echo '' . \esc_html__( 'Insert custom code snippets into your site header and footer without editing theme files.', 'functionalities' ) . '
';
+
+ echo '';
+ echo '
' . \esc_html__( 'What This Module Does', 'functionalities' ) . ' ';
+ echo '
';
+ echo '' . \esc_html__( 'Native Google Analytics 4 integration - just enter your Measurement ID', 'functionalities' ) . ' ';
+ echo '' . \esc_html__( 'Multiple snippets per location — each independently toggleable', 'functionalities' ) . ' ';
+ echo '' . \esc_html__( 'Custom code for meta tags, scripts, styles, and tracking codes', 'functionalities' ) . ' ';
+ echo '' . \esc_html__( 'Automatically skips admin pages, feeds, and REST API requests', 'functionalities' ) . ' ';
+ echo ' ';
+ echo '
';
+
+ echo '';
+ echo '
' . \esc_html__( 'Allowed Tags', 'functionalities' ) . ' ';
+ echo '
';
+ echo '<script>, <style>, <link>, <meta>, <noscript>';
+ echo '
';
+ echo '
';
+
+ echo '';
+ echo '
' . \esc_html__( 'For Developers', 'functionalities' ) . ' ';
+ echo '
';
+ echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_snippets_output_enabled — ' . \esc_html__( 'disable on specific pages', 'functionalities' ) . ' ';
+ echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_snippets_ga4_enabled — ' . \esc_html__( 'control GA4 per user/page', 'functionalities' ) . ' ';
+ echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_snippets_header_code — ' . \esc_html__( 'modify header code', 'functionalities' ) . ' ';
+ echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_snippets_footer_code — ' . \esc_html__( 'modify footer code', 'functionalities' ) . ' ';
+ echo \esc_html__( 'Actions:', 'functionalities' ) . ' functionalities_before/after_header/footer_snippets';
+ echo '
';
+ echo '
';
+ },
+ 'functionalities_snippets'
+ );
+
+ \add_settings_field(
+ 'enabled',
+ \__( 'Enable Header & Footer', 'functionalities' ),
+ function () {
+ $o = self::get_snippets_options();
+ $checked = ! empty( $o['enabled'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Enable header and footer code injection', 'functionalities' ) . ' ';
+ },
+ 'functionalities_snippets',
+ 'functionalities_snippets_section'
+ );
+
+ \add_settings_field(
+ 'enable_ga4',
+ \__( 'Enable Google Analytics 4', 'functionalities' ),
+ function () {
+ $o = self::get_snippets_options();
+ $checked = ! empty( $o['enable_ga4'] ) ? 'checked' : '';
+ echo ' ' . \esc_html__( 'Insert GA4 gtag in head', 'functionalities' ) . ' ';
+ },
+ 'functionalities_snippets',
+ 'functionalities_snippets_section'
+ );
+ \add_settings_field(
+ 'ga4_id',
+ \__( 'GA4 Measurement ID', 'functionalities' ),
+ function () {
+ $o = self::get_snippets_options();
+ $val = isset( $o['ga4_id'] ) ? (string) $o['ga4_id'] : '';
+ echo ' ';
+ },
+ 'functionalities_snippets',
+ 'functionalities_snippets_section'
+ );
+ \add_settings_field(
+ 'header_snippets',
+ \__( 'Header Snippets', 'functionalities' ),
+ function () {
+ self::field_snippets_repeater( 'header', 'wp_head' );
+ },
+ 'functionalities_snippets',
+ 'functionalities_snippets_section'
+ );
+ \add_settings_field(
+ 'body_open_snippets',
+ \__( 'Body Open Snippets', 'functionalities' ),
+ function () {
+ self::field_snippets_repeater( 'body_open', 'wp_body_open' );
+ },
+ 'functionalities_snippets',
+ 'functionalities_snippets_section'
+ );
+ \add_settings_field(
+ 'footer_snippets',
+ \__( 'Footer Snippets', 'functionalities' ),
+ function () {
+ self::field_snippets_repeater( 'footer', 'wp_footer' );
+ },
+ 'functionalities_snippets',
+ 'functionalities_snippets_section'
+ );
+
+ // Schema settings
+ \register_setting(
+ 'functionalities_schema',
+ 'functionalities_schema',
+ array(
+ 'sanitize_callback' => array( __CLASS__, 'sanitize_schema' ),
+ 'default' => array(
+ 'enabled' => false,
+ 'enable_site_schema' => true,
+ 'site_itemtype' => 'WebPage',
+ 'enable_header_part' => true,
+ 'enable_footer_part' => true,
+ 'enable_article' => true,
+ 'article_itemtype' => 'Article',
+ 'add_headline' => true,
+ 'add_dates' => true,
+ 'add_author' => true,
+ 'enable_breadcrumbs' => false,
+ ),
+ )
+ );
+
+ \add_settings_section(
+ 'functionalities_schema_section',
+ \__( 'Schema Settings', 'functionalities' ),
+ function () {
+ echo '' . \esc_html__( 'Add Schema.org microdata attributes to improve search engine understanding of your content.', 'functionalities' ) . '
';
+
+ echo '';
+ echo '
' . \esc_html__( 'What This Module Does', 'functionalities' ) . ' ';
+ echo '
';
+ echo '' . \esc_html__( 'Adds itemscope/itemtype to the HTML element for page-level schema', 'functionalities' ) . ' ';
+ echo '' . \esc_html__( 'Wraps article content with Article/BlogPosting microdata', 'functionalities' ) . ' ';
+ echo '' . \esc_html__( 'Adds structured data for headlines, dates, and authors', 'functionalities' ) . ' ';
+ echo '' . \esc_html__( 'Marks header and footer regions with WPHeader/WPFooter types', 'functionalities' ) . ' ';
+ echo ' ';
+ echo '
';
+
+ echo '';
+ echo '
' . \esc_html__( 'Supported Types', 'functionalities' ) . ' ';
+ echo '
';
+ echo '' . \esc_html__( 'Site:', 'functionalities' ) . ' WebPage, AboutPage, ContactPage, Blog, SearchResultsPage ';
+ echo '' . \esc_html__( 'Article:', 'functionalities' ) . ' Article, BlogPosting, NewsArticle';
+ echo '
';
+ echo '
';
+
+ echo '';
+ echo '
' . \esc_html__( 'For Developers', 'functionalities' ) . ' ';
+ echo '
';
+ echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_schema_enabled — ' . \esc_html__( 'toggle all schema output', 'functionalities' ) . ' ';
+ echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_schema_site_type — ' . \esc_html__( 'modify site itemtype', 'functionalities' ) . ' ';
+ echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_schema_article_type — ' . \esc_html__( 'modify article itemtype', 'functionalities' ) . ' ';
+ echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_schema_content — ' . \esc_html__( 'modify wrapped content', 'functionalities' );
+ echo '
';
+ echo '
';
+ },
+ 'functionalities_schema'
+ );
+
+ \add_settings_field(
+ 'enabled',
+ \__( 'Enable Schema', 'functionalities' ),
+ function () {
+ $o = self::get_schema_options();
+ $checked = ! empty( $o['enabled'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Enable schema microdata output', 'functionalities' ) . ' ';
+ },
+ 'functionalities_schema',
+ 'functionalities_schema_section'
+ );
+
+ \add_settings_field(
+ 'enable_site_schema',
+ \__( 'Enable site schema (html tag)', 'functionalities' ),
+ function () {
+ $o = self::get_schema_options();
+ $checked = ! empty( $o['enable_site_schema'] ) ? 'checked' : '';
+ echo ' ' . \esc_html__( 'Add itemscope/itemtype to ', 'functionalities' ) . ' ';
+ },
+ 'functionalities_schema',
+ 'functionalities_schema_section'
+ );
+ \add_settings_field(
+ 'site_itemtype',
+ \__( 'Site itemtype', 'functionalities' ),
+ function () {
+ $o = self::get_schema_options();
+ $val = $o['site_itemtype'] ?? 'WebPage';
+ $opts = array( 'WebPage', 'AboutPage', 'ContactPage', 'Blog', 'SearchResultsPage' );
+ echo '';
+ foreach ( $opts as $opt ) {
+ $sel = selected( $val, $opt, false );
+ echo '' . esc_html( $opt ) . ' ';
+ }
+ echo ' ';
+ },
+ 'functionalities_schema',
+ 'functionalities_schema_section'
+ );
+ \add_settings_field(
+ 'enable_header_part',
+ \__( 'Add WPHeader microdata', 'functionalities' ),
+ function () {
+ $o = self::get_schema_options();
+ $checked = ! empty( $o['enable_header_part'] ) ? 'checked' : '';
+ echo ' ' . \esc_html__( 'Output a microdata hasPart for header', 'functionalities' ) . ' ';
+ },
+ 'functionalities_schema',
+ 'functionalities_schema_section'
+ );
+ \add_settings_field(
+ 'enable_footer_part',
+ \__( 'Add WPFooter microdata', 'functionalities' ),
+ function () {
+ $o = self::get_schema_options();
+ $checked = ! empty( $o['enable_footer_part'] ) ? 'checked' : '';
+ echo ' ' . \esc_html__( 'Output a microdata hasPart for footer', 'functionalities' ) . ' ';
+ },
+ 'functionalities_schema',
+ 'functionalities_schema_section'
+ );
+ \add_settings_field(
+ 'enable_article',
+ \__( 'Enable Article microdata in content', 'functionalities' ),
+ function () {
+ $o = self::get_schema_options();
+ $checked = ! empty( $o['enable_article'] ) ? 'checked' : '';
+ echo ' ' . \esc_html__( 'Wrap content with Article microdata on singular', 'functionalities' ) . ' ';
+ },
+ 'functionalities_schema',
+ 'functionalities_schema_section'
+ );
+ \add_settings_field(
+ 'article_itemtype',
+ \__( 'Article itemtype', 'functionalities' ),
+ function () {
+ $o = self::get_schema_options();
+ $val = $o['article_itemtype'] ?? 'Article';
+ $opts = array( 'Article', 'BlogPosting', 'NewsArticle' );
+ echo '';
+ foreach ( $opts as $opt ) {
+ $sel = selected( $val, $opt, false );
+ echo '' . esc_html( $opt ) . ' ';
+ }
+ echo ' ';
+ },
+ 'functionalities_schema',
+ 'functionalities_schema_section'
+ );
+ \add_settings_field(
+ 'add_headline',
+ \__( 'Add headline from first heading', 'functionalities' ),
+ function () {
+ $o = self::get_schema_options();
+ $checked = ! empty( $o['add_headline'] ) ? 'checked' : '';
+ echo ' ' . \esc_html__( 'Add itemprop="headline"', 'functionalities' ) . ' ';
+ },
+ 'functionalities_schema',
+ 'functionalities_schema_section'
+ );
+ \add_settings_field(
+ 'add_dates',
+ \__( 'Add published/modified dates', 'functionalities' ),
+ function () {
+ $o = self::get_schema_options();
+ $checked = ! empty( $o['add_dates'] ) ? 'checked' : '';
+ echo ' ' . \esc_html__( 'Add itemprop dates to time tags', 'functionalities' ) . ' ';
+ },
+ 'functionalities_schema',
+ 'functionalities_schema_section'
+ );
+ \add_settings_field(
+ 'add_author',
+ \__( 'Add author microdata', 'functionalities' ),
+ function () {
+ $o = self::get_schema_options();
+ $checked = ! empty( $o['add_author'] ) ? 'checked' : '';
+ echo ' ' . \esc_html__( 'Add itemprop="author" where possible', 'functionalities' ) . ' ';
+ },
+ 'functionalities_schema',
+ 'functionalities_schema_section'
+ );
+ \add_settings_field(
+ 'enable_breadcrumbs',
+ \__( 'Enable BreadcrumbList', 'functionalities' ),
+ function () {
+ $o = self::get_schema_options();
+ $checked = ! empty( $o['enable_breadcrumbs'] ) ? 'checked' : '';
+ echo ' ' . \esc_html__( 'Add BreadcrumbList JSON-LD to singular pages', 'functionalities' ) . ' ';
+ },
+ 'functionalities_schema',
+ 'functionalities_schema_section'
+ );
+
+ // Components settings
+ \register_setting(
+ 'functionalities_components',
+ 'functionalities_components',
+ array(
+ 'sanitize_callback' => array( __CLASS__, 'sanitize_components' ),
+ 'default' => array(
+ 'enabled' => false,
+ 'items' => self::default_components(),
+ ),
+ )
+ );
+
+ \add_settings_section(
+ 'functionalities_components_section',
+ \__( 'Components', 'functionalities' ),
+ function () {
+ echo '' . \esc_html__( 'Create reusable CSS components that are automatically loaded across your entire site.', 'functionalities' ) . '
';
+
+ echo '';
+ echo '
' . \esc_html__( 'What This Module Does', 'functionalities' ) . ' ';
+ echo '
';
+ echo '' . \esc_html__( 'Define CSS class names and their style rules in one place', 'functionalities' ) . ' ';
+ echo '' . \esc_html__( 'Components are compiled into a single CSS file for optimal caching', 'functionalities' ) . ' ';
+ echo '' . \esc_html__( 'Available on both frontend and admin pages', 'functionalities' ) . ' ';
+ echo '' . \esc_html__( 'Includes default utility components like visually-hidden, skip-link, and marquee', 'functionalities' ) . ' ';
+ echo ' ';
+ echo '
';
+
+ echo '';
+ echo '
' . \esc_html__( 'How to Use', 'functionalities' ) . ' ';
+ echo '
' . \esc_html__( 'Add components by entering a CSS selector (e.g., .my-button) and CSS rules (e.g., background: blue; color: white;). Use the grid below to manage your components.', 'functionalities' ) . '
';
+ echo '
';
+
+ echo '';
+ echo '
' . \esc_html__( 'For Developers', 'functionalities' ) . ' ';
+ echo '
';
+ echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_components_enabled — ' . \esc_html__( 'toggle output', 'functionalities' ) . ' ';
+ echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_components_items — ' . \esc_html__( 'add components dynamically', 'functionalities' ) . ' ';
+ echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_components_css — ' . \esc_html__( 'modify generated CSS', 'functionalities' ) . ' ';
+ echo \esc_html__( 'Action:', 'functionalities' ) . ' functionalities_components_updated — ' . \esc_html__( 'fires when CSS file regenerates', 'functionalities' );
+ echo '
';
+ echo '
';
+ },
+ 'functionalities_components'
+ );
+ \add_settings_field(
+ 'enabled',
+ \__( 'Enable components CSS', 'functionalities' ),
+ function () {
+ $o = self::get_components_options();
+ $checked = ! empty( $o['enabled'] ) ? 'checked' : '';
+ echo ' ' . \esc_html__( 'Output components CSS on frontend', 'functionalities' ) . ' ';
+ },
+ 'functionalities_components',
+ 'functionalities_components_section'
+ );
+ \add_settings_field(
+ 'items',
+ \__( 'Component list', 'functionalities' ),
+ array( __CLASS__, 'field_components_items' ),
+ 'functionalities_components',
+ 'functionalities_components_section'
+ );
+
+ // Miscellaneous (bloat control)
+ \register_setting(
+ 'functionalities_misc',
+ 'functionalities_misc',
+ array(
+ 'sanitize_callback' => array( __CLASS__, 'sanitize_misc' ),
+ 'default' => array(
+ 'enabled' => false,
+ 'disable_block_widgets' => false,
+ 'load_separate_core_block_assets' => false,
+ 'disable_emojis' => false,
+ 'disable_embeds' => false,
+ 'remove_rest_api_links_head' => false,
+ 'remove_rsd_wlw_shortlink' => false,
+ 'remove_generator_meta' => false,
+ 'disable_xmlrpc' => false,
+ 'disable_xmlrpc_pingbacks' => false,
+ 'disable_feeds' => false,
+ 'disable_gravatars' => false,
+ 'disable_self_pingbacks' => false,
+ 'remove_query_strings' => false,
+ 'remove_dns_prefetch' => false,
+ 'remove_recent_comments_css' => false,
+ 'limit_revisions' => false,
+ 'disable_dashicons_for_guests' => false,
+ 'disable_heartbeat' => false,
+ 'disable_admin_bar_front' => false,
+ 'remove_jquery_migrate' => false,
+ 'enable_prism_admin' => false,
+ 'enable_textarea_fullscreen' => false,
+ ),
+ )
+ );
+
+ \add_settings_section(
+ 'functionalities_misc_section',
+ \__( 'Miscellaneous (Bloat Control)', 'functionalities' ),
+ function () {
+ echo '' . \esc_html__( 'Remove unnecessary WordPress features to improve performance and security.', 'functionalities' ) . '
';
+
+ echo '';
+ echo '
' . \esc_html__( 'What This Module Does', 'functionalities' ) . ' ';
+ echo '
';
+ echo '' . \esc_html__( 'Remove bloat like emojis, oEmbeds, and unnecessary meta tags', 'functionalities' ) . ' ';
+ echo '' . \esc_html__( 'Disable security concerns like XML-RPC and version disclosure', 'functionalities' ) . ' ';
+ echo '' . \esc_html__( 'Improve performance by removing unused scripts and styles', 'functionalities' ) . ' ';
+ echo '' . \esc_html__( 'Add useful enhancements like PrismJS and fullscreen textareas', 'functionalities' ) . ' ';
+ echo ' ';
+ echo '
';
+
+ echo '';
+ echo '
' . \esc_html__( 'Caution', 'functionalities' ) . ' ';
+ echo '
' . \esc_html__( 'Some options may break functionality if plugins depend on them. Test after enabling. Disable Heartbeat API with care if you use auto-save or real-time features.', 'functionalities' ) . '
';
+ echo '
';
+
+ echo '';
+ echo '
' . \esc_html__( 'For Developers', 'functionalities' ) . ' ';
+ echo '
';
+ echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_misc_options — ' . \esc_html__( 'modify options before application', 'functionalities' ) . ' ';
+ echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_misc_disable_emojis — ' . \esc_html__( 'control emoji removal', 'functionalities' ) . ' ';
+ echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_misc_disable_embeds — ' . \esc_html__( 'control embed removal', 'functionalities' );
+ echo '
';
+ echo '
';
+ },
+ 'functionalities_misc'
+ );
+
+ \add_settings_field(
+ 'enabled',
+ \__( 'Enable Performance & Cleanup', 'functionalities' ),
+ function () {
+ $o = self::get_misc_options();
+ $checked = ! empty( $o['enabled'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Enable performance and cleanup features', 'functionalities' ) . ' ';
+ },
+ 'functionalities_misc',
+ 'functionalities_misc_section'
+ );
+
+ self::add_misc_field( 'disable_block_widgets', \__( 'Disable block-based widget editor (use classic widgets)', 'functionalities' ) );
+ self::add_misc_field( 'load_separate_core_block_assets', \__( 'Load core block styles separately (per-block CSS)', 'functionalities' ) );
+ self::add_misc_field( 'disable_emojis', \__( 'Disable emojis scripts/styles', 'functionalities' ) );
+ self::add_misc_field( 'disable_embeds', \__( 'Disable oEmbed scripts and endpoints', 'functionalities' ) );
+ self::add_misc_field( 'remove_rest_api_links_head', \__( 'Remove REST API and oEmbed discovery links from ', 'functionalities' ) );
+ self::add_misc_field( 'remove_rsd_wlw_shortlink', \__( 'Remove RSD, WLWManifest, and shortlink tags', 'functionalities' ) );
+ self::add_misc_field( 'remove_generator_meta', \__( 'Remove WordPress version meta (generator)', 'functionalities' ) );
+ self::add_misc_field( 'disable_xmlrpc', \__( 'Disable XML-RPC (complete)', 'functionalities' ) );
+ self::add_misc_field( 'disable_xmlrpc_pingbacks', \__( 'Disable only XML-RPC Pingbacks', 'functionalities' ) );
+ self::add_misc_field( 'disable_feeds', \__( 'Disable RSS/Atom feeds (redirect to homepage)', 'functionalities' ) );
+ self::add_misc_field( 'disable_gravatars', \__( 'Disable Gravatars (site-wide)', 'functionalities' ) );
+ self::add_misc_field( 'disable_self_pingbacks', \__( 'Disable self-pingbacks (pings to own site)', 'functionalities' ) );
+ self::add_misc_field( 'remove_query_strings', \__( 'Remove query strings from static resources (?ver=)', 'functionalities' ) );
+ self::add_misc_field( 'remove_dns_prefetch', \__( 'Remove DNS prefetch links from ', 'functionalities' ) );
+ self::add_misc_field( 'remove_recent_comments_css', \__( 'Remove Recent Comments inline CSS', 'functionalities' ) );
+ self::add_misc_field( 'limit_revisions', \__( 'Limit post revisions to 10', 'functionalities' ) );
+ self::add_misc_field( 'disable_dashicons_for_guests', \__( 'Disable Dashicons on frontend for non-logged-in users', 'functionalities' ) );
+ self::add_misc_field( 'disable_heartbeat', \__( 'Disable Heartbeat API', 'functionalities' ) );
+ self::add_misc_field( 'disable_admin_bar_front', \__( 'Disable admin bar on the frontend', 'functionalities' ) );
+ self::add_misc_field( 'remove_jquery_migrate', \__( 'Remove jQuery Migrate from frontend', 'functionalities' ) );
+ self::add_misc_field( 'enable_prism_admin', \__( 'Load PrismJS on admin screens (code highlighting where applicable)', 'functionalities' ) );
+ self::add_misc_field( 'enable_textarea_fullscreen', \__( 'Enable fullscreen toggle for all backend textareas', 'functionalities' ) );
+
+ // Fonts settings
+ \register_setting(
+ 'functionalities_fonts',
+ 'functionalities_fonts',
+ array(
+ 'sanitize_callback' => array( __CLASS__, 'sanitize_fonts' ),
+ 'default' => array(
+ 'enabled' => false,
+ 'items' => array(),
+ ),
+ )
+ );
+ \add_settings_section(
+ 'functionalities_fonts_section',
+ \__( 'Font Families', 'functionalities' ),
+ function () {
+ echo '' . \esc_html__( 'Self-host custom fonts with automatic @font-face CSS generation.', 'functionalities' ) . '
';
+
+ echo '';
+ echo '
' . \esc_html__( 'What This Module Does', 'functionalities' ) . ' ';
+ echo '
';
+ echo '' . \esc_html__( 'Generate @font-face CSS rules for self-hosted fonts', 'functionalities' ) . ' ';
+ echo '' . \esc_html__( 'Support for variable fonts with weight ranges (e.g., 100 900)', 'functionalities' ) . ' ';
+ echo '' . \esc_html__( 'WOFF2 format for modern browsers, optional WOFF fallback', 'functionalities' ) . ' ';
+ echo '' . \esc_html__( 'Configurable font-display strategy (swap, auto, block, etc.)', 'functionalities' ) . ' ';
+ echo ' ';
+ echo '
';
+
+ echo '';
+ echo '
' . \esc_html__( 'How to Use', 'functionalities' ) . ' ';
+ echo '
' . \esc_html__( 'Upload font files to your media library or server, then add font entries below with the family name and file URLs. Use the generated font-family name in your CSS.', 'functionalities' ) . '
';
+ echo '
';
+
+ echo '';
+ echo '
' . \esc_html__( 'For Developers', 'functionalities' ) . ' ';
+ echo '
';
+ echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_fonts_enabled — ' . \esc_html__( 'toggle output', 'functionalities' ) . ' ';
+ echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_fonts_items — ' . \esc_html__( 'add fonts dynamically', 'functionalities' ) . ' ';
+ echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_fonts_css — ' . \esc_html__( 'modify generated CSS', 'functionalities' ) . ' ';
+ echo \esc_html__( 'Action:', 'functionalities' ) . ' functionalities_fonts_before_output';
+ echo '
';
+ echo '
';
+ },
+ 'functionalities_fonts'
+ );
+ \add_settings_field(
+ 'fonts_enabled',
+ \__( 'Enable fonts output', 'functionalities' ),
+ function () {
+ $o = self::get_fonts_options();
+ $checked = ! empty( $o['enabled'] ) ? 'checked' : '';
+ echo ' ' . \esc_html__( 'Output @font-face CSS across site (front and admin)', 'functionalities' ) . ' ';
+ },
+ 'functionalities_fonts',
+ 'functionalities_fonts_section'
+ );
+ \add_settings_field(
+ 'fonts_items',
+ \__( 'Families', 'functionalities' ),
+ array( __CLASS__, 'field_fonts_items' ),
+ 'functionalities_fonts',
+ 'functionalities_fonts_section'
+ );
+ \add_settings_field(
+ 'fonts_assignments',
+ \__( 'Typography Assignments', 'functionalities' ),
+ array( __CLASS__, 'field_fonts_assignments' ),
+ 'functionalities_fonts',
+ 'functionalities_fonts_section'
+ );
+
+ // Login Security settings.
+ \register_setting(
+ 'functionalities_login_security',
+ 'functionalities_login_security',
+ array(
+ 'sanitize_callback' => array( __CLASS__, 'sanitize_login_security' ),
+ 'default' => array(
+ 'enabled' => false,
+ 'limit_login_attempts' => true,
+ 'max_attempts' => 5,
+ 'lockout_duration' => 15,
+ 'disable_xmlrpc_auth' => true,
+ 'disable_application_passwords' => false,
+ 'hide_login_errors' => true,
+ 'trust_proxy_headers' => false,
+ 'custom_logo_url' => '',
+ 'custom_background_color' => '',
+ 'custom_form_background' => '',
+ ),
+ )
+ );
+ \add_settings_section(
+ 'functionalities_login_security_section',
+ \__( 'Login Security Settings', 'functionalities' ),
+ function () {
+ echo '' . \esc_html__( 'Protect your login page and customize its appearance.', 'functionalities' ) . '
';
+ echo '';
+ echo '
' . \esc_html__( 'Security Features', 'functionalities' ) . ' ';
+ echo '
';
+ echo '' . \esc_html__( 'Limit failed login attempts to prevent brute force attacks', 'functionalities' ) . ' ';
+ echo '' . \esc_html__( 'Disable XML-RPC authentication to block remote login attacks', 'functionalities' ) . ' ';
+ echo '' . \esc_html__( 'Hide specific login errors to prevent username enumeration', 'functionalities' ) . ' ';
+ echo '' . \esc_html__( 'Customize the login page with your logo and colors', 'functionalities' ) . ' ';
+ echo ' ';
+ echo '
';
+ $logs = \Functionalities\Features\Login_Security::get_lockout_log( 5 );
+ if ( ! empty( $logs ) ) {
+ echo '';
+ echo '
' . \esc_html__( 'Recent Lockouts', 'functionalities' ) . ' ';
+ echo '
';
+ foreach ( $logs as $log ) {
+ echo '' . \esc_html( $log['ip'] ) . ' — ' . \esc_html( $log['username'] ) . ' (' . \esc_html( $log['time'] ) . ') ';
+ }
+ echo ' ';
+ echo '
';
+ }
+ },
+ 'functionalities_login_security'
+ );
+ \add_settings_field(
+ 'login_enabled',
+ \__( 'Enable Login Security', 'functionalities' ),
+ function () {
+ $o = self::get_login_security_options();
+ echo ' ' . \esc_html__( 'Enable login security features', 'functionalities' ) . ' ';
+ },
+ 'functionalities_login_security',
+ 'functionalities_login_security_section'
+ );
+ \add_settings_field(
+ 'limit_login_attempts',
+ \__( 'Limit Login Attempts', 'functionalities' ),
+ function () {
+ $o = self::get_login_security_options();
+ echo ' ' . \esc_html__( 'Block IPs after too many failed attempts', 'functionalities' ) . ' ';
+ },
+ 'functionalities_login_security',
+ 'functionalities_login_security_section'
+ );
+ \add_settings_field(
+ 'max_attempts',
+ \__( 'Max Attempts', 'functionalities' ),
+ function () {
+ $o = self::get_login_security_options();
+ echo ' ' . \esc_html__( 'failed attempts before lockout', 'functionalities' );
+ },
+ 'functionalities_login_security',
+ 'functionalities_login_security_section'
+ );
+ \add_settings_field(
+ 'lockout_duration',
+ \__( 'Lockout Duration', 'functionalities' ),
+ function () {
+ $o = self::get_login_security_options();
+ echo ' ' . \esc_html__( 'minutes', 'functionalities' );
+ },
+ 'functionalities_login_security',
+ 'functionalities_login_security_section'
+ );
+ \add_settings_field(
+ 'disable_xmlrpc_auth',
+ \__( 'Disable XML-RPC', 'functionalities' ),
+ function () {
+ $o = self::get_login_security_options();
+ echo ' ' . \esc_html__( 'Disable XML-RPC authentication (recommended)', 'functionalities' ) . ' ';
+ },
+ 'functionalities_login_security',
+ 'functionalities_login_security_section'
+ );
+ \add_settings_field(
+ 'disable_application_passwords',
+ \__( 'Disable App Passwords', 'functionalities' ),
+ function () {
+ $o = self::get_login_security_options();
+ echo ' ' . \esc_html__( 'Disable WordPress Application Passwords', 'functionalities' ) . ' ';
+ },
+ 'functionalities_login_security',
+ 'functionalities_login_security_section'
+ );
+ \add_settings_field(
+ 'hide_login_errors',
+ \__( 'Hide Login Errors', 'functionalities' ),
+ function () {
+ $o = self::get_login_security_options();
+ echo ' ' . \esc_html__( 'Show generic error instead of specific username/password errors', 'functionalities' ) . ' ';
+ },
+ 'functionalities_login_security',
+ 'functionalities_login_security_section'
+ );
+ \add_settings_field(
+ 'trust_proxy_headers',
+ \__( 'Trust Proxy Headers', 'functionalities' ),
+ function () {
+ $o = self::get_login_security_options();
+ echo ' ' . \esc_html__( 'Read client IP from X-Forwarded-For / Client-IP headers', 'functionalities' ) . ' ';
+ echo '' . \esc_html__( 'Only enable when this site sits behind a trusted reverse proxy or CDN (Cloudflare, nginx, etc.). With this OFF, lockouts are keyed by REMOTE_ADDR and cannot be spoofed via headers — the secure default.', 'functionalities' ) . '
';
+ },
+ 'functionalities_login_security',
+ 'functionalities_login_security_section'
+ );
+ \add_settings_field(
+ 'custom_logo_url',
+ \__( 'Custom Logo URL', 'functionalities' ),
+ function () {
+ $o = self::get_login_security_options();
+ echo ' ';
+ },
+ 'functionalities_login_security',
+ 'functionalities_login_security_section'
+ );
+ \add_settings_field(
+ 'custom_background_color',
+ \__( 'Background Color', 'functionalities' ),
+ function () {
+ $o = self::get_login_security_options();
+ echo ' ';
+ },
+ 'functionalities_login_security',
+ 'functionalities_login_security_section'
+ );
+
+ // Meta & Copyright settings.
+ \register_setting(
+ 'functionalities_meta',
+ 'functionalities_meta',
+ array(
+ 'sanitize_callback' => array( __CLASS__, 'sanitize_meta' ),
+ 'default' => array(
+ 'enabled' => false,
+ 'enable_copyright_meta' => true,
+ 'enable_dublin_core' => true,
+ 'enable_license_metabox' => true,
+ 'enable_schema_integration' => true,
+ 'default_license' => 'all-rights-reserved',
+ 'default_license_url' => '',
+ 'post_types' => array( 'post' ),
+ 'copyright_holder_type' => 'author',
+ 'custom_copyright_holder' => '',
+ 'dc_language' => '',
+ ),
+ )
+ );
+
+ \add_settings_section(
+ 'functionalities_meta_section',
+ \__( 'Meta & Copyright Settings', 'functionalities' ),
+ array( __CLASS__, 'section_meta' ),
+ 'functionalities_meta'
+ );
+
+ \add_settings_field(
+ 'meta_enabled',
+ \__( 'Enable Meta Module', 'functionalities' ),
+ function () {
+ $o = self::get_meta_options();
+ $checked = ! empty( $o['enabled'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Enable copyright, Dublin Core, and licensing features', 'functionalities' ) . ' ';
+ },
+ 'functionalities_meta',
+ 'functionalities_meta_section'
+ );
+
+ \add_settings_field(
+ 'enable_copyright_meta',
+ \__( 'Copyright Meta Tags', 'functionalities' ),
+ function () {
+ $o = self::get_meta_options();
+ $checked = ! empty( $o['enable_copyright_meta'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Output copyright, author, owner, and rights meta tags', 'functionalities' ) . ' ';
+ },
+ 'functionalities_meta',
+ 'functionalities_meta_section'
+ );
+
+ \add_settings_field(
+ 'enable_dublin_core',
+ \__( 'Dublin Core (DCMI) Metadata', 'functionalities' ),
+ function () {
+ $o = self::get_meta_options();
+ $checked = ! empty( $o['enable_dublin_core'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Output Dublin Core metadata (DC.title, DC.creator, DC.rights, etc.)', 'functionalities' ) . ' ';
+ },
+ 'functionalities_meta',
+ 'functionalities_meta_section'
+ );
+
+ \add_settings_field(
+ 'enable_license_metabox',
+ \__( 'License Metabox', 'functionalities' ),
+ function () {
+ $o = self::get_meta_options();
+ $checked = ! empty( $o['enable_license_metabox'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Show license selection metabox in post editor', 'functionalities' ) . ' ';
+ },
+ 'functionalities_meta',
+ 'functionalities_meta_section'
+ );
+
+ \add_settings_field(
+ 'enable_schema_integration',
+ \__( 'SEO Plugin Schema Integration', 'functionalities' ),
+ function () {
+ $o = self::get_meta_options();
+ $checked = ! empty( $o['enable_schema_integration'] ) ? 'checked' : '';
+ $detected = \Functionalities\Features\Meta::detect_seo_plugin();
+ $plugin_names = array(
+ 'rank-math' => 'Rank Math',
+ 'yoast' => 'Yoast SEO',
+ 'seo-framework' => 'The SEO Framework',
+ 'seopress' => 'SEOPress',
+ 'aioseo' => 'All in One SEO',
+ 'none' => \__( 'None detected', 'functionalities' ),
+ );
+ echo ' ';
+ echo \esc_html__( 'Add copyright data to SEO plugin Schema.org output', 'functionalities' ) . ' ';
+ echo '';
+ if ( $detected !== 'none' ) {
+ echo '✓ ' . \esc_html__( 'Detected:', 'functionalities' ) . ' ' . \esc_html( $plugin_names[ $detected ] ) . ' ';
+ } else {
+ echo \esc_html__( 'Supports: Rank Math, Yoast SEO, The SEO Framework, SEOPress, AIOSEO', 'functionalities' );
+ }
+ echo '
';
+ },
+ 'functionalities_meta',
+ 'functionalities_meta_section'
+ );
+
+ \add_settings_field(
+ 'default_license',
+ \__( 'Default License', 'functionalities' ),
+ function () {
+ $o = self::get_meta_options();
+ $val = isset( $o['default_license'] ) ? (string) $o['default_license'] : 'all-rights-reserved';
+ $licenses = array(
+ 'all-rights-reserved' => \__( 'All Rights Reserved', 'functionalities' ),
+ 'cc-by' => 'CC BY 4.0',
+ 'cc-by-sa' => 'CC BY-SA 4.0',
+ 'cc-by-nc' => 'CC BY-NC 4.0',
+ 'cc-by-nc-sa' => 'CC BY-NC-SA 4.0',
+ 'cc-by-nd' => 'CC BY-ND 4.0',
+ 'cc-by-nc-nd' => 'CC BY-NC-ND 4.0',
+ 'cc0' => 'CC0 1.0 (Public Domain)',
+ );
+ echo '';
+ foreach ( $licenses as $key => $label ) {
+ $sel = selected( $val, $key, false );
+ echo '' . \esc_html( $label ) . ' ';
+ }
+ echo ' ';
+ echo '' . \esc_html__( 'Default license for new posts (can be overridden per-post).', 'functionalities' ) . '
';
+ },
+ 'functionalities_meta',
+ 'functionalities_meta_section'
+ );
+
+ \add_settings_field(
+ 'default_license_url',
+ \__( 'Custom License URL', 'functionalities' ),
+ function () {
+ $o = self::get_meta_options();
+ $val = isset( $o['default_license_url'] ) ? (string) $o['default_license_url'] : '';
+ echo ' ';
+ echo '' . \esc_html__( 'Custom URL for "All Rights Reserved" license (e.g., your terms/disclaimer page).', 'functionalities' ) . '
';
+ },
+ 'functionalities_meta',
+ 'functionalities_meta_section'
+ );
+
+ \add_settings_field(
+ 'meta_post_types',
+ \__( 'Post Types', 'functionalities' ),
+ function () {
+ $o = self::get_meta_options();
+ $selected = isset( $o['post_types'] ) && is_array( $o['post_types'] ) ? $o['post_types'] : array( 'post' );
+ $pts = \get_post_types( array( 'public' => true ), 'objects' );
+ echo '';
+ foreach ( $pts as $name => $obj ) {
+ $is_checked = in_array( $name, $selected, true ) ? 'checked' : '';
+ $label = sprintf( '%s (%s)', $obj->labels->singular_name ?? $name, $name );
+ echo ' ' . \esc_html( $label ) . ' ';
+ }
+ echo ' ';
+ echo '' . \esc_html__( 'Select post types where meta tags and license metabox should appear.', 'functionalities' ) . '
';
+ },
+ 'functionalities_meta',
+ 'functionalities_meta_section'
+ );
+
+ \add_settings_field(
+ 'copyright_holder_type',
+ \__( 'Copyright Holder', 'functionalities' ),
+ function () {
+ $o = self::get_meta_options();
+ $val = isset( $o['copyright_holder_type'] ) ? (string) $o['copyright_holder_type'] : 'author';
+ $options = array(
+ 'author' => \__( 'Post Author', 'functionalities' ),
+ 'site' => \__( 'Site Name', 'functionalities' ),
+ 'custom' => \__( 'Custom Name', 'functionalities' ),
+ );
+ echo '';
+ foreach ( $options as $key => $label ) {
+ $sel = selected( $val, $key, false );
+ echo '' . \esc_html( $label ) . ' ';
+ }
+ echo ' ';
+ echo '' . \esc_html__( 'Who should be listed as the copyright holder.', 'functionalities' ) . '
';
+ },
+ 'functionalities_meta',
+ 'functionalities_meta_section'
+ );
+
+ \add_settings_field(
+ 'custom_copyright_holder',
+ \__( 'Custom Copyright Holder Name', 'functionalities' ),
+ function () {
+ $o = self::get_meta_options();
+ $val = isset( $o['custom_copyright_holder'] ) ? (string) $o['custom_copyright_holder'] : '';
+ echo ' ';
+ echo '' . \esc_html__( 'Used when "Custom Name" is selected above.', 'functionalities' ) . '
';
+ },
+ 'functionalities_meta',
+ 'functionalities_meta_section'
+ );
+
+ \add_settings_field(
+ 'dc_language',
+ \__( 'Dublin Core Language', 'functionalities' ),
+ function () {
+ $o = self::get_meta_options();
+ $val = isset( $o['dc_language'] ) ? (string) $o['dc_language'] : '';
+ $site_lang = \get_bloginfo( 'language' );
+ echo ' ';
+ echo '' . \esc_html__( 'Leave empty to use site language. Use ISO 639 codes (en, en-US, de, etc.).', 'functionalities' ) . '
';
+ },
+ 'functionalities_meta',
+ 'functionalities_meta_section'
+ );
+
+ // Content Regression Detection settings.
+ \register_setting(
+ 'functionalities_content_regression',
+ 'functionalities_content_regression',
+ array(
+ 'sanitize_callback' => array( __CLASS__, 'sanitize_content_regression' ),
+ 'default' => array(
+ 'enabled' => false,
+ 'post_types' => array( 'post', 'page' ),
+ 'link_drop_enabled' => true,
+ 'link_drop_percent' => 30,
+ 'link_drop_absolute' => 3,
+ 'exclude_nofollow_links' => false,
+ 'word_count_enabled' => true,
+ 'word_count_drop_percent' => 35,
+ 'word_count_min_age_days' => 30,
+ 'word_count_compare_average' => false,
+ 'exclude_shortcodes' => false,
+ 'heading_enabled' => true,
+ 'detect_missing_h1' => true,
+ 'detect_multiple_h1' => true,
+ 'detect_skipped_levels' => true,
+ 'snapshot_rolling_count' => 5,
+ 'show_post_column' => true,
+ ),
+ )
+ );
+
+ \add_settings_section(
+ 'functionalities_content_regression_section',
+ \__( 'Content Integrity Settings', 'functionalities' ),
+ array( __CLASS__, 'section_content_regression' ),
+ 'functionalities_content_regression'
+ );
+
+ // Enable module.
+ \add_settings_field(
+ 'regression_enabled',
+ \__( 'Enable Content Integrity', 'functionalities' ),
+ function () {
+ $o = self::get_content_regression_options();
+ $checked = ! empty( $o['enabled'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Detect structural regressions when posts are updated', 'functionalities' ) . ' ';
+ },
+ 'functionalities_content_regression',
+ 'functionalities_content_regression_section'
+ );
+
+ // Post types.
+ \add_settings_field(
+ 'regression_post_types',
+ \__( 'Post Types', 'functionalities' ),
+ function () {
+ $o = self::get_content_regression_options();
+ $selected = isset( $o['post_types'] ) && is_array( $o['post_types'] ) ? $o['post_types'] : array( 'post', 'page' );
+ $pts = \get_post_types( array( 'public' => true ), 'objects' );
+ echo '';
+ foreach ( $pts as $name => $obj ) {
+ if ( 'attachment' === $name ) {
+ continue;
+ }
+ $is_checked = in_array( $name, $selected, true ) ? 'checked' : '';
+ $label = sprintf( '%s (%s)', $obj->labels->singular_name ?? $name, $name );
+ echo ' ' . \esc_html( $label ) . ' ';
+ }
+ echo ' ';
+ },
+ 'functionalities_content_regression',
+ 'functionalities_content_regression_section'
+ );
+
+ // Link drop detection header.
+ \add_settings_field(
+ 'link_drop_header',
+ '' . \__( 'Internal Link Detection', 'functionalities' ) . ' ',
+ function () {
+ echo '' . \esc_html__( 'Detect when internal links are removed from content.', 'functionalities' ) . '
';
+ },
+ 'functionalities_content_regression',
+ 'functionalities_content_regression_section'
+ );
+
+ \add_settings_field(
+ 'link_drop_enabled',
+ \__( 'Enable Link Detection', 'functionalities' ),
+ function () {
+ $o = self::get_content_regression_options();
+ $checked = ! empty( $o['link_drop_enabled'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Warn when internal links drop significantly', 'functionalities' ) . ' ';
+ },
+ 'functionalities_content_regression',
+ 'functionalities_content_regression_section'
+ );
+
+ \add_settings_field(
+ 'link_drop_percent',
+ \__( 'Link Drop Threshold (%)', 'functionalities' ),
+ function () {
+ $o = self::get_content_regression_options();
+ $val = isset( $o['link_drop_percent'] ) ? (int) $o['link_drop_percent'] : 30;
+ echo ' %';
+ echo '' . \esc_html__( 'Warn if internal links drop by this percentage or more.', 'functionalities' ) . '
';
+ },
+ 'functionalities_content_regression',
+ 'functionalities_content_regression_section'
+ );
+
+ \add_settings_field(
+ 'link_drop_absolute',
+ \__( 'Link Drop Absolute Threshold', 'functionalities' ),
+ function () {
+ $o = self::get_content_regression_options();
+ $val = isset( $o['link_drop_absolute'] ) ? (int) $o['link_drop_absolute'] : 3;
+ echo ' ' . \esc_html__( 'links', 'functionalities' );
+ echo '' . \esc_html__( 'Also warn if this many links are removed (whichever triggers first).', 'functionalities' ) . '
';
+ },
+ 'functionalities_content_regression',
+ 'functionalities_content_regression_section'
+ );
+
+ \add_settings_field(
+ 'exclude_nofollow_links',
+ \__( 'Exclude Nofollow Links', 'functionalities' ),
+ function () {
+ $o = self::get_content_regression_options();
+ $checked = ! empty( $o['exclude_nofollow_links'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Do not count links with rel="nofollow"', 'functionalities' ) . ' ';
+ },
+ 'functionalities_content_regression',
+ 'functionalities_content_regression_section'
+ );
+
+ // Word count detection header.
+ \add_settings_field(
+ 'word_count_header',
+ '' . \__( 'Word Count Detection', 'functionalities' ) . ' ',
+ function () {
+ echo '' . \esc_html__( 'Detect when content is shortened significantly.', 'functionalities' ) . '
';
+ },
+ 'functionalities_content_regression',
+ 'functionalities_content_regression_section'
+ );
+
+ \add_settings_field(
+ 'word_count_enabled',
+ \__( 'Enable Word Count Detection', 'functionalities' ),
+ function () {
+ $o = self::get_content_regression_options();
+ $checked = ! empty( $o['word_count_enabled'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Warn when word count drops significantly', 'functionalities' ) . ' ';
+ },
+ 'functionalities_content_regression',
+ 'functionalities_content_regression_section'
+ );
+
+ \add_settings_field(
+ 'word_count_drop_percent',
+ \__( 'Word Count Drop Threshold (%)', 'functionalities' ),
+ function () {
+ $o = self::get_content_regression_options();
+ $val = isset( $o['word_count_drop_percent'] ) ? (int) $o['word_count_drop_percent'] : 35;
+ echo ' %';
+ echo '' . \esc_html__( 'Warn if word count drops by this percentage.', 'functionalities' ) . '
';
+ },
+ 'functionalities_content_regression',
+ 'functionalities_content_regression_section'
+ );
+
+ \add_settings_field(
+ 'word_count_min_age_days',
+ \__( 'Minimum Post Age (days)', 'functionalities' ),
+ function () {
+ $o = self::get_content_regression_options();
+ $val = isset( $o['word_count_min_age_days'] ) ? (int) $o['word_count_min_age_days'] : 30;
+ echo ' ' . \esc_html__( 'days', 'functionalities' );
+ echo '' . \esc_html__( 'Only check word count for posts older than this (to avoid alerts on new posts).', 'functionalities' ) . '
';
+ },
+ 'functionalities_content_regression',
+ 'functionalities_content_regression_section'
+ );
+
+ \add_settings_field(
+ 'exclude_shortcodes',
+ \__( 'Exclude Shortcodes', 'functionalities' ),
+ function () {
+ $o = self::get_content_regression_options();
+ $checked = ! empty( $o['exclude_shortcodes'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Remove shortcode content from word count', 'functionalities' ) . ' ';
+ },
+ 'functionalities_content_regression',
+ 'functionalities_content_regression_section'
+ );
+
+ // Heading detection header.
+ \add_settings_field(
+ 'heading_header',
+ '' . \__( 'Heading Structure Detection', 'functionalities' ) . ' ',
+ function () {
+ echo '' . \esc_html__( 'Detect accessibility issues with heading hierarchy.', 'functionalities' ) . '
';
+ },
+ 'functionalities_content_regression',
+ 'functionalities_content_regression_section'
+ );
+
+ \add_settings_field(
+ 'heading_enabled',
+ \__( 'Enable Heading Detection', 'functionalities' ),
+ function () {
+ $o = self::get_content_regression_options();
+ $checked = ! empty( $o['heading_enabled'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Check heading structure for accessibility issues', 'functionalities' ) . ' ';
+ },
+ 'functionalities_content_regression',
+ 'functionalities_content_regression_section'
+ );
+
+ \add_settings_field(
+ 'detect_missing_h1',
+ \__( 'Detect Missing H1', 'functionalities' ),
+ function () {
+ $o = self::get_content_regression_options();
+ $checked = ! empty( $o['detect_missing_h1'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Warn when no H1 heading is present', 'functionalities' ) . ' ';
+ },
+ 'functionalities_content_regression',
+ 'functionalities_content_regression_section'
+ );
+
+ \add_settings_field(
+ 'detect_multiple_h1',
+ \__( 'Detect Multiple H1s', 'functionalities' ),
+ function () {
+ $o = self::get_content_regression_options();
+ $checked = ! empty( $o['detect_multiple_h1'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Warn when multiple H1 headings exist', 'functionalities' ) . ' ';
+ },
+ 'functionalities_content_regression',
+ 'functionalities_content_regression_section'
+ );
+
+ \add_settings_field(
+ 'detect_skipped_levels',
+ \__( 'Detect Skipped Levels', 'functionalities' ),
+ function () {
+ $o = self::get_content_regression_options();
+ $checked = ! empty( $o['detect_skipped_levels'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Warn when heading levels are skipped (e.g., H2 to H4)', 'functionalities' ) . ' ';
+ },
+ 'functionalities_content_regression',
+ 'functionalities_content_regression_section'
+ );
+
+ // Advanced settings header.
+ \add_settings_field(
+ 'advanced_header',
+ '' . \__( 'Advanced Settings', 'functionalities' ) . ' ',
+ function () {
+ echo '' . \esc_html__( 'Configure snapshot and display options.', 'functionalities' ) . '
';
+ },
+ 'functionalities_content_regression',
+ 'functionalities_content_regression_section'
+ );
+
+ \add_settings_field(
+ 'snapshot_rolling_count',
+ \__( 'Snapshots to Keep', 'functionalities' ),
+ function () {
+ $o = self::get_content_regression_options();
+ $val = isset( $o['snapshot_rolling_count'] ) ? (int) $o['snapshot_rolling_count'] : 5;
+ echo ' ';
+ echo '' . \esc_html__( 'Number of historical snapshots to retain per post.', 'functionalities' ) . '
';
+ },
+ 'functionalities_content_regression',
+ 'functionalities_content_regression_section'
+ );
+
+ \add_settings_field(
+ 'show_post_column',
+ \__( 'Show Post List Column', 'functionalities' ),
+ function () {
+ $o = self::get_content_regression_options();
+ $checked = ! empty( $o['show_post_column'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Display integrity status icon in post list table', 'functionalities' ) . ' ';
+ },
+ 'functionalities_content_regression',
+ 'functionalities_content_regression_section'
+ );
+
+ // Assumption Detection settings.
+ \register_setting(
+ 'functionalities_assumption_detection',
+ 'functionalities_assumption_detection',
+ array(
+ 'sanitize_callback' => array( __CLASS__, 'sanitize_assumption_detection' ),
+ 'default' => array(
+ 'enabled' => false,
+ 'detect_schema_collision' => true,
+ 'detect_analytics_dupe' => true,
+ 'detect_font_redundancy' => true,
+ 'detect_inline_css_growth' => true,
+ 'inline_css_threshold_kb' => 50,
+ 'detect_jquery_conflicts' => true,
+ 'detect_meta_duplication' => true,
+ 'detect_rest_exposure' => true,
+ 'detect_lazy_load_conflict' => true,
+ 'detect_mixed_content' => true,
+ 'detect_missing_security_headers' => true,
+ 'detect_debug_exposure' => true,
+ 'detect_cron_issues' => true,
+ ),
+ )
+ );
+
+ \add_settings_section(
+ 'functionalities_assumption_detection_section',
+ \__( 'Assumption Detection Settings', 'functionalities' ),
+ array( __CLASS__, 'section_assumption_detection' ),
+ 'functionalities_assumption_detection'
+ );
+
+ \add_settings_field(
+ 'assumption_enabled',
+ \__( 'Enable Assumption Detection', 'functionalities' ),
+ function () {
+ $o = self::get_assumption_detection_options();
+ $checked = ! empty( $o['enabled'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Detect when implicit site assumptions stop being true', 'functionalities' ) . ' ';
+ },
+ 'functionalities_assumption_detection',
+ 'functionalities_assumption_detection_section'
+ );
+
+ \add_settings_field(
+ 'detect_schema_collision',
+ \__( 'Schema Collision Detection', 'functionalities' ),
+ function () {
+ $o = self::get_assumption_detection_options();
+ $checked = ! empty( $o['detect_schema_collision'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Warn when multiple sources output the same schema type', 'functionalities' ) . ' ';
+ },
+ 'functionalities_assumption_detection',
+ 'functionalities_assumption_detection_section'
+ );
+
+ \add_settings_field(
+ 'detect_analytics_dupe',
+ \__( 'Analytics Duplication Detection', 'functionalities' ),
+ function () {
+ $o = self::get_assumption_detection_options();
+ $checked = ! empty( $o['detect_analytics_dupe'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Warn when analytics scripts load multiple times', 'functionalities' ) . ' ';
+ },
+ 'functionalities_assumption_detection',
+ 'functionalities_assumption_detection_section'
+ );
+
+ \add_settings_field(
+ 'detect_font_redundancy',
+ \__( 'Font Redundancy Detection', 'functionalities' ),
+ function () {
+ $o = self::get_assumption_detection_options();
+ $checked = ! empty( $o['detect_font_redundancy'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Warn when fonts load from multiple sources', 'functionalities' ) . ' ';
+ },
+ 'functionalities_assumption_detection',
+ 'functionalities_assumption_detection_section'
+ );
+
+ \add_settings_field(
+ 'detect_inline_css_growth',
+ \__( 'Inline CSS Growth Detection', 'functionalities' ),
+ function () {
+ $o = self::get_assumption_detection_options();
+ $checked = ! empty( $o['detect_inline_css_growth'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Track inline CSS size over time', 'functionalities' ) . ' ';
+ },
+ 'functionalities_assumption_detection',
+ 'functionalities_assumption_detection_section'
+ );
+
+ \add_settings_field(
+ 'inline_css_threshold_kb',
+ \__( 'Inline CSS Threshold (KB)', 'functionalities' ),
+ function () {
+ $o = self::get_assumption_detection_options();
+ $val = isset( $o['inline_css_threshold_kb'] ) ? (int) $o['inline_css_threshold_kb'] : 50;
+ echo ' KB';
+ echo '' . \esc_html__( 'Warn when inline CSS exceeds this size.', 'functionalities' ) . '
';
+ },
+ 'functionalities_assumption_detection',
+ 'functionalities_assumption_detection_section'
+ );
+
+ \add_settings_field(
+ 'detect_jquery_conflicts',
+ \__( 'jQuery Conflict Detection', 'functionalities' ),
+ function () {
+ $o = self::get_assumption_detection_options();
+ $checked = ! empty( $o['detect_jquery_conflicts'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Warn when multiple jQuery versions or sources are loaded', 'functionalities' ) . ' ';
+ },
+ 'functionalities_assumption_detection',
+ 'functionalities_assumption_detection_section'
+ );
+
+ \add_settings_field(
+ 'detect_meta_duplication',
+ \__( 'Meta Tag Duplication Detection', 'functionalities' ),
+ function () {
+ $o = self::get_assumption_detection_options();
+ $checked = ! empty( $o['detect_meta_duplication'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Warn when duplicate meta tags are detected (viewport, robots, OG tags)', 'functionalities' ) . ' ';
+ },
+ 'functionalities_assumption_detection',
+ 'functionalities_assumption_detection_section'
+ );
+
+ \add_settings_field(
+ 'detect_rest_exposure',
+ \__( 'REST API Exposure Detection', 'functionalities' ),
+ function () {
+ $o = self::get_assumption_detection_options();
+ $checked = ! empty( $o['detect_rest_exposure'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Warn when REST API exposes user information publicly', 'functionalities' ) . ' ';
+ },
+ 'functionalities_assumption_detection',
+ 'functionalities_assumption_detection_section'
+ );
+
+ \add_settings_field(
+ 'detect_lazy_load_conflict',
+ \__( 'Lazy Loading Conflict Detection', 'functionalities' ),
+ function () {
+ $o = self::get_assumption_detection_options();
+ $checked = ! empty( $o['detect_lazy_load_conflict'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Warn when multiple lazy loading implementations are detected', 'functionalities' ) . ' ';
+ },
+ 'functionalities_assumption_detection',
+ 'functionalities_assumption_detection_section'
+ );
+
+ \add_settings_field(
+ 'detect_mixed_content',
+ \__( 'Mixed Content Detection', 'functionalities' ),
+ function () {
+ $o = self::get_assumption_detection_options();
+ $checked = ! empty( $o['detect_mixed_content'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Warn when HTTP resources are loaded on HTTPS pages', 'functionalities' ) . ' ';
+ },
+ 'functionalities_assumption_detection',
+ 'functionalities_assumption_detection_section'
+ );
+
+ \add_settings_field(
+ 'detect_missing_security_headers',
+ \__( 'Security Headers Detection', 'functionalities' ),
+ function () {
+ $o = self::get_assumption_detection_options();
+ $checked = ! empty( $o['detect_missing_security_headers'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Warn when critical security headers are missing', 'functionalities' ) . ' ';
+ },
+ 'functionalities_assumption_detection',
+ 'functionalities_assumption_detection_section'
+ );
+
+ \add_settings_field(
+ 'detect_debug_exposure',
+ \__( 'Debug Mode Exposure', 'functionalities' ),
+ function () {
+ $o = self::get_assumption_detection_options();
+ $checked = ! empty( $o['detect_debug_exposure'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Warn when WP_DEBUG or error display is enabled in production', 'functionalities' ) . ' ';
+ },
+ 'functionalities_assumption_detection',
+ 'functionalities_assumption_detection_section'
+ );
+
+ \add_settings_field(
+ 'detect_cron_issues',
+ \__( 'Cron Issues Detection', 'functionalities' ),
+ function () {
+ $o = self::get_assumption_detection_options();
+ $checked = ! empty( $o['detect_cron_issues'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Warn when WP-Cron is disabled or has stuck jobs', 'functionalities' ) . ' ';
+ },
+ 'functionalities_assumption_detection',
+ 'functionalities_assumption_detection_section'
+ );
+
+ // Explicit save button field - ensures visibility.
+ \add_settings_field(
+ 'assumption_save_settings',
+ '',
+ function () {
+ echo '';
+ \submit_button( \__( 'Save Settings', 'functionalities' ), 'primary', 'submit', false );
+ echo ' ';
+ echo '';
+ echo \esc_html__( 'Run Detection Now', 'functionalities' );
+ echo ' ';
+ echo '
';
+ },
+ 'functionalities_assumption_detection',
+ 'functionalities_assumption_detection_section'
+ );
+
+ \add_settings_field(
+ 'detected_assumptions',
+ \__( 'Detected Assumptions', 'functionalities' ),
+ array( __CLASS__, 'field_detected_assumptions' ),
+ 'functionalities_assumption_detection',
+ 'functionalities_assumption_detection_section'
+ );
+
+ // PWA settings.
+ \register_setting(
+ 'functionalities_pwa',
+ 'functionalities_pwa',
+ array(
+ 'sanitize_callback' => array( __CLASS__, 'sanitize_pwa' ),
+ 'default' => array(
+ 'enabled' => false,
+ ),
+ )
+ );
+
+ \add_settings_section(
+ 'functionalities_pwa_identity',
+ \__( 'App Identity', 'functionalities' ),
+ function () {
+ echo '' . \esc_html__( 'Basic information about your progressive web app.', 'functionalities' ) . '
';
+ },
+ 'functionalities_pwa'
+ );
+
+ \add_settings_field(
+ 'pwa_enabled',
+ \__( 'Enable PWA', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ $checked = ! empty( $o['enabled'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Enable Progressive Web App features', 'functionalities' ) . ' ';
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_identity'
+ );
+
+ \add_settings_field(
+ 'pwa_app_name',
+ \__( 'App Name', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ echo ' ';
+ echo '' . \esc_html__( 'Full name displayed on install. Leave blank to use site title.', 'functionalities' ) . '
';
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_identity'
+ );
+
+ \add_settings_field(
+ 'pwa_short_name',
+ \__( 'Short Name', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ echo ' ';
+ echo '' . \esc_html__( 'Short name for the home screen (12 chars max).', 'functionalities' ) . '
';
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_identity'
+ );
+
+ \add_settings_field(
+ 'pwa_description',
+ \__( 'Description', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ echo '';
+ echo '' . \esc_html__( 'Shown in app stores and install dialogs. Leave blank to use site tagline.', 'functionalities' ) . '
';
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_identity'
+ );
+
+ \add_settings_field(
+ 'pwa_start_url',
+ \__( 'Start URL', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ echo ' ';
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_identity'
+ );
+
+ \add_settings_field(
+ 'pwa_scope',
+ \__( 'Scope', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ echo ' ';
+ echo '' . \esc_html__( 'Navigation scope. "/" means the entire site.', 'functionalities' ) . '
';
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_identity'
+ );
+
+ \add_settings_field(
+ 'pwa_categories',
+ \__( 'Categories', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ echo ' ';
+ echo '' . \esc_html__( 'Comma-separated W3C categories (e.g. news, education, business).', 'functionalities' ) . '
';
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_identity'
+ );
+
+ \add_settings_field(
+ 'pwa_display',
+ \__( 'Display Mode', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ $modes = array( 'standalone', 'fullscreen', 'minimal-ui', 'browser' );
+ echo '';
+ foreach ( $modes as $mode ) {
+ echo '' . \esc_html( $mode ) . ' ';
+ }
+ echo ' ';
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_identity'
+ );
+
+ \add_settings_field(
+ 'pwa_orientation',
+ \__( 'Orientation', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ $orientations = array( 'any', 'portrait', 'landscape', 'portrait-primary', 'landscape-primary' );
+ echo '';
+ foreach ( $orientations as $orient ) {
+ echo '' . \esc_html( $orient ) . ' ';
+ }
+ echo ' ';
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_identity'
+ );
+
+ // Appearance section.
+ \add_settings_section(
+ 'functionalities_pwa_appearance',
+ \__( 'Appearance & Icons', 'functionalities' ),
+ function () {
+ echo '' . \esc_html__( 'Theme colors and app icons. Icons must be square PNG files.', 'functionalities' ) . '
';
+ },
+ 'functionalities_pwa'
+ );
+
+ \add_settings_field(
+ 'pwa_theme_color',
+ \__( 'Theme Color', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ echo ' ';
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_appearance'
+ );
+
+ \add_settings_field(
+ 'pwa_background_color',
+ \__( 'Background Color', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ echo ' ';
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_appearance'
+ );
+
+ \add_settings_field(
+ 'pwa_icon_512',
+ \__( 'Icon 512x512', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ self::render_media_field( 'functionalities_pwa[icon_512]', $o['icon_512'], \__( 'Primary app icon (required for installability).', 'functionalities' ) );
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_appearance'
+ );
+
+ \add_settings_field(
+ 'pwa_icon_192',
+ \__( 'Icon 192x192', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ self::render_media_field( 'functionalities_pwa[icon_192]', $o['icon_192'], \__( 'Smaller icon for home screen and splash.', 'functionalities' ) );
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_appearance'
+ );
+
+ \add_settings_field(
+ 'pwa_maskable_icon_512',
+ \__( 'Maskable Icon 512x512', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ self::render_media_field( 'functionalities_pwa[maskable_icon_512]', $o['maskable_icon_512'], \__( 'Maskable icon for adaptive icon support (safe zone padding recommended).', 'functionalities' ) );
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_appearance'
+ );
+
+ \add_settings_field(
+ 'pwa_maskable_icon_192',
+ \__( 'Maskable Icon 192x192', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ self::render_media_field( 'functionalities_pwa[maskable_icon_192]', $o['maskable_icon_192'], \__( 'Smaller maskable icon.', 'functionalities' ) );
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_appearance'
+ );
+
+ // Install Prompt section.
+ \add_settings_section(
+ 'functionalities_pwa_prompt',
+ \__( 'Install Prompt', 'functionalities' ),
+ function () {
+ echo '' . \esc_html__( 'Customize the in-page install prompt shown to visitors.', 'functionalities' ) . '
';
+ },
+ 'functionalities_pwa'
+ );
+
+ \add_settings_field(
+ 'pwa_install_prompt',
+ \__( 'Show Install Prompt', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ $checked = ! empty( $o['install_prompt'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Display a custom install prompt to visitors', 'functionalities' ) . ' ';
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_prompt'
+ );
+
+ \add_settings_field(
+ 'pwa_prompt_title',
+ \__( 'Prompt Title', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ echo ' ';
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_prompt'
+ );
+
+ \add_settings_field(
+ 'pwa_prompt_text',
+ \__( 'Prompt Text', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ echo ' ';
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_prompt'
+ );
+
+ \add_settings_field(
+ 'pwa_prompt_button',
+ \__( 'Button Text', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ echo ' ';
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_prompt'
+ );
+
+ \add_settings_field(
+ 'pwa_prompt_dismiss',
+ \__( 'Dismiss Text', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ echo ' ';
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_prompt'
+ );
+
+ \add_settings_field(
+ 'pwa_prompt_position',
+ \__( 'Position', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ $positions = array(
+ 'bottom' => \__( 'Bottom', 'functionalities' ),
+ 'top' => \__( 'Top', 'functionalities' ),
+ 'center' => \__( 'Center (modal)', 'functionalities' ),
+ );
+ echo '';
+ foreach ( $positions as $val => $label ) {
+ echo '' . \esc_html( $label ) . ' ';
+ }
+ echo ' ';
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_prompt'
+ );
+
+ \add_settings_field(
+ 'pwa_prompt_style',
+ \__( 'Style', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ $styles = array(
+ 'banner' => \__( 'Banner', 'functionalities' ),
+ 'card' => \__( 'Card', 'functionalities' ),
+ );
+ echo '';
+ foreach ( $styles as $val => $label ) {
+ echo '' . \esc_html( $label ) . ' ';
+ }
+ echo ' ';
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_prompt'
+ );
+
+ \add_settings_field(
+ 'pwa_prompt_frequency',
+ \__( 'Re-show After (days)', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ echo ' ';
+ echo '' . \esc_html__( 'Days to wait before showing prompt again after dismissal.', 'functionalities' ) . '
';
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_prompt'
+ );
+
+ // Offline & Caching section.
+ \add_settings_section(
+ 'functionalities_pwa_caching',
+ \__( 'Offline & Caching', 'functionalities' ),
+ function () {
+ echo '' . \esc_html__( 'Service worker caching and offline behavior.', 'functionalities' ) . '
';
+ },
+ 'functionalities_pwa'
+ );
+
+ \add_settings_field(
+ 'pwa_cache_version',
+ \__( 'Cache Version', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ echo ' ';
+ echo '' . \esc_html__( 'Increment to force cache refresh (e.g. v1, v2).', 'functionalities' ) . '
';
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_caching'
+ );
+
+ \add_settings_field(
+ 'pwa_precache_urls',
+ \__( 'Precache URLs', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ echo '';
+ echo '' . \esc_html__( 'One URL per line. These pages will be cached immediately when the service worker installs.', 'functionalities' ) . '
';
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_caching'
+ );
+
+ // Shortcuts section.
+ \add_settings_section(
+ 'functionalities_pwa_shortcuts',
+ \__( 'Shortcuts', 'functionalities' ),
+ function () {
+ echo '' . \esc_html__( 'App shortcut links shown on long-press of the app icon. Up to 4 shortcuts.', 'functionalities' ) . '
';
+ },
+ 'functionalities_pwa'
+ );
+
+ \add_settings_field(
+ 'pwa_shortcuts',
+ \__( 'Shortcuts', 'functionalities' ),
+ array( __CLASS__, 'field_pwa_shortcuts' ),
+ 'functionalities_pwa',
+ 'functionalities_pwa_shortcuts'
+ );
+
+ // Screenshots section.
+ \add_settings_section(
+ 'functionalities_pwa_screenshots',
+ \__( 'Screenshots', 'functionalities' ),
+ function () {
+ echo '' . \esc_html__( 'App screenshots shown in install dialogs and app stores. Provide wide and narrow sizes.', 'functionalities' ) . '
';
+ },
+ 'functionalities_pwa'
+ );
+
+ \add_settings_field(
+ 'pwa_screenshots',
+ \__( 'Screenshots', 'functionalities' ),
+ array( __CLASS__, 'field_pwa_screenshots' ),
+ 'functionalities_pwa',
+ 'functionalities_pwa_screenshots'
+ );
+
+ // Advanced section.
+ \add_settings_section(
+ 'functionalities_pwa_advanced',
+ \__( 'Advanced', 'functionalities' ),
+ function () {
+ echo '' . \esc_html__( 'Advanced PWA features. Only change these if you know what you\'re doing.', 'functionalities' ) . '
';
+ },
+ 'functionalities_pwa'
+ );
+
+ \add_settings_field(
+ 'pwa_display_override',
+ \__( 'Display Override', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ $checked = ! empty( $o['display_override'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Enable display_override with window-controls-overlay fallback chain', 'functionalities' ) . ' ';
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_advanced'
+ );
+
+ \add_settings_field(
+ 'pwa_edge_side_panel',
+ \__( 'Edge Side Panel', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ $checked = ! empty( $o['edge_side_panel'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Allow opening in Microsoft Edge side panel', 'functionalities' ) . ' ';
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_advanced'
+ );
+
+ \add_settings_field(
+ 'pwa_launch_handler',
+ \__( 'Launch Handler', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ $handlers = array(
+ '' => \__( 'Default', 'functionalities' ),
+ 'focus-existing' => 'focus-existing',
+ 'navigate-new' => 'navigate-new',
+ 'navigate-existing' => 'navigate-existing',
+ );
+ echo '';
+ foreach ( $handlers as $val => $label ) {
+ echo '' . \esc_html( $label ) . ' ';
+ }
+ echo ' ';
+ echo '' . \esc_html__( 'Controls behavior when the app is already open and a new navigation is triggered.', 'functionalities' ) . '
';
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_advanced'
+ );
+
+ \add_settings_field(
+ 'pwa_share_target',
+ \__( 'Share Target', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ $checked = ! empty( $o['share_target_enabled'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Register as a Web Share Target', 'functionalities' ) . ' ';
+ echo '';
+ echo '' . \esc_html__( 'Action URL:', 'functionalities' ) . ' ';
+ echo ' ';
+ echo '
';
+ $methods = array(
+ 'GET' => 'GET',
+ 'POST' => 'POST',
+ );
+ echo '';
+ echo '' . \esc_html__( 'Method:', 'functionalities' ) . ' ';
+ echo '';
+ foreach ( $methods as $val => $label ) {
+ echo '' . \esc_html( $label ) . ' ';
+ }
+ echo ' ';
+ echo '
';
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_advanced'
+ );
+
+ \add_settings_field(
+ 'pwa_advanced_manifest',
+ \__( 'Custom Manifest JSON', 'functionalities' ),
+ function () {
+ $o = self::get_pwa_options();
+ echo '';
+ echo '' . \esc_html__( 'Raw JSON to merge into the manifest. Must be a valid JSON object.', 'functionalities' ) . '
';
+ },
+ 'functionalities_pwa',
+ 'functionalities_pwa_advanced'
+ );
+ }
+
+ public static function field_nofollow_external(): void {
+ $opts = self::get_link_management_options();
+ $checked = ! empty( $opts['nofollow_external'] ) ? 'checked' : '';
+ echo ' ' . \esc_html__( 'Enable rel="nofollow" for all external links', 'functionalities' ) . ' ';
+ }
+
+ /**
+ * Render exceptions field.
+ *
+ * @return void
+ */
+ public static function field_exceptions(): void {
+ $opts = self::get_link_management_options();
+ $value = isset( $opts['exceptions'] ) ? (string) $opts['exceptions'] : '';
+ echo '';
+ echo '' . \esc_html__( 'One per line. Supports full URLs (https://example.com/page), domains (example.com), or partial matches (e.g., /partner/).', 'functionalities' ) . '
';
+ }
+
+ /**
+ * Render section description for link management.
+ *
+ * @return void
+ */
+ public static function section_link_management(): void {
+ echo '' . \esc_html__( 'Control how external and internal links are handled across your site.', 'functionalities' ) . '
';
+
+ // Get module docs.
+ $docs = Module_Docs::get( 'link-management' );
+
+ // Render documentation accordions.
+ echo '';
+
+ if ( ! empty( $docs['features'] ) ) {
+ $list = '
';
+ foreach ( $docs['features'] as $feature ) {
+ $list .= '' . \esc_html( $feature ) . ' ';
+ }
+ $list .= ' ';
+ Admin_UI::render_docs_section( \__( 'What This Module Does', 'functionalities' ), $list, 'info' );
+ }
+
+ if ( ! empty( $docs['hooks'] ) ) {
+ $hooks_html = '
';
+ foreach ( $docs['hooks'] as $hook ) {
+ $hooks_html .= '' . \esc_html( $hook['name'] ) . ' ';
+ $hooks_html .= '' . \esc_html( $hook['description'] ) . ' ';
+ }
+ $hooks_html .= ' ';
+ Admin_UI::render_docs_section( \__( 'Developer Hooks', 'functionalities' ), $hooks_html, 'developer' );
+ }
+
+ echo '
';
+ }
+
+ /**
+ * Render open external links in new tab field.
+ *
+ * @return void
+ */
+ public static function field_open_external_new_tab(): void {
+ $opts = self::get_link_management_options();
+ $checked = ! empty( $opts['open_external_new_tab'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Adds target="_blank" and rel="noopener" to external links', 'functionalities' ) . ' ';
+ }
+
+ /**
+ * Render open internal links in new tab field.
+ *
+ * @return void
+ */
+ public static function field_open_internal_new_tab(): void {
+ $opts = self::get_link_management_options();
+ $checked = ! empty( $opts['open_internal_new_tab'] ) ? 'checked' : '';
+ echo ' ';
+ echo \esc_html__( 'Adds target="_blank" to same-domain links (see exceptions)', 'functionalities' ) . ' ';
+ }
+
+ /**
+ * Render internal new tab exceptions field.
+ *
+ * @return void
+ */
+ public static function field_internal_new_tab_exceptions(): void {
+ $opts = self::get_link_management_options();
+ $val = isset( $opts['internal_new_tab_exceptions'] ) ? (string) $opts['internal_new_tab_exceptions'] : '';
+ echo '';
+ echo '' . \esc_html__( 'One domain per line. Matching hosts will NOT be forced to open in a new tab when internal option is enabled.', 'functionalities' ) . '
';
+ }
+
+ /**
+ * Render JSON preset URL field.
+ *
+ * @return void
+ */
+ public static function field_json_preset_url(): void {
+ $opts = self::get_link_management_options();
+ $val = isset( $opts['json_preset_url'] ) ? (string) $opts['json_preset_url'] : '';
+
+ // Check for theme exception-urls.json.
+ $theme_json_path = \get_stylesheet_directory() . '/exception-urls.json';
+ $theme_json_exists = file_exists( $theme_json_path );
+ $parent_json_path = \get_template_directory() . '/exception-urls.json';
+ $parent_json_exists = \get_stylesheet_directory() !== \get_template_directory() && file_exists( $parent_json_path );
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ get( 'Name' ) )
+ );
+ ?>
+
+
+
+
+
+ get( 'Name' ) )
+ );
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
+/**
+ * Add trusted domains to nofollow exceptions.
+ * Links to these domains will NOT have nofollow added.
+ */
+add_filter( 'functionalities_exception_domains', function( $domains ) {
+ // Add your trusted domains here
+ $trusted_domains = array(
+ 'trusted-partner.com',
+ 'another-trusted-site.org',
+ 'your-other-website.net',
+ );
+
+ return array_merge( $domains, $trusted_domains );
+} );
+
+
+
+
+
+
+
+
+
+
+
<?php
+/**
+ * Add specific URLs to nofollow exceptions.
+ * These specific URLs will NOT have nofollow added.
+ */
+add_filter( 'functionalities_exception_urls', function( $urls ) {
+ // Add specific URLs or patterns
+ $trusted_urls = array(
+ 'https://example.com/specific-page',
+ 'https://partner.com/affiliate/*',
+ 'https://trusted.org/blog/*',
+ );
+
+ return array_merge( $urls, $trusted_urls );
+} );
+
+
+
+
+
+
+
+
+
+
+
<?php
+/**
+ * Dynamically add exception domains based on conditions.
+ * Example: Trust all domains for administrators.
+ */
+add_filter( 'functionalities_exception_domains', function( $domains ) {
+ // Skip nofollow for administrators
+ if ( current_user_can( 'manage_options' ) ) {
+ // Return wildcard to match all domains
+ $domains[] = '*';
+ }
+
+ // Add domains from a custom option
+ $custom_domains = get_option( 'my_trusted_domains', array() );
+ if ( is_array( $custom_domains ) ) {
+ $domains = array_merge( $domains, $custom_domains );
+ }
+
+ // Add sponsor domains on specific post types
+ if ( is_singular( 'sponsored_post' ) ) {
+ $domains[] = 'sponsor-website.com';
+ }
+
+ return $domains;
+} );
+
+
+
+
+
+
+
+
+
+
+
<?php
+/**
+ * Use a custom JSON file path for exception URLs.
+ */
+add_filter( 'functionalities_json_preset_path', function( $default_path ) {
+ // Use a JSON file from your theme
+ $theme_json = get_stylesheet_directory() . '/config/exceptions.json';
+
+ if ( file_exists( $theme_json ) ) {
+ return $theme_json;
+ }
+
+ // Or use an external URL (use with caution!)
+ // return 'https://your-cdn.com/exception-urls.json';
+
+ return $default_path;
+} );
+
+
+
+
+
+
+
+
+
+
+ ' . \esc_html__( 'Remove Gutenberg block-specific CSS classes from your frontend HTML for cleaner markup.', 'functionalities' ) . '
';
+
+ // Get module docs.
+ $docs = Module_Docs::get( 'block-cleanup' );
+
+ // Render documentation accordions.
+ echo '';
+
+ if ( ! empty( $docs['features'] ) ) {
+ $list = '
';
+ foreach ( $docs['features'] as $feature ) {
+ $list .= '' . \esc_html( $feature ) . ' ';
+ }
+ $list .= ' ';
+ Admin_UI::render_docs_section( \__( 'What This Module Does', 'functionalities' ), $list, 'info' );
+ }
+
+ // Classes removed info.
+ $classes_html = '
';
+ $classes_html .= 'wp-block-heading → ' . \esc_html__( 'from h1-h6 elements', 'functionalities' ) . ' ';
+ $classes_html .= 'wp-block-list → ' . \esc_html__( 'from ul/ol elements', 'functionalities' ) . ' ';
+ $classes_html .= 'wp-block-image → ' . \esc_html__( 'from figure/div elements', 'functionalities' ) . ' ';
+ $classes_html .= ' ';
+ Admin_UI::render_docs_section( \__( 'Classes Removed', 'functionalities' ), $classes_html, 'usage' );
+
+ if ( ! empty( $docs['hooks'] ) ) {
+ $hooks_html = '
';
+ foreach ( $docs['hooks'] as $hook ) {
+ $hooks_html .= '' . \esc_html( $hook['name'] ) . ' ';
+ $hooks_html .= '' . \esc_html( $hook['description'] ) . ' ';
+ }
+ $hooks_html .= ' ';
+ Admin_UI::render_docs_section( \__( 'Developer Hooks', 'functionalities' ), $hooks_html, 'developer' );
+ }
+
+ echo '
';
+ }
+
+ // Block Cleanup: fields & helpers
+ public static function field_bc_remove_heading(): void {
+ $opts = self::get_block_cleanup_options();
+ $checked = ! empty( $opts['remove_heading_block_class'] ) ? 'checked' : '';
+ echo ' ' . \esc_html__( 'Remove class "wp-block-heading" from headings (h1–h6)', 'functionalities' ) . ' ';
+ }
+ public static function field_bc_remove_list(): void {
+ $opts = self::get_block_cleanup_options();
+ $checked = ! empty( $opts['remove_list_block_class'] ) ? 'checked' : '';
+ echo ' ' . \esc_html__( 'Remove class "wp-block-list" from ul/ol', 'functionalities' ) . ' ';
+ }
+ public static function field_bc_remove_image(): void {
+ $opts = self::get_block_cleanup_options();
+ $checked = ! empty( $opts['remove_image_block_class'] ) ? 'checked' : '';
+ echo ' ' . \esc_html__( 'Remove class ".wp-block-image" from frontend', 'functionalities' ) . ' ';
+ }
+
+ // Editor Links: fields & helpers
+ public static function field_el_enable(): void {
+ $opts = self::get_editor_links_options();
+ $checked = ! empty( $opts['enable_limit'] ) ? 'checked' : '';
+ echo ' ' . \esc_html__( 'Limit editor link suggestions to selected post types', 'functionalities' ) . ' ';
+ }
+ public static function field_el_post_types(): void {
+ $opts = self::get_editor_links_options();
+ $selected = isset( $opts['post_types'] ) && is_array( $opts['post_types'] ) ? $opts['post_types'] : array();
+ $pts = get_post_types( array( 'public' => true ), 'objects' );
+ echo '';
+ foreach ( $pts as $name => $obj ) {
+ $is_checked = in_array( $name, $selected, true ) ? 'checked' : '';
+ $label = sprintf( '%s (%s)', $obj->labels->singular_name ?? $name, $name );
+ echo ' ' . esc_html( $label ) . ' ';
+ }
+ echo ' ';
+ }
+
+ public static function field_components_items(): void {
+ $o = self::get_components_options();
+ $items = isset( $o['items'] ) && is_array( $o['items'] ) ? $o['items'] : array();
+ $per_page = 24;
+ $total_items = count( $items );
+ $total_pages = (int) max( 1, ceil( $total_items / $per_page ) );
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1 ) : ?>
+
+
+
+
+
+
+
+
+
+
.new-component
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ' . esc_html( $label ) . '';
+ },
+ 'functionalities_misc',
+ 'functionalities_misc_section'
+ );
+ }
+
+ public static function field_fonts_items(): void {
+ $o = self::get_fonts_options();
+ $items = isset( $o['items'] ) && is_array( $o['items'] ) ? $o['items'] : array();
+ $total_items = count( $items );
+
+ // Enqueue media uploader
+ \wp_enqueue_media();
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id="fc-assign-toggle">
+
+
+
+
+
+
+
+
+ ' . \esc_html__( 'Add copyright metadata, Dublin Core (DCMI) tags, and per-post licensing options. Works standalone or integrates with major SEO plugins.', 'functionalities' ) . '';
+
+ // Get module docs.
+ $docs = Module_Docs::get( 'meta' );
+
+ // Render documentation accordions.
+ echo '';
+
+ if ( ! empty( $docs['features'] ) ) {
+ $list = '
';
+ foreach ( $docs['features'] as $feature ) {
+ $list .= '' . \esc_html( $feature ) . ' ';
+ }
+ $list .= ' ';
+ Admin_UI::render_docs_section( \__( 'What This Module Does', 'functionalities' ), $list, 'info' );
+ }
+
+ // Schema.org Support (dynamic based on detected plugin).
+ $plugin_names = array(
+ 'rank-math' => 'Rank Math',
+ 'yoast' => 'Yoast SEO',
+ 'seo-framework' => 'The SEO Framework',
+ 'seopress' => 'SEOPress',
+ 'aioseo' => 'All in One SEO',
+ );
+
+ if ( $detected !== 'none' ) {
+ $schema_content = '
✓ ' . \esc_html__( 'Detected:', 'functionalities' ) . ' ' . \esc_html( $plugin_names[ $detected ] ?? $detected ) . '
';
+ $schema_content .= '
' . \esc_html__( 'Copyright data will be added to your SEO plugin\'s existing schema output.', 'functionalities' ) . '
';
+ } else {
+ $schema_content = '
✓ ' . \esc_html__( 'Standalone Mode', 'functionalities' ) . '
';
+ $schema_content .= '
' . \esc_html__( 'No SEO plugin detected. Complete Article schema with copyright will be output independently.', 'functionalities' ) . '
';
+ }
+ Admin_UI::render_docs_section( \__( 'Schema.org Support', 'functionalities' ), $schema_content, 'developer', true );
+
+ // Compatible plugins.
+ $plugins_html = '
';
+ $plugins_html .= 'Rank Math ';
+ $plugins_html .= 'Yoast SEO ';
+ $plugins_html .= 'The SEO Framework ';
+ $plugins_html .= 'SEOPress ';
+ $plugins_html .= 'All in One SEO ';
+ $plugins_html .= '' . \esc_html__( 'or Standalone', 'functionalities' ) . ' ';
+ $plugins_html .= ' ';
+ Admin_UI::render_docs_section( \__( 'Compatible SEO Plugins', 'functionalities' ), $plugins_html, 'usage' );
+
+ if ( ! empty( $docs['hooks'] ) ) {
+ $hooks_html = '
';
+ foreach ( $docs['hooks'] as $hook ) {
+ $hooks_html .= '' . \esc_html( $hook['name'] ) . ' ';
+ $hooks_html .= '' . \esc_html( $hook['description'] ) . ' ';
+ }
+ $hooks_html .= ' ';
+ Admin_UI::render_docs_section( \__( 'Developer Hooks', 'functionalities' ), $hooks_html, 'developer' );
+ }
+
+ echo '
';
+ }
+
+ /**
+ * Render section description for content regression detection.
+ *
+ * @return void
+ */
+ public static function section_content_regression(): void {
+ echo '' . \esc_html__( 'Detect structural regressions when posts are updated. This module compares each post against its own historical baseline.', 'functionalities' ) . '
';
+
+ // Get module docs.
+ $docs = Module_Docs::get( 'content-regression' );
+
+ // Render documentation accordions.
+ echo '';
+
+ if ( ! empty( $docs['features'] ) ) {
+ $list = '
';
+ foreach ( $docs['features'] as $feature ) {
+ $list .= '' . \esc_html( $feature ) . ' ';
+ }
+ $list .= ' ';
+ Admin_UI::render_docs_section( \__( 'What This Module Does', 'functionalities' ), $list, 'info' );
+ }
+
+ if ( ! empty( $docs['usage'] ) ) {
+ Admin_UI::render_docs_section( \__( 'Philosophy', 'functionalities' ), '
' . \esc_html( $docs['usage'] ) . '
', 'usage' );
+ }
+
+ if ( ! empty( $docs['hooks'] ) ) {
+ $hooks_html = '
';
+ foreach ( $docs['hooks'] as $hook ) {
+ $hooks_html .= '' . \esc_html( $hook['name'] ) . ' ';
+ $hooks_html .= '' . \esc_html( $hook['description'] ) . ' ';
+ }
+ $hooks_html .= ' ';
+ Admin_UI::render_docs_section( \__( 'Developer Hooks', 'functionalities' ), $hooks_html, 'developer' );
+ }
+
+ echo '
';
+ }
+
+ /**
+ * Assumption detection section callback.
+ *
+ * @return void
+ */
+ public static function section_assumption_detection(): void {
+ echo '' . \esc_html__( 'Detects when technical assumptions stop being true. This module notices changes, not problems.', 'functionalities' ) . '
';
+
+ // Get module docs.
+ $docs = Module_Docs::get( 'assumption-detection' );
+
+ // Render documentation accordions.
+ if ( ! empty( $docs['features'] ) ) {
+ $list = '';
+ foreach ( $docs['features'] as $feature ) {
+ $list .= '' . \esc_html( $feature ) . ' ';
+ }
+ $list .= ' ';
+ Admin_UI::render_docs_section( \__( 'What This Module Does', 'functionalities' ), $list, 'info' );
+ }
+
+ if ( ! empty( $docs['usage'] ) ) {
+ Admin_UI::render_docs_section( \__( 'How to Use', 'functionalities' ), '' . \esc_html( $docs['usage'] ) . '
', 'usage' );
+ }
+
+ if ( ! empty( $docs['hooks'] ) ) {
+ $hooks_html = '';
+ foreach ( $docs['hooks'] as $hook ) {
+ $hooks_html .= '' . \esc_html( $hook['name'] ) . ' ';
+ $hooks_html .= '' . \esc_html( $hook['description'] ) . ' ';
+ }
+ $hooks_html .= ' ';
+ Admin_UI::render_docs_section( \__( 'Developer Hooks', 'functionalities' ), $hooks_html, 'developer' );
+ }
+ }
+
+ /**
+ * Detected assumptions field callback.
+ *
+ * @return void
+ */
+ public static function field_detected_assumptions(): void {
+ $assumptions = \Functionalities\Features\Assumption_Detection::get_detected_assumptions();
+ $ignored = \Functionalities\Features\Assumption_Detection::get_ignored_assumptions();
+
+ // Filter out expired ignored items and already-ignored assumptions.
+ $active_warnings = array();
+ foreach ( $assumptions as $assumption ) {
+ $hash = self::generate_warning_hash( $assumption );
+ // Skip if ignored and not expired.
+ if ( isset( $ignored[ $hash ] ) && $ignored[ $hash ]['expires'] > time() ) {
+ continue;
+ }
+ $assumption['_hash'] = $hash;
+ $active_warnings[] = $assumption;
+ }
+
+ if ( empty( $active_warnings ) ) {
+ echo '';
+ echo ' ';
+ echo '' . \esc_html__( 'No assumption changes detected. All monitored items are consistent.', 'functionalities' ) . ' ';
+ echo '
';
+ return;
+ }
+
+ echo '';
+
+ foreach ( $active_warnings as $assumption ) {
+ $hash = $assumption['_hash'];
+ $warning_type = $assumption['type'] ?? 'unknown';
+ $type_class = 'warning';
+ $icon = 'dashicons-warning';
+
+ echo '
';
+ echo '';
+
+ // Actions.
+ echo '
';
+ echo '';
+ echo \esc_html__( 'Dismiss', 'functionalities' );
+ echo ' ';
+ echo '';
+ echo \esc_html__( 'Snooze 7 days', 'functionalities' );
+ echo ' ';
+ echo '';
+ echo \esc_html__( 'Ignore permanently', 'functionalities' );
+ echo ' ';
+ echo '
';
+
+ echo '
';
+ }
+
+ echo '
';
+
+ // Inline script for AJAX actions.
+ $nonce = \wp_create_nonce( 'functionalities_assumptions' );
+ ?>
+
+ false ) );
+
+ // Handle enable/disable toggle.
+ if ( isset( $_POST['functionalities_task_manager_toggle'] ) && \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'functionalities_task_manager_toggle' ) ) {
+ $tm_opts['enabled'] = ! empty( $_POST['enabled'] );
+ \update_option( 'functionalities_task_manager', $tm_opts );
+ echo '' . \esc_html__( 'Settings saved.', 'functionalities' ) . '
';
+ }
+ ?>
+
+
+
+
+
+
+
+
+
+
+ ›
+
+
+
+
+ ›
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ' . \esc_html( WP_CONTENT_DIR . '/functionalities/tasks/' ) . ''
+ );
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $project ) :
+ $stats = \Functionalities\Features\Task_Manager::get_stats( $project );
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
>
+
+
+
+
+
+ !
+
+
+
+ #
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false ) );
+
+ // Handle enable/disable toggle.
+ if ( isset( $_POST['functionalities_redirect_manager_toggle'] ) && \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'functionalities_redirect_manager_toggle' ) ) {
+ $rm_opts['enabled'] = ! empty( $_POST['enabled'] );
+ \update_option( 'functionalities_redirect_manager', $rm_opts );
+ echo '' . \esc_html__( 'Settings saved.', 'functionalities' ) . '
';
+ }
+ if ( isset( $_POST['functionalities_redirect_monitor_settings'] ) && \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'functionalities_redirect_monitor_settings' ) ) {
+ $rm_opts['monitor_404'] = ! empty( $_POST['monitor_404'] );
+ $rm_opts['monitor_cap'] = max( 25, min( 2000, (int) ( $_POST['monitor_cap'] ?? 500 ) ) );
+ $rm_opts['monitor_retention_days'] = max( 1, min( 365, (int) ( $_POST['monitor_retention_days'] ?? 30 ) ) );
+ $rm_opts['monitor_exclusions'] = \sanitize_textarea_field( \wp_unslash( $_POST['monitor_exclusions'] ?? '' ) );
+ \update_option( 'functionalities_redirect_manager', $rm_opts );
+ echo '' . \esc_html__( '404 monitor settings saved.', 'functionalities' ) . '
';
+ }
+ $not_found = \Functionalities\Features\Redirect_Manager::get_404_log();
+ ?>
+
+
+
+
+
+
+
+
+
+
+ ›
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 301
+ 302
+ 307
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ' . \esc_html__( 'Settings saved.', 'functionalities' ) . '
';
+ }
+ ?>
+
+
+
+
+
+
+
+
+
+
+ ›
+
+
+
+
+
+
+
+
+ onchange="this.form.submit()" />
+
+
+
+
+
+
+
+ ';
+ foreach ( $docs['features'] as $feature ) {
+ $list .= '
' . \esc_html( $feature ) . ' ';
+ }
+ $list .= '';
+ Admin_UI::render_docs_section( \__( 'What This Module Does', 'functionalities' ), $list, 'info' );
+ }
+ if ( ! empty( $docs['usage'] ) ) {
+ Admin_UI::render_docs_section( \__( 'How to Use', 'functionalities' ), '
' . \esc_html( $docs['usage'] ) . '
', 'usage' );
+ }
+ if ( ! empty( $docs['hooks'] ) ) {
+ $hooks_html = '
';
+ foreach ( $docs['hooks'] as $hook ) {
+ $hooks_html .= '' . \esc_html( $hook['name'] ) . ' ';
+ $hooks_html .= '' . \esc_html( $hook['description'] ) . ' ';
+ }
+ $hooks_html .= ' ';
+ Admin_UI::render_docs_section( \__( 'Developer Hooks', 'functionalities' ), $hooks_html, 'developer' );
+ }
+ ?>
+
+
+
+
+
+
+
()
+
+
+
+
+
+ $icon ) : ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true,
+ 'class' => true,
+ 'style' => true,
+ 'fill' => true,
+ 'fill-opacity' => true,
+ 'fill-rule' => true,
+ 'stroke' => true,
+ 'stroke-width' => true,
+ 'stroke-linecap' => true,
+ 'stroke-linejoin' => true,
+ 'stroke-dasharray' => true,
+ 'stroke-dashoffset' => true,
+ 'stroke-opacity' => true,
+ 'opacity' => true,
+ 'transform' => true,
+ 'clip-path' => true,
+ 'clip-rule' => true,
+ 'mask' => true,
+ );
+
+ return array(
+ 'svg' => array_merge(
+ $common_attrs,
+ array(
+ 'xmlns' => true,
+ 'xmlns:xlink' => true,
+ 'viewbox' => true,
+ 'width' => true,
+ 'height' => true,
+ 'aria-hidden' => true,
+ 'role' => true,
+ 'focusable' => true,
+ 'preserveaspectratio' => true,
+ 'version' => true,
+ 'xml:space' => true,
+ 'enable-background' => true,
+ )
+ ),
+ 'g' => $common_attrs,
+ 'path' => array_merge( $common_attrs, array( 'd' => true ) ),
+ 'circle' => array_merge(
+ $common_attrs,
+ array(
+ 'cx' => true,
+ 'cy' => true,
+ 'r' => true,
+ )
+ ),
+ 'ellipse' => array_merge(
+ $common_attrs,
+ array(
+ 'cx' => true,
+ 'cy' => true,
+ 'rx' => true,
+ 'ry' => true,
+ )
+ ),
+ 'rect' => array_merge(
+ $common_attrs,
+ array(
+ 'x' => true,
+ 'y' => true,
+ 'width' => true,
+ 'height' => true,
+ 'rx' => true,
+ 'ry' => true,
+ )
+ ),
+ 'line' => array_merge(
+ $common_attrs,
+ array(
+ 'x1' => true,
+ 'y1' => true,
+ 'x2' => true,
+ 'y2' => true,
+ )
+ ),
+ 'polyline' => array_merge( $common_attrs, array( 'points' => true ) ),
+ 'polygon' => array_merge( $common_attrs, array( 'points' => true ) ),
+ 'defs' => array( 'id' => true ),
+ 'clippath' => array( 'id' => true ),
+ 'mask' => array( 'id' => true ),
+ 'use' => array(
+ 'xlink:href' => true,
+ 'href' => true,
+ 'x' => true,
+ 'y' => true,
+ 'width' => true,
+ 'height' => true,
+ ),
+ 'symbol' => array(
+ 'id' => true,
+ 'viewbox' => true,
+ 'preserveaspectratio' => true,
+ ),
+ 'title' => array(),
+ 'desc' => array(),
+ 'lineargradient' => array(
+ 'id' => true,
+ 'gradientunits' => true,
+ 'gradienttransform' => true,
+ 'spreadmethod' => true,
+ 'x1' => true,
+ 'y1' => true,
+ 'x2' => true,
+ 'y2' => true,
+ ),
+ 'radialgradient' => array(
+ 'id' => true,
+ 'gradientunits' => true,
+ 'gradienttransform' => true,
+ 'spreadmethod' => true,
+ 'cx' => true,
+ 'cy' => true,
+ 'r' => true,
+ 'fx' => true,
+ 'fy' => true,
+ ),
+ 'stop' => array(
+ 'offset' => true,
+ 'stop-color' => true,
+ 'stop-opacity' => true,
+ 'style' => true,
+ ),
+ );
+ }
+
+ // -------------------------------------------------------------------------
+ // PWA Field Renderers
+ // -------------------------------------------------------------------------
+
+ /**
+ * Render a media upload field.
+ *
+ * @param string $name Input name attribute.
+ * @param string $value Current URL value.
+ * @param string $desc Description text.
+ * @return void
+ */
+ private static function render_media_field( string $name, string $value, string $desc = '' ): void {
+ $id = 'func-media-' . \sanitize_key( str_replace( array( '[', ']' ), '-', $name ) );
+ echo '';
+ }
+
+ /**
+ * Render snippets repeater field for a given location.
+ *
+ * @since 1.4.0
+ *
+ * @param string $location Location key: header, body_open, or footer.
+ * @param string $hook_name WordPress hook name for description.
+ * @return void
+ */
+ public static function field_snippets_repeater( string $location, string $hook_name ): void {
+ $o = self::get_snippets_options();
+ $items = ! empty( $o[ $location ] ) && \is_array( $o[ $location ] ) ? $o[ $location ] : array();
+ $cid = 'func-snippets-' . $location;
+
+ echo '';
+ $i = 0;
+ foreach ( $items as $item ) {
+ self::render_snippet_row( $location, $i, $item );
+ ++$i;
+ }
+ echo '
';
+
+ echo '';
+ echo \esc_html__( '+ Add Snippet', 'functionalities' ) . ' ';
+ echo '' . \sprintf(
+ /* translators: %s: WordPress hook name */
+ \esc_html__( 'Output in %s. Each snippet can be individually toggled.', 'functionalities' ),
+ '' . \esc_html( $hook_name ) . ''
+ ) . '
';
+
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Inline JS for repeater template, encoded via wp_json_encode.
+ echo '';
+ }
+
+ /**
+ * Render a single snippet repeater row.
+ *
+ * @since 1.4.0
+ *
+ * @param string $location Location key.
+ * @param int $index Row index.
+ * @param array $data Snippet data (label, code, enabled).
+ * @return void
+ */
+ private static function render_snippet_row( string $location, int $index, array $data ): void {
+ $label = $data['label'] ?? '';
+ $code = $data['code'] ?? '';
+ $enabled = ! empty( $data['enabled'] );
+ $prefix = 'functionalities_snippets[' . $location . '][' . $index . ']';
+ $has_code = '' !== $code;
+
+ echo '';
+
+ // Header bar — always visible.
+ echo '';
+
+ // Body — collapsible.
+ echo '
';
+ echo '' . \esc_textarea( $code ) . ' ';
+ echo '
';
+
+ echo '
';
+ }
+
+ /**
+ * Detect snippet type from code for badge display.
+ *
+ * @since 1.4.0
+ *
+ * @param string $code Snippet code.
+ * @return string Badge label.
+ */
+ private static function snippet_type_badge( string $code ): string {
+ $code_lower = strtolower( ltrim( $code ) );
+ if ( 0 === strpos( $code_lower, '