PluginProbe
Media Cloud Sync / 1.4.2
Media Cloud Sync v1.4.2
1.4.2 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 All 36 releases
media-cloud-sync / includes / compatbility / compatibility.php

compatibility.php in Media Cloud Sync 1.4.2, at includes/compatbility/compatibility.php

526 lines 17.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Dudlewebs\WPMCS;
3
4 defined('ABSPATH') || exit;
5
6 use WP_CLI;
7
8 class Compatibility {
9 private static $instance = null;
10 private $assets_url;
11 private $version;
12 private $token;
13 /**
14 * Whether to wait for the attachment metadata to be regenerated
15 * before re-offloading the file.
16 *
17 * @var bool
18 */
19 public $wait_for_generate_attachment_metadata = false;
20
21 /**
22 * Files to remove that were restored
23 * @var array
24 */
25 private $restored_files = array();
26
27 /**
28 * Compatibility constructor.
29 * @since 1.0.0
30 */
31 private function __construct() {
32 $this->assets_url = WPMCS_ASSETS_URL;
33 $this->version = WPMCS_VERSION;
34 $this->token = WPMCS_TOKEN;
35
36 // Initialize setup
37 $this->init();
38
39 }
40
41 public function init() {
42 if(!Utils::is_service_enabled()) {
43 return;
44 }
45
46 /*
47 * Image Editor Handler
48 * /wp-admin/includes/image-edit.php
49 */
50 add_filter( 'wpmcs_get_attached_file_noop', array( $this, 'image_editor_download_file' ), 10, 4 );
51 add_filter( 'wpmcs_get_attached_file', array( $this, 'image_editor_download_file' ), 10, 4 );
52 add_filter( 'wpmcs_get_attached_file', array( $this, 'customizer_crop_download_file' ), 10, 4 );
53 add_filter( 'wpmcs_pre_update_item_additional_files_to_remove_from_server', [ $this, 'customizer_crop_remove_restored_files' ], 10, 5 );
54 add_filter( 'wpmcs_do_reupload_media', [ $this, 'force_reupload_on_image_editor_save' ], 10, 3 );
55
56 /*
57 * WP_Customize_Control
58 * /wp-includes/class-wp-customize_control.php
59 */
60 add_filter('attachment_url_to_postid', [$this, 'customizer_background_image'], 10, 2);
61
62 /*
63 * Legacy filter
64 * 'wpmcs_get_attached_file_copy_back_to_server'
65 */
66 add_filter('wpmcs_get_attached_file', [$this, 'legacy_copy_back_to_server'], 10, 4);
67
68 /*
69 * Regenerate Thumbnails (before v3)
70 * https://wordpress.org/plugins/regenerate-thumbnails/
71 */
72 add_filter('wpmcs_get_attached_file', [$this, 'regenerate_thumbnails_download_file'], 10, 4);
73
74 /**
75 * Regenerate Thumbnails v3+ and other REST-API using plugins that need a server file.
76 */
77 add_filter('rest_dispatch_request', [$this, 'rest_dispatch_request_copy_back_to_server'], 10, 4);
78 add_filter('rest_request_after_callbacks', [$this, 'rest_request_after_callbacks_remove_from_server'], 10, 3);
79 add_filter('wpmcs_wait_for_generate_attachment_metadata', array( $this, 'wait_for_generate_attachment_metadata' ) );
80
81 /*
82 * WP-CLI Compatibility
83 */
84 if (defined('WP_CLI') && class_exists('WP_CLI')) {
85 WP_CLI::add_hook('before_invoke:media regenerate', [$this, 'enable_copy_back_and_wait_for_generate_metadata']);
86 }
87 }
88
89
90 /**
91 * Allow the WordPress Customizer to crop images that have been copied to bucket
92 * but removed from the local server, by copying them back temporarily.
93 *
94 * @param string $url
95 * @param string $file
96 * @param int $attachment_id
97 * @param array $item
98 *
99 * @return string
100 */
101 public function customizer_crop_download_file( $url, $file, $attachment_id, $item ) {
102 if ( false === $this->is_customizer_crop_action() ) {
103 return $url;
104 }
105
106 // Check if file was restored already and return the URL
107 // Avoid removed files being copied back multiple times
108 if(in_array($file, $this->restored_files)) {
109 return $url;
110 }
111
112 if ( ( $file = $this->copy_provider_file_to_server( $attachment_id, $file ) ) ) {
113 // Return the file if successfully downloaded from bucket.
114 return $file;
115 }
116
117 return $url;
118 }
119
120 /**
121 * Generic check for Customizer crop actions
122 *
123 * @return bool
124 */
125 public function is_customizer_crop_action() {
126 $header_crop = $this->maybe_process_on_action( 'custom-header-crop', true );
127
128 $context = array( 'site-icon', 'custom_logo' );
129 $image_crop = $this->maybe_process_on_action( 'crop-image', true, $context );
130
131 if ( ! $header_crop && ! $image_crop ) {
132 // Not doing a Customizer action
133 return false;
134 }
135
136 return true;
137 }
138
139 /**
140 * Additional filter to remove any restored files from the server
141 * during a Customizer crop action.
142 * @param array $files_to_remove
143 * @param int $source_id
144 * @param array $data
145 * @param string $source_type
146 * @return array
147 */
148 public function customizer_crop_remove_restored_files($files_to_remove, $source_id, $new_item, $old_item=[], $source_type = 'media_library') {
149 $upload_dir = wp_get_upload_dir();
150
151 if (false === $this->is_customizer_crop_action()) {
152 return $files_to_remove;
153 }
154
155 if (isset($old_item['source_path']) && $old_item['source_path'] !== $new_item['source_path']) {
156 // The file has changed, so we need to remove the old file from the server
157 $files_to_remove[] = trailingslashit( $upload_dir['basedir'] ) . $old_item['source_path'];
158 }
159
160 if( isset($old_item['original_source_path']) && !empty($old_item['original_source_path']) ) {
161 $files_to_remove[] = trailingslashit( $upload_dir['basedir'] ) . $old_item['original_source_path'];
162 }
163
164 return array_merge($files_to_remove, $this->restored_files);
165 }
166
167
168 /**
169 * Allow the WordPress Image Editor to edit files that have been copied to provider
170 * but removed from the local server, by copying them back temporarily
171 *
172 * @param string $url
173 * @param string $file
174 * @param int $attachment_id
175 * @param array $item
176 *
177 * @return string
178 */
179 public function image_editor_download_file($url, $file, $attachment_id, $item) {
180 // If this filter expects a file path, $url is a URL or a file path
181 if (!Utils::is_ajax()) {
182 return $url;
183 }
184
185 $action = Utils::filter_input('action', INPUT_GET) ?: Utils::filter_input('action', INPUT_POST);
186 $do = Utils::filter_input('do', INPUT_POST);
187
188 // Avoid multiple rewrites when restoring/saving.
189 if ($action === 'image-editor' && in_array($do, ['restore', 'save'], true)) {
190 return $file; // Return the file if image editor is doing a restore or save
191 }
192
193 // Copy back only once during a save triggered by the image editor.
194 if ($do === 'save' && in_array($action, ['image-editor', 'imgedit-preview'], true)) {
195 foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 15) as $caller) {
196 if (!empty($caller['function']) && $caller['function'] === '_load_image_to_edit_path') {
197 $provider_file = $this->copy_provider_file_to_server($attachment_id, $file);
198 return $provider_file ?: $url;
199 }
200 }
201 }
202
203 return $url;
204 }
205
206 /**
207 * A flip or 180° rotation changes pixel content without changing any
208 * declared width/height, which shouldForceReupload() can't detect —
209 * force it explicitly for any image-editor commit (save or restore).
210 * @since 1.4.3
211 */
212 public function force_reupload_on_image_editor_save($do_reupload, $attachment_id, $source_type) {
213 if ($do_reupload) {
214 return $do_reupload;
215 }
216
217 $action = Utils::filter_input('action', INPUT_GET) ?: Utils::filter_input('action', INPUT_POST);
218 $do = Utils::filter_input('do', INPUT_POST);
219
220 if (in_array($do, ['save', 'restore'], true) && in_array($action, ['image-editor', 'imgedit-preview'], true)) {
221 return true;
222 }
223
224 return $do_reupload;
225 }
226
227
228
229
230 /**
231 * Called after REST API callback, and removes server files from the server for routes that need it.
232 *
233 * @param WP_HTTP_Response $response The response object.
234 * @param WP_REST_Server $handler The response handler.
235 * @param WP_REST_Request $request The current request.
236 *
237 * @return WP_HTTP_Response The filtered response object.
238 */
239 public function rest_request_after_callbacks_remove_from_server($response, $handler, $request) {
240 $routes = [
241 '/regenerate-thumbnails/v\d+/regenerate/',
242 ];
243
244 // Get the current REST route
245 $route = $request->get_route();
246
247 // Apply filter so devs can modify routes
248 $routes = apply_filters('wpmcs_rest_api_enable_get_attached_file_remove_from_server', $routes);
249 $routes = is_array($routes) ? $routes : (array) $routes;
250
251 if (!empty($routes)) {
252 foreach ($routes as $match_route) {
253 if (preg_match('@' . $match_route . '@i', $route)) {
254 if ($request->get_param('id')) {
255 $attachment_id = absint($request->get_param('id'));
256 // Remove the file from the server
257 Item::instance()->may_be_delete_server_files_by_id($attachment_id, 'media_library', true, true);
258 }
259 break;
260 }
261 }
262 }
263
264 return $response;
265 }
266
267 /**
268 * Filters the REST dispatch request to determine whether route needs compatibility actions.
269 *
270 * @param bool $dispatch_result Dispatch result, will be used if not empty.
271 * @param WP_REST_Request $request Request used to generate the response.
272 * @param string $route Route matched for the request.
273 * @param array $handler Route handler used for the request.
274 *
275 * @return bool
276 */
277 public function rest_dispatch_request_copy_back_to_server($dispatch_result, $request, $route, $handler) {
278 $routes = [
279 '/regenerate-thumbnails/v\d+/regenerate/',
280 ];
281
282 $routes = apply_filters('wpmcs_rest_api_enable_get_attached_file_copy_back_to_server', $routes);
283 $routes = is_array($routes) ? $routes : (array)$routes;
284
285 if (!empty($routes)) {
286 foreach ($routes as $match_route) {
287 if (preg_match('@' . $match_route . '@i', $route)) {
288 $this->enable_copy_back_and_wait_for_generate_metadata();
289 break;
290 }
291 }
292 }
293
294 return $dispatch_result;
295 }
296
297 /**
298 * Enable copying back attachments from provider
299 * and waiting for their metadata to be regenerated
300 * before re-offloading.
301 *
302 * @handles WP_CLI:before_invoke:media regenerate
303 */
304 public function enable_copy_back_and_wait_for_generate_metadata() {
305 add_filter('wpmcs_get_attached_file_copy_back_to_server', '__return_true');
306 $this->enable_get_attached_file_copy_back_to_server();
307 $this->wait_for_generate_attachment_metadata = true;
308 }
309
310 /**
311 * Enables copying missing local files back to the server when `get_attached_file` filter is called.
312 */
313 public function enable_get_attached_file_copy_back_to_server() {
314 add_filter('wpmcs_get_attached_file_copy_back_to_server', '__return_true');
315
316 add_filter( 'wp_generate_attachment_metadata', array( $this, 'wp_generate_attachment_metadata' ) );
317 }
318
319 /**
320 * Handler for wp_generate_attachment_metadata. Updates class
321 * member variable when the filter has fired.
322 *
323 * @handles wp_generate_attachment_metadata
324 *
325 * @param mixed $metadata
326 *
327 * @return mixed
328 */
329 public function wp_generate_attachment_metadata( $metadata ) {
330 $this->wait_for_generate_attachment_metadata = false;
331
332 return $metadata;
333 }
334
335 /**
336 * Are we waiting for the wp_generate_attachment_metadata filter and
337 * if so, has it run yet?
338 *
339 * @handles wpmcs_wait_for_generate_attachment_metadata
340 *
341 * @param bool $wait
342 *
343 * @return bool
344 */
345 public function wait_for_generate_attachment_metadata( $wait ) {
346 if ( $this->wait_for_generate_attachment_metadata ) {
347 return true;
348 }
349
350 return $wait;
351 }
352
353 /**
354 * Allow the Regenerate Thumbnails plugin to copy the bucket file back to the server
355 * server when the file is missing on the server via get_attached_file.
356 *
357 * @param string $url
358 * @param string $file
359 * @param int $attachment_id
360 *
361 * @return string
362 */
363 public function regenerate_thumbnails_download_file($url, $file, $attachment_id, $item) {
364 return $this->copy_image_to_server_on_action('regeneratethumbnail', true, $url, $file, $attachment_id);
365 }
366
367 /**
368 * Allow any process to trigger the copy back to server with
369 * the filter 'wpmcs_get_attached_file_copy_back_to_server'
370 *
371 * @param string $url
372 * @param string $file
373 * @param int $attachment_id
374 *
375 * @return string
376 */
377 public function legacy_copy_back_to_server($url, $file, $attachment_id, $wpmcs_item) {
378 $copy_back_to_server = apply_filters('wpmcs_get_attached_file_copy_back_to_server', false, $file, $attachment_id, $wpmcs_item);
379 if (false === $copy_back_to_server) {
380 // Not copying back file
381 return $url;
382 }
383
384 if (($file = $this->copy_provider_file_to_server($attachment_id, $file))) {
385 // Return the file if successfully downloaded from S3
386 return $file;
387 }
388
389 // Return S3 URL as a fallback
390 return $url;
391 }
392
393 /**
394 * Show the correct background image in the customizer
395 *
396 * @param int|null $post_id
397 * @param string $url
398 *
399 * @return int|null
400 */
401 public function customizer_background_image($post_id, $url) {
402 if (!empty($post_id)) {
403 return $post_id;
404 }
405
406 // There seems to be a bug in the WP Customizer whereby sometimes it puts the attachment ID on the URL.
407 if (is_numeric($url)) {
408 $item = Item::instance()->get($url);
409
410 // If we found an offloaded Media Library item for that ID, job's a good'n'.
411 if (!Utils::is_empty($item)) {
412 $post_id = $url;
413 }
414 } else {
415 $path = Utils::get_attachment_source_path($url);
416 if (!Utils::is_empty($path)) {
417 $item = Item::instance()->get_items_by_paths( $path, true, true );
418 if (!Utils::is_empty($item)) {
419 // If we found an offloaded Media Library item for that path, job's a good'n'.
420 $post_id = $item['source_id'];
421 }
422 }
423 }
424
425 // Must return null if not found.
426 return empty($post_id) ? null : $post_id;
427 }
428
429 /**
430 * Check the current request is a specific one based on action and
431 * optional context
432 *
433 * @param string $action_key
434 * @param bool $ajax
435 * @param null|string|array $context_key
436 *
437 * @return bool
438 */
439 public function maybe_process_on_action($action_key, $ajax, $context_key = null) {
440 if ($ajax !== Utils::is_ajax()) {
441 return false;
442 }
443
444 $var_type = 'GET';
445
446 if (isset($_GET['action'])) {
447 $action = Utils::filter_input('action');
448 } elseif (isset($_POST['action'])) {
449 $var_type = 'POST';
450 $action = Utils::filter_input('action', INPUT_POST);
451 } else {
452 return false;
453 }
454
455 $context_check = true;
456 if (!is_null($context_key)) {
457 $global = constant('INPUT_' . $var_type);
458 $context = Utils::filter_input('context', $global);
459
460 if (is_array($context_key)) {
461 $context_check = in_array($context, $context_key);
462 } else {
463 $context_check = ($context_key === $context);
464 }
465 }
466
467 return ($action_key === sanitize_key($action) && $context_check);
468 }
469
470 /**
471 * Generic method for copying back an S3 file to the server on a specific AJAX action
472 *
473 * @return string
474 */
475 public function copy_image_to_server_on_action($action_key, $ajax, $url, $file, $attachment_id) {
476 if (false === $this->maybe_process_on_action($action_key, $ajax)) {
477 return $url;
478 }
479
480 if (($file = $this->copy_provider_file_to_server($attachment_id, $file))) {
481 // Return the file if successfully downloaded from S3
482 return $file;
483 }
484
485 return $url;
486 }
487
488 /**
489 * Download a file from bucket if the file does not exist serverly and places it where
490 * the attachment's file should be.
491 *
492 * @return string|bool File if downloaded, false on failure
493 */
494 public function copy_provider_file_to_server($attachment_id, $file) {
495 // Download files
496 if (!Item::instance()->moveToServerBySourcePath($attachment_id, $file, 'media_library')) {
497 return false;
498 }
499
500 $this->restored_files[] = $file;
501
502 // Shared with woocommerce.php — feeds the same pre_update_item pipeline as
503 // below when a save happens this request, with a shutdown fallback for the
504 // other three callers here (image editor, legacy copy-back, Regenerate
505 // Thumbnails pre-v3), which otherwise have no cleanup at all.
506 Item::instance()->track_restored_for_cleanup( [ $file ] );
507
508 return $file;
509 }
510
511 /**
512 * Get the singleton instance of the Compatibility class.
513 *
514 * @return Compatibility
515 */
516 public static function instance() {
517 if (self::$instance === null) {
518 self::$instance = new self();
519 }
520 return self::$instance;
521 }
522
523 private function __clone() {}
524 public function __wakeup() { throw new \Exception('Cannot unserialize singleton'); }
525 }
526