Skip to content

Commit 6d30260

Browse files
Address PR review: fix event-scope/race bugs, sanitize input, harden 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.
1 parent 89dcda9 commit 6d30260

11 files changed

Lines changed: 184 additions & 74 deletions

‎js/cloudinary.js‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎js/deactivate.asset.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<?php return array('dependencies' => array('wp-api-fetch'), 'version' => 'e3528eb2e86ccc7789df');
1+
<?php return array('dependencies' => array('wp-api-fetch'), 'version' => '9c563a4d047b892cd84e');

‎js/deactivate.js‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎php/assets/class-rest-assets.php‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -339,9 +339,14 @@ public function rest_get_caches( $request ) {
339339
$current_page = $page ? $page : 1;
340340
$data = $this->get_assets( $parent->ID, $search, $current_page );
341341

342-
$analytics = get_plugin_instance()->get_component( 'analytics' );
343-
if ( $analytics ) {
344-
$analytics->track( 'cache_items_viewed', 'cache', null, array( 'cache_point' => (string) $url ) );
342+
// This endpoint is also hit for pagination and search-as-you-type
343+
// within an already-open cache point; only the initial open (page 1,
344+
// no search term) represents a genuine "viewed" event.
345+
if ( 1 === (int) $current_page && empty( $search ) ) {
346+
$analytics = get_plugin_instance()->get_component( 'analytics' );
347+
if ( $analytics ) {
348+
$analytics->track( 'cache_items_viewed', 'cache', null, array( 'cache_point' => (string) $url ) );
349+
}
345350
}
346351

347352
return rest_ensure_response( $data );

‎php/class-admin.php‎

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,18 @@ class Admin {
7777
*/
7878
const NOTICE_SLUG = '_cld_notices';
7979

80+
/**
81+
* Settings slugs (by page) that directly inject transformation qualifiers
82+
* (format, quality, freeform string) into delivered URLs, as opposed to
83+
* plain feature-enablement toggles.
84+
*
85+
* @var array
86+
*/
87+
const GLOBAL_TRANSFORMATION_SLUGS = array(
88+
'image_settings' => array( 'image_format', 'image_quality', 'image_freeform' ),
89+
'video_settings' => array( 'video_format', 'video_quality', 'video_freeform' ),
90+
);
91+
8092
/**
8193
* Initiate the settings object.
8294
*
@@ -134,14 +146,14 @@ public function rest_endpoints( $endpoints ) {
134146
* @param WP_REST_Request $request The request object.
135147
*/
136148
public function rest_dismiss_notice( WP_REST_Request $request ) {
137-
$token = $request->get_param( 'token' );
149+
$token = sanitize_key( $request->get_param( 'token' ) );
138150
$duration = $request->get_param( 'duration' );
139151

140152
set_transient( $token, true, $duration );
141153

142154
$analytics = $this->plugin->get_component( 'analytics' );
143155
if ( $analytics ) {
144-
$analytics->track( 'notice_dismissed', 'settings', null, array( 'notice_id' => (string) $token ) );
156+
$analytics->track( 'notice_dismissed', 'settings', null, array( 'notice_id' => $token ) );
145157
}
146158
}
147159

@@ -495,18 +507,6 @@ protected function save_settings( $submission, $data ) {
495507
do_action( 'cloudinary_flush_cache' );
496508
}
497509

498-
/**
499-
* Settings slugs (by page) that directly inject transformation qualifiers
500-
* (format, quality, freeform string) into delivered URLs, as opposed to
501-
* plain feature-enablement toggles.
502-
*
503-
* @var array
504-
*/
505-
const GLOBAL_TRANSFORMATION_SLUGS = array(
506-
'image_settings' => array( 'image_format', 'image_quality', 'image_freeform' ),
507-
'video_settings' => array( 'video_format', 'video_quality', 'video_freeform' ),
508-
);
509-
510510
/**
511511
* Emits `transformation_applied` (scope: global) when a settings save
512512
* changed a global transformation field.

‎php/class-connect.php‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -644,12 +644,30 @@ public function updated_option() {
644644
/**
645645
* Check the status of Cloudinary.
646646
*
647+
* Also called interactively from `test_connection()` (wizard live-typing
648+
* checks and connection saves) — `connectivity_check_failed` is NOT
649+
* tracked here so those don't get miscounted as background connectivity
650+
* failures. See `cron_check_status()` for the cron-only variant that does.
651+
*
647652
* @return array|WP_Error
648653
*/
649654
public function check_status() {
650655
$status = $this->test_ping();
651656
$this->settings->get_setting( 'status' )->save_value( $status );
652657

658+
return $status;
659+
}
660+
661+
/**
662+
* Cron-only wrapper around `check_status()` that tracks connectivity
663+
* failures. Kept separate from `check_status()` so interactive callers
664+
* (the wizard's live check, connection saves) never emit this event.
665+
*
666+
* @return array|WP_Error
667+
*/
668+
public function cron_check_status() {
669+
$status = $this->check_status();
670+
653671
if ( is_wp_error( $status ) ) {
654672
$analytics = $this->plugin->get_component( 'analytics' );
655673
if ( $analytics ) {
@@ -825,7 +843,7 @@ public function setup() {
825843
* Setup Status cron.
826844
*/
827845
protected function setup_status_cron() {
828-
Cron::register_process( 'check_status', array( $this, 'check_status' ), DAY_IN_SECONDS );
846+
Cron::register_process( 'check_status', array( $this, 'cron_check_status' ), DAY_IN_SECONDS );
829847
}
830848

831849
/**

‎php/sync/class-push-sync.php‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,13 +180,22 @@ public function rest_start_sync( \WP_REST_Request $request ) {
180180
'success' => false,
181181
);
182182
if ( empty( $start ) ) {
183-
$this->queue->stop_queue();
183+
// A real, user-driven stop (the bulk-sync toggle was switched off)
184+
// rather than `stop_maybe()`'s internal restart-to-catch-new-items
185+
// cycle, so this needs to actually close out the tracked run —
186+
// `shutdown_queue()` does that via `track_run_completed()`, unlike
187+
// the bare `stop_queue()` that cycle uses.
188+
$this->queue->shutdown_queue( 'queue' );
184189
} else {
190+
// Set before starting the queue: `start_queue()` immediately spawns
191+
// background threads whose sync results would otherwise be tallied
192+
// against a run that doesn't exist yet and get silently dropped. On
193+
// a failed start, `start_queue()` itself calls `shutdown_queue()`,
194+
// which cleans up the run state this just staged.
195+
$this->queue->mark_run_started( $type );
185196
$state['success'] = $this->queue->start_queue( $type );
186197

187198
if ( $state['success'] ) {
188-
$this->queue->mark_run_started( $type );
189-
190199
$analytics = $this->plugin->get_component( 'analytics' );
191200
if ( $analytics ) {
192201
$queue = $this->queue->get_queue( $type );

‎php/sync/class-sync-queue.php‎

Lines changed: 70 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,24 @@ class Sync_Queue {
6262
const RUN_STARTED_KEY = '_cloudinary_sync_run_started';
6363

6464
/**
65-
* Option key for the running success/error tally of the current run.
65+
* Option key for the running count of successful syncs in the current run.
66+
*
67+
* Kept as its own scalar option (rather than a field in a single
68+
* serialized array) so it can be incremented atomically — the queue runs
69+
* multiple concurrent threads as separate requests, and a
70+
* get_option()/update_option() read-modify-write round trip on a shared
71+
* array would lose updates under that concurrency.
6672
*
6773
* @var string
6874
*/
69-
const RUN_TALLY_KEY = '_cloudinary_sync_run_tally';
75+
const RUN_TALLY_SYNCED_KEY = '_cloudinary_sync_run_tally_synced';
76+
77+
/**
78+
* Option key for the running count of failed syncs in the current run.
79+
*
80+
* @var string
81+
*/
82+
const RUN_TALLY_ERRORS_KEY = '_cloudinary_sync_run_tally_errors';
7083

7184
/**
7285
* The cron frequency to ensure that the queue is progressing.
@@ -226,14 +239,8 @@ public function mark_run_started( $type = 'queue' ) {
226239
return;
227240
}
228241
update_option( self::RUN_STARTED_KEY, time(), false );
229-
update_option(
230-
self::RUN_TALLY_KEY,
231-
array(
232-
'synced' => 0,
233-
'errors' => 0,
234-
),
235-
false
236-
);
242+
delete_option( self::RUN_TALLY_SYNCED_KEY );
243+
delete_option( self::RUN_TALLY_ERRORS_KEY );
237244
}
238245

239246
/**
@@ -249,17 +256,27 @@ public function tally_run_result( $success ) {
249256
if ( false === get_option( self::RUN_STARTED_KEY, false ) ) {
250257
return;
251258
}
252-
$tally = get_option(
253-
self::RUN_TALLY_KEY,
254-
array(
255-
'synced' => 0,
256-
'errors' => 0,
259+
$this->increment_option( $success ? self::RUN_TALLY_SYNCED_KEY : self::RUN_TALLY_ERRORS_KEY );
260+
}
261+
262+
/**
263+
* Atomically increments an integer option, creating it as 1 if missing.
264+
*
265+
* @param string $option_name The option to increment.
266+
*
267+
* @return void
268+
*/
269+
protected function increment_option( $option_name ) {
270+
global $wpdb;
271+
272+
$wpdb->query( // phpcs:ignore WordPress.DB
273+
$wpdb->prepare(
274+
"INSERT INTO {$wpdb->options} (option_name, option_value, autoload) VALUES (%s, '1', 'no')
275+
ON DUPLICATE KEY UPDATE option_value = option_value + 1",
276+
$option_name
257277
)
258278
);
259-
$key = $success ? 'synced' : 'errors';
260-
261-
$tally[ $key ] = isset( $tally[ $key ] ) ? $tally[ $key ] + 1 : 1;
262-
update_option( self::RUN_TALLY_KEY, $tally, false );
279+
wp_cache_delete( $option_name, 'options' );
263280
}
264281

265282
/**
@@ -274,15 +291,11 @@ protected function track_run_completed() {
274291
if ( false === $started ) {
275292
return;
276293
}
277-
$tally = get_option(
278-
self::RUN_TALLY_KEY,
279-
array(
280-
'synced' => 0,
281-
'errors' => 0,
282-
)
283-
);
294+
$synced = (int) get_option( self::RUN_TALLY_SYNCED_KEY, 0 );
295+
$errors = (int) get_option( self::RUN_TALLY_ERRORS_KEY, 0 );
284296
delete_option( self::RUN_STARTED_KEY );
285-
delete_option( self::RUN_TALLY_KEY );
297+
delete_option( self::RUN_TALLY_SYNCED_KEY );
298+
delete_option( self::RUN_TALLY_ERRORS_KEY );
286299

287300
$analytics = $this->plugin->get_component( 'analytics' );
288301
if ( $analytics ) {
@@ -291,8 +304,8 @@ protected function track_run_completed() {
291304
'sync',
292305
null,
293306
array(
294-
'total_synced' => isset( $tally['synced'] ) ? (int) $tally['synced'] : 0,
295-
'total_errors' => isset( $tally['errors'] ) ? (int) $tally['errors'] : 0,
307+
'total_synced' => $synced,
308+
'total_errors' => $errors,
296309
'duration_sec' => time() - (int) $started,
297310
)
298311
);
@@ -330,19 +343,33 @@ public function track_setting_changed( $new_value, $current_value, $setting ) {
330343
$parts = explode( '.', $full_slug );
331344
$slug = end( $parts );
332345

333-
if ( in_array( $slug, $tracked_slugs, true ) ) {
334-
$analytics = $this->plugin->get_component( 'analytics' );
335-
if ( $analytics ) {
336-
$analytics->track(
337-
'sync_settings_changed',
338-
'sync',
339-
null,
340-
array(
341-
'setting_key' => $slug,
342-
'new_value' => $new_value,
343-
)
344-
);
345-
}
346+
if ( ! in_array( $slug, $tracked_slugs, true ) ) {
347+
return $new_value;
348+
}
349+
350+
// Not every `set_pending()` caller passes $current_value (e.g. the
351+
// wizard's save re-submits `auto_sync` unconditionally); fall back to
352+
// the setting's own current value, which `@data` still reflects here
353+
// since `set_pending()` only stages the pending write.
354+
if ( null === $current_value ) {
355+
$current_value = $setting->get_value();
356+
}
357+
358+
if ( $new_value === $current_value ) {
359+
return $new_value;
360+
}
361+
362+
$analytics = $this->plugin->get_component( 'analytics' );
363+
if ( $analytics ) {
364+
$analytics->track(
365+
'sync_settings_changed',
366+
'sync',
367+
null,
368+
array(
369+
'setting_key' => $slug,
370+
'new_value' => $new_value,
371+
)
372+
);
346373
}
347374

348375
return $new_value;
@@ -699,7 +726,7 @@ public function stop_maybe( $type = 'queue' ) {
699726
*
700727
* @param string $type The type of queue to shutdown.
701728
*/
702-
protected function shutdown_queue( $type = 'queue' ) {
729+
public function shutdown_queue( $type = 'queue' ) {
703730
if ( 'queue' === $type ) {
704731
delete_option( self::$queue_enabled );
705732
$this->track_run_completed();

0 commit comments

Comments
 (0)