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 +527 -102 1.2.61.4.1 View file →
@@ -30,21 +30,39 @@
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' );
43 44
45 + $this->add_route( '/getSettings', 'getSettings' );
44 46 $this->add_route( '/saveConfig', 'saveConfig', 'POST' );
45 - $this->add_route( '/getSettings', 'getSettings' );
46 - $this->add_route( '/uploadCredentials', 'uploadCredentials', 'POST' );
47 + $this->add_route( '/refreshCounts', 'refreshCounts', 'POST' );
48 + $this->add_route( '/getErrorLogs', 'getErrorLogs', 'POST' );
49 + $this->add_route( '/clearErrorLogs', 'clearErrorLogs', 'POST' );
50 +
51 + // Upgrade Database Routes
52 + $this->add_route( '/upgrade/status', 'getUpgradeStatus' );
53 + $this->add_route( '/upgrade/start', 'startUpgrade', 'POST' );
54 +
55 + // Sync Routes
56 + $this->add_route( '/sync/status', 'getStatus' );
57 + $this->add_route( '/sync/start', 'startSync', 'POST' );
58 + $this->add_route( '/sync/pause', 'pauseSync', 'POST' );
59 + $this->add_route( '/sync/resume', 'resumeSync', 'POST' );
60 + $this->add_route( '/sync/stop', 'stopSync', 'POST' );
61 + $this->add_route( '/sync/tick', 'tickSync', 'POST' );
62 + $this->add_route( '/sync/setMethod', 'setSyncMethod', 'POST' );
63 +
64 + $this->add_route( '/sync/retrySingle', 'retrySingle', 'POST' );
47 65 }
48 66
49 67 /**
50 68 * Verify the Service Credentials
@@ -66,17 +84,31 @@
66 84 public function createBucket( $data ) {
67 85 return new WP_REST_Response( Service::instance()->createBucket($data->get_params()), 200 );
68 86 }
69 87
70 -
71 88 /**
72 - * Verify the Bucket Write Permission
89 + * Saved connection status checks (storage, CDN).
90 + * @since 1.3.11
73 91 */
74 - public function verifyObjectWrite( $data ) {
75 - 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 );
76 96 }
77 97
78 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 + /**
79 111 * Verify the Bucket delete Permission
80 112 */
81 113 public function verifyObjectDelete( $data ) {
82 114 return new WP_REST_Response( Service::instance()->verifyObjectDeletePermission($data->get_params()), 200 );
@@ -82,12 +114,13 @@
82 114 return new WP_REST_Response( Service::instance()->verifyObjectDeletePermission($data->get_params()), 200 );
83 115 }
84 116
85 117 /**
86 - * Verify the Bucket Read Permission
118 + * Verify delivery read access (legacy route).
119 + * @since 1.3.11
87 120 */
88 - public function verifyObjectRead( $data ) {
89 - return new WP_REST_Response( Service::instance()->verifyObjectReadPermission($data->get_params()), 200 );
121 + public function verifyRead( $data ) {
122 + return new WP_REST_Response( Service::instance()->verifyStatus( 'cdn' ), 200 );
90 123 }
91 124
92 125 /**
93 126 * Get the Security Settings
@@ -115,16 +148,30 @@
115 148 /**
116 149 * Get Settings
117 150 */
118 151 public function getSettings( ) {
152 + // Fetch and update media counts for make sure they are up to date
153 + Counter::fetch_and_update();
154 +
119 155 $data = [
120 - 'credentials' => Utils::get_credentials(),
121 - 'common' => [
122 - 'version' => WPMCS_VERSION,
123 - 'uploaded' => Counter::get( 'uploaded' ),
156 + 'credentials' => Utils::get_credentials( '', [], true ),
157 + 'serviceEnabled' => Utils::is_service_enabled(),
158 + 'serviceError' => Utils::get_service_configuration_error(),
159 + 'common' => [
160 + 'version' => defined('WPMCS_PRO_VERSION') ? WPMCS_PRO_VERSION : WPMCS_VERSION,
161 + 'counts' => [
162 + 'all' => Counter::get_count(),
163 + 'categorized' => $this->getSortedMediaCounts()
164 + ],
124 165 'status' => Utils::get_status('', false),
125 166 'settings' => Utils::get_settings(),
126 - ]
167 + ],
168 + 'sync' => Sync::instance()->get_status(),
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 + ] : [],
127 174 ];
128 175
129 176 return new WP_REST_Response( $data, 200 );
130 177 }
@@ -145,8 +192,9 @@
145 192 $service = isset($saveData['service']) ? $saveData['service'] : false;
146 193 $serviceLabel = isset($saveData['serviceLabel']) ? $saveData['serviceLabel'] : '';
147 194 $cdn = isset($saveData['cdn']) ? $saveData['cdn'] : [];
148 195 $config = isset($saveData['config']) ? $saveData['config'] : false;
196 + $configSource = isset($saveData['configSource']) ? $saveData['configSource'] : 'database';
149 197 $bucketConfig = isset($saveData['bucketConfig']) ? $saveData['bucketConfig'] : [];
150 198 $security = isset($saveData['security']) ? $saveData['security'] : [];
151 199 $settings = isset($saveData['settings']) ? $saveData['settings'] : [];
152 200
@@ -153,20 +201,42 @@
153 201 $serviceOk = true;
154 202 $settingsOk = true;
155 203
156 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 +
157 217 $existing = Utils::get_credentials();
158 218 $existing['cdn'] = $cdn;
159 -
219 +
160 220 $updated = Utils::update_option('credentials', $existing, Schema::getConstant('GLOBAL_SETTINGS_KEY'));
161 221 $updatedSettings = Utils::update_option('settings', $settings, Schema::getConstant('GLOBAL_SETTINGS_KEY'));
162 222
163 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 + }
164 232 }
165 233
166 234
167 235 if($action === 'all' || $action === 'service'){
168 - 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 );
169 239
170 240 $updated = Utils::update_option(
171 241 'credentials',
172 242 [
@@ -172,9 +242,10 @@
172 242 [
173 243 'service' => $service,
174 244 'serviceLabel' => $serviceLabel,
175 245 'cdn' => $cdn,
176 - 'config' => $config,
246 + 'config' => $configSource === 'config' ? [] : $config,
247 + 'configSource' => $configSource,
177 248 'bucketConfig' => $bucketConfig,
178 249 'security' => $security
179 250 ],
180 251 Schema::getConstant('GLOBAL_SETTINGS_KEY')
@@ -179,22 +250,63 @@
179 250 ],
180 251 Schema::getConstant('GLOBAL_SETTINGS_KEY')
181 252 );
182 253 if(!$updated) $serviceOk = false;
254 +
255 + if($serviceOk) {
256 + Utils::set_status('cdnRead', [
257 + 'status' => false,
258 + 'message' => '',
259 + 'lastChecked' => null
260 + ]);
261 + Utils::set_status('storageCredentials', [
262 + 'status' => false,
263 + 'message' => '',
264 + 'lastChecked' => null
265 + ]);
266 + }
183 267 }
184 268
185 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 +
186 282 $updatedSettings = Utils::update_option('settings', $settings, Schema::getConstant('GLOBAL_SETTINGS_KEY'));
187 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 + }
188 291 }
189 292
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 + }
190 298
299 + if($action === 'all' || $action === 'settings') {
300 + Utils::clear_all_content_meta(false, false);
301 + }
302 +
303 + Cache::flush_object_cache();
304 + Counter::instance()->fetch_and_update();
305 + }
306 +
307 +
191 308 if($serviceOk && $settingsOk) {
192 - Utils::set_status('cdnRead', [
193 - 'status' => false,
194 - 'message' => '',
195 - 'lastChecked' => null
196 - ]);
197 309 $result = [
198 310 'success' => true,
199 311 'message' => esc_html__('Configuration saved successfully', 'media-cloud-sync')
200 312 ];
@@ -204,98 +316,411 @@
204 316 }
205 317
206 318
207 319 /**
208 - * Upload Credentials
209 - * @since 1.0.0
210 - */
211 - public function uploadCredentials($request) {
212 - if (isset($_FILES['file']) && !empty($_FILES['file'])) {
213 - $config_file = $_FILES['file'];
214 - if (isset($config_file['type']) && $config_file['type'] == 'application/json') {
215 - if (file_exists($config_file["tmp_name"])) {
320 + * Refresh the Media Counts
321 + *
322 + */
323 + public function refreshCounts( ) {
324 + Cache::flush_object_cache();
325 + Integration::instance()->clear_all_meta();
326 + Counter::instance()->fetch_and_update();
216 327
217 - add_filter( 'upload_dir', [ $this, 'change_file_upload_dir' ]);
218 - add_filter( 'mime_types', [ $this, 'add_custom_mime_type_json' ]);
328 + $result = [
329 + 'success' => true,
330 + 'counts' => [
331 + 'all' => Counter::get_count(),
332 + 'categorized' => $this->getSortedMediaCounts()
333 + ],
334 + 'message' => esc_html__('Media counts refreshed successfully', 'media-cloud-sync')
335 + ];
336 + return new WP_REST_Response( $result, 200 );
337 + }
219 338
220 - if ( ! function_exists( 'wp_handle_upload' ) ) {
221 - require_once( ABSPATH . 'wp-admin/includes/file.php' );
222 - }
223 339
224 - $upload_overrides = [
225 - 'test_form' => false,
226 - 'test_type' => true,
227 - 'mimes' => [ 'json'=>'application/json' ]
228 - ];
340 + /**
341 + * Start Upgrade
342 + * @since 1.3.6
343 + */
344 + public function startUpgrade( $request ) {
345 + $result = Upgrade::instance()->start_upgrade();
346 + return new WP_REST_Response( $result, 200 );
347 + }
229 348
230 - $movefile = wp_handle_upload( $config_file, $upload_overrides );
231 349
232 - remove_filter( 'upload_dir', [ $this, 'change_file_upload_dir' ]);
233 - remove_filter( 'mime_types', [ $this, 'add_custom_mime_type_json' ]);
350 + /**
351 + * Get Upgrade Status
352 + * @since 1.3.6
353 + */ public function getUpgradeStatus( $request ) {
354 + $result = Upgrade::instance()->get_progress();
355 + return new WP_REST_Response( $result, 200 );
356 + }
234 357
235 - if ($movefile && !isset($movefile['error'])) {
236 - $result = [
237 - 'success' => true,
238 - 'file_path' => $movefile['file'],
239 - 'file_name' => basename($movefile['file']),
240 - ];
241 - return new WP_REST_Response($result, 200);
242 - } else {
243 - $result = [
244 - 'success' => false,
245 - 'message' => $movefile['error'],
246 - ];
247 - return new WP_REST_Response($result, 200);
248 - }
249 - }
250 - } else {
251 - $result = [
252 - 'success' => false,
253 - 'message' => esc_html__('Invalid file format', 'media-cloud-sync'),
254 - ];
255 - return new WP_REST_Response($result, 200);
256 - }
257 - } else {
258 - $result = [
259 - 'success' => false,
260 - 'message' => esc_html__('Insufficient data. Please try again', 'media-cloud-sync'),
358 +
359 + /**
360 + * Get Media Error Logs
361 + * @since 1.2.13
362 + */
363 + public function getErrorLogs( $request ) {
364 + $params = $request->get_params();
365 + $type = isset($params['type']) ? $params['type'] : 'all';
366 + $page = isset($params['page']) ? $params['page'] : null;
367 + $per_page = isset($params['per_page']) ? $params['per_page'] : null;
368 +
369 + if($type !== 'all') {
370 + $logs = Logger::instance()->get_logs_by_type($type, $page, $per_page);
371 + } else {
372 + $logs = Logger::instance()->get_all_logs($page, $per_page);
373 + }
374 +
375 + $result = [
376 + 'success' => true,
377 + 'logs' => $logs,
378 + 'message' => esc_html__('Error logs fetched successfully', 'media-cloud-sync')
379 + ];
380 + return new WP_REST_Response( $result, 200 );
381 + }
382 +
383 +
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 + /**
409 + * Get Status
410 + * @since 1.2.13
411 + */
412 + public function getStatus( $request ) {
413 + // Fetch and update media counts for make sure they are up to date
414 + $action = isset($request->get_params()['action']) ? $request->get_params()['action'] : 'all';
415 + $status = Sync::instance()->get_status();
416 +
417 + if($action !== 'all') {
418 + $status = isset($status[$action]) ? $status[$action] : [];
419 + }
420 +
421 + $result = [
422 + 'success' => true,
423 + 'status' => $status,
424 + 'counts' => [
425 + 'all' => Counter::get_count(),
426 + 'categorized' => $this->getSortedMediaCounts()
427 + ],
428 + 'message' => esc_html__('Status fetched successfully', 'media-cloud-sync')
429 + ];
430 +
431 + return new WP_REST_Response( $result, 200 );
432 + }
433 +
434 +
435 + /**
436 + * Start Sync
437 + * @since 1.2.13
438 + */
439 + public function startSync( $data ) {
440 + $action = isset($data->get_params()['action']) ? $data->get_params()['action'] : '';
441 + if ( ! Sync::instance()->get_class_by_action( $action ) ) {
442 + $result = [
443 + 'success' => false,
444 + 'message' => esc_html__('Invalid action', 'media-cloud-sync')
261 445 ];
262 - return new WP_REST_Response($result, 200);
263 - }
264 - }
446 + return new WP_REST_Response( $result, 200 );
447 + }
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);
265 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 +
464 + $result = [
465 + 'success' => true,
466 + 'status' => $status,
467 + 'message' => esc_html__('Sync started successfully', 'media-cloud-sync')
468 + ];
469 + return new WP_REST_Response( $result, 200 );
470 + }
471 +
472 +
266 473 /**
267 - * Change File Upload Directory
268 - * @since 1.0.0
269 - */
270 - public function change_file_upload_dir($upload) {
271 - $wpmcs_path = '/' . Schema::getConstant('UPLOADS');
272 - $path = $upload['basedir'] . $wpmcs_path;
273 - $url = $upload['baseurl'] . $wpmcs_path;
474 + * Pause Sync
475 + * @since 1.2.13
476 + */
477 + public function pauseSync( $data ) {
478 + $action = isset($data->get_params()['action']) ? $data->get_params()['action'] : '';
479 + if ( ! Sync::instance()->get_class_by_action( $action ) ) {
480 + $result = [
481 + 'success' => false,
482 + 'message' => esc_html__('Invalid action', 'media-cloud-sync')
483 + ];
484 + return new WP_REST_Response( $result, 200 );
485 + }
486 + $status = Sync::instance()->pause($action);
274 487
275 - if (!is_dir($path)) {
276 - do_action( $this->token.'_create_plugin_dir' );
277 - }
488 + $result = [
489 + 'success' => true,
490 + 'status' => $status,
491 + 'message' => esc_html__('Sync paused successfully', 'media-cloud-sync')
492 + ];
493 + return new WP_REST_Response( $result, 200 );
494 + }
278 495
279 - $upload['subdir'] = $wpmcs_path;
280 - $upload['path'] = $upload['basedir'] . $wpmcs_path;
281 - $upload['url'] = $upload['baseurl'] . $wpmcs_path;
282 496
283 - return $upload;
284 - }
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']) : '';
285 505
286 - /**
287 - * Add Custom Mime Type JSON
288 - * @since 1.0.0
289 - */
290 - public function add_custom_mime_type_json($mimes) {
291 - $mimes['json'] = 'application/json';
292 - // Return the array back to the function with our added mime type.
293 - return $mimes;
294 - }
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 + }
295 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 + }
296 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 +
297 529 /**
530 + * Resume Sync
531 + * @since 1.2.13
532 + */
533 + public function resumeSync( $data ) {
534 + $action = isset($data->get_params()['action']) ? $data->get_params()['action'] : '';
535 + if ( ! Sync::instance()->get_class_by_action( $action ) ) {
536 + $result = [
537 + 'success' => false,
538 + 'message' => esc_html__('Invalid action', 'media-cloud-sync')
539 + ];
540 + return new WP_REST_Response( $result, 200 );
541 + }
542 + $status = Sync::instance()->resume($action);
543 +
544 + $result = [
545 + 'success' => true,
546 + 'status' => $status,
547 + 'message' => esc_html__('Sync resumed successfully', 'media-cloud-sync')
548 + ];
549 + return new WP_REST_Response( $result, 200 );
550 + }
551 +
552 +
553 + /**
554 + * Stop Sync
555 + * @since 1.2.13
556 + */
557 + public function stopSync( $data ) {
558 + $action = isset($data->get_params()['action']) ? $data->get_params()['action'] : '';
559 + if ( ! Sync::instance()->get_class_by_action( $action ) ) {
560 + $result = [
561 + 'success' => false,
562 + 'message' => esc_html__('Invalid action', 'media-cloud-sync')
563 + ];
564 + return new WP_REST_Response( $result, 200 );
565 + }
566 + $status = Sync::instance()->stop($action);
567 +
568 + $result = [
569 + 'success' => true,
570 + 'status' => $status,
571 + 'counts' => [
572 + 'all' => Counter::get_count(),
573 + 'categorized' => $this->getSortedMediaCounts()
574 + ],
575 + 'message' => esc_html__('Sync stopped successfully', 'media-cloud-sync')
576 + ];
577 + return new WP_REST_Response( $result, 200 );
578 + }
579 +
580 +
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 + /**
641 + * Retry a Media Sync Error
642 + */
643 + public function retrySingle( $data ) {
644 + $params = $data->get_params();
645 + $id = isset($params['id']) ? $params['id'] : '';
646 + $source_type = isset($params['source_type']) ? $params['source_type'] : 'media_library';
647 + $type = isset($params['type']) ? $params['type'] : '';
648 + $status = false;
649 + if(empty($id) || empty($source_type) || empty($type)) {
650 + $result = [
651 + 'success' => false,
652 + 'message' => esc_html__('Invalid parameters', 'media-cloud-sync')
653 + ];
654 + return new WP_REST_Response( $result, 200 );
655 + }
656 + // Retry Sync
657 + $handler = Sync::instance()->get_class_by_action($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 );
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);
673 +
674 + $status = Utils::is_empty(Logger::instance()->get_log($type, $id, $source_type)) ? true : false;
675 +
676 + $result = [
677 + 'success' => $status,
678 + 'message' => $status ? esc_html__('Media synced successfully', 'media-cloud-sync') : esc_html__('Media sync failed', 'media-cloud-sync')
679 + ];
680 + return new WP_REST_Response( $result, 200 );
681 + }
682 +
683 +
684 + /**
685 + * Get Sorted Media Counts
686 + * Sorts media counts by source type
687 + * @since 1.2.13
688 + */
689 + private function getSortedMediaCounts() {
690 + $sorted_counts = [];
691 + $detailed_counts = Counter::get_count( 'all', false );
692 + if(!empty($detailed_counts)) {
693 + $source_labels = Integration::$source_labels;
694 + if(!empty($source_labels)) {
695 + $prefixes = array_keys($source_labels);
696 + foreach($detailed_counts as $source_type => $counts) {
697 + $found_prefix = false;
698 + foreach($prefixes as $prefix) {
699 + $found_prefix = strpos($source_type, $prefix) === 0 ? $prefix : false;
700 + if($found_prefix !== false) break;
701 + }
702 +
703 + if($found_prefix !== false) {
704 + $sorted_counts[$found_prefix] = [
705 + 'label' => $source_labels[$found_prefix],
706 + 'uploaded' => isset( $sorted_counts[ $found_prefix ]['uploaded'] )
707 + ? $sorted_counts[ $found_prefix ]['uploaded'] + $counts['uploaded']
708 + : $counts['uploaded'],
709 + 'total' => isset( $sorted_counts[ $found_prefix ]['total'] )
710 + ? $sorted_counts[ $found_prefix ]['total'] + $counts['total']
711 + : $counts['total']
712 + ];
713 + }
714 + }
715 + }
716 + }
717 +
718 + return $sorted_counts;
719 + }
720 +
721 +
722 + /**
298 723 * Helper function to create Adding Route
299 724 * @since 1.0.0
300 725 */
301 726 private function add_route( $slug, $callBack, $method = 'GET' ) {
@@ -312,12 +737,12 @@
312 737 /**
313 738 * Permission Callback
314 739 **/
315 740 public function getPermission() {
316 - if ( current_user_can( 'administrator' ) ) {
741 + if ( current_user_can( 'manage_options' ) ) {
317 742 return true;
318 743 } else {
319 - return true;
744 + return false;
320 745 }
321 746 }
322 747
323 748 /**