PluginProbe
Media Cloud Sync / 1.3.0
Media Cloud Sync v1.3.0
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 1.3.0, at includes/compatbility/compatibility.php

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