Skip to content

3.3.6 (develop to UAT) - #1236

Merged
gabrielcld2 merged 74 commits into
uatfrom
develop
Aug 13, 2026
Merged

3.3.6 (develop to UAT)#1236
gabrielcld2 merged 74 commits into
uatfrom
develop

Conversation

PatelUtkarsh and others added 30 commits June 24, 2026 15:04
Bump the analysis level from 0 to 1 and resolve every new finding in code
rather than baselining, matching the level-0 approach.

Real bugs fixed:
- extract_cname() checked an undefined $test['query'] instead of the
  $parsed_url['query'] parameter, so the query-string CNAME branch never ran.
- connection-string.php passed two args to wp_kses_post() (1 arg) and the
  string was not translated; use esc_html__() with a translators comment.
- remove_filter() in class-video.php was called with 4 args (max 3); drop the
  stray accepted_args value.

Defensive / dead code simplified:
- upgrade_connection() initialised $data and now bails when no cloudinary_url
  is present, instead of accessing a possibly-undefined variable.
- Api::upload() initialises $result before the chunk loop so an empty file
  returns a WP_Error rather than reading an undefined variable.
- Removed always-falsy $errors guard in Admin::save_settings().
- Removed redundant empty()/elseif branches in Delivery and Upgrade.

Static-analysis support:
- url_hash added to the Relationship @Property list (valid magic property
  backed by a DB column).
- State and REST_API constructors now use the injected $plugin instead of
  get_plugin_instance()/discarding it, removing unused-parameter warnings.
- New tests/phpstan/stubs/constants.php declares runtime-defined constants
  (CLDN_*, CLOUDINARY_ENDPOINTS_*, CLOUDINARY_CONNECTION_STRING) and the WP
  core constants WPINC/LOGGED_IN_COOKIE that the stub package omits.
- Exclude php/templates/* since view files are included into a class scope,
  so $this is bound at runtime in a way PHPStan cannot model standalone.

Run with: composer phpstan
`get_size_from_slug()` returned the registered width/height from
`wp_get_registered_image_subsizes()` for any matching slug, ignoring the
`crop` flag. For non-crop sizes ( crop => false ) those values are a maximum
bounding box, not exact dimensions: WordPress scales the image to fit inside
the box while preserving aspect ratio.

Treating them as a fixed crop made the delivery layer emit, for the default
`large` ( 1024x1024, crop => false ), a transformation of `w_1024,h_1024,c_scale`
applied to a landscape/portrait source, visibly stretching it into a square.
This surfaced on blocks whose stored `<img src>` had no size suffix (so the
URL-based size lookup returned nothing and the figure-class slug fallback ran).

Return early for non-crop sizes so only genuine hard-crop sizes are pinned to
exact dimensions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
install() called get_plugin_instance()->init() unconditionally, which
re-ran the bootstrap out of sequence during plugin activation and could
destabilise plugin load order for the request (e.g. triggering ACF to
initialise too early). Only run init() when the plugin version has not
already been populated; the DB routines only need the version.
Raise the PHPStan analysis level from 1 to 2 and resolve the resulting
issues that are safe, mechanical fixes:

- Correct malformed PHPDoc tags (trailing periods, invalid array()/[]
  syntax, \array, $var ordering).
- Use fully-qualified class names in PHPDoc where the containing
  namespace shadowed WP/global classes (WP_REST_*, Cloudinary\Connect).
- Add an Elementor stub for the optional integration and register it.
- Replace the removed Requests_Transport_cURL type hint with resource.
- Narrow get_component()/managers[...] union return types at call sites
  so type-specific method/property access resolves.
- Add \@property/@method annotations for magic __get/__call access
  (Settings, Setting, Relationship, Api).
- Fix genuine argument-count/type bugs: save_value($media), drop ignored
  extra args to has_param()/get_settings(), cast numeric stats before
  arithmetic, correct WP_Post case, make Relationship::$post_id public.
- Suppress one intentional dynamic WP_Post property write inline.
Repair genuine runtime bugs surfaced by PHPStan level 2 that referenced
methods deleted in the earlier settings/UI decoupling refactor:

- class-cache.php: the cache settings tabs called the removed
  Settings::create_setting() via a callback indirection that would fatal.
  Convert add_plugin/theme/wp/content_settings() to return their params
  and embed them inline in settings() (matching the working Assets
  pattern), and register the cache_point path hooks directly in
  setup_setting_tabs(). Remove the now-unused get_cache_settings().
- class-cache-status.php: box_status() built a table via the removed
  create_setting()/render_component(); rebuild it using the component
  part system so it renders correctly.
- class-page.php + class-admin.php: notice() called the non-existent
  Setting::get_admin_notices(); route it through the Admin component and
  have get_admin_notices() return the renderable notice Setting built via
  init_components() (consistent with render_notices()).

Also convert the inline union-narrowing @var hints added for the level
bump to multi-line docblocks to satisfy WPCS.
Raise the PHPStan analysis level from 2 to 3 and resolve the resulting
issues, all safe PHPDoc/annotation corrections with no behavior change:

- Correct return-type PHPDoc to match actual behavior: Assets::
  generate_storage_signature (bool), Assets::build_item (array|null),
  Media::get_transformation (int|string|false), Media::cloudinary_url
  (string|null), Media::filter_downsize (array|false), Media::
  get_context_options (string), Settings::save (string[]), Api::sign
  (string), Api::call (array|string|WP_Error), Page::form/notice and
  Sync::action (array|null), On_Off::sanitize_value (string).
- Widen base Text::sanitize_value PHPDoc to mixed so the array/bool
  overrides (Checkbox, Cron, Crops, React, Tags_Input, On_Off) are
  covariant-compatible.
- Fix settings/setting type confusion: correct Component\Settings
  interface param to Cloudinary\Settings, widen Settings_Component::
  $settings and Delivery_Feature::$settings to Settings|Setting, align
  Gallery/Extensions/Global_Transformations settings property types.
- Fix Setting::$parent (string slug, not self) and get_option_parent
  return (Setting|null).
- Fix Sync::$managers to include Sync_Queue.
- Fix Line_Stat $connect type (Cloudinary\Connect) and narrow at call
  site; allow numeric used_percent/limit.
- Cast ini_get in Cron::$daemon_watcher_interval; default Api::
  $pending_url to null; widen Url_Object::get_id to int|string|null.
- Correct get_terms return-type PHPDoc and annotate video metadata to
  resolve offset access on WP stub arrays.
init_asset_parents() and process_parent_assets() run internal WP_Query
lookups over Cloudinary's own asset post type. These are not public-facing
content queries, but they still fire third-party the_posts/posts_results
filters, which can cause a theme/plugin to run expensive per-query work
(e.g. author-archive injection) on every one of Cloudinary's internal
queries until the request exhausts memory.

Set suppress_filters => true on these fully-specified internal queries so
they skip content filters. The WordPressVIPMinimum SuppressFilters rule is
suppressed with an explanation: these queries do not rely on the
posts_where/join/orderby filters the rule protects.
Adds a GitHub Actions workflow to build and publish PHP hook
documentation to GitHub Pages, replacing the need for a dedicated
long-lived docs branch.

- Workflow triggers on push to master, release publish, and manual dispatch
- Adds jsdoc + wp-hookdoc (with patch) + patch-package
- Adds build:docs script; does not modify main build pipeline
- Ignores generated docs/ output (published to gh-pages)
Release process GH Action
Raise PHPStan to level 1 and fix all findings
Bump PHPStan to level 2 and fix all resulting issues
Bump PHPStan to level 3 and fix all resulting issues
Bump PHPStan to level 4 and fix all resulting issues
chore(docs): add hookdoc build workflow
Bump PHPStan to level 5 and fix type/annotation issues
WordPress 7.1 enforces the iframed post editor and ships React 19.
Update the Cloudinary Gallery block accordingly:

- Bump the block to Block API v2 (compatible with the plugin's declared
  minimum of WP 5.6) and wrap the edit output with useBlockProps so it
  renders correctly inside the enforced iframed editor.
- Keep the save output unchanged (bare container div) so existing
  published galleries remain valid without a deprecation/migration. The
  front-end render only depends on the inner container class, not a block
  wrapper, so no markup change is needed.
- Move the block editor stylesheet to enqueue_block_assets so WP injects
  it into the iframe correctly (avoids the "added to the iframe
  incorrectly" 7.1 warning).
- Move the render-time setAttributes calls into effects to resolve the
  React 19 "Cannot update a component while rendering a different
  component" warning.
- Fix the generated container id, which produced "undefined<id>" now
  that className is no longer passed to Edit under API v2.

Verified against WordPress 7.1-beta3: fresh insert/save/reload and the
upgrade path (loading legacy v1 content) both validate with no console
errors or block validation warnings.
The Cloudinary inspector controls added to core/image and core/video
blocks had two WordPress 7.1 issues:

- setAttributes was called during render when the attachment had
  transformations, which triggers the React 19 "Cannot update a component
  while rendering a different component" warning. Moved into an effect.
- The controls read InspectorControls from the deprecated wp.editor alias.
  Switched to wp.blockEditor and declared the script dependencies
  explicitly, since the block editor globals were previously relied on
  without being registered.
The gallery settings page enqueued js/gallery.js under two handles:
'gallery_config' in enqueue_admin_scripts() and 'gallery-widget' via the
React UI component's script param. The bundle executed twice, calling
createRoot() twice on the same container, which React 19 (WordPress 7.1)
reports as an error on every settings page load.

Drop the script param from the React panel definition and keep the single
enqueue that already carries the generated asset dependencies.
src/js/blocks.js and its components accessed @WordPress packages through
the wp.* globals (wp.hooks, wp.blockEditor), which the dependency
extraction plugin cannot detect, so js/block-editor.asset.php was missing
wp-block-editor and wp-hooks. The hand-written dependency list in
class-video.php compensated for those but omitted wp-api-fetch, which
blocks.js genuinely imports and only worked because other editor scripts
loaded it first.

Convert the wp.hooks/wp.blockEditor global usages to @wordpress/hooks and
@wordpress/block-editor imports so the generated asset file is complete,
and consume it in enqueue_block_assets() instead of the static list.
Bumps @wordpress/components, @wordpress/scripts, copy-webpack-plugin,
css-minimizer-webpack-plugin, jsdoc, eslint, and @wordpress/eslint-plugin
to their latest majors, and replaces the unmaintained npm-run-all with
the actively maintained npm-run-all2 fork.

ESLint 10 drops support for legacy .eslintrc.json/.eslintignore, so
those are replaced with a flat eslint.config.js.

Adds overrides to force patched versions of nested transitive deps that
npm's resolver couldn't otherwise reach: eslint-import-resolver-typescript
(pinned to the last version compatible with eslint-plugin-import),
serialize-javascript, uuid, and brace-expansion (fixes GHSA-mh99-v99m-4gvg).

Remaining vulnerabilities are confined to the grunt-based build/deploy
toolchain (grunt, grunt-contrib-*, grunt-wp-deploy, grunt-wp-i18n,
load-grunt-tasks) and dot-object, which are already at their latest
published versions with no upstream fix available.

Vulnerability count: 94 -> 36.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
gabriel-detassigny and others added 29 commits August 6, 2026 10:58
Ensure compatibility with WordPress 7.1
Covers bulk_sync_started (via a real authenticated REST call, matching what
the "Start bulk sync" button does), sync_completed + asset_sync_failed (via
direct invocation of Sync_Queue's run-tracking state machine — a full
real-upload sync run is slow/flaky in CI, and this exercises the exact same
code path already verified manually), and sync_settings_changed (real UI:
toggling the auto-sync radio on the Connect page).

Needed test isolation: a real bulk_sync_started call kicks off a genuine
background sync thread via a non-blocking loopback request, which would
otherwise keep running and race later tests.
Covers asset_edited + transformation_applied (scope: asset) via a real
authenticated REST call to save_asset, and transformation_applied
(scope: global) via a real settings save on the Image Settings page.

transformation_count reliably comes back 0 for the asset-scope case since
the synthetic (fileless) test attachment has no real resource type to
derive it from — asserted the event shape instead of the count value.
Covers cache_items_viewed, cache_items_toggled, asset_cache_purged,
all_cache_purged, and cache_uploaded — all via real authenticated REST
calls / direct action dispatch against the live Rest_Assets endpoints.

Found along the way: rest_purge_all() resolves its `parent` param via
Assets::get_param(), which is only ever populated by the in-memory
activate_parent() call inside Assets::activate_parents() for settings-
configured paths — a different (and non-persisted-across-requests)
lookup than get_asset_parent() (DB-backed, used by show_cache/etc). A
custom test-created cache point can't satisfy that lookup without
also registering a real settings path, so the purge test targets one
of the plugin's own default non-media paths instead, which is already
active for exactly this reason.
Covers extension_toggled (real UI: a native checkbox click, bypassing
Playwright's visibility check for the collapsed Extensions sidebar panel)
and gallery_configured (via Admin::save_settings() directly with a
synthetic gallery_config payload, matching what the React gallery panel
serializes — avoids driving the full React UI for one settings save).

Two timing/state gotchas fixed along the way: extensions.js debounces its
change handler by 1000ms before firing, and gallery_config payloads need a
value guaranteed to differ from whatever's already saved, since
save_settings() silently no-ops on an unchanged value.
… plugin_uninstalled

Rounds out connection management and deactivation analytics coverage.
Also fixes fakeCloudinaryConnected() to upsert cloudinary_connect instead
of a raw UPDATE, which silently no-ops once a real plugin_uninstalled run
deletes the option row.
…fix asset_cache_purged flake

The e2e capture mu-plugin only logged outgoing analytics-api.cloudinary.com
requests and let them proceed, so every local/CI test run was leaking
synthetic events and deactivation-reason submissions into the real
production collector. It now preempts with a synthetic 200 instead.

Also fixes cache-analytics.spec.js's asset_cache_purged test: none of the
plugin's default non-media cache paths are active on a fresh install (they
default off), so the test now explicitly enables the uploads path via a real
settings save before purging, rather than assuming it's already active.
…-visit side effect

Enabling the cache path was already explicit via a real settings save, but
materializing the underlying asset-parent post depended on a subsequent
admin page load's Assets::update_asset_paths() side effect — a
timing-sensitive path that flaked once under CI load (passed on retry).
Create the post directly instead, removing that dependency entirely.
json_decode($content, true) in String_Replace::replace_strings() collapsed
empty JSON objects ({}) into PHP arrays, which wp_json_encode() then
re-serialized as []. Since this runs on every REST API response site-wide,
it silently corrupted the shape of any JSON payload containing empty
objects - including MCP JSON-RPC handshakes (e.g. Novamira), whose clients
validate responses against a strict schema and reject [] where {} is
expected.

Decode as objects instead of associative arrays so shape survives the
round trip.
Add taffydb dev dependency required for docs generation
Fix JSON shape corruption in REST API responses breaking MCP integrations (Novamira)
…exit tracking

- connectivity_check_failed: split check_status() into a pure method and a
  new cron_check_status() wrapper, so interactive connection checks/saves
  (test_connection()) no longer emit it alongside connection_test_result.
- bulk sync: rest_start_sync() now calls mark_run_started() before
  start_queue() (was after, dropping early thread results from the tally),
  and uses shutdown_queue('queue') instead of stop_queue() on manual stop
  so the run state doesn't leak into the next run's sync_completed.
- sync_completed tally: split into two atomically-incremented options
  (RUN_TALLY_SYNCED_KEY/RUN_TALLY_ERRORS_KEY) instead of a single
  get/modify/update array, which could lose updates across concurrent
  sync threads.
- sync_settings_changed: added a diff guard so no-op saves (e.g. the
  wizard re-submitting auto_sync unconditionally) don't fire the event.
- cache_items_viewed: only tracked on page 1 with no search term, instead
  of on every pagination/search request against an open cache point.
- notice_dismissed: sanitize the notice token via sanitize_key() before
  using it as the transient key and event param.
- deactivation_skipped: replaced a 150ms setTimeout guess with a new
  Analytics.trackReliable() using navigator.sendBeacon(), which the
  browser guarantees to dispatch across the immediate navigation.
- Moved a misplaced class constant to the top of Admin with the others.

tests/e2e/sync-analytics.spec.js's cleanup helper updated for the new
split tally option names. Full 32-test e2e suite verified green.
…e-dismissal regression

- increment_option(): after the raw SQL increment, also purge the option
  from the 'notoptions' WP object-cache group. mark_run_started()'s
  delete_option() calls mark these keys as known-absent there; on sites
  with a persistent object cache (Redis/Memcached — not present in
  wp-env/CI, so this needed direct verification via simulation) the raw
  insert never cleared that flag, so every later get_option() would
  short-circuit to 0 and sync_completed would always report 0/0.
- rest_dismiss_notice(): switch from sanitize_key() to a dot-preserving
  regex for the notice token. The token is a dotted setting slug (e.g.
  `_cld_notices.0`) that Notice::is_enabled() looks up verbatim via
  get_transient(); sanitize_key() stripped the dot, writing the transient
  under a key that never matched, so dismissible notices reappeared on
  every page load.

Both regressions were introduced by the previous review-fix commit
(6d30260) and verified directly via wpEvalFile since the e2e suite
cannot exercise a persistent object cache. Full 32-test suite still green.
Instrument connection, sync, settings, media, cache, features, and deactivation analytics events
…ull-init

Avoid re-running full plugin init during activation
@gabrielcld2
gabrielcld2 merged commit da61523 into uat Aug 13, 2026
10 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.

5 participants