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
← All changes | includes/compatbility/compatibility.php +287 -23 1.2.12 → 1.4.2 View file →
@@ -9,10 +9,23 @@
9 9 private static $instance = null;
10 10 private $assets_url;
11 11 private $version;
12 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;
13 20
14 21 /**
22 + * Files to remove that were restored
23 + * @var array
24 + */
25 + private $restored_files = array();
26 +
27 + /**
15 28 * Compatibility constructor.
16 29 * @since 1.0.0
17 30 */
18 31 private function __construct() {
@@ -21,12 +34,27 @@
21 34 $this->token = WPMCS_TOKEN;
22 35
23 36 // Initialize setup
24 37 $this->init();
38 +
25 39 }
26 40
27 41 public function init() {
42 + if(!Utils::is_service_enabled()) {
43 + return;
44 + }
45 +
28 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 + /*
29 57 * WP_Customize_Control
30 58 * /wp-includes/class-wp-customize_control.php
31 59 */
32 60 add_filter('attachment_url_to_postid', [$this, 'customizer_background_image'], 10, 2);
@@ -32,11 +60,11 @@
32 60 add_filter('attachment_url_to_postid', [$this, 'customizer_background_image'], 10, 2);
33 61
34 62 /*
35 63 * Legacy filter
36 - * 'wpmcs_get_attached_file_copy_back_to_local'
64 + * 'wpmcs_get_attached_file_copy_back_to_server'
37 65 */
38 - add_filter('wpmcs_get_attached_file', [$this, 'legacy_copy_back_to_local'], 10, 4);
66 + add_filter('wpmcs_get_attached_file', [$this, 'legacy_copy_back_to_server'], 10, 4);
39 67
40 68 /*
41 69 * Regenerate Thumbnails (before v3)
42 70 * https://wordpress.org/plugins/regenerate-thumbnails/
@@ -43,21 +71,201 @@
43 71 */
44 72 add_filter('wpmcs_get_attached_file', [$this, 'regenerate_thumbnails_download_file'], 10, 4);
45 73
46 74 /**
47 - * Regenerate Thumbnails v3+ and other REST-API using plugins that need a local file.
75 + * Regenerate Thumbnails v3+ and other REST-API using plugins that need a server file.
48 76 */
49 - add_filter('rest_dispatch_request', [$this, 'rest_dispatch_request_copy_back_to_local'], 10, 4);
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' ) );
50 80
51 81 /*
52 82 * WP-CLI Compatibility
53 83 */
54 84 if (defined('WP_CLI') && class_exists('WP_CLI')) {
55 - WP_CLI::add_hook('before_invoke:media regenerate', [$this, 'enable_copy_back_media']);
85 + WP_CLI::add_hook('before_invoke:media regenerate', [$this, 'enable_copy_back_and_wait_for_generate_metadata']);
56 86 }
57 87 }
58 88
89 +
59 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 + /**
60 268 * Filters the REST dispatch request to determine whether route needs compatibility actions.
61 269 *
62 270 * @param bool $dispatch_result Dispatch result, will be used if not empty.
63 271 * @param WP_REST_Request $request Request used to generate the response.
@@ -65,20 +273,20 @@
65 273 * @param array $handler Route handler used for the request.
66 274 *
67 275 * @return bool
68 276 */
69 - public function rest_dispatch_request_copy_back_to_local($dispatch_result, $request, $route, $handler) {
277 + public function rest_dispatch_request_copy_back_to_server($dispatch_result, $request, $route, $handler) {
70 278 $routes = [
71 279 '/regenerate-thumbnails/v\d+/regenerate/',
72 280 ];
73 281
74 - $routes = apply_filters('wpmcs_rest_api_enable_get_attached_file_copy_back_to_local', $routes);
282 + $routes = apply_filters('wpmcs_rest_api_enable_get_attached_file_copy_back_to_server', $routes);
75 283 $routes = is_array($routes) ? $routes : (array)$routes;
76 284
77 285 if (!empty($routes)) {
78 286 foreach ($routes as $match_route) {
79 287 if (preg_match('@' . $match_route . '@i', $route)) {
80 - $this->enable_copy_back_media();
288 + $this->enable_copy_back_and_wait_for_generate_metadata();
81 289 break;
82 290 }
83 291 }
84 292 }
@@ -92,14 +300,59 @@
92 300 * before re-offloading.
93 301 *
94 302 * @handles WP_CLI:before_invoke:media regenerate
95 303 */
96 - public function enable_copy_back_media() {
97 - add_filter('wpmcs_get_attached_file_copy_back_to_local', '__return_true');
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;
98 308 }
99 309
100 310 /**
101 - * Allow the Regenerate Thumbnails plugin to copy the bucket file back to the local
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
102 355 * server when the file is missing on the server via get_attached_file.
103 356 *
104 357 * @param string $url
105 358 * @param string $file
@@ -111,10 +364,10 @@
111 364 return $this->copy_image_to_server_on_action('regeneratethumbnail', true, $url, $file, $attachment_id);
112 365 }
113 366
114 367 /**
115 - * Allow any process to trigger the copy back to local with
116 - * the filter 'wpmcs_get_attached_file_copy_back_to_local'
368 + * Allow any process to trigger the copy back to server with
369 + * the filter 'wpmcs_get_attached_file_copy_back_to_server'
117 370 *
118 371 * @param string $url
119 372 * @param string $file
120 373 * @param int $attachment_id
@@ -120,11 +373,11 @@
120 373 * @param int $attachment_id
121 374 *
122 375 * @return string
123 376 */
124 - public function legacy_copy_back_to_local($url, $file, $attachment_id, $wpmcs_item) {
125 - $copy_back_to_local = apply_filters('wpmcs_get_attached_file_copy_back_to_local', false, $file, $attachment_id, $wpmcs_item);
126 - if (false === $copy_back_to_local) {
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) {
127 380 // Not copying back file
128 381 return $url;
129 382 }
130 383
@@ -145,15 +398,15 @@
145 398 *
146 399 * @return int|null
147 400 */
148 401 public function customizer_background_image($post_id, $url) {
149 - if (!is_null($post_id)) {
402 + if (!empty($post_id)) {
150 403 return $post_id;
151 404 }
152 405
153 406 // There seems to be a bug in the WP Customizer whereby sometimes it puts the attachment ID on the URL.
154 407 if (is_numeric($url)) {
155 - $item = Item::get($url);
408 + $item = Item::instance()->get($url);
156 409
157 410 // If we found an offloaded Media Library item for that ID, job's a good'n'.
158 411 if (!Utils::is_empty($item)) {
159 412 $post_id = $url;
@@ -160,12 +413,12 @@
160 413 }
161 414 } else {
162 415 $path = Utils::get_attachment_source_path($url);
163 416 if (!Utils::is_empty($path)) {
164 - $items = Item::instance()->get_items_by_source_paths([$path]);
165 - if (!Utils::is_empty($items)) {
417 + $item = Item::instance()->get_items_by_paths( $path, true, true );
418 + if (!Utils::is_empty($item)) {
166 419 // If we found an offloaded Media Library item for that path, job's a good'n'.
167 - $post_id = $items[0]['source_id'];
420 + $post_id = $item['source_id'];
168 421 }
169 422 }
170 423 }
171 424
@@ -232,9 +485,9 @@
232 485 return $url;
233 486 }
234 487
235 488 /**
236 - * Download a file from bucket if the file does not exist locally and places it where
489 + * Download a file from bucket if the file does not exist serverly and places it where
237 490 * the attachment's file should be.
238 491 *
239 492 * @return string|bool File if downloaded, false on failure
240 493 */
@@ -239,12 +492,20 @@
239 492 * @return string|bool File if downloaded, false on failure
240 493 */
241 494 public function copy_provider_file_to_server($attachment_id, $file) {
242 495 // Download files
243 - if (!Item::instance()->moveToServerBySourcePath($attachment_id, $file)) {
496 + if (!Item::instance()->moveToServerBySourcePath($attachment_id, $file, 'media_library')) {
244 497 return false;
245 498 }
246 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 +
247 508 return $file;
248 509 }
249 510
250 511 /**
@@ -257,5 +518,8 @@
257 518 self::$instance = new self();
258 519 }
259 520 return self::$instance;
260 521 }
522 +
523 + private function __clone() {}
524 + public function __wakeup() { throw new \Exception('Cannot unserialize singleton'); }
261 525 }