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 +252 -39 1.3.101.4.1 View file →
@@ -30,13 +30,14 @@
30 30 */
31 31
32 32 public function register_routes() {
33 33 $this->add_route( '/verifyCredentials', 'verifyCredentials', 'POST' );
34 - $this->add_route( '/permissions/checkExist', 'verifyBucketExist', 'POST' );
35 - $this->add_route( '/permissions/addNewBucket', 'createBucket', 'POST' );
36 - $this->add_route( '/permissions/write', 'verifyObjectWrite', 'POST' );
34 + $this->add_route( '/bucket/checkExist', 'verifyBucketExist', 'POST' );
35 + $this->add_route( '/bucket/addNew', 'createBucket', 'POST' );
36 + $this->add_route( '/permissions/status', 'verifyStatus', 'POST' );
37 + $this->add_route( '/permissions/write', 'verifyWrite', 'POST' );
37 38 $this->add_route( '/permissions/delete', 'verifyObjectDelete', 'POST' );
38 - $this->add_route( '/permissions/read', 'verifyObjectRead', 'POST' );
39 + $this->add_route( '/permissions/read', 'verifyRead', 'POST' );
39 40
40 41 $this->add_route( '/security/get_security', 'getBucketSecuritySettings', 'POST' );
41 42 $this->add_route( '/security/block_public_access', 'changePublicAccess', 'POST' );
42 43 $this->add_route( '/security/object_ownership_enforce', 'changeObjectOwnership', 'POST' );
@@ -44,8 +45,9 @@
44 45 $this->add_route( '/getSettings', 'getSettings' );
45 46 $this->add_route( '/saveConfig', 'saveConfig', 'POST' );
46 47 $this->add_route( '/refreshCounts', 'refreshCounts', 'POST' );
47 48 $this->add_route( '/getErrorLogs', 'getErrorLogs', 'POST' );
49 + $this->add_route( '/clearErrorLogs', 'clearErrorLogs', 'POST' );
48 50
49 51 // Upgrade Database Routes
50 52 $this->add_route( '/upgrade/status', 'getUpgradeStatus' );
51 53 $this->add_route( '/upgrade/start', 'startUpgrade', 'POST' );
@@ -55,8 +57,10 @@
55 57 $this->add_route( '/sync/start', 'startSync', 'POST' );
56 58 $this->add_route( '/sync/pause', 'pauseSync', 'POST' );
57 59 $this->add_route( '/sync/resume', 'resumeSync', 'POST' );
58 60 $this->add_route( '/sync/stop', 'stopSync', 'POST' );
61 + $this->add_route( '/sync/tick', 'tickSync', 'POST' );
62 + $this->add_route( '/sync/setMethod', 'setSyncMethod', 'POST' );
59 63
60 64 $this->add_route( '/sync/retrySingle', 'retrySingle', 'POST' );
61 65 }
62 66
@@ -80,17 +84,31 @@
80 84 public function createBucket( $data ) {
81 85 return new WP_REST_Response( Service::instance()->createBucket($data->get_params()), 200 );
82 86 }
83 87
84 -
85 88 /**
86 - * Verify the Bucket Write Permission
89 + * Saved connection status checks (storage, CDN).
90 + * @since 1.3.11
87 91 */
88 - public function verifyObjectWrite( $data ) {
89 - return new WP_REST_Response( Service::instance()->verifyObjectWritePermission($data->get_params()), 200 );
92 + public function verifyStatus( $data ) {
93 + $params = $data->get_params();
94 + $check = isset( $params['check'] ) ? sanitize_text_field( $params['check'] ) : 'storage';
95 + return new WP_REST_Response( Service::instance()->verifyStatus( $check ), 200 );
90 96 }
91 97
92 98 /**
99 + * Verify bucket write permission (configure wizard) or saved storage status (legacy route).
100 + * @since 1.3.11
101 + */
102 + public function verifyWrite( $data ) {
103 + $params = $data->get_params();
104 + if ( !empty( $params['service'] ) ) {
105 + return new WP_REST_Response( Service::instance()->verifyObjectWritePermission( $params ), 200 );
106 + }
107 + return new WP_REST_Response( Service::instance()->verifyStatus( 'storage' ), 200 );
108 + }
109 +
110 + /**
93 111 * Verify the Bucket delete Permission
94 112 */
95 113 public function verifyObjectDelete( $data ) {
96 114 return new WP_REST_Response( Service::instance()->verifyObjectDeletePermission($data->get_params()), 200 );
@@ -96,12 +114,13 @@
96 114 return new WP_REST_Response( Service::instance()->verifyObjectDeletePermission($data->get_params()), 200 );
97 115 }
98 116
99 117 /**
100 - * Verify the Bucket Read Permission
118 + * Verify delivery read access (legacy route).
119 + * @since 1.3.11
101 120 */
102 - public function verifyObjectRead( $data ) {
103 - return new WP_REST_Response( Service::instance()->verifyObjectReadPermission(), 200 );
121 + public function verifyRead( $data ) {
122 + return new WP_REST_Response( Service::instance()->verifyStatus( 'cdn' ), 200 );
104 123 }
105 124
106 125 /**
107 126 * Get the Security Settings
@@ -133,10 +152,12 @@
133 152 // Fetch and update media counts for make sure they are up to date
134 153 Counter::fetch_and_update();
135 154
136 155 $data = [
137 - 'credentials' => Utils::get_credentials( '', [], true ),
138 - 'common' => [
156 + 'credentials' => Utils::get_credentials( '', [], true ),
157 + 'serviceEnabled' => Utils::is_service_enabled(),
158 + 'serviceError' => Utils::get_service_configuration_error(),
159 + 'common' => [
139 160 'version' => defined('WPMCS_PRO_VERSION') ? WPMCS_PRO_VERSION : WPMCS_VERSION,
140 161 'counts' => [
141 162 'all' => Counter::get_count(),
142 163 'categorized' => $this->getSortedMediaCounts()
@@ -145,8 +166,12 @@
145 166 'settings' => Utils::get_settings(),
146 167 ],
147 168 'sync' => Sync::instance()->get_status(),
148 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 + ] : [],
149 174 ];
150 175
151 176 return new WP_REST_Response( $data, 200 );
152 177 }
@@ -167,8 +192,9 @@
167 192 $service = isset($saveData['service']) ? $saveData['service'] : false;
168 193 $serviceLabel = isset($saveData['serviceLabel']) ? $saveData['serviceLabel'] : '';
169 194 $cdn = isset($saveData['cdn']) ? $saveData['cdn'] : [];
170 195 $config = isset($saveData['config']) ? $saveData['config'] : false;
196 + $configSource = isset($saveData['configSource']) ? $saveData['configSource'] : 'database';
171 197 $bucketConfig = isset($saveData['bucketConfig']) ? $saveData['bucketConfig'] : [];
172 198 $security = isset($saveData['security']) ? $saveData['security'] : [];
173 199 $settings = isset($saveData['settings']) ? $saveData['settings'] : [];
174 200
@@ -175,20 +201,42 @@
175 201 $serviceOk = true;
176 202 $settingsOk = true;
177 203
178 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 +
179 217 $existing = Utils::get_credentials();
180 218 $existing['cdn'] = $cdn;
181 -
219 +
182 220 $updated = Utils::update_option('credentials', $existing, Schema::getConstant('GLOBAL_SETTINGS_KEY'));
183 221 $updatedSettings = Utils::update_option('settings', $settings, Schema::getConstant('GLOBAL_SETTINGS_KEY'));
184 222
185 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 + }
186 232 }
187 233
188 234
189 235 if($action === 'all' || $action === 'service'){
190 - if ( $service === false || empty($config) || empty($bucketConfig)) return new WP_REST_Response( $result, 200 );
236 + // In 'config' source mode the credentials live in the WPMCS_CONFIG constant,
237 + // so an empty config payload is expected and must not block the save.
238 + if ( $service === false || ($configSource !== 'config' && empty($config)) || empty($bucketConfig)) return new WP_REST_Response( $result, 200 );
191 239
192 240 $updated = Utils::update_option(
193 241 'credentials',
194 242 [
@@ -194,9 +242,10 @@
194 242 [
195 243 'service' => $service,
196 244 'serviceLabel' => $serviceLabel,
197 245 'cdn' => $cdn,
198 - 'config' => $config,
246 + 'config' => $configSource === 'config' ? [] : $config,
247 + 'configSource' => $configSource,
199 248 'bucketConfig' => $bucketConfig,
200 249 'security' => $security
201 250 ],
202 251 Schema::getConstant('GLOBAL_SETTINGS_KEY')
@@ -208,28 +257,51 @@
208 257 'status' => false,
209 258 'message' => '',
210 259 'lastChecked' => null
211 260 ]);
261 + Utils::set_status('storageCredentials', [
262 + 'status' => false,
263 + 'message' => '',
264 + 'lastChecked' => null
265 + ]);
212 266 }
213 267 }
214 268
215 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 +
216 282 $updatedSettings = Utils::update_option('settings', $settings, Schema::getConstant('GLOBAL_SETTINGS_KEY'));
217 -
218 - // Clear content meta cache if settings updated
219 - Utils::clear_all_content_meta();
220 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 + }
221 291 }
222 292
223 - // If action is all, service or settings, clear caches and update counts
224 - if($action === 'all' || $action === 'service' || $action === 'settings') {
225 - //clear object cache
293 + // Clear plugin caches and refresh counts after any configuration save.
294 + if(in_array($action, ['all', 'service', 'settings', 'cdn'], true)) {
295 + if($action === 'all' || $action === 'service' || $action === 'settings') {
296 + Integration::instance()->clear_all_meta(false);
297 + }
298 +
299 + if($action === 'all' || $action === 'settings') {
300 + Utils::clear_all_content_meta(false, false);
301 + }
302 +
226 303 Cache::flush_object_cache();
227 - // clear all meta of attachments
228 - Integration::instance()->clear_all_meta();
229 - // clear all content meta
230 - Utils::clear_all_content_meta();
231 - // update counts
232 304 Counter::instance()->fetch_and_update();
233 305 }
234 306
235 307
@@ -309,8 +381,32 @@
309 381 }
310 382
311 383
312 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 + /**
313 409 * Get Status
314 410 * @since 1.2.13
315 411 */
316 412 public function getStatus( $request ) {
@@ -341,9 +437,9 @@
341 437 * @since 1.2.13
342 438 */
343 439 public function startSync( $data ) {
344 440 $action = isset($data->get_params()['action']) ? $data->get_params()['action'] : '';
345 - if(empty($action)) {
441 + if ( ! Sync::instance()->get_class_by_action( $action ) ) {
346 442 $result = [
347 443 'success' => false,
348 444 'message' => esc_html__('Invalid action', 'media-cloud-sync')
349 445 ];
@@ -348,10 +444,24 @@
348 444 'message' => esc_html__('Invalid action', 'media-cloud-sync')
349 445 ];
350 446 return new WP_REST_Response( $result, 200 );
351 447 }
352 - $status = Sync::instance()->start($action);
448 + if(!Utils::is_service_enabled()) {
449 + $result = [
450 + 'success' => false,
451 + 'message' => Utils::get_service_configuration_error() ?: esc_html__('Storage is not configured correctly.', 'media-cloud-sync')
452 + ];
453 + return new WP_REST_Response( $result, 200 );
454 + }
455 + $options = isset($data->get_params()['options']) && is_array($data->get_params()['options']) ? $data->get_params()['options'] : [];
456 + $status = Sync::instance()->start($action, $options);
353 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 +
354 464 $result = [
355 465 'success' => true,
356 466 'status' => $status,
357 467 'message' => esc_html__('Sync started successfully', 'media-cloud-sync')
@@ -365,9 +475,9 @@
365 475 * @since 1.2.13
366 476 */
367 477 public function pauseSync( $data ) {
368 478 $action = isset($data->get_params()['action']) ? $data->get_params()['action'] : '';
369 - if(empty($action)) {
479 + if ( ! Sync::instance()->get_class_by_action( $action ) ) {
370 480 $result = [
371 481 'success' => false,
372 482 'message' => esc_html__('Invalid action', 'media-cloud-sync')
373 483 ];
@@ -384,14 +494,46 @@
384 494 }
385 495
386 496
387 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 + /**
388 530 * Resume Sync
389 531 * @since 1.2.13
390 532 */
391 533 public function resumeSync( $data ) {
392 534 $action = isset($data->get_params()['action']) ? $data->get_params()['action'] : '';
393 - if(empty($action)) {
535 + if ( ! Sync::instance()->get_class_by_action( $action ) ) {
394 536 $result = [
395 537 'success' => false,
396 538 'message' => esc_html__('Invalid action', 'media-cloud-sync')
397 539 ];
@@ -413,9 +555,9 @@
413 555 * @since 1.2.13
414 556 */
415 557 public function stopSync( $data ) {
416 558 $action = isset($data->get_params()['action']) ? $data->get_params()['action'] : '';
417 - if(empty($action)) {
559 + if ( ! Sync::instance()->get_class_by_action( $action ) ) {
418 560 $result = [
419 561 'success' => false,
420 562 'message' => esc_html__('Invalid action', 'media-cloud-sync')
421 563 ];
@@ -436,8 +578,67 @@
436 578 }
437 579
438 580
439 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 + /**
440 641 * Retry a Media Sync Error
441 642 */
442 643 public function retrySingle( $data ) {
443 644 $params = $data->get_params();
@@ -453,11 +654,23 @@
453 654 return new WP_REST_Response( $result, 200 );
454 655 }
455 656 // Retry Sync
456 657 $handler = Sync::instance()->get_class_by_action($type);
457 - if($handler) {
458 - $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 );
459 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);
460 673
461 674 $status = Utils::is_empty(Logger::instance()->get_log($type, $id, $source_type)) ? true : false;
462 675
463 676 $result = [
@@ -482,9 +695,9 @@
482 695 $prefixes = array_keys($source_labels);
483 696 foreach($detailed_counts as $source_type => $counts) {
484 697 $found_prefix = false;
485 698 foreach($prefixes as $prefix) {
486 - $found_prefix = strpos($source_type, $prefix) === 0 ? $prefix : false;;
699 + $found_prefix = strpos($source_type, $prefix) === 0 ? $prefix : false;
487 700 if($found_prefix !== false) break;
488 701 }
489 702
490 703 if($found_prefix !== false) {
@@ -489,13 +702,13 @@
489 702
490 703 if($found_prefix !== false) {
491 704 $sorted_counts[$found_prefix] = [
492 705 'label' => $source_labels[$found_prefix],
493 - 'uploaded' => isset($sorted_counts[$found_prefix]['data']['uploaded'])
494 - ? $sorted_counts[$found_prefix]['data']['uploaded'] + $counts['uploaded']
706 + 'uploaded' => isset( $sorted_counts[ $found_prefix ]['uploaded'] )
707 + ? $sorted_counts[ $found_prefix ]['uploaded'] + $counts['uploaded']
495 708 : $counts['uploaded'],
496 - 'total' => isset($sorted_counts[$found_prefix]['data']['total'])
497 - ? $sorted_counts[$found_prefix]['data']['total'] + $counts['total']
709 + 'total' => isset( $sorted_counts[ $found_prefix ]['total'] )
710 + ? $sorted_counts[ $found_prefix ]['total'] + $counts['total']
498 711 : $counts['total']
499 712 ];
500 713 }
501 714 }