PluginProbe
Media Cloud Sync / 1.4.1
Media Cloud Sync v1.4.1
1.4.1 1.4.0 1.3.12 1.3.11 1.3.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.2.0 1.2.10 1.2.11 1.2.12 1.2.13 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 All 35 releases
← All changes | includes/api.php +193 -13 1.3.111.4.1 View file →
@@ -45,8 +45,9 @@
45 45 $this->add_route( '/getSettings', 'getSettings' );
46 46 $this->add_route( '/saveConfig', 'saveConfig', 'POST' );
47 47 $this->add_route( '/refreshCounts', 'refreshCounts', 'POST' );
48 48 $this->add_route( '/getErrorLogs', 'getErrorLogs', 'POST' );
49 + $this->add_route( '/clearErrorLogs', 'clearErrorLogs', 'POST' );
49 50
50 51 // Upgrade Database Routes
51 52 $this->add_route( '/upgrade/status', 'getUpgradeStatus' );
52 53 $this->add_route( '/upgrade/start', 'startUpgrade', 'POST' );
@@ -56,8 +57,10 @@
56 57 $this->add_route( '/sync/start', 'startSync', 'POST' );
57 58 $this->add_route( '/sync/pause', 'pauseSync', 'POST' );
58 59 $this->add_route( '/sync/resume', 'resumeSync', 'POST' );
59 60 $this->add_route( '/sync/stop', 'stopSync', 'POST' );
61 + $this->add_route( '/sync/tick', 'tickSync', 'POST' );
62 + $this->add_route( '/sync/setMethod', 'setSyncMethod', 'POST' );
60 63
61 64 $this->add_route( '/sync/retrySingle', 'retrySingle', 'POST' );
62 65 }
63 66
@@ -163,8 +166,12 @@
163 166 'settings' => Utils::get_settings(),
164 167 ],
165 168 'sync' => Sync::instance()->get_status(),
166 169 'license' => defined('WPMCS_PRO_VERSION') ? Utils::get_safe_license_data() : [],
170 + 'file_manager' => defined('WPMCS_PRO_VERSION') ? [
171 + 'delete_enabled' => (bool) Utils::get_option('delete_enabled', false, Schema::getConstant('FILE_MANAGER_SETTINGS_KEY')),
172 + 'root_folder' => Utils::get_option('root_folder', '', Schema::getConstant('FILE_MANAGER_SETTINGS_KEY')),
173 + ] : [],
167 174 ];
168 175
169 176 return new WP_REST_Response( $data, 200 );
170 177 }
@@ -194,15 +201,35 @@
194 201 $serviceOk = true;
195 202 $settingsOk = true;
196 203
197 204 if( $action === 'cdn' ){
205 + // Switching delivery provider resets the previously selected one's own fields
206 + // back to their defaults (see ChooseCDN.jsx) — that can include private_path/
207 + // enable_private_media, since those live in a delivery entry's own field list,
208 + // so this write needs the same guard the plain settings save uses below.
209 + $validation = apply_filters( 'wpmcs_validate_settings_save', ['ok' => true, 'message' => ''], $settings, $action );
210 + if ( empty( $validation['ok'] ) ) {
211 + return new WP_REST_Response( [
212 + 'success' => false,
213 + 'message' => !empty($validation['message']) ? $validation['message'] : esc_html__('Settings could not be saved.', 'media-cloud-sync')
214 + ], 200 );
215 + }
216 +
198 217 $existing = Utils::get_credentials();
199 218 $existing['cdn'] = $cdn;
200 -
219 +
201 220 $updated = Utils::update_option('credentials', $existing, Schema::getConstant('GLOBAL_SETTINGS_KEY'));
202 221 $updatedSettings = Utils::update_option('settings', $settings, Schema::getConstant('GLOBAL_SETTINGS_KEY'));
203 222
204 223 if(!$updated) $serviceOk = false;
224 + if(!$updatedSettings) $settingsOk = false;
225 +
226 + // Only once both writes actually persisted — a failed credentials write can
227 + // leave the object cache holding an unpersisted bucket identity (see
228 + // Cache::set_object_cache()), so gating on $settingsOk alone isn't enough.
229 + if ($serviceOk && $settingsOk) {
230 + do_action('wpmcs_after_settings_save', $settings, $action);
231 + }
205 232 }
206 233
207 234
208 235 if($action === 'all' || $action === 'service'){
@@ -239,10 +266,29 @@
239 266 }
240 267 }
241 268
242 269 if(($action === 'all' || $action === 'settings')) {
270 + // Extension point for a pro-only settings-save guard (e.g. private media
271 + // refusing a private_path/base_path change while items are still private
272 + // under the currently-applied path) — free has no opinion here, default
273 + // is "allow". ['ok' => bool, 'message' => string].
274 + $validation = apply_filters( 'wpmcs_validate_settings_save', ['ok' => true, 'message' => ''], $settings, $action );
275 + if ( empty( $validation['ok'] ) ) {
276 + return new WP_REST_Response( [
277 + 'success' => false,
278 + 'message' => !empty($validation['message']) ? $validation['message'] : esc_html__('Settings could not be saved.', 'media-cloud-sync')
279 + ], 200 );
280 + }
281 +
243 282 $updatedSettings = Utils::update_option('settings', $settings, Schema::getConstant('GLOBAL_SETTINGS_KEY'));
244 283 if(!$updatedSettings) $settingsOk = false;
284 +
285 + // Only once both writes actually persisted — a failed credentials write can
286 + // leave the object cache holding an unpersisted bucket identity (see
287 + // Cache::set_object_cache()), so gating on $settingsOk alone isn't enough.
288 + if ($serviceOk && $settingsOk) {
289 + do_action('wpmcs_after_settings_save', $settings, $action);
290 + }
245 291 }
246 292
247 293 // Clear plugin caches and refresh counts after any configuration save.
248 294 if(in_array($action, ['all', 'service', 'settings', 'cdn'], true)) {
@@ -335,8 +381,32 @@
335 381 }
336 382
337 383
338 384 /**
385 + * Clear all error logs for a sync type (e.g. stale entries from earlier failed attempts).
386 + * @since 1.3.13
387 + */
388 + public function clearErrorLogs( $request ) {
389 + $type = isset($request->get_params()['type']) ? $request->get_params()['type'] : '';
390 +
391 + if (empty($type) || !Sync::instance()->get_class_by_action($type)) {
392 + return new WP_REST_Response( [
393 + 'success' => false,
394 + 'message' => esc_html__('Invalid action', 'media-cloud-sync')
395 + ], 200 );
396 + }
397 +
398 + Logger::instance()->remove_logs_by_type($type);
399 +
400 + $result = [
401 + 'success' => true,
402 + 'message' => esc_html__('Error logs cleared', 'media-cloud-sync')
403 + ];
404 + return new WP_REST_Response( $result, 200 );
405 + }
406 +
407 +
408 + /**
339 409 * Get Status
340 410 * @since 1.2.13
341 411 */
342 412 public function getStatus( $request ) {
@@ -367,9 +437,9 @@
367 437 * @since 1.2.13
368 438 */
369 439 public function startSync( $data ) {
370 440 $action = isset($data->get_params()['action']) ? $data->get_params()['action'] : '';
371 - if(empty($action)) {
441 + if ( ! Sync::instance()->get_class_by_action( $action ) ) {
372 442 $result = [
373 443 'success' => false,
374 444 'message' => esc_html__('Invalid action', 'media-cloud-sync')
375 445 ];
@@ -381,10 +451,17 @@
381 451 'message' => Utils::get_service_configuration_error() ?: esc_html__('Storage is not configured correctly.', 'media-cloud-sync')
382 452 ];
383 453 return new WP_REST_Response( $result, 200 );
384 454 }
385 - $status = Sync::instance()->start($action);
455 + $options = isset($data->get_params()['options']) && is_array($data->get_params()['options']) ? $data->get_params()['options'] : [];
456 + $status = Sync::instance()->start($action, $options);
386 457
458 + // Sync::start() passes a job's own precondition-failure array straight through
459 + // (e.g. no destination configured) instead of masking it as a started status.
460 + if (is_array($status) && isset($status['success']) && $status['success'] === false) {
461 + return new WP_REST_Response( $status, 200 );
462 + }
463 +
387 464 $result = [
388 465 'success' => true,
389 466 'status' => $status,
390 467 'message' => esc_html__('Sync started successfully', 'media-cloud-sync')
@@ -398,9 +475,9 @@
398 475 * @since 1.2.13
399 476 */
400 477 public function pauseSync( $data ) {
401 478 $action = isset($data->get_params()['action']) ? $data->get_params()['action'] : '';
402 - if(empty($action)) {
479 + if ( ! Sync::instance()->get_class_by_action( $action ) ) {
403 480 $result = [
404 481 'success' => false,
405 482 'message' => esc_html__('Invalid action', 'media-cloud-sync')
406 483 ];
@@ -417,14 +494,46 @@
417 494 }
418 495
419 496
420 497 /**
498 + * Save the sync method ('ajax'|'cron'|'mixed') preference for a sync type.
499 + * @since 1.3.13
500 + */
501 + public function setSyncMethod( $data ) {
502 + $params = $data->get_params();
503 + $action = isset($params['action']) ? sanitize_text_field($params['action']) : '';
504 + $method = isset($params['sync_method']) ? sanitize_text_field($params['sync_method']) : '';
505 +
506 + if ( ! Sync::instance()->get_class_by_action( $action ) ) {
507 + return new WP_REST_Response( [
508 + 'success' => false,
509 + 'message' => esc_html__('Invalid action', 'media-cloud-sync')
510 + ], 200 );
511 + }
512 +
513 + $saved = Sync::instance()->set_sync_method($action, $method);
514 + if(!$saved) {
515 + return new WP_REST_Response( [
516 + 'success' => false,
517 + 'message' => esc_html__('Invalid sync method', 'media-cloud-sync')
518 + ], 200 );
519 + }
520 +
521 + $result = [
522 + 'success' => true,
523 + 'sync_method' => $method,
524 + 'status' => Sync::instance()->get_class_by_action($action)->get_status(),
525 + ];
526 + return new WP_REST_Response( $result, 200 );
527 + }
528 +
529 + /**
421 530 * Resume Sync
422 531 * @since 1.2.13
423 532 */
424 533 public function resumeSync( $data ) {
425 534 $action = isset($data->get_params()['action']) ? $data->get_params()['action'] : '';
426 - if(empty($action)) {
535 + if ( ! Sync::instance()->get_class_by_action( $action ) ) {
427 536 $result = [
428 537 'success' => false,
429 538 'message' => esc_html__('Invalid action', 'media-cloud-sync')
430 539 ];
@@ -446,9 +555,9 @@
446 555 * @since 1.2.13
447 556 */
448 557 public function stopSync( $data ) {
449 558 $action = isset($data->get_params()['action']) ? $data->get_params()['action'] : '';
450 - if(empty($action)) {
559 + if ( ! Sync::instance()->get_class_by_action( $action ) ) {
451 560 $result = [
452 561 'success' => false,
453 562 'message' => esc_html__('Invalid action', 'media-cloud-sync')
454 563 ];
@@ -469,8 +578,67 @@
469 578 }
470 579
471 580
472 581 /**
582 + * Tick Sync — process a small batch synchronously, driving Ajax/Mixed sync mode.
583 + * @since 1.3.13
584 + */
585 + public function tickSync( $data ) {
586 + $params = $data->get_params();
587 + $action = isset($params['action']) ? $params['action'] : '';
588 +
589 + if ( ! Sync::instance()->get_class_by_action( $action ) ) {
590 + $result = [
591 + 'success' => false,
592 + 'message' => esc_html__('Invalid action', 'media-cloud-sync')
593 + ];
594 + return new WP_REST_Response( $result, 200 );
595 + }
596 +
597 + // Defense in depth — the frontend should never call this in free/unlicensed mode, but
598 + // the endpoint must not trust that, and must not silently no-op: an unlicensed call
599 + // needs a clear rejection so the frontend's tick loop stops instead of retrying forever.
600 + if ( ! Utils::is_pro_licensed() ) {
601 + $result = [
602 + 'success' => false,
603 + 'message' => esc_html__('Ajax sync requires an active Pro license', 'media-cloud-sync')
604 + ];
605 + return new WP_REST_Response( $result, 200 );
606 + }
607 +
608 + // Clamp regardless of what the frontend sends — the endpoint itself must enforce the
609 + // ceiling, since a batch of slow uploads processed sequentially in one request risks
610 + // exceeding reverse-proxy/webserver timeouts that process_iterations() can't see.
611 + $batch = isset($params['batch']) ? (int) $params['batch'] : 5;
612 + $batch = min(10, max(1, $batch));
613 +
614 + $status = Sync::instance()->tick($action, $batch);
615 +
616 + $result = [
617 + 'success' => true,
618 + 'status' => $status,
619 + 'message' => esc_html__('Sync ticked successfully', 'media-cloud-sync')
620 + ];
621 +
622 + // Deliberately omitted on every tick (this endpoint may be hit ~once/second while a
623 + // sync runs, and Counter::get_count() is a real query) — but the Dashboard's own
624 + // Statistics widget reads a *different* redux slice (common.counts) than the Sync
625 + // screen's progress (sync[action]), only refreshed via the slower ~2-3s useSyncPolling
626 + // cadence. That's an acceptable few-seconds lag while a sync is actively running, but
627 + // right when it finishes it becomes a visibly wrong, stuck-looking number on the
628 + // Dashboard until the next poll happens to catch up. Only pay for the query at that
629 + // one transition, not on every tick.
630 + if ( isset( $status['status'] ) && $status['status'] !== 'running' ) {
631 + $result['counts'] = [
632 + 'all' => Counter::get_count(),
633 + 'categorized' => $this->getSortedMediaCounts()
634 + ];
635 + }
636 + return new WP_REST_Response( $result, 200 );
637 + }
638 +
639 +
640 + /**
473 641 * Retry a Media Sync Error
474 642 */
475 643 public function retrySingle( $data ) {
476 644 $params = $data->get_params();
@@ -486,11 +654,23 @@
486 654 return new WP_REST_Response( $result, 200 );
487 655 }
488 656 // Retry Sync
489 657 $handler = Sync::instance()->get_class_by_action($type);
490 - if($handler) {
491 - $handler->retry_single($id, $source_type);
658 + if(!$handler) {
659 + $result = [
660 + 'success' => false,
661 + 'message' => esc_html__('Invalid action', 'media-cloud-sync')
662 + ];
663 + return new WP_REST_Response( $result, 200 );
492 664 }
665 + if(!method_exists($handler, 'retry_single')) {
666 + $result = [
667 + 'success' => false,
668 + 'message' => esc_html__('Retry is not available for this sync type.', 'media-cloud-sync')
669 + ];
670 + return new WP_REST_Response( $result, 200 );
671 + }
672 + $handler->retry_single($id, $source_type);
493 673
494 674 $status = Utils::is_empty(Logger::instance()->get_log($type, $id, $source_type)) ? true : false;
495 675
496 676 $result = [
@@ -515,9 +695,9 @@
515 695 $prefixes = array_keys($source_labels);
516 696 foreach($detailed_counts as $source_type => $counts) {
517 697 $found_prefix = false;
518 698 foreach($prefixes as $prefix) {
519 - $found_prefix = strpos($source_type, $prefix) === 0 ? $prefix : false;;
699 + $found_prefix = strpos($source_type, $prefix) === 0 ? $prefix : false;
520 700 if($found_prefix !== false) break;
521 701 }
522 702
523 703 if($found_prefix !== false) {
@@ -522,13 +702,13 @@
522 702
523 703 if($found_prefix !== false) {
524 704 $sorted_counts[$found_prefix] = [
525 705 'label' => $source_labels[$found_prefix],
526 - 'uploaded' => isset($sorted_counts[$found_prefix]['data']['uploaded'])
527 - ? $sorted_counts[$found_prefix]['data']['uploaded'] + $counts['uploaded']
706 + 'uploaded' => isset( $sorted_counts[ $found_prefix ]['uploaded'] )
707 + ? $sorted_counts[ $found_prefix ]['uploaded'] + $counts['uploaded']
528 708 : $counts['uploaded'],
529 - 'total' => isset($sorted_counts[$found_prefix]['data']['total'])
530 - ? $sorted_counts[$found_prefix]['data']['total'] + $counts['total']
709 + 'total' => isset( $sorted_counts[ $found_prefix ]['total'] )
710 + ? $sorted_counts[ $found_prefix ]['total'] + $counts['total']
531 711 : $counts['total']
532 712 ];
533 713 }
534 714 }