PluginProbe
Media Cloud Sync / trunk
Media Cloud Sync vtrunk
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 1.3.0 All 34 releases
← All changes | includes/api.php +504 -107 1.2.0trunk View file →
@@ -30,20 +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' );
39 + $this->add_route( '/permissions/read', 'verifyRead', 'POST' );
38 40
39 41 $this->add_route( '/security/get_security', 'getBucketSecuritySettings', 'POST' );
40 42 $this->add_route( '/security/block_public_access', 'changePublicAccess', 'POST' );
41 43 $this->add_route( '/security/object_ownership_enforce', 'changeObjectOwnership', 'POST' );
42 44
45 + $this->add_route( '/getSettings', 'getSettings' );
43 46 $this->add_route( '/saveConfig', 'saveConfig', 'POST' );
44 - $this->add_route( '/getSettings', 'getSettings' );
45 - $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' );
46 65 }
47 66
48 67 /**
49 68 * Verify the Service Credentials
@@ -48,10 +67,9 @@
48 67 /**
49 68 * Verify the Service Credentials
50 69 */
51 70 public function verifyCredentials( $data ) {
52 - global $wpmcsService;
53 - return new WP_REST_Response( $wpmcsService->verifyCredentials($data->get_params()), 200 );
71 + return new WP_REST_Response( Service::instance()->verifyCredentials($data->get_params()), 200 );
54 72 }
55 73
56 74 /**
57 75 * Verify the Bucket exist
@@ -56,10 +74,9 @@
56 74 /**
57 75 * Verify the Bucket exist
58 76 */
59 77 public function verifyBucketExist( $data ) {
60 - global $wpmcsService;
61 - return new WP_REST_Response( $wpmcsService->verifyBucketExist($data->get_params()), 200 );
78 + return new WP_REST_Response( Service::instance()->verifyBucketExist($data->get_params()), 200 );
62 79 }
63 80
64 81 /**
65 82 * Create the bucket
@@ -64,35 +81,53 @@
64 81 /**
65 82 * Create the bucket
66 83 */
67 84 public function createBucket( $data ) {
68 - global $wpmcsService;
69 - return new WP_REST_Response( $wpmcsService->createBucket($data->get_params()), 200 );
85 + return new WP_REST_Response( Service::instance()->createBucket($data->get_params()), 200 );
70 86 }
71 87
72 -
73 88 /**
74 - * Verify the Bucket Write Permission
89 + * Saved connection status checks (storage, CDN).
90 + * @since 1.3.11
75 91 */
76 - public function verifyObjectWrite( $data ) {
77 - global $wpmcsService;
78 - return new WP_REST_Response( $wpmcsService->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 );
79 96 }
80 97
81 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 + /**
82 111 * Verify the Bucket delete Permission
83 112 */
84 113 public function verifyObjectDelete( $data ) {
85 - global $wpmcsService;
86 - return new WP_REST_Response( $wpmcsService->verifyObjectDeletePermission($data->get_params()), 200 );
114 + return new WP_REST_Response( Service::instance()->verifyObjectDeletePermission($data->get_params()), 200 );
87 115 }
88 116
89 117 /**
118 + * Verify delivery read access (legacy route).
119 + * @since 1.3.11
120 + */
121 + public function verifyRead( $data ) {
122 + return new WP_REST_Response( Service::instance()->verifyStatus( 'cdn' ), 200 );
123 + }
124 +
125 + /**
90 126 * Get the Security Settings
91 127 */
92 128 public function getBucketSecuritySettings( $data ) {
93 - global $wpmcsService;
94 - return new WP_REST_Response( $wpmcsService->getBucketSecuritySettings($data->get_params()), 200 );
129 + return new WP_REST_Response( Service::instance()->getBucketSecuritySettings($data->get_params()), 200 );
95 130 }
96 131
97 132
98 133 /**
@@ -98,10 +133,9 @@
98 133 /**
99 134 * Change the Public Access
100 135 */
101 136 public function changePublicAccess( $data ) {
102 - global $wpmcsService;
103 - return new WP_REST_Response( $wpmcsService->changePublicAccess($data->get_params()), 200 );
137 + return new WP_REST_Response( Service::instance()->changePublicAccess($data->get_params()), 200 );
104 138 }
105 139
106 140
107 141 /**
@@ -107,10 +141,9 @@
107 141 /**
108 142 * Change the Object Ownership
109 143 */
110 144 public function changeObjectOwnership( $data ) {
111 - global $wpmcsService;
112 - return new WP_REST_Response( $wpmcsService->changeObjectOwnership($data->get_params()), 200 );
145 + return new WP_REST_Response( Service::instance()->changeObjectOwnership($data->get_params()), 200 );
113 146 }
114 147
115 148 /**
116 149 * Get Settings
@@ -115,15 +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 + ],
165 + 'status' => Utils::get_status('', false),
124 166 'settings' => Utils::get_settings(),
125 - ]
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 + ] : [],
126 174 ];
127 175
128 176 return new WP_REST_Response( $data, 200 );
129 177 }
@@ -141,10 +189,12 @@
141 189 $action = isset($saveData['action']) ? $saveData['action'] : false;
142 190 if($action === false) return new WP_REST_Response( $result, 200 );
143 191
144 192 $service = isset($saveData['service']) ? $saveData['service'] : false;
193 + $serviceLabel = isset($saveData['serviceLabel']) ? $saveData['serviceLabel'] : '';
145 194 $cdn = isset($saveData['cdn']) ? $saveData['cdn'] : [];
146 195 $config = isset($saveData['config']) ? $saveData['config'] : false;
196 + $configSource = isset($saveData['configSource']) ? $saveData['configSource'] : 'database';
147 197 $bucketConfig = isset($saveData['bucketConfig']) ? $saveData['bucketConfig'] : [];
148 198 $security = isset($saveData['security']) ? $saveData['security'] : [];
149 199 $settings = isset($saveData['settings']) ? $saveData['settings'] : [];
150 200
@@ -153,22 +203,30 @@
153 203
154 204 if( $action === 'cdn' ){
155 205 $existing = Utils::get_credentials();
156 206 $existing['cdn'] = $cdn;
207 +
157 208 $updated = Utils::update_option('credentials', $existing, Schema::getConstant('GLOBAL_SETTINGS_KEY'));
209 + $updatedSettings = Utils::update_option('settings', $settings, Schema::getConstant('GLOBAL_SETTINGS_KEY'));
210 +
158 211 if(!$updated) $serviceOk = false;
212 + if(!$updatedSettings) $settingsOk = false;
159 213 }
160 214
161 215
162 216 if($action === 'all' || $action === 'service'){
163 - if ( $service === false || empty($config) || empty($bucketConfig)) return new WP_REST_Response( $result, 200 );
217 + // In 'config' source mode the credentials live in the WPMCS_CONFIG constant,
218 + // so an empty config payload is expected and must not block the save.
219 + if ( $service === false || ($configSource !== 'config' && empty($config)) || empty($bucketConfig)) return new WP_REST_Response( $result, 200 );
164 220
165 221 $updated = Utils::update_option(
166 222 'credentials',
167 223 [
168 224 'service' => $service,
225 + 'serviceLabel' => $serviceLabel,
169 226 'cdn' => $cdn,
170 - 'config' => $config,
227 + 'config' => $configSource === 'config' ? [] : $config,
228 + 'configSource' => $configSource,
171 229 'bucketConfig' => $bucketConfig,
172 230 'security' => $security
173 231 ],
174 232 Schema::getConstant('GLOBAL_SETTINGS_KEY')
@@ -173,8 +231,21 @@
173 231 ],
174 232 Schema::getConstant('GLOBAL_SETTINGS_KEY')
175 233 );
176 234 if(!$updated) $serviceOk = false;
235 +
236 + if($serviceOk) {
237 + Utils::set_status('cdnRead', [
238 + 'status' => false,
239 + 'message' => '',
240 + 'lastChecked' => null
241 + ]);
242 + Utils::set_status('storageCredentials', [
243 + 'status' => false,
244 + 'message' => '',
245 + 'lastChecked' => null
246 + ]);
247 + }
177 248 }
178 249
179 250 if(($action === 'all' || $action === 'settings')) {
180 251 $updatedSettings = Utils::update_option('settings', $settings, Schema::getConstant('GLOBAL_SETTINGS_KEY'));
@@ -180,9 +251,23 @@
180 251 $updatedSettings = Utils::update_option('settings', $settings, Schema::getConstant('GLOBAL_SETTINGS_KEY'));
181 252 if(!$updatedSettings) $settingsOk = false;
182 253 }
183 254
255 + // Clear plugin caches and refresh counts after any configuration save.
256 + if(in_array($action, ['all', 'service', 'settings', 'cdn'], true)) {
257 + if($action === 'all' || $action === 'service' || $action === 'settings') {
258 + Integration::instance()->clear_all_meta(false);
259 + }
184 260
261 + if($action === 'all' || $action === 'settings') {
262 + Utils::clear_all_content_meta(false, false);
263 + }
264 +
265 + Cache::flush_object_cache();
266 + Counter::instance()->fetch_and_update();
267 + }
268 +
269 +
185 270 if($serviceOk && $settingsOk) {
186 271 $result = [
187 272 'success' => true,
188 273 'message' => esc_html__('Configuration saved successfully', 'media-cloud-sync')
@@ -193,98 +278,410 @@
193 278 }
194 279
195 280
196 281 /**
197 - * Upload Credentials
198 - * @since 1.0.0
199 - */
200 - public function uploadCredentials($request) {
201 - if (isset($_FILES['file']) && !empty($_FILES['file'])) {
202 - $config_file = $_FILES['file'];
203 - if (isset($config_file['type']) && $config_file['type'] == 'application/json') {
204 - if (file_exists($config_file["tmp_name"])) {
282 + * Refresh the Media Counts
283 + *
284 + */
285 + public function refreshCounts( ) {
286 + Cache::flush_object_cache();
287 + Integration::instance()->clear_all_meta();
288 + Counter::instance()->fetch_and_update();
205 289
206 - add_filter( 'upload_dir', [ $this, 'change_file_upload_dir' ]);
207 - add_filter( 'mime_types', [ $this, 'add_custom_mime_type_json' ]);
290 + $result = [
291 + 'success' => true,
292 + 'counts' => [
293 + 'all' => Counter::get_count(),
294 + 'categorized' => $this->getSortedMediaCounts()
295 + ],
296 + 'message' => esc_html__('Media counts refreshed successfully', 'media-cloud-sync')
297 + ];
298 + return new WP_REST_Response( $result, 200 );
299 + }
208 300
209 - if ( ! function_exists( 'wp_handle_upload' ) ) {
210 - require_once( ABSPATH . 'wp-admin/includes/file.php' );
211 - }
212 301
213 - $upload_overrides = [
214 - 'test_form' => false,
215 - 'test_type' => true,
216 - 'mimes' => [ 'json'=>'application/json' ]
217 - ];
302 + /**
303 + * Start Upgrade
304 + * @since 1.3.6
305 + */
306 + public function startUpgrade( $request ) {
307 + $result = Upgrade::instance()->start_upgrade();
308 + return new WP_REST_Response( $result, 200 );
309 + }
218 310
219 - $movefile = wp_handle_upload( $config_file, $upload_overrides );
220 311
221 - remove_filter( 'upload_dir', [ $this, 'change_file_upload_dir' ]);
222 - remove_filter( 'mime_types', [ $this, 'add_custom_mime_type_json' ]);
312 + /**
313 + * Get Upgrade Status
314 + * @since 1.3.6
315 + */ public function getUpgradeStatus( $request ) {
316 + $result = Upgrade::instance()->get_progress();
317 + return new WP_REST_Response( $result, 200 );
318 + }
223 319
224 - if ($movefile && !isset($movefile['error'])) {
225 - $result = [
226 - 'success' => true,
227 - 'file_path' => $movefile['file'],
228 - 'file_name' => basename($movefile['file']),
229 - ];
230 - return new WP_REST_Response($result, 200);
231 - } else {
232 - $result = [
233 - 'success' => false,
234 - 'message' => $movefile['error'],
235 - ];
236 - return new WP_REST_Response($result, 200);
237 - }
238 - }
239 - } else {
240 - $result = [
241 - 'success' => false,
242 - 'message' => esc_html__('Invalid file format', 'media-cloud-sync'),
243 - ];
244 - return new WP_REST_Response($result, 200);
245 - }
246 - } else {
247 - $result = [
248 - 'success' => false,
249 - 'message' => esc_html__('Insufficient data. Please try again', 'media-cloud-sync'),
320 +
321 + /**
322 + * Get Media Error Logs
323 + * @since 1.2.13
324 + */
325 + public function getErrorLogs( $request ) {
326 + $params = $request->get_params();
327 + $type = isset($params['type']) ? $params['type'] : 'all';
328 + $page = isset($params['page']) ? $params['page'] : null;
329 + $per_page = isset($params['per_page']) ? $params['per_page'] : null;
330 +
331 + if($type !== 'all') {
332 + $logs = Logger::instance()->get_logs_by_type($type, $page, $per_page);
333 + } else {
334 + $logs = Logger::instance()->get_all_logs($page, $per_page);
335 + }
336 +
337 + $result = [
338 + 'success' => true,
339 + 'logs' => $logs,
340 + 'message' => esc_html__('Error logs fetched successfully', 'media-cloud-sync')
341 + ];
342 + return new WP_REST_Response( $result, 200 );
343 + }
344 +
345 +
346 + /**
347 + * Clear all error logs for a sync type (e.g. stale entries from earlier failed attempts).
348 + * @since 1.3.13
349 + */
350 + public function clearErrorLogs( $request ) {
351 + $type = isset($request->get_params()['type']) ? $request->get_params()['type'] : '';
352 +
353 + if (empty($type) || !Sync::instance()->get_class_by_action($type)) {
354 + return new WP_REST_Response( [
355 + 'success' => false,
356 + 'message' => esc_html__('Invalid action', 'media-cloud-sync')
357 + ], 200 );
358 + }
359 +
360 + Logger::instance()->remove_logs_by_type($type);
361 +
362 + $result = [
363 + 'success' => true,
364 + 'message' => esc_html__('Error logs cleared', 'media-cloud-sync')
365 + ];
366 + return new WP_REST_Response( $result, 200 );
367 + }
368 +
369 +
370 + /**
371 + * Get Status
372 + * @since 1.2.13
373 + */
374 + public function getStatus( $request ) {
375 + // Fetch and update media counts for make sure they are up to date
376 + $action = isset($request->get_params()['action']) ? $request->get_params()['action'] : 'all';
377 + $status = Sync::instance()->get_status();
378 +
379 + if($action !== 'all') {
380 + $status = isset($status[$action]) ? $status[$action] : [];
381 + }
382 +
383 + $result = [
384 + 'success' => true,
385 + 'status' => $status,
386 + 'counts' => [
387 + 'all' => Counter::get_count(),
388 + 'categorized' => $this->getSortedMediaCounts()
389 + ],
390 + 'message' => esc_html__('Status fetched successfully', 'media-cloud-sync')
391 + ];
392 +
393 + return new WP_REST_Response( $result, 200 );
394 + }
395 +
396 +
397 + /**
398 + * Start Sync
399 + * @since 1.2.13
400 + */
401 + public function startSync( $data ) {
402 + $action = isset($data->get_params()['action']) ? $data->get_params()['action'] : '';
403 + if ( ! Sync::instance()->get_class_by_action( $action ) ) {
404 + $result = [
405 + 'success' => false,
406 + 'message' => esc_html__('Invalid action', 'media-cloud-sync')
250 407 ];
251 - return new WP_REST_Response($result, 200);
252 - }
253 - }
408 + return new WP_REST_Response( $result, 200 );
409 + }
410 + if(!Utils::is_service_enabled()) {
411 + $result = [
412 + 'success' => false,
413 + 'message' => Utils::get_service_configuration_error() ?: esc_html__('Storage is not configured correctly.', 'media-cloud-sync')
414 + ];
415 + return new WP_REST_Response( $result, 200 );
416 + }
417 + $status = Sync::instance()->start($action);
254 418
419 + // Sync::start() passes a job's own precondition-failure array straight through
420 + // (e.g. no destination configured) instead of masking it as a started status.
421 + if (is_array($status) && isset($status['success']) && $status['success'] === false) {
422 + return new WP_REST_Response( $status, 200 );
423 + }
424 +
425 + $result = [
426 + 'success' => true,
427 + 'status' => $status,
428 + 'message' => esc_html__('Sync started successfully', 'media-cloud-sync')
429 + ];
430 + return new WP_REST_Response( $result, 200 );
431 + }
432 +
433 +
255 434 /**
256 - * Change File Upload Directory
257 - * @since 1.0.0
258 - */
259 - public function change_file_upload_dir($upload) {
260 - $wpmcs_path = '/' . Schema::getConstant('UPLOADS');
261 - $path = $upload['basedir'] . $wpmcs_path;
262 - $url = $upload['baseurl'] . $wpmcs_path;
435 + * Pause Sync
436 + * @since 1.2.13
437 + */
438 + public function pauseSync( $data ) {
439 + $action = isset($data->get_params()['action']) ? $data->get_params()['action'] : '';
440 + if ( ! Sync::instance()->get_class_by_action( $action ) ) {
441 + $result = [
442 + 'success' => false,
443 + 'message' => esc_html__('Invalid action', 'media-cloud-sync')
444 + ];
445 + return new WP_REST_Response( $result, 200 );
446 + }
447 + $status = Sync::instance()->pause($action);
263 448
264 - if (!is_dir($path)) {
265 - do_action( $this->token.'_create_plugin_dir' );
266 - }
449 + $result = [
450 + 'success' => true,
451 + 'status' => $status,
452 + 'message' => esc_html__('Sync paused successfully', 'media-cloud-sync')
453 + ];
454 + return new WP_REST_Response( $result, 200 );
455 + }
267 456
268 - $upload['subdir'] = $wpmcs_path;
269 - $upload['path'] = $upload['basedir'] . $wpmcs_path;
270 - $upload['url'] = $upload['baseurl'] . $wpmcs_path;
271 457
272 - return $upload;
273 - }
458 + /**
459 + * Save the sync method ('ajax'|'cron'|'mixed') preference for a sync type.
460 + * @since 1.3.13
461 + */
462 + public function setSyncMethod( $data ) {
463 + $params = $data->get_params();
464 + $action = isset($params['action']) ? sanitize_text_field($params['action']) : '';
465 + $method = isset($params['sync_method']) ? sanitize_text_field($params['sync_method']) : '';
274 466
275 - /**
276 - * Add Custom Mime Type JSON
277 - * @since 1.0.0
278 - */
279 - public function add_custom_mime_type_json($mimes) {
280 - $mimes['json'] = 'application/json';
281 - // Return the array back to the function with our added mime type.
282 - return $mimes;
283 - }
467 + if ( ! Sync::instance()->get_class_by_action( $action ) ) {
468 + return new WP_REST_Response( [
469 + 'success' => false,
470 + 'message' => esc_html__('Invalid action', 'media-cloud-sync')
471 + ], 200 );
472 + }
284 473
474 + $saved = Sync::instance()->set_sync_method($action, $method);
475 + if(!$saved) {
476 + return new WP_REST_Response( [
477 + 'success' => false,
478 + 'message' => esc_html__('Invalid sync method', 'media-cloud-sync')
479 + ], 200 );
480 + }
285 481
482 + $result = [
483 + 'success' => true,
484 + 'sync_method' => $method,
485 + 'status' => Sync::instance()->get_class_by_action($action)->get_status(),
486 + ];
487 + return new WP_REST_Response( $result, 200 );
488 + }
489 +
286 490 /**
491 + * Resume Sync
492 + * @since 1.2.13
493 + */
494 + public function resumeSync( $data ) {
495 + $action = isset($data->get_params()['action']) ? $data->get_params()['action'] : '';
496 + if ( ! Sync::instance()->get_class_by_action( $action ) ) {
497 + $result = [
498 + 'success' => false,
499 + 'message' => esc_html__('Invalid action', 'media-cloud-sync')
500 + ];
501 + return new WP_REST_Response( $result, 200 );
502 + }
503 + $status = Sync::instance()->resume($action);
504 +
505 + $result = [
506 + 'success' => true,
507 + 'status' => $status,
508 + 'message' => esc_html__('Sync resumed successfully', 'media-cloud-sync')
509 + ];
510 + return new WP_REST_Response( $result, 200 );
511 + }
512 +
513 +
514 + /**
515 + * Stop Sync
516 + * @since 1.2.13
517 + */
518 + public function stopSync( $data ) {
519 + $action = isset($data->get_params()['action']) ? $data->get_params()['action'] : '';
520 + if ( ! Sync::instance()->get_class_by_action( $action ) ) {
521 + $result = [
522 + 'success' => false,
523 + 'message' => esc_html__('Invalid action', 'media-cloud-sync')
524 + ];
525 + return new WP_REST_Response( $result, 200 );
526 + }
527 + $status = Sync::instance()->stop($action);
528 +
529 + $result = [
530 + 'success' => true,
531 + 'status' => $status,
532 + 'counts' => [
533 + 'all' => Counter::get_count(),
534 + 'categorized' => $this->getSortedMediaCounts()
535 + ],
536 + 'message' => esc_html__('Sync stopped successfully', 'media-cloud-sync')
537 + ];
538 + return new WP_REST_Response( $result, 200 );
539 + }
540 +
541 +
542 + /**
543 + * Tick Sync — process a small batch synchronously, driving Ajax/Mixed sync mode.
544 + * @since 1.3.13
545 + */
546 + public function tickSync( $data ) {
547 + $params = $data->get_params();
548 + $action = isset($params['action']) ? $params['action'] : '';
549 +
550 + if ( ! Sync::instance()->get_class_by_action( $action ) ) {
551 + $result = [
552 + 'success' => false,
553 + 'message' => esc_html__('Invalid action', 'media-cloud-sync')
554 + ];
555 + return new WP_REST_Response( $result, 200 );
556 + }
557 +
558 + // Defense in depth — the frontend should never call this in free/unlicensed mode, but
559 + // the endpoint must not trust that, and must not silently no-op: an unlicensed call
560 + // needs a clear rejection so the frontend's tick loop stops instead of retrying forever.
561 + if ( ! Utils::is_pro_licensed() ) {
562 + $result = [
563 + 'success' => false,
564 + 'message' => esc_html__('Ajax sync requires an active Pro license', 'media-cloud-sync')
565 + ];
566 + return new WP_REST_Response( $result, 200 );
567 + }
568 +
569 + // Clamp regardless of what the frontend sends — the endpoint itself must enforce the
570 + // ceiling, since a batch of slow uploads processed sequentially in one request risks
571 + // exceeding reverse-proxy/webserver timeouts that process_iterations() can't see.
572 + $batch = isset($params['batch']) ? (int) $params['batch'] : 5;
573 + $batch = min(10, max(1, $batch));
574 +
575 + $status = Sync::instance()->tick($action, $batch);
576 +
577 + $result = [
578 + 'success' => true,
579 + 'status' => $status,
580 + 'message' => esc_html__('Sync ticked successfully', 'media-cloud-sync')
581 + ];
582 +
583 + // Deliberately omitted on every tick (this endpoint may be hit ~once/second while a
584 + // sync runs, and Counter::get_count() is a real query) — but the Dashboard's own
585 + // Statistics widget reads a *different* redux slice (common.counts) than the Sync
586 + // screen's progress (sync[action]), only refreshed via the slower ~2-3s useSyncPolling
587 + // cadence. That's an acceptable few-seconds lag while a sync is actively running, but
588 + // right when it finishes it becomes a visibly wrong, stuck-looking number on the
589 + // Dashboard until the next poll happens to catch up. Only pay for the query at that
590 + // one transition, not on every tick.
591 + if ( isset( $status['status'] ) && $status['status'] !== 'running' ) {
592 + $result['counts'] = [
593 + 'all' => Counter::get_count(),
594 + 'categorized' => $this->getSortedMediaCounts()
595 + ];
596 + }
597 + return new WP_REST_Response( $result, 200 );
598 + }
599 +
600 +
601 + /**
602 + * Retry a Media Sync Error
603 + */
604 + public function retrySingle( $data ) {
605 + $params = $data->get_params();
606 + $id = isset($params['id']) ? $params['id'] : '';
607 + $source_type = isset($params['source_type']) ? $params['source_type'] : 'media_library';
608 + $type = isset($params['type']) ? $params['type'] : '';
609 + $status = false;
610 + if(empty($id) || empty($source_type) || empty($type)) {
611 + $result = [
612 + 'success' => false,
613 + 'message' => esc_html__('Invalid parameters', 'media-cloud-sync')
614 + ];
615 + return new WP_REST_Response( $result, 200 );
616 + }
617 + // Retry Sync
618 + $handler = Sync::instance()->get_class_by_action($type);
619 + if(!$handler) {
620 + $result = [
621 + 'success' => false,
622 + 'message' => esc_html__('Invalid action', 'media-cloud-sync')
623 + ];
624 + return new WP_REST_Response( $result, 200 );
625 + }
626 + if(!method_exists($handler, 'retry_single')) {
627 + $result = [
628 + 'success' => false,
629 + 'message' => esc_html__('Retry is not available for this sync type.', 'media-cloud-sync')
630 + ];
631 + return new WP_REST_Response( $result, 200 );
632 + }
633 + $handler->retry_single($id, $source_type);
634 +
635 + $status = Utils::is_empty(Logger::instance()->get_log($type, $id, $source_type)) ? true : false;
636 +
637 + $result = [
638 + 'success' => $status,
639 + 'message' => $status ? esc_html__('Media synced successfully', 'media-cloud-sync') : esc_html__('Media sync failed', 'media-cloud-sync')
640 + ];
641 + return new WP_REST_Response( $result, 200 );
642 + }
643 +
644 +
645 + /**
646 + * Get Sorted Media Counts
647 + * Sorts media counts by source type
648 + * @since 1.2.13
649 + */
650 + private function getSortedMediaCounts() {
651 + $sorted_counts = [];
652 + $detailed_counts = Counter::get_count( 'all', false );
653 + if(!empty($detailed_counts)) {
654 + $source_labels = Integration::$source_labels;
655 + if(!empty($source_labels)) {
656 + $prefixes = array_keys($source_labels);
657 + foreach($detailed_counts as $source_type => $counts) {
658 + $found_prefix = false;
659 + foreach($prefixes as $prefix) {
660 + $found_prefix = strpos($source_type, $prefix) === 0 ? $prefix : false;
661 + if($found_prefix !== false) break;
662 + }
663 +
664 + if($found_prefix !== false) {
665 + $sorted_counts[$found_prefix] = [
666 + 'label' => $source_labels[$found_prefix],
667 + 'uploaded' => isset( $sorted_counts[ $found_prefix ]['uploaded'] )
668 + ? $sorted_counts[ $found_prefix ]['uploaded'] + $counts['uploaded']
669 + : $counts['uploaded'],
670 + 'total' => isset( $sorted_counts[ $found_prefix ]['total'] )
671 + ? $sorted_counts[ $found_prefix ]['total'] + $counts['total']
672 + : $counts['total']
673 + ];
674 + }
675 + }
676 + }
677 + }
678 +
679 + return $sorted_counts;
680 + }
681 +
682 +
683 + /**
287 684 * Helper function to create Adding Route
288 685 * @since 1.0.0
289 686 */
290 687 private function add_route( $slug, $callBack, $method = 'GET' ) {
@@ -301,12 +698,12 @@
301 698 /**
302 699 * Permission Callback
303 700 **/
304 701 public function getPermission() {
305 - if ( current_user_can( 'administrator' ) ) {
702 + if ( current_user_can( 'manage_options' ) ) {
306 703 return true;
307 704 } else {
308 - return true;
705 + return false;
309 706 }
310 707 }
311 708
312 709 /**