Skip to content

v1.10.1 - #789

Merged
ilicfilip merged 31 commits into
mainfrom
develop
Sep 24, 2026
Merged

ilicfilip merged 31 commits into
mainfrom
develop

Conversation

@ilicfilip

Copy link
Copy Markdown
Collaborator

No description provided.

tacoverdo and others added 30 commits August 27, 2026 16:30
Show a notice that support for the plugin is ending, on the Progress
Planner dashboard and on the plugins overview page. The notice thanks
users and links to more information, and lets them download a printable
single-page A4 certificate showing their site, how long they have used
Progress Planner, and all completed badges.

The notice is hidden when the pp-hosts companion plugin is installed or
active, or when a branding ID is defined, since support continues for
hosted users. A progress_planner_show_sunset_notice filter is available
as an escape hatch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There will be no blog post to link to, so the notice now only contains
the thank-you message (plus the certificate link/button).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The plugins-page row used core's `update-message` class, which injects the
red circular-arrows dashicon (\f463) via `.update-message p:before`. That
made the sunset notice visually identical to an "update available" row —
the opposite of what it means.

Drop `update-message` and render a warning dashicon instead. Core keys the
row spacing off `.notice`, not `update-message`, so the layout is unchanged.

The style is inline because `assets/css/admin.css` is only enqueued on
`toplevel_page_progress-planner`, so a rule there would never reach the
plugins page.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011jLxPbbCpRuFZFHMBrSoPq
Exploratory. Nothing reads these files yet; they exist to find out whether
recommendations can be expressed as data rather than PHP, and what the format
would have to carry.

All 49 existing providers written out as markdown, which produced 42 files:
every Yoast recommendation has an All in One SEO twin expressing the same
goal, and once a rule states an outcome instead of a setting, the pair
collapses into one. That collapse is the first evidence the approach works --
one rule, no plugin named, and a model works out how to satisfy it on whatever
the site actually has.

Two findings shaped the format.

verified_by is the load-bearing field. Thirty rules the site can answer for
itself by reading an option, querying content or fetching a URL. Twelve it
cannot: d/m/Y and m/d/Y are both valid date formats, and which is right depends
on who reads the site. For those the goal is that the owner has looked once,
which is exactly what the existing PHP checks by consulting the activity log.
That is not a missing state check; it is the correct check for the goal.

Structured fields are read by the model, not by PHP. The set produced 13
applies_when predicates and 10 target.find keys, most used once, which would be
runaway DSL growth if something had to interpret them. Nothing does. The
structure is precision for the model -- unambiguous and close to the query it
will build -- so a new key costs nothing and needs no release. It also means
incoming_internal_links is expressible at all, which it would not be in PHP
without reading an SEO plugin's link index.

The validator checks what review misses: that the prose and the frontmatter
agree. It found six files where verified_by said owner_confirmation while the
How to verify section described a perfectly good check, because the two axes
-- can the site tell, and should a person decide -- had been conflated. It
deliberately does not check the applies_when or target.find vocabulary, since
constraining those would reintroduce the coupling the format exists to avoid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tjxa3zi5Z7eTKWoaxB3HGT
Turns the rules in /recommendations into real tasks so the format can be
exercised end to end without a server: drop a file in, see the task appear,
have a model act on it. Off unless a site opts in via the
PROGRESS_PLANNER_MARKDOWN_RECOMMENDATIONS constant or the matching filter --
this is a harness, not a feature.

The loader deliberately does not interpret the rule. applies_when and
target.find are read by the model, and building an interpreter here would
recreate the coupling the format exists to remove. The single exception is
any_plugin_active, checked before a task is created: without it a bare site
collects every SEO rule it can never satisfy and the model spends a call
discovering that. Detection reuses the constants and classes the SEO data
collector already knows, so the two cannot disagree about what "Yoast is
active" means.

Verified on a live site: 42 rules parse, 36 become tasks, a second run creates
none, and a rule requiring only an inactive plugin is correctly skipped. The
nine SEO rules list both Yoast and AIOSEO as alternatives, so one being active
satisfies them all.

Two things the testing corrected. The provider prefix was md: and became md-,
because the provider ID ends up as a taxonomy term slug and sanitize_title()
silently drops a colon -- md:foo stored as mdfoo and broke every prefix check
downstream. And the summary pattern terminated on a newline, so it matched
nothing on a wrapped paragraph, which is every paragraph.

Rules now name Yoast by the slug the plugin itself uses, wordpress-seo, rather
than yoast-seo, so no alias table is needed anywhere.

Known gap: list-recommendations drops any task whose provider class is missing,
which is every task created here. Fixing that belongs with the abilities work
on the other branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tjxa3zi5Z7eTKWoaxB3HGT
Each rule now becomes a Markdown_Rule provider, registered through the same
progress_planner_suggested_tasks_providers filter third parties already use.

The previous commit wrote tasks straight into the database with nothing behind
them, which left every consumer needing a special case for "a task whose
provider does not exist" -- the abilities layer drops exactly those, so all 36
were invisible to a model. Registering a provider removes the special case
rather than patching around it: the dashboard renders these, points and badges
count them, capability filtering applies, and the abilities layer lists them
with no change at all. Verified: 36 visible, none dropped.

Everything the interface asks for comes from the rule's frontmatter. The two
methods that cannot are evaluate_task() and is_task_completed(), which always
return false. A goal like "attachment URLs must not be indexable" is satisfied
by a redirect on one site and a header on another, which is why the rule is
prose for a model rather than a condition for PHP, so completion arrives from
outside and is never inferred here.

Verified on a live site: 42 providers built, Tasks_Manager holds 83 without
complaint, 36 tasks inject through the normal pipeline, and each resolves back
to its provider. The six that do not inject are the per_item templates, which
need a model to find their targets first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tjxa3zi5Z7eTKWoaxB3HGT
The `prpl_recommendations` CPT is registered with `show_in_rest => true` but
no `capability_type`/`capabilities`, so it inherited WordPress's default
`post` capabilities. Its REST controller added no permission overrides. This
opened two holes (1.10.0 release audit S1/S2):

S1 (HIGH) — any Contributor/Author (`edit_posts`) could, via core REST:
  - create a task carrying a known recommendation slug in `trash` status;
    `Suggested_Tasks_DB::add()` treats any existing post with that slug as
    "the task already exists" and returns early, so the genuine
    recommendation (e.g. "Perform all updates") is silently suppressed and
    never shown to the admin;
  - create `pending`/`publish` tasks with attacker-chosen titles that are
    then rendered in every admin's dashboard widget.
An Editor could also trash/delete admin-only tasks over REST.

S2 (MEDIUM) — any published task was readable anonymously by ID
(`GET /wp/v2/prpl_recommendations/<id>`), leaking pending-update state,
draft/unpublished titles, and the active SEO plugin. The existing collection
filter (`rest_api_tax_query`) only guards the list route, not single items;
and core's `check_read_permission()` allows any `publish` post to be read
before it consults `read_post`, so capability mapping alone cannot close it.

Fix: override the four `*_permissions_check` methods plus
`get_item_permissions_check` in `Recommendations_Controller` to require
`edit_others_posts` — the exact gate the plugin already uses for its admin UI
(`Base::init()`, `Dashboard_Widget::$capability`). Editors and admins keep
full read/write access; every role below is rejected. Also map the provider
taxonomy's term-write capabilities to the same gate.

Chose controller overrides over remapping the CPT's `capability_type`: it is
surgical, fixes the anonymous single-item read that caps cannot, and avoids
the `map_meta_cap` blast radius on a surface shipped since v1.6.0.

Adds 20 regression tests: contributor/author/subscriber/anonymous are denied
create/read/update/delete (verified red against the unfixed code), while
Editor and admin flows — including creating/deleting their own task and
assigning the provider term — still pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`Branding::get_admin_submenu_position()` declared a `mixed` return type.
`mixed` only became a type in PHP 8.0; on PHP 7.4 it is resolved as a class
name, so returning `-1000` or `null` throws:

    TypeError: Return value of ...::get_admin_submenu_position()
    must be an instance of mixed, int returned

The method is called from `Admin\Page::add_page()` on the `admin_menu` hook,
so this fataled on every wp-admin page view for sites on PHP 7.4 — which the
plugin still advertises support for (`Requires PHP: 7.4`).

Introduced in 1c6684b ("Fix PHP 7.4 linting issue"), which replaced the
union type `int|null` (also 8.0+) with `mixed` — swapping one PHP 8-only
syntax for another. Shipped in v1.10.0; not present in v1.9.1.

Removes the native return type and keeps the `@return int|null` docblock,
which is the only form valid on 7.4.

Why CI did not catch it: the lint job does run PHP 7.4, but `php -l` only
checks syntax, and `(): mixed` is syntactically valid on 7.4 — the TypeError
only fires when the function is called. The PHPUnit matrix, which does
execute code, starts at PHP 8.2, and PHPStan ran against the latest PHP.
All three gates reported green.

Pinning `phpVersion: 70400` in phpstan.neon.dist closes that gap: PHPStan
now resolves `mixed` as an unknown class and fails, catching this and the
rest of the PHP 8-only syntax class statically, at no CI cost. Verified it
flags the original code and passes on the fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Branding::get_branding_id()` read `$_GET['pp_branding_id']` for any request,
with no authentication. The param is a preview seam for branded-host
rendering (added in #633, "auto-register branded websites"), but because the
branding ID also gates the auto-onboard remote call in `Base::init()`, an
anonymous visitor appending `?pp_branding_id=N` could force a non-zero
branding ID and thereby trigger the server-side onboarding request and a
stored license key (1.10.0 release audit S3).

Gate the param on `is_user_logged_in()`. This keeps the preview flow working
for any authenticated user on both the front end and wp-admin (an admin stays
logged in across both), while an anonymous request now falls through to the
host-based default and can no longer steer branding.

Note: the report's broader S3 remedies (re-gating the Base::init() block,
adding an onboarding back-off transient) are intentionally not applied — in
practice sites are redirected to the capability-gated, nonce-checked
onboarding screen on activation, and the SaaS is fast and CDN-cached, so the
blocking-call/DoS concern does not apply. The unauthenticated param was the
only real defect.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`rest_api_tax_query()` passed `$request['provider']` and
`$request['exclude_provider']` straight to `explode()`. When the param
arrives as an array (`?provider[]=a&provider[]=b`) that raised a PHP 8
TypeError and a 500 response (1.10.0 release audit S7.1).

Route both through a new `parse_provider_param()` helper that branches on the
input type — comma-splitting a string, casting an array — and sanitises each
slug with `sanitize_key()`.

Note: with the recommendations endpoint now gated to `edit_others_posts`
(see the REST-permissions commit), this path is no longer reachable
anonymously, so the fix hardens the request handling for authorised callers
rather than closing an unauthenticated 500.

Adds 3 regression tests (array `provider[]`, array `exclude_provider[]`, and
the string form), verified red against the pre-fix code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The email-sending provider's `init()` minted a fresh task-completion token on
every request (it runs on the `init` hook, front end included). Because the
token lives in a single per-task/per-user transient, this overwrote the token
that `ajax_test_email_sending()` stored when the test email was actually sent
— so the "mark as completed" link in the user's inbox was invalidated by the
next request (a Heartbeat poll or page navigation), and clicking it silently
did nothing (1.10.0 release audit B2).

The token generated in `init()` was never used: the AJAX handler builds and
sends its own email body with a freshly generated token at send time, and
`$this->email_content` (the property `init()` populated) has no readers. The
token block was authored redundantly alongside the handler's own generation
and never consumed.

Remove the token generation and `$this->email_content` build from `init()`,
and drop the now-unused property. `$this->email_subject` stays — it is used by
`wp_mail()` and localized to the popover view. This also removes a needless
`set_transient` + `sprintf` from every front-end request.

Sending a new test email still (correctly) mints a new token and supersedes
the previous link.

Adds a regression test asserting `init()` no longer overwrites a token stored
by the send flow (verified red against the pre-fix code).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The set-page interactive task (About / Contact / FAQ) could be completed with
"I have this page" chosen but no page actually selected. The handler only
checked that have_page was non-empty, so a submission with have_page = 'yes'
and id = 0 was saved and marked successful — recording that a page exists
while pointing at no page (1.10.0 release audit B8).

Reject have_page = 'yes' when no valid page id (>= 1) is supplied. The other
two options ("I don't have this page yet" and "My site doesn't need this
page") legitimately carry no page id and are unaffected.

Adds regression tests: yes-without-page is rejected (verified red against the
pre-fix code), while yes-with-a-real-page and the no-page options still
complete.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The set-page popover passes 'context' => 'popover' to the page-select view,
but the view compared it against 'popovers' (plural). The check never matched,
so the "Create this page" link always fell through to target="_blank" and
opened a new tab even when the user was already inside the popover flow
(1.10.0 release audit B10).

Match the strings: the view now checks 'popover', so the link opens _self
from within the popover as intended. No test — this is a rendered-template
attribute with no unit-test seam; verified via the full suite and lint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The monthly badge computed its activity window with
gmdate( 't', strtotime( $month ) ), where $month is the badge NAME (e.g.
"Felix February"), not a date. strtotime() returned false and gmdate( 't',
false ) always yielded 31, so the end date became "<year>-<month>-31" — which
DateTime overflows for shorter months. February's window ran to March 3 and
30-day months spilled one day into the next, so activity earned early in the
following month was counted toward the previous month's badge. This also
disagreed with the monthly-badges widget, which windows correctly.

Derive the window from the month number instead:
  $start_date = DateTime::createFromFormat( '!Y-n-j', "{$year}-{$month_num}-1" );
  $end_date   = ( clone $start_date )->modify( 'last day of this month' );

Verified for 28/30/31-day months and a leap-year February. The now-unused
$month (badge name) lookup is removed.

Adds regression tests: March 2 activity does not count toward the February
badge (verified red against the pre-fix code), while February activity —
including February 28 — still does.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The earlier S1 fix added permission checks to the REST controller, but the
CPT still used the default `post` capabilities, so write paths that check
caps but bypass the controller — XML-RPC `wp.newPost`, the block editor —
still let a Contributor/Author create (and slug-squat) recommendations
(1.10.0 audit S1, review gap 1).

Map the CPT's primitive capabilities to `edit_others_posts` with
`map_meta_cap => true`. Only the primitive caps are listed; core derives the
meta caps (edit_post/read_post/delete_post) per-post — listing those too
triggers a `_doing_it_wrong` notice in WP 6.1+.

Verified this does NOT break internal writes: `Suggested_Tasks_DB` injects and
updates with raw `wp_insert_post()`/`wp_update_post()`, which do not run
capability checks, so task injection, the AJAX action, CLI commands and
migrations are unaffected. Contributors/Authors are now blocked from creating
via any cap-checked path; Editors and admins still can.

Adds regression tests: the create capability is `edit_others_posts` and gated
per role, and internal injection still works as a Subscriber.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The earlier S3 fix honoured `?pp_branding_id` for any logged-in user, so a
Subscriber could still steer the branding ID; and `Base::init()`'s auto-onboard
still made the blocking remote calls for any logged-in visitor on a branded
host (review gap 3).

- `Branding::get_branding_id()`: honour `?pp_branding_id` only for
  `current_user_can( 'manage_options' )` (was `is_user_logged_in()`).
- `Base::init()`: require `manage_options` (and skip cron) before the
  auto-onboard remote call.

Deliberately NOT gated on `is_admin()`: on a branded host (pp-hosts) the
administrator is redirected to the front-end homepage after Extendify set-up,
and this block is the only path that fetches the license key — gating it to
wp-admin would leave that flow un-onboarded. The branding check runs before
the capability check, so `current_user_can()` is only reached on a branded,
unlicensed site (never at bootstrap on a default install).

Updates the S3 test: a Subscriber's `pp_branding_id` is now ignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…roller

The surface `edit_others_posts` gate let an editor read, update or delete ANY
task over REST, including admin-only ones such as `update-core` (which needs
`update_core`). An editor could trash it and have it count as completed
(review gap 2). The AJAX handler already applies the task's provider
capability; the REST controller did not.

Add `current_user_can_manage_requested_task()`, mirroring the AJAX gate, to the
`get_item`, `update_item` and `delete_item` permission checks: the current user
must also satisfy the task's provider `capability_required()`. Tasks whose
provider cannot be resolved fall back to the surface gate (matching the AJAX
handler). `create` is not per-task gated — there is no existing post to resolve
a provider from, and the CPT capabilities + surface gate cover creation.

Also give the `user` provider `CAPABILITY = 'edit_others_posts'`. It previously
inherited the base `manage_options`, so editors could not complete their own
to-dos even though the widget shows them (a pre-existing bug) — and it would
have made the new per-task check reject editors from their own `user` tasks.

Verified: an editor can read/update/delete their own `user` task and complete
one via AJAX, but is blocked (403) from `update-core`; an admin can manage
`update-core`. The editor-blocked-from-update-core case is red against the
pre-fix controller.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ment

On multisite, `update_core` is reserved for super admins, so a plain site
administrator is correctly denied the `update-core` task and
`test_admin_can_manage_admin_only_task` failed on the `(+ ms)` CI jobs.
Grant super admin in that test when running on multisite.

Also remove the duplicated auto-onboard comment block in `Base::init()`,
keeping the shorter version.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The per-task provider check covered read/update/delete but not create, so an
Editor could still suppress an admin-only recommendation: creating a task that
carries the admin task's slug (e.g. an `update-core` slug) in `trash` status
made `Suggested_Tasks_DB::add()` treat the real task as already existing, so it
was never injected for the admin. Same class as S1, limited to Editors+ (the
CPT capabilities already block Contributors/Authors), but the PR claims S1 is
fixed. Reported in the follow-up review.

Two layers:

- `create_item_permissions_check`: users without `manage_options` may only
  create their own personal (`user`) to-dos — no client-supplied slug, and
  only the `user` provider term (or none). Admins are unrestricted (the plugin
  creates provider tasks as an admin, and internal injection uses raw
  wp_insert_post which bypasses this).

- `Suggested_Tasks_DB::add()`: only treat an existing post as "the task" when
  its provider matches the one being injected, so a post that merely squats the
  slug under a different provider no longer suppresses the real recommendation.
  Filtered in PHP on the resolved provider (a name + tax_query on a trashed post
  does not reliably AND-combine in WP_Query).

Verified live: an Editor's slug-squat / non-user-provider create is now 403,
their own `user` to-do still 201, admins unrestricted; a wrong-provider squat
no longer suppresses injection while a matching-provider task still dedupes.
Tests are red against the pre-fix code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…it-fixes

1.10.1 release audit fixes: R1 (PHP 7.4 fatal), S1/S2/S3/S7.1, B2/B4/B8/B10
Add sunset notice with downloadable certificate
Patch release: end-of-support sunset notice, the PHP 7.4 admin fatal fix,
capability/permission hardening, and several task/badge bug fixes.

Version bumped in progress-planner.php and readme.txt (Stable tag); the
changelog entry is added to both readme.txt and CHANGELOG.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Restores develop to the tree it had at 7c18816, immediately before
a03361d was pushed to it on 2026-09-23.

a03361d is a merge of origin/develop INTO filip/recommendation-format,
made to bring that branch up to date. It was then pushed to develop
rather than to the feature branch, which carried three days of prototype
work onto the release branch by accident.

Nothing shipped: the two classes this removes, Markdown_Recommendations
and Markdown_Rule, have no references anywhere in the plugin. Nothing
instantiates them and nothing hooks them to the provider filter, so they
autoloaded and sat there. The 42 markdown files are data no code reads.
The risk was packaging and review, not behaviour.

Reverted with -m 2, so the mainline is old develop and only the material
that arrived from the feature branch is removed. PR #777's sunset notice
is untouched, and `git diff 7c18816` against this commit is empty.

WHEN THE EXPERIMENT COMES BACK

Merging filip/recommendation-format after this will bring back nothing:
git treats those commits as merged and then reverted. Revert this commit
first, or rebase the work onto a fresh branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…periment

Revert the markdown recommendation experiment from develop
@ilicfilip
ilicfilip requested a review from tacoverdo September 24, 2026 13:51
@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Test merged PR on Playground
Test this pull request on the Playground
or download the zip

@github-actions

Copy link
Copy Markdown
Contributor

✅ Code Coverage Report

Metric Value
Total Coverage 34.27% 📉
Base Coverage 32.28%
Difference 📈 1.99%

⚠️ Coverage below recommended 40% threshold

🎉 Great job maintaining/improving code coverage!

📊 File-level Coverage Changes (13 files)

🆕 New Files

Class Coverage Lines
🟢 Progress_Planner\Admin\Sunset_Notice 92.68% 38/41

📈 Coverage Improved

Class Before After Change
Progress_Planner\Activities\Suggested_Task 50.00% 88.89% +38.89%
Progress_Planner\Suggested_Tasks\Providers\Set_Page_Task 0.00% 36.67% +36.67%
Progress_Planner\Rest\Recommendations_Controller 66.67% 92.86% +26.19%
Progress_Planner\Admin\Page_Settings 39.34% 60.66% +21.32%
Progress_Planner\Suggested_Tasks 9.60% 22.18% +12.58%
Progress_Planner\Suggested_Tasks\Providers\Email_Sending 0.00% 7.79% +7.79%
Progress_Planner\Suggested_Tasks\Tasks_Manager 62.83% 68.14% +5.31%
Progress_Planner\Base 47.40% 51.59% +4.19%
Progress_Planner\Page_Types 52.68% 55.36% +2.68%
Progress_Planner\Badges\Monthly 75.19% 76.52% +1.33%
Progress_Planner\UI\Branding 29.91% 30.77% +0.86%
Progress_Planner\Suggested_Tasks_DB 90.11% 90.58% +0.47%
ℹ️ About this report
  • All tests run in a single job with Xdebug coverage
  • Security tests excluded from coverage to prevent output issues
  • Coverage calculated from line coverage percentages

@github-actions

Copy link
Copy Markdown
Contributor

🔍 WordPress Plugin Check Report

⚠️ Status: Passed with warnings

📊 Report

🎯 Total Issues ❌ Errors ⚠️ Warnings
10 0 10

⚠️ Warnings (10)

📁 classes/suggested-tasks/data-collector/class-unpublished-content.php (1 warning)
📍 Line 🔖 Check 💬 Message
103 WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in Using exclusionary parameters, like post__not_in, in calls to get_posts() should be done with caution, see https://docs.wpvip.com/databases/optimize-queries/using-post__not_in/ for more information.
📁 classes/suggested-tasks/providers/class-content-review.php (4 warnings)
📍 Line 🔖 Check 💬 Message
232 WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in Using exclusionary parameters, like post__not_in, in calls to get_posts() should be done with caution, see https://docs.wpvip.com/databases/optimize-queries/using-post__not_in/ for more information.
377 WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in Using exclusionary parameters, like post__not_in, in calls to get_posts() should be done with caution, see https://docs.wpvip.com/databases/optimize-queries/using-post__not_in/ for more information.
381 WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in Using exclusionary parameters, like post__not_in, in calls to get_posts() should be done with caution, see https://docs.wpvip.com/databases/optimize-queries/using-post__not_in/ for more information.
388 WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in Using exclusionary parameters, like post__not_in, in calls to get_posts() should be done with caution, see https://docs.wpvip.com/databases/optimize-queries/using-post__not_in/ for more information.
📁 classes/activities/class-query.php (2 warnings)
📍 Line 🔖 Check 💬 Message
71 PluginCheck.Security.DirectDB.UnescapedDBParameter Unescaped parameter $table_name used in $wpdb->query()\n$table_name assigned unsafely at line 58.
163 PluginCheck.Security.DirectDB.UnescapedDBParameter Unescaped parameter $where_args used in $wpdb->get_results()\n$where_args assigned unsafely at line 153.
📁 classes/suggested-tasks/data-collector/class-yoast-orphaned-content.php (1 warning)
📍 Line 🔖 Check 💬 Message
111 PluginCheck.Security.DirectDB.UnescapedDBParameter Unescaped parameter $query used in $wpdb->get_row()\n$query assigned unsafely at line 98.
📁 classes/suggested-tasks/data-collector/class-terms-without-description.php (1 warning)
📍 Line 🔖 Check 💬 Message
108 PluginCheck.Security.DirectDB.UnescapedDBParameter Unescaped parameter $query used in $wpdb->get_results()\n$query assigned unsafely at line 106.
📁 classes/suggested-tasks/data-collector/class-terms-without-posts.php (1 warning)
📍 Line 🔖 Check 💬 Message
120 PluginCheck.Security.DirectDB.UnescapedDBParameter Unescaped parameter $query used in $wpdb->get_results()\n$query assigned unsafely at line 118.

🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check

@tacoverdo tacoverdo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@ilicfilip
ilicfilip merged commit fed4b6c into main Sep 24, 2026
42 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants