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
media-cloud-sync / includes / compatbility / compatibility.php

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

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