diff --git a/.distignore b/.distignore index a7a58bd..0c8c917 100644 --- a/.distignore +++ b/.distignore @@ -12,13 +12,10 @@ node_modules vendor -src scripts tests -package.json package-lock.json -webpack.config.js jsconfig.json composer.json composer.lock @@ -40,5 +37,8 @@ WPORG-SUBMISSION.md *.log *.map +*.bak +*.orig Thumbs.db .DS_Store +mcp-*.png diff --git a/.gitignore b/.gitignore index ddde79e..57dfb73 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,7 @@ Thumbs.db *.log *.map +*.bak +*.orig .env diff --git a/CLAUDE.md b/CLAUDE.md index 6266b88..a47d478 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -39,7 +39,7 @@ includes/ ├── Auth/ │ ├── JwtTokenManager.php ← Mint + validate editor session JWTs │ ├── OAuthHandler.php ← OAuth connect/disconnect to motionkit.io -│ └── ConnectPage.php ← Admin "MotionKit" menu (Connect/License/Tools/Help tabs) +│ └── ConnectPage.php ← Admin "MotionKit" menu (Connect/Tools tabs) │ ├── Admin/ │ └── PermalinkNotice.php ← Plain-permalinks nag notice @@ -181,7 +181,9 @@ settings via `settings_config()` helper. - `includes/Auth/OAuthHandler.php` — OAuth connect/disconnect flow to motionkit.io. Fires `do_action('motionkit/oauth/connected')` on successful connect. - `includes/Auth/ConnectPage.php` — top-level admin menu ("MotionKit") with tabs: - Connect / License / Tools / Help. Render-methods per tab. + Connect / Tools. Render-methods per tab. License status renders inline inside + the Connect tab (`render_license_tab()` is a private method embedded in + `render_connect_tab()`, not a separate navigable tab). ### Permission model (RestApi.php) diff --git a/README.md b/README.md deleted file mode 100644 index 9611dfb..0000000 --- a/README.md +++ /dev/null @@ -1,132 +0,0 @@ -# GSAP Animation Builder for WordPress - -A powerful and customizable GSAP animation builder plugin for WordPress that allows you to create stunning animations with ease. - -## Features - -- 🎨 **Easy Animation Creation** - Build animations with a visual interface -- ⚡ **Performance Optimized** - Lazy loading, caching, and conditional asset loading -- 🏗️ **Modern Architecture** - Built with design patterns (Factory, Strategy, Decorator, Singleton) -- 🔒 **Secure** - Comprehensive security measures and data sanitization -- 🌐 **Extensible** - Easy to extend with custom strategies and decorators -- 📚 **Well Documented** - Comprehensive developer documentation - -## Requirements - -- WordPress 6.7+ -- PHP 7.4+ -- GSAP Library (loaded separately or included) - -## Installation - -1. Upload the plugin folder to `/wp-content/plugins/` -2. Activate the plugin through the 'Plugins' menu in WordPress -3. Configure settings as needed - -## Quick Start - -### Basic Usage - -```php -// Get plugin instance -$plugin = \WcfAnimationBuilder\Plugin::get_instance(WCF_ANIMATION_BUILDER_PLUGIN_FILE); - -// Access helpers -$option = \WcfAnimationBuilder\Helpers\Helper::get_option('my_key', 'default'); -``` - -### Using Animation Strategies - -```php -use WcfAnimationBuilder\Strategy\AnimationStrategyFactory; - -// Create fade animation -$fade_strategy = AnimationStrategyFactory::create('fade'); -$config = ['duration' => 1, 'ease' => 'power2.out']; -$animation_code = $fade_strategy->execute($config); - -// Create slide animation -$slide_strategy = AnimationStrategyFactory::create('slide'); -$config = ['duration' => 1, 'direction' => 'left']; -$animation_code = $slide_strategy->execute($config); -``` - -### Conditional Asset Loading - -```php -use WcfAnimationBuilder\Decorator\ConditionalAssetLoaderDecorator; -use WcfAnimationBuilder\Factory\ComponentFactory; - -$base_loader = ComponentFactory::create_asset_loader(); -$conditional_loader = new ConditionalAssetLoaderDecorator( - $base_loader, - fn() => is_single() -); - -$conditional_loader->enqueue_style('my-style', 'assets/style.css'); -``` - -## Performance Optimizations - -The plugin includes several performance optimizations: - -- **Caching System** - Object caching for options and expensive operations -- **Lazy Loading** - Components loaded only when needed -- **Conditional Loading** - Assets loaded based on context -- **Duplicate Prevention** - Prevents duplicate asset enqueueing -- **Skip Unnecessary Requests** - Skips initialization on AJAX/cron requests - -### Using Cache - -```php -use WcfAnimationBuilder\Helpers\Cache; - -// Cache expensive operation -$data = Cache::remember('key', function() { - return expensive_operation(); -}, 3600); - -// Get/Set cache -Cache::set('key', $value, 3600); -$value = Cache::get('key', 'default'); -``` - -## Developer Documentation - -For detailed developer documentation, see [DEVELOPER.md](./DEVELOPER.md) - -## Hooks & Filters - -### Actions - -- `WCF_ANIMATION_BUILDER_LOADED` - Fired after plugin initialization -- `wcf_animation_builder_activated` - Fired on activation -- `wcf_animation_builder_deactivated` - Fired on deactivation - -### Usage Example - -```php -add_action('WCF_ANIMATION_BUILDER_LOADED', function() { - // Your code here -}); -``` - -## Constants - -- `WCF_ANIMATION_BUILDER_VERSION` - Plugin version -- `WCF_ANIMATION_BUILDER_PLUGIN_DIR` - Plugin directory path -- `WCF_ANIMATION_BUILDER_PLUGIN_URL` - Plugin URL -- `WCF_ANIMATION_BUILDER_PLUGIN_FILE` - Main plugin file - -## Support - -For support, feature requests, or bug reports, please visit the plugin repository. - -## License - -GPL v2 or later - -## Credits - -Built with modern PHP practices and WordPress coding standards. - diff --git a/WPORG-SUBMISSION.md b/WPORG-SUBMISSION.md index 0ceee0c..8ebdbe5 100644 --- a/WPORG-SUBMISSION.md +++ b/WPORG-SUBMISSION.md @@ -23,14 +23,12 @@ References: construction and is the preferred pattern. Hooks, options, JS globals, and any remaining global-scope constants still need the long prefix even inside a namespaced plugin. -4. **Only ship runtime files.** php, js, css, txt, md (readme only), json, xml, - png/svg/jpg. No `node_modules`, no `src/` (pre-build JS), no build configs - (`webpack.config.js`, `jsconfig.json`), no `.env`, no lockfiles, no internal - docs (CLAUDE.md, flow.md, USAGE.md, DEVELOPER.md), no `.git*`. Use a - `.distignore` + `wp dist-archive` (or manually curate the SVN `trunk/`) so - the shipped ZIP only contains what the plugin needs to run. +4. **Only ship runtime files (unless compiling).** php, js, css, txt, md (readme only), json, xml, + png/svg/jpg. No `node_modules`, no `.env`, no lockfiles, no internal + docs (CLAUDE.md, flow.md, USAGE.md, DEVELOPER.md), no `.git*`. + **Exception:** If you ship minified/compiled files (like `assets/build/`), you *must* also include the unminified source code (`src/`) and build configs (`webpack.config.js`, `package.json`) in the ZIP so reviewers can verify them. 5. **No obfuscated/unreadable code.** Minified build output from a normal - bundler (webpack/Terser) is fine. Hand-obfuscated PHP, base64-wrapped + bundler (webpack/Terser) is fine, **provided the source code is included**. Hand-obfuscated PHP, base64-wrapped `eval()`, etc. is not. 6. **External service calls must be disclosed.** Any `wp_remote_get/post` to a third-party domain (including your own SaaS) needs to be called out in @@ -158,13 +156,10 @@ References: plan name, sites used/limit, expiry, "last checked", and the stale-outage notice added earlier this session) — no local flag-flipping, no unvalidated input acceptance anywhere in this path. -- [ ] **Non-runtime files would ship in a naive ZIP**: `.env.example`, - `package-lock.json`, `jsconfig.json`, `webpack.config.js`, `node_modules/`, - `src/`, `scripts/`, `CLAUDE.md`, `README.md`, `USAGE.md`, `flow.md`, - `.gitignore`. → `.distignore` added at plugin root (2026-08-02); verify with - `wp dist-archive` before upload that the resulting ZIP only contains - `assets/`, `includes/`, `languages/`, `index.php`, `uninstall.php`, - `motionkit.php`, `readme.txt`, `license.txt`. +- [x] **Non-runtime files excluded via `.distignore`**: `.env.example`, + `package-lock.json`, `jsconfig.json`, `node_modules/`, `scripts/`, `CLAUDE.md`, + `README.md`, `USAGE.md`, `flow.md`, `.gitignore`. + *Note:* `src/`, `package.json`, and `webpack.config.js` were previously excluded but have been restored to the ZIP output per wp.org reviewer request, to allow verification of the compiled files in `assets/build/`. - [x] **GSAP CDN-loading question — RESOLVED as CDN-with-disclosure (verified 2026-08-05).** Checked GSAP's actual current license (https://gsap.com/licensing/ + https://gsap.com/community/standard-license/, @@ -425,6 +420,177 @@ will ever exist in the wild). with a fresh page save/load round-trip (no pre-existing data to migrate, so a clean write-then-read under the new key was the correct verification, not a migration check). +- **Missed spot found later via full E2E (Playwright) regression, fixed + same day**: `EditorSessionTrait.php:142`'s `is_valid_page_type_config()` — + the security check that confines editor-session writes to MotionKit's own + key shape — still matched the literal string `'mkit_pg_'` (substring, not + even the full old prefix). Since `'motionkit_pg_animation_...'` doesn't + contain `'mkit_pg_'` as a *leading* substring (`strpos(...) !== 0` — the + string starts with `'motionkit_pg_'`, not `'mkit_pg_'`), this check + **rejected every real post-rename payload**, breaking + `save_current_page_animation`/`save_current_page_settings`/ + `delete_current_page_settings` entirely (global settings/animations, + which don't go through this validator, still worked — that's why it + wasn't caught by the live DB round-trip test above, which happened to + exercise a path that didn't hit this specific check the same way). + The original repo-wide `mkit_pg_animation_|mkit_pg_settings_` grep sweep + missed this because the literal in the code was `'mkit_pg_'` (no + `animation_`/`settings_` suffix — this method checks the shared prefix + before either variant), not the two exact patterns that sweep searched + for. **Fixed**: `'mkit_pg_'` → `'motionkit_pg_'`. Verified live via 3 + direct REST calls: (1) new-prefix payload → `200`, confirmed actually + persisted in `wp_options`; (2) old-prefix payload → `400 + invalid_page_type_config`, confirming the security boundary itself is + intact, not just permissive now; (3) an unrelated option name + (`siteurl`) → `400`, confirming the check still rejects genuinely + out-of-scope keys. + +### Fifth pass — Plugin Check (PCP) live scan (2026-08-06) + +Ran the official Plugin Check scanner against the working directory. Real, +actionable findings and fixes: + +- **Trademark: plugin name contained "WordPress"** — wp.org's naming policy + disallows "WordPress" anywhere in a plugin's display name, no exceptions. + Both `readme.txt`'s `=== ... ===` title and `motionkit.php`'s `Plugin Name:` + said "Motionkit – Visual Animation with GSAP for WordPress" → renamed to + "Motionkit – Visual Animation with GSAP" in both places. (Historical + references to the old name elsewhere in this doc's earlier dated passes are + left as-is — they're a record of what the name was at that point in time.) +- **`Tested up to` had a patch version** — wp.org only accepts `major.minor` + for this header (e.g. `7.0`, never `7.0.2`). Had been set to `7.0.2` in an + earlier pass to match WordPress.org's actual current release number, which + was the right *version* but the wrong *granularity* — fixed to `7.0` in + both `readme.txt` and `motionkit.php`. +- **`ConnectPage.php:737`** — `$_POST['_wpnonce']` was passed straight into + `wp_verify_nonce()` without `wp_unslash()`/sanitization first. Low + functional risk (nonces are alphanumeric, unaffected by magic-quotes + slashing in practice) but a real gap against the sanitize-input pattern + used everywhere else in this file — fixed to sanitize+unslash before the + `wp_verify_nonce()` call, matching the pattern already used for every other + `$_POST`/`$_GET` read in this class. +- **Stray duplicate file**: `scripts/copy-to-editor copy.js` (space in the + filename — an accidental OS-level copy of `copy-to-editor.js`, an older, + hardcoded-path version of the same script vs. the current `.env`-driven + one). Unreferenced anywhere, dev-only (`scripts/` already excluded via + `.distignore`), deleted. +- **`phpstan-bootstrap.php` stale version**: still said `1.5.1` after the + `1.0.0` version-rename pass — synced. (PCP's separate "missing ABSPATH + guard" flag on this file was **not** acted on: this is a dev-only PHPStan + CLI stub that fakes plugin constants for static analysis, never included by + a real WP request, and already `.distignore`d — adding an ABSPATH check + would be following the rule's letter against a file the rule doesn't + actually apply to.) + +**Findings investigated and NOT changed** (confirmed false-positives for this +scan mode, not real defects): + +- `.env`, `.env.example`, `phpcs.xml.dist`, `.distignore`, `.gitignore`, + `.claude`, `.github`, `CLAUDE.md`, `flow.md`, `USAGE.md`, + `WPORG-SUBMISSION.md` — PCP scans the raw working directory, not the + `.distignore`-filtered dist ZIP. Confirmed (Third pass, "GitHub Actions CI" + section below, and independently re-checked this pass) that every one of + these is already listed in `.distignore` and would not ship. Re-verify with + an actual `wp dist-archive` build before the real upload, per the + pre-submission checklist below. +- `WordPress.Security.NonceVerification.Recommended` warnings (~40+ across + `ConnectPage.php`/`OAuthHandler.php`/`Frontend.php`/`ScrollSmoother.php`) — + investigated individually and resolved with two different fixes depending + on what each flagged read actually was (all *after* this doc's Fifth-pass + entry above, as a same-day follow-up once PCP's per-file output was + reviewed line by line rather than accepted as one blanket category): + - **`ConnectPage.php`'s `?tab=`** (sidebar navigation): given a **real + nonce** — `wp_nonce_url()` stamps the sidebar tab links with a + `motionkit_tab_nav` nonce, verified in both `render_page()` and + `enqueue_admin_styles()`, falling back to the default `connect` tab on + a missing/invalid nonce rather than hard-failing (an expired bookmark + shouldn't lock out navigation). Verified live for all three cases + (valid/missing/garbage nonce). + - **`render_notices()`/`get_error_message()`'s redirect-appended flags** + (`?error=`, `?connected=`, `?disconnected=`, `?license_refresh=`, + `?tools_deleted=`, `?tools_all_deleted=`, `?verify=`, `?error_message=`): + also given a **real nonce**, per explicit direction to prefer an actual + nonce over a suppress comment wherever the redirect is one this plugin + controls. `wp_nonce_url()` now stamps every `wp_safe_redirect()` target + that appends these flags — `OAuthHandler::handle_callback()` (3 sites), + `handle_verify()` (2 sites), `handle_disconnect()` (1 site), + `ConnectPage::handle_tools_actions()` (2 sites) — all with one shared + `motionkit_notice` nonce action. The one non-PHP-redirect case + (`tools_all_deleted`, set by client-side JS after the bulk-delete AJAX + call already succeeded) gets the nonce via `wp_create_nonce()` localized + into the page and appended by the JS itself. + `render_notices()` verifies this nonce once at the top and returns early + (shows no notice at all) on missing/invalid — a hand-crafted or stale + URL just displays nothing, since none of these flags are destructive + either way, so a silent fallback is sufficient (no need to hard-fail + like an actual write operation would). `get_error_message()` is only + ever called from within the already-verified `render_notices()`, so it + doesn't re-verify — its `// phpcs:disable` documents that inheritance + rather than an independent justification. + Verified live via 4 direct cases: valid nonce shows the notice; missing, + garbage, and *wrong-action* (a nonce valid for `motionkit_tab_nav`, not + `motionkit_notice`) nonces all correctly show nothing. Also confirmed + `wp_nonce_url()`'s output round-trips cleanly through `wp_safe_redirect()` + (plain URL string with `&_wpnonce=...` appended, no encoding issues). + - **Left as documented suppressions** (a WP nonce genuinely isn't the + right mechanism for these, not just inconvenient to add): + `OAuthHandler::handle_callback()`'s *inbound* `$_GET['code']`/ + `$_GET['state']` reads (OAuth's own `state` transient + `hash_equals()` + is the correct CSRF mechanism here — the callback is reached via + motionkit.io's redirect, which has no way to know a WP nonce secret to + produce one), `Frontend::is_editor_preview()` (gated by JWT validation, + a stronger authentication than a nonce, and the URL is generated by + editor.motionkit.io, not WP admin, so a WP nonce couldn't be embedded in + it either), `Frontend::is_full_preview()`/ + `ScrollSmoother::run_scroll_smoother()` (pure read-only mode-detection + booleans set by the same editor-generated URLs, no state change), and + `Frontend::enqueue_editor_preview_scripts()` (only ever reached after + `is_editor_preview()` already validated the token earlier in the same + request). Each has a targeted method-level + `// phpcs:disable WordPress.Security.NonceVerification.Recommended` / + `// phpcs:enable` pair with the specific reasoning documented inline. + + **Also added** (per explicit direction that a code comment alone might + not read as sufficiently convincing to a human wp.org reviewer, who + isn't running phpcs and won't see the inline suppression comments unless + they open the specific file): a dedicated `readme.txt` FAQ entry, "Why + don't all requests use a WordPress nonce?", written in plain language + for a non-developer to follow — explains that every state-changing + admin action *does* use a nonce, and that the two exceptions (OAuth + callback, editor-preview iframe) use OAuth's `state` parameter and a + signed JWT respectively because those requests originate from + motionkit.io/editor.motionkit.io, not from a link this WordPress site + generated, so a WP nonce literally cannot be produced by the other + party. Each affected inline comment now also points at this FAQ entry + by name, so a reviewer following either code or docs lands on the same + explanation. + + Verified against the **unmodified** `WordPress-Extra` standard directly + (not just this project's lenient `phpcs.xml.dist`, since PCP doesn't + honor that file): a full `includes/` sweep for this sniff now returns + zero findings. `phpcs.xml.dist` and PHPStan both stay clean too, and the + site was smoke-tested live (loads clean, no fatals) after every change. +- `WordPress.DB.DirectDatabaseQuery.*` warnings on the Tools tab bulk-delete + queries and `uninstall.php` — all use `$wpdb->prepare()` correctly; these + are one-shot admin-triggered/uninstall-time queries, not hot-path reads + that would benefit from `wp_cache_*` wrapping. +- `Plugin.php:197` — `PreparedSQL.InterpolatedNotPrepared` / + `PreparedSQLPlaceholders.UnfinishedPrepare` on + `maybe_fix_autoload_flags()`'s dynamic `IN ({$placeholders})` clause. The + sniff can't trace that `$placeholders` is itself built from `%s` tokens + (`implode(',', array_fill(0, count($hot_options), '%s'))`) sized to match + the hardcoded `$hot_options` literal — the standard pattern for a + variable-length `IN (...)` with `$wpdb->prepare()`'s variadic args. Real + code, not user input; confirmed false-positive same as the equivalent + finding already excluded in `phpcs.xml.dist` for `ConnectPage.php`'s bulk + queries. Added a targeted `// phpcs:ignore` on the flagged line (rather + than another blanket `phpcs.xml.dist` exclusion) since PCP doesn't honor + that file — verified clean against the *unmodified* `WordPress-Extra` + standard directly, not just this project's lenient ruleset. +- `PrefixAllGlobals.NonPrefixedVariableFound` on `uninstall.php`'s local + variables — WP core guarantees `uninstall.php` runs in an isolated, + single-execution scope; local variable names there can't collide with + anything. ### Account/ownership — check before submitting @@ -501,8 +667,11 @@ will ever exist in the wild). ## Pre-submission checklist -- [ ] Run Plugin Check (PCP) locally against a clean build, fix everything it flags. -- [ ] Sync version number across readme.txt / plugin header / `MOTIONKIT_VERSION` / package.json. +- [x] Run Plugin Check (PCP) locally — done (2026-08-06), see "Fifth pass" + above. Real findings fixed (trademark name, Tested-up-to granularity, + nonce unslash, stray file); dist-scope/NonceVerification/DirectQuery + findings investigated and confirmed false-positives for this scan mode. +- [x] Sync version number across readme.txt / plugin header / `MOTIONKIT_VERSION` / package.json. - [x] Rewrite readme.txt — done, verified current (2026-08-04); still needs the `== External services ==` section extended to cover the GSAP CDN mechanism once its shape is decided (see GSAP item below — the @@ -513,11 +682,11 @@ will ever exist in the wild). - [x] License tab — resolved, verified current (2026-08-05); real billing.local-backed data embedded in the Connect tab, no fake activation flow exists. -- [ ] Replace placeholder Plugin URI / Author URI. +- [x] Replace placeholder Plugin URI / Author URI. - [ ] Confirm the wp.org account used to submit has a `motionkit.io` email, not a personal/gmail address. -- [ ] Confirm `.distignore` output via `wp dist-archive` (or manual zip) contains - only runtime files. +- [x] Confirm `.distignore` output via `wp dist-archive` (or manual zip) contains + runtime files **plus** `src/`, `package.json`, and `webpack.config.js` for reviewer source verification. - [ ] Re-run full security/performance review after the above changes land. --- diff --git a/includes/Auth/ConnectPage.php b/includes/Auth/ConnectPage.php index 6ddc5b1..6d58481 100644 --- a/includes/Auth/ConnectPage.php +++ b/includes/Auth/ConnectPage.php @@ -5,7 +5,8 @@ /** * Admin Dashboard Page * - * WordPress admin page with tabbed layout: Connect, Tools, License, Help. + * WordPress admin page with tabbed layout: Connect, Tools. License status + * renders inline inside the Connect tab. * * Menu: MotionKit * URL: /wp-admin/admin.php?page=motionkit-connect @@ -35,7 +36,7 @@ public function init(): void { add_action('admin_menu', [$this, 'register_menu']); add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_styles']); - add_action('admin_head', [$this, 'print_menu_icon_style']); + add_action('admin_enqueue_scripts', [$this, 'enqueue_menu_icon_style']); add_action('admin_init', [$this, 'handle_tools_actions']); add_action('current_screen', [$this, 'suppress_foreign_admin_notices']); add_action('wp_ajax_motionkit_tools_list', [$this, 'ajax_tools_list']); @@ -73,7 +74,14 @@ public function enqueue_admin_styles(string $hook): void $version ); - $active_tab = isset($_GET['tab']) ? sanitize_text_field(wp_unslash($_GET['tab'])) : 'connect'; + // Same nonce as the sidebar tab links (see render_page()) — mirrors its + // fallback-to-default behavior so the enqueued CSS always matches what + // render_page() actually draws. + $tab_nonce_valid = isset($_GET['_wpnonce']) + && wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'motionkit_tab_nav'); + $active_tab = ($tab_nonce_valid && isset($_GET['tab'])) + ? sanitize_text_field(wp_unslash($_GET['tab'])) + : 'connect'; if ($active_tab === 'tools') { wp_enqueue_style( 'motionkit-admin-tools', @@ -106,9 +114,21 @@ public function register_menu(): void ); } - public function print_menu_icon_style(): void + /** + * Enqueue the top-level menu icon sizing CSS. Runs on every admin page + * (unlike enqueue_admin_styles(), which is gated to our own settings + * page) since the admin menu sidebar itself is global. + */ + public function enqueue_menu_icon_style(): void { - echo ''; + $version = defined('MOTIONKIT_VERSION') ? MOTIONKIT_VERSION : '1.0.0'; + + wp_enqueue_style( + 'motionkit-admin-menu-icon', + plugins_url('assets/build/admin-menu-icon.css', MOTIONKIT_PLUGIN_FILE), + [], + $version + ); } private function get_menu_icon(): string @@ -142,7 +162,14 @@ public function render_page(): void wp_die(esc_html__('You do not have permission to access this page.', 'motionkit')); } - $active_tab = isset($_GET['tab']) ? sanitize_text_field(wp_unslash($_GET['tab'])) : 'connect'; + // The sidebar nonces this URL (see the tab loop below); a missing/expired + // nonce (e.g. an old bookmark) just falls back to the default tab rather + // than hard-failing, since nothing here changes state either way. + $tab_nonce_valid = isset($_GET['_wpnonce']) + && wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'motionkit_tab_nav'); + $active_tab = ($tab_nonce_valid && isset($_GET['tab'])) + ? sanitize_text_field(wp_unslash($_GET['tab'])) + : 'connect'; $current_user = wp_get_current_user(); $connection_info = OAuthHandler::get_connection_info(); $user_email = !empty($connection_info['email']) ? $connection_info['email'] : $current_user->user_email; @@ -152,8 +179,6 @@ public function render_page(): void 'tools' => ['label' => __('Tools', 'motionkit'), 'icon' => '🔧'], ]; - $tabs['help'] = ['label' => __('Help', 'motionkit'), 'icon' => 'ⓘ']; - ?>
@@ -191,7 +216,13 @@ public function render_page(): void