Skip to content

Raise PHPStan to level 1 and fix all findings#1188

Open
utkarshcloudinary wants to merge 34 commits into
chore/phpstan-setupfrom
phpstan-level1
Open

Raise PHPStan to level 1 and fix all findings#1188
utkarshcloudinary wants to merge 34 commits into
chore/phpstan-setupfrom
phpstan-level1

Conversation

@utkarshcloudinary

Copy link
Copy Markdown
Collaborator

Summary

Bumps PHPStan analysis from level 0 to level 1 and resolves every new finding in code rather than baselining, matching the level-0 approach.

composer phpstanNo errors at level 1.

Real bugs fixed

  • extract_cname() checked an undefined $test['query'] (typo) 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() (takes 1) and the string was not translated; now uses esc_html__() with a translators comment.
  • remove_filter() in class-video.php was called with 4 args (max 3); dropped the stray accepted_args value.

Defensive / dead code simplified

  • upgrade_connection() initialises $data and 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.

Test plan

  • composer phpstan reports no errors at level 1
  • phpcs passes on changed files

gabriel-detassigny and others added 25 commits May 5, 2026 10:37
When Cloudinary is not yet connected (or the usage API returns an
unexpected non-array response), Connect::$usage['media_limits'] can be
a string. PHP 8.x then fatals with 'Cannot access offset of type string
on string' as soon as any front-end request resolves an attachment URL
(e.g. Elementor's get_the_post_thumbnail_url during wp_head).

Guard the lookup and treat the asset as not oversize when limits are
unavailable, matching the defensive pattern already used in
Connect::get_usage_stat().
When the Cloudinary usage API has never returned a valid response (fresh
install, expired credentials, or a transient API error), both
`Connect::$usage` and the persisted `last_usage` setting can be a string
rather than the expected array. PHP 8.x then fatals with 'Cannot access
offset of type string on string' in any code that dereferences these
without checking.

This adds defensive checks alongside the existing Media::is_oversize_media
fix:

- Plan_Details::plan() (php/ui/component/class-plan-details.php): bail
  to empty plan/requests values when last_usage is not an array.
- settings-sidebar.php Account status description: guard the plan name
  access.
- Special_Offer::is_special_offer_available(): return false when
  last_usage isn't a usable array.
- Connect::usage_stats(): only cache derived max image/video sizes when
  media_limits is actually an array, so a stringy API response doesn't
  poison the transient and last_usage option for the rest of the hour.
Following the usage/last_usage fix, audit found the same PHP 8.x
'Cannot access offset of type string on string' / 'foreach on string'
hazard in other code paths that read from get_option / get_transient
and immediately dereference or iterate the result:

- Connect::history(): $history option may be a string; reset to array
  before doing $history[$plan][$date] lookups. Also defend the
  $plan resolution when $this->usage or $this->credentials is
  unexpected.
- Sync_Queue::get_thread_queue(): thread option may be a string;
  reset to defaults before array_merge.
- Cron::load_schedule(): cron schedule option may be a string;
  reset to array before foreach and guard inner item type.
- WPML::wp_generate_attachment_metadata(): transient may be a
  scalar; cast to array before the $data[$id] check.

These are defensive guards only - normal happy-path behaviour is
unchanged.
Media::setup() only runs when the plugin is connected, leaving
$this->sync null otherwise. The Elementor integration registers
elementor/element/parse_css unconditionally, so on a disconnected
site the filter chain reaches Media::cloudinary_id() and fatals on
$this->sync->is_synced() at class-media.php:1740.

Bail early from replace_background_images_in_css() when the plugin
is not connected.
When a parsed tag has no src attribute, get_tag_element() accessed
$attributes['src'] directly, emitting 'Undefined array key "src"' and
passing null downstream into sanitize_url(), which then triggered
'strlen(): Passing null' and 'strtok(): Passing null' deprecations on
PHP 8.4.

- Default missing src to an empty string and bail when no usable URL.
- Guard sanitize_url() against non-string/empty input.
…arnings

Fix PHP 8.4 null deprecation warnings in class-delivery.php
…tring-offset

Fix fatal in `is_oversize_media` when usage data is a string
…rite fails

When Cloudinary returns `existing: true` (overwrite=false, asset already exists),
the plugin retried with a unique suffix, creating a new Cloudinary asset. If the
server killed the PHP process after the upload but before saving `_public_id`,
WordPress never recorded the asset. Each subsequent autosync cycle repeated this,
producing one new Cloudinary duplicate per cycle (102 in the reported case).

When WordPress has no `_public_id` for an attachment, the conflicting Cloudinary
asset is from a previously failed upload attempt. Overwriting it is safe and avoids
creating another suffixed duplicate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add filter to disable Elementor background images integration
…ets-on-existing-retry

Fix duplicate assets on DB write fails scenarios
3.3.4 - build-2 (develop to uat)
Phase 1 POC scaffolding for plugin analytics. Adds a fail-silent,
server-side custom-events emitter and a client-side bridge, reusing the
existing Cloudinary analytics collector (analytics-api.cloudinary.com) —
the same endpoint already used for deactivation feedback. No funnel event
call-sites are wired yet; that follows in a separate PR.

- New Analytics component: builds the global parameter envelope (mandatory
  params always, cloud_name/plan once connected), emits via non-blocking
  wp_remote_post, and never disrupts wp-admin on failure.
- Internal REST route (cloudinary/v1/events) so client-side events are
  enriched server-side, avoiding the collector's GET-only CORS.
- JS analytics module exposing a fail-silent track() helper.
- Flag-gated smoke-test event (cloudinary_analytics_smoke_test, off by
  default) to validate the collector path end-to-end during the POC.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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
gabriel-detassigny and others added 4 commits June 24, 2026 12:41
Add analytics POC infrastructure (activation-funnel custom events)
Builds on the analytics infrastructure (#1187) to emit the activation
funnel defined in the spec. Server-side events go through the Analytics
component; client-side wizard events go through the JS track() bridge.

Server-side:
- plugin_activated (step 1) with install-type detection
  (fresh_install / reactivation / upgrade / downgrade) from the
  db_version install marker + version compare, emitted on the next admin
  load via a transient. Persists _cloudinary_last_active on deactivation
  to derive days_since_last_active.
- connection_test_result (3d) in rest_test_connection.
- wizard_setup_submitted (5) in rest_save_wizard.
- first_sync_started (8, one-time) in Push_Sync::process_assets.
- first_api_consumption (9, one-time) via the cloudinary_uploaded_asset action.

Client-side (wizard.js -> /events bridge): wizard_started,
wizard_signup_clicked, wizard_connect_viewed, credentials_entry_started,
credentials_format_validated, connection_test_started,
wizard_settings_viewed, wizard_setting_toggled, wizard_completed,
wizard_dashboard_clicked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Collector body is now JSON-encoded so booleans (is_multisite,
  format_valid, enabled, wizard flags) stay real JSON booleans per spec
  §2.3; the REST bridge preserves bool/int/float and only sanitizes strings.
- connection_test_result: thread the real http_status (surfaced from the
  WP_Error code in test_connection); guard $result['type'] with isset().
- wizard_setup_submitted: add http_status.
- first_api_consumption: add http_status; one-time guard now keyed by
  cloud_name (per-account, re-emits after an account switch).
- first_sync_started: one-time guard keyed by cloud_name; add trigger
  (auto/manual via wp_doing_cron).
- wizard_started: add entry_point + single-fire guard; persist start time.
- credentials_format_validated: add invalid_reason.
- wizard_completed: add time_to_complete_sec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

6 participants