PluginProbe
WP-Stateless – Google Cloud Storage / 3.1.0
WP-Stateless – Google Cloud Storage v3.1.0
4.4.3 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.2.7 2.3.0 2.3.1 2.3.2 3.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.2.0 3.2.1 3.2.2 All 62 releases
wp-stateless / lib / classes / class-utility.php

class-utility.php in WP-Stateless – Google Cloud Storage 3.1.0, at lib/classes/class-utility.php

1,077 lines 40.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Helper Functions List
5 *
6 * Can be called via Singleton. Since Singleton uses magic method __call().
7 * Example:
8 *
9 * Add Media to GS storage:
10 * ud_get_stateless_media()->add_media( false, $post_id );
11 *
12 * @class Utility
13 */
14
15 namespace wpCloud\StatelessMedia {
16
17 use wpCloud\StatelessMedia\Sync\BackgroundSync;
18
19 if (!class_exists('wpCloud\StatelessMedia\Utility')) {
20
21 class Utility {
22
23 static $can_delete_attachment = [];
24
25 /**
26 * ChromeLogger
27 *
28 * @author potanin@UD
29 * @param $data
30 */
31 static public function log($data) {
32
33 if (!class_exists('wpCloud\StatelessMedia\Logger')) {
34 include_once(__DIR__ . '/class-logger.php');
35 }
36
37 if (!class_exists('wpCloud\StatelessMedia\Logger')) {
38 return;
39 }
40
41 if (defined('WP_STATELESS_CONSOLE_LOG') && WP_STATELESS_CONSOLE_LOG) {
42 Logger::log('[wp-stateless]', $data);
43 }
44 }
45
46 /**
47 * Override Cache Control
48 * @param $cacheControl
49 * @return mixed
50 */
51 public static function override_cache_control($cacheControl) {
52 return ud_get_stateless_media()->get('sm.cache_control');
53 }
54
55 /**
56 * wp_normalize_path was added in 3.9.0
57 *
58 * @param $path
59 * @return mixed|string
60 *
61 */
62 public static function normalize_path($path) {
63
64 if (function_exists('wp_normalize_path')) {
65 return wp_normalize_path($path);
66 }
67
68 $path = str_replace('\\', '/', $path);
69 $path = preg_replace('|/+|', '/', $path);
70 return $path;
71 }
72
73 /**
74 * Randomize file name
75 * @param $filename
76 * @return string
77 */
78 public static function randomize_filename($filename) {
79 $return = apply_filters('stateless_skip_cache_busting', null, $filename);
80 if ($return) {
81 return $return;
82 }
83
84 if (preg_match('/^[a-f0-9]{8}-/', $filename)) {
85 return $filename;
86 }
87
88 $info = pathinfo($filename);
89 $ext = empty($info['extension']) ? '' : '' . $info['extension'];
90 $_parts = array();
91 $rand = substr(md5(time()), 0, 8);
92
93 if (strpos($info['filename'], '@')) {
94 $_cleanName = explode('@', $info['filename'])[0];
95 $_retna = explode('@', $info['filename'])[1];
96 $_parts[] = $rand;
97 $_parts[] = '-';
98 $_parts[] = strtolower($_cleanName);
99 $_parts[] = '@' . strtolower($_retna);
100 } else {
101 $_parts[] = $rand;
102 $_parts[] = '-';
103 $_parts[] = strtolower($info['filename']);
104 }
105
106 $filename = join('', $_parts);
107 if (!empty($ext)) {
108 $filename .= '.' . $ext;
109 }
110
111 return $filename;
112 }
113
114 /**
115 * Get Media Item Content Disposition
116 *
117 * @param null $attachment_id
118 * @param array $metadata
119 * @param array $data
120 * @return string
121 */
122 public static function getContentDisposition($attachment_id = null, $metadata = array(), $data = array()) {
123 // return 'Content-Disposition: attachment; filename=some-file.sql';
124 return apply_filters('sm:item:contentDisposition', null, array('attachment_id' => $attachment_id, 'mime_type' => get_post_mime_type($attachment_id), 'metadata' => $metadata, 'data' => $data));
125 }
126
127 /**
128 * @param null $attachment_id
129 * @param array $metadata
130 * @param array $data
131 * @return string
132 */
133 public static function getCacheControl($attachment_id = null, $metadata = array(), $data = array()) {
134 if (!$attachment_id) {
135 return apply_filters('sm:item:cacheControl', 'private, no-cache, no-store', $attachment_id, array('attachment_id' => null, 'mime_type' => null, 'metadata' => $metadata, 'data' => $data));
136 }
137
138 $_mime_type = get_post_mime_type($attachment_id);
139
140 // Treat images as public.
141 if (strpos($_mime_type, 'image/') !== false) {
142 return apply_filters('sm:item:cacheControl', 'public, max-age=36000, must-revalidate', array('attachment_id' => $attachment_id, 'mime_type' => null, 'metadata' => $metadata, 'data' => $data));
143 }
144
145 // Treat images as public.
146 if (strpos($_mime_type, 'sql') !== false) {
147 return apply_filters('sm:item:cacheControl', 'private, no-cache, no-store', array('attachment_id' => $attachment_id, 'mime_type' => null, 'metadata' => $metadata, 'data' => $data));
148 }
149
150 return apply_filters('sm:item:cacheControl', 'public, max-age=30, no-store, must-revalidate', array('attachment_id' => $attachment_id, 'mime_type' => null, 'metadata' => $metadata, 'data' => $data));
151 }
152
153 /**
154 * Add/Update Media to Bucket
155 * Fired for every action with image add or update
156 *
157 * $force and $args params will no be passed on media library uploads.
158 * This two will be passed on by compatibility.
159 *
160 * @action wp_generate_attachment_metadata
161 * @author peshkov@UD
162 * @param $metadata
163 * @param $attachment_id
164 * @param boolean $force Whether to force the upload incase of it's already exists.
165 * @param array $args Whether to only sync the full size image.
166 * @return bool|string
167 */
168 public static function add_media($metadata, $attachment_id, $force = false, $args = array()) {
169 $sm_mode = ud_get_stateless_media()->get('sm.mode');
170 $file = '';
171 $upload_dir = wp_upload_dir();
172 $args = wp_parse_args($args, array('is_webp' => '', // expected value ".webp";
173 ));
174
175 /* Get metadata in case if method is called directly. */
176 if (current_filter() !== 'wp_generate_attachment_metadata' && current_filter() !== 'wp_update_attachment_metadata' && current_filter() !== 'intermediate_image_sizes_advanced') {
177 $metadata = wp_get_attachment_metadata($attachment_id);
178 }
179
180 // making sure meta data isn't null.
181 if (empty($metadata)) {
182 $metadata = array();
183 }
184
185 /**
186 * To skip the sync process.
187 *
188 * Returning a non-null value
189 * will effectively short-circuit the function.
190 *
191 * $force and $args params will no be passed on non media library uploads.
192 * This two will be passed on by compatibility.
193 *
194 * @since 2.2.4
195 *
196 * @param bool $value This should return true if want to skip the sync.
197 * @param int $metadata Metadata for the attachment.
198 * @param string $attachment_id Attachment ID.
199 * @param bool $force (optional) Whether to force the sync even the file already exist in GCS.
200 * @param bool $args (optional) Whether to only sync the full size image.
201 */
202 $check = apply_filters('wp_stateless_skip_add_media', null, $metadata, $attachment_id, $force, $args);
203
204 $client = ud_get_stateless_media()->get_client();
205
206 if ((!is_wp_error($client) || ($sm_mode == 'stateless' && !wp_doing_ajax())) && !$check) {
207
208 $image_host = ud_get_stateless_media()->get_gs_host();
209 $bucketLink = apply_filters('wp_stateless_bucket_link', $image_host);
210 $fullsizepath = wp_normalize_path(get_attached_file($attachment_id));
211 $_cacheControl = self::getCacheControl($attachment_id, $metadata, null);
212 $_contentDisposition = self::getContentDisposition($attachment_id, $metadata, null);
213
214 // Ensure image upload to GCS when attachment is updated,
215 // by checking if the attachment metadata is changed.
216 if ($attachment_id && !empty($metadata) && !$force) {
217 $db_metadata = get_post_meta($attachment_id, '_wp_attachment_metadata', true);
218 if ($db_metadata != $metadata) {
219 $force = true;
220 }
221 }
222
223 /**
224 * To skip removing files from server
225 *
226 * Returning a non-null value
227 * will effectively short-circuit the function.
228 *
229 * $force and $args params will no be passed on non media library uploads.
230 * This two will be passed on by compatibility.
231 *
232 * @since 3.0
233 *
234 * @param bool $value This should return true if want to skip the sync.
235 * @param int $metadata Metadata for the attachment.
236 * @param string $attachment_id Attachment ID.
237 * @param bool $force (optional) Whether to force the sync even the file already exist in GCS.
238 * @param bool $args (optional) Whether to only sync the full size image.
239 */
240 $skip_remove_media = apply_filters('wp_stateless_skip_remove_media', false, $metadata, $attachment_id, $force, $args);
241
242 // Make non-images uploadable.
243 // empty $metadata['file'] can cause problem, so we need to generate it.
244 if (empty($metadata['file']) && $attachment_id) {
245 $mime_type = get_post_mime_type($attachment_id);
246 $file = str_replace(wp_normalize_path(trailingslashit($upload_dir['basedir'])), '', $fullsizepath);
247
248 // We shouldn't create $metadata["file"] if it's PDF file.
249 if ($mime_type != "application/pdf") {
250 $metadata["file"] = $file;
251 }
252 }
253
254 $cloud_meta = get_post_meta($attachment_id, 'sm_cloud', true);
255
256 $cloud_meta = wp_parse_args($cloud_meta, array(
257 'name' => '',
258 'bucket' => ud_get_stateless_media()->get('sm.bucket'),
259 'fileLink' => '',
260 'mediaLink' => '',
261 'cacheControl' => $_cacheControl,
262 'contentDisposition' => $_contentDisposition,
263 'sizes' => array(),
264 ));
265
266 /**
267 * Storing file size to sm_cloud first,
268 * Because assigning directly to $metadata['filesize'] don't work.
269 * Maybe filesize gets removed in first run (when file exists).
270 */
271 if (file_exists($fullsizepath)) {
272 $cloud_meta['filesize'] = filesize($fullsizepath);
273 }
274 // Getting file size from sm_cloud.
275 if (!empty($cloud_meta['filesize'])) {
276 $metadata['filesize'] = $cloud_meta['filesize'];
277 }
278
279 $image_sizes = self::get_path_and_url($metadata, $attachment_id);
280 foreach ($image_sizes as $size => $img) {
281 if ((isset($_REQUEST['size']) && $_REQUEST['size'] == $size) || empty($_REQUEST['size'])) {
282 // GCS metadata
283 $_metadata = array(
284 "width" => $img['width'],
285 "height" => $img['height'],
286 'child-of' => $attachment_id,
287 'file-hash' => md5($file),
288 'size' => $size,
289 );
290
291 // adding extra GCS meta for full size image.
292 if (!$img['is_thumb']) {
293 unset($_metadata['child-of']); // no need in full size image.
294 $_metadata['object-id'] = $attachment_id;
295 $_metadata['source-id'] = md5($attachment_id . ud_get_stateless_media()->get('sm.bucket'));
296 }
297
298 $media_args = array_filter(array(
299 'force' => $force,
300 'name' => $img['gs_name'],
301 'is_webp' => $args['is_webp'],
302 'mimeType' => $img['mime_type'],
303 'metadata' => $_metadata,
304 'absolutePath' => $img['path'],
305 'cacheControl' => $_cacheControl,
306 'contentDisposition' => $_contentDisposition,
307 ));
308
309 if ($sm_mode == 'stateless' && !wp_doing_ajax()) {
310 global $gs_client;
311
312 $media_args = wp_parse_args($media_args, array(
313 'use_root' => true,
314 'force' => false,
315 'name' => false,
316 'absolutePath' => false,
317 'mimeType' => 'image/jpeg',
318 'metadata' => array(),
319 'is_webp' => '',
320 ));
321 $media_args = apply_filters('wp_stateless_add_media_args', $media_args);
322
323 //Bucket
324 $bucket = ud_get_stateless_media()->get('sm.bucket');
325
326 $bucket = $gs_client->bucket($bucket);
327 $object = $bucket->object($media_args['name']);
328
329 /**
330 * Updating object metadata, ACL, CacheControl and contentDisposition
331 * @return media object
332 */
333 try {
334 $media = $object->update(array('metadata' => $media_args['metadata']) +
335 array(
336 'cacheControl' => $_cacheControl,
337 'predefinedAcl' => 'publicRead',
338 'contentDisposition' => $_contentDisposition
339 ));
340
341 $cloud_meta = self::generate_cloud_meta($cloud_meta, $media, $size, $img, $bucketLink);
342 } catch (\Throwable $th) {
343 //throw $th;
344 }
345
346 $cloud_meta = self::generate_cloud_meta($cloud_meta, $media, $size, $img, $bucketLink);
347 } else {
348 /* Add default image */
349 $media = $client->add_media($media_args);
350
351 /* Break if we have errors. */
352 if (!is_wp_error($media)) {
353 // @note We don't add storageClass because it's same as parent...
354 $cloud_meta = self::generate_cloud_meta($cloud_meta, $media, $size, $img, $bucketLink);
355
356 /**
357 * Ephemeral and stateless mode: we don't need the local version.
358 * Except when uploading the full size image first.
359 */
360 if (self::can_delete_attachment($attachment_id, $args) && !$skip_remove_media) {
361 @unlink($img['path']);
362 }
363 }
364 }
365 }
366 }
367 // End of image sync loop
368 if (!$args['is_webp']) {
369 update_post_meta($attachment_id, 'sm_cloud', $cloud_meta);
370 } else {
371 // There is no use case for is_webp meta.
372 // $cloud_meta = get_post_meta( $attachment_id, 'sm_cloud', true);
373 // $cloud_meta['is_webp'] = true;
374 // update_post_meta( $attachment_id, 'sm_cloud', $cloud_meta );
375 }
376
377 /**
378 * Triggers when the media and it's thumbs are synced.
379 *
380 * $force and $args params will no be passed on non media library uploads.
381 * This two will be passed on by compatibility.
382 *
383 * @since 2.2.5
384 *
385 * @param int $metadata Metadata for the attachment.
386 * @param string $attachment_id Attachment ID.
387 * @param bool $force (optional) Whether to force the sync even the file already exist in GCS.
388 * @param bool $args (optional) Whether to only sync the full size image.
389 */
390 $metadata = apply_filters('wp_stateless_media_synced', $metadata, $attachment_id, $force, $args);
391 }
392
393 return $metadata;
394 }
395
396 /**
397 * Remove Media from Bucket by post ID
398 * Fired on calling function wp_delete_attachment()
399 *
400 * @todo: add error logging. peshkov@UD
401 * @see wp_delete_attachment()
402 * @action delete_attachment
403 * @author peshkov@UD
404 * @param $post_id
405 */
406 public static function remove_media($post_id) {
407 /* Get attachments metadata */
408 $metadata = wp_get_attachment_metadata($post_id);
409
410 /* Be sure we have the same bucket in settings and have GS object's name before proceed. */
411 if (isset($metadata['gs_name']) && isset($metadata['gs_bucket']) && $metadata['gs_bucket'] == ud_get_stateless_media()->get('sm.bucket')) {
412 $client = ud_get_stateless_media()->get_client();
413 if (!is_wp_error($client)) {
414
415 /* Remove default image */
416 $client->remove_media($metadata['gs_name'], $post_id);
417 // Remove webp
418 $client->remove_media($metadata['gs_name'] . '.webp', $post_id, true, "", true);
419
420 /* Now, go through all sizes and remove 'image sizes' images from Bucket too. */
421 if (!empty($metadata['sizes']) && is_array($metadata['sizes'])) {
422 foreach ($metadata['sizes'] as $k => $v) {
423 if (!empty($v['gs_name'])) {
424 $client->remove_media($v['gs_name'], $post_id, true, $k);
425 $client->remove_media($v['gs_name'] . '.webp', $post_id, true, $k, true);
426 }
427 }
428 }
429 }
430 }
431 }
432
433 /**
434 * Return URL and path for all image sizes of a attachment.
435 * @param $metadata
436 * @param $attachment_id
437 * @return mixed
438 */
439 public static function get_path_and_url($metadata, $attachment_id) {
440 /* Get metadata in case if method is called directly. */
441 if (empty($metadata) && current_filter() !== 'wp_generate_attachment_metadata' && current_filter() !== 'wp_update_attachment_metadata') {
442 $metadata = wp_get_attachment_metadata($attachment_id);
443 }
444
445 $gs_name_path = array();
446 $full_size_path = get_attached_file($attachment_id);
447 $base_dir = dirname($full_size_path);
448
449 $gs_name = apply_filters('wp_stateless_file_name', $full_size_path, true, $attachment_id, '');
450 $gs_base_dir = dirname($gs_name) == '.' ? '' : trailingslashit(dirname($gs_name));
451
452 if (!isset($metadata['width']) && file_exists($full_size_path)) {
453 try {
454 $_image_size = getimagesize($full_size_path);
455 if ($_image_size !== false && is_array($_image_size)) {
456 $metadata['width'] = $_image_size[0];
457 $metadata['height'] = $_image_size[1];
458 }
459 } catch (\Exception $e) {
460 // lets do nothing.
461 }
462 }
463
464 $gs_name_path['__full'] = array(
465 'gs_name' => $gs_name,
466 'path' => $full_size_path,
467 'sm_meta' => true,
468 'is_thumb' => false,
469 'mime_type' => get_post_mime_type($attachment_id),
470 'width' => isset($metadata['width']) ? $metadata['width'] : null,
471 'height' => isset($metadata['height']) ? $metadata['height'] : null,
472 );
473
474 /* Now we go through all available image sizes and upload them to Google Storage */
475 if (!empty($metadata['sizes']) && is_array($metadata['sizes'])) {
476 foreach ($metadata['sizes'] as $image_size => $data) {
477 if (empty($data['file'])) continue;
478 $absolutePath = wp_normalize_path($base_dir . '/' . $data['file']);
479 $gs_name = $gs_base_dir . $data['file'];
480 $gs_name = apply_filters('wp_stateless_file_name', $gs_name, true, $attachment_id, $image_size);
481
482 $gs_name_path[$image_size] = array(
483 'gs_name' => $gs_name,
484 'path' => $absolutePath,
485 'sm_meta' => true,
486 'is_thumb' => true,
487 'mime_type' => $data['mime-type'],
488 'width' => $data['width'],
489 'height' => $data['height'],
490 );
491 }
492 }
493
494 return apply_filters('wp_stateless_get_path_and_url', $gs_name_path, $metadata, $attachment_id);
495 }
496
497 /**
498 * Return URL and path for all image sizes of a attachment.
499 * @param $cloud_meta
500 * @param $media
501 * @param $image_size
502 * @param $img
503 * @param $bucketLink
504 * @return mixed
505 */
506 public static function generate_cloud_meta($cloud_meta, $media, $image_size, $img, $bucketLink) {
507 $gs_name = !empty($media['name']) ? $media['name'] : $img['gs_name'];
508 $fileLink = trailingslashit($bucketLink) . $gs_name;
509 $version = get_option('wp_sm_version', false);
510
511 if ($img['is_thumb']) {
512 // Cloud meta for thumbs.
513 $cloud_meta['sizes'][$image_size]['name'] = $gs_name;
514 $cloud_meta['sizes'][$image_size]['fileLink'] = $fileLink;
515 $cloud_meta['sizes'][$image_size]['mediaLink'] = $media['mediaLink'];
516 $cloud_meta['sizes'][$image_size]['width'] = ($media['metadata']['width']) ? $media['metadata']['width'] : $img['width'];
517 $cloud_meta['sizes'][$image_size]['height'] = ($media['metadata']['height']) ? $media['metadata']['height'] : $img['height'];
518 } else {
519 // cloud meta for full size image.
520 $cloud_meta['name'] = $gs_name;
521 $cloud_meta['fileLink'] = $fileLink;
522 $cloud_meta['mediaLink'] = $media['mediaLink'];
523 $cloud_meta['width'] = isset($media['metadata']['width']) ? $media['metadata']['width'] : ($img['width'] ? $img['width'] : 0);
524 $cloud_meta['height'] = isset($media['metadata']['height']) ? $media['metadata']['height'] : ($img['height'] ? $img['height'] : 0);
525 $cloud_meta['bucket'] = ud_get_stateless_media()->get('sm.bucket');
526 $cloud_meta['sm_version'] = $version;
527 }
528 return apply_filters('wp_stateless_generate_cloud_meta', $cloud_meta, $media, $image_size, $img, $bucketLink);
529 }
530
531 /**
532 * join_url
533 *
534 * @param array $parts
535 * @param boolean $encode
536 * @return string $url
537 */
538 public static function join_url($parts, $encode = TRUE) {
539 if ($encode) {
540 if (isset($parts['user']))
541 $parts['user'] = rawurlencode($parts['user']);
542 if (isset($parts['pass']))
543 $parts['pass'] = rawurlencode($parts['pass']);
544 if (
545 isset($parts['host']) &&
546 !preg_match('!^(\[[\da-f.:]+\]])|([\da-f.:]+)$!ui', $parts['host'])
547 )
548 $parts['host'] = rawurlencode($parts['host']);
549 if (!empty($parts['path']))
550 $parts['path'] = preg_replace(
551 '!%2F!ui',
552 '/',
553 rawurlencode($parts['path'])
554 );
555 if (isset($parts['query']))
556 $parts['query'] = rawurlencode($parts['query']);
557 if (isset($parts['fragment']))
558 $parts['fragment'] = rawurlencode($parts['fragment']);
559 }
560
561 $url = '';
562 if (!empty($parts['scheme']))
563 $url .= $parts['scheme'] . ':';
564 if (isset($parts['host'])) {
565 $url .= '//';
566 if (isset($parts['user'])) {
567 $url .= $parts['user'];
568 if (isset($parts['pass']))
569 $url .= ':' . $parts['pass'];
570 $url .= '@';
571 }
572 if (preg_match('!^[\da-f]*:[\da-f.:]+$!ui', $parts['host']))
573 $url .= '[' . $parts['host'] . ']'; // IPv6
574 else
575 $url .= $parts['host']; // IPv4 or name
576 if (isset($parts['port']))
577 $url .= ':' . $parts['port'];
578 if (!empty($parts['path']) && $parts['path'][0] != '/')
579 $url .= '/';
580 }
581 if (!empty($parts['path']))
582 $url .= $parts['path'];
583 if (isset($parts['query']))
584 $url .= '?' . $parts['query'];
585 if (isset($parts['fragment']))
586 $url .= '#' . $parts['fragment'];
587 return $url;
588 }
589
590 /**
591 * add_webp_mime
592 * @param $t
593 * @param $user
594 * @return mixed
595 */
596 public function add_webp_mime($t, $user) {
597 $t['webp'] = 'image/webp';
598 return $t;
599 }
600
601 /**
602 * Store attachment id in a static variable on 'intermediate_image_sizes_advanced' filter.
603 * To indicate that we can now delete attachment from server now.
604 *
605 * @param array $new_sizes
606 * @param array $image_meta
607 * @param int $attachment_id
608 * @return array $new_sizes
609 */
610 public static function store_can_delete_attachment($new_sizes, $image_meta, $attachment_id) {
611 if (!in_array($attachment_id, self::$can_delete_attachment)) {
612 self::$can_delete_attachment[] = $attachment_id;
613 }
614 return $new_sizes;
615 }
616
617 /**
618 * Check whether to delete attachment from server or not.
619 *
620 * @param int $attachment_id
621 * @param array $args
622 * @return boolean
623 */
624 public static function can_delete_attachment($attachment_id, $args) {
625 $sm_mode = ud_get_stateless_media()->get('sm.mode');
626
627 if (in_array($sm_mode, array('ephemeral', 'stateless'))) {
628 // checks whether it's WP 5.3 and 'intermediate_image_sizes_advanced' is passed.
629 // To be sure that we don't delete full size image before thumbnails are generated.
630 if (
631 wp_attachment_is_image($attachment_id) &&
632 function_exists('is_wp_version_compatible') &&
633 is_wp_version_compatible('5.3-RC4-46673') &&
634 !in_array($attachment_id, self::$can_delete_attachment)
635 ) {
636 return false;
637 }
638 return true;
639 }
640 return false;
641 }
642
643 /**
644 * Useful when there is a need to do things depending on a call stack.
645 * Returns true if any of the conditions met. Returns false otherwise.
646 *
647 * @param $callstack array Result of debug_backtrace function.
648 * @param $conditions array CallStack fingerprint with `stack_level` integer.
649 *
650 * Example:
651 * array(
652 * array(
653 * 'stack_level' => 4,
654 * 'function' => '__construct',
655 * 'class' => 'ET_Core_PageResource'
656 * ),
657 * array(
658 * 'stack_level' => 4,
659 * 'function' => 'get_cache_filename',
660 * 'class' => 'ET_Builder_Element'
661 * )
662 * )
663 *
664 * @return bool
665 */
666 public static function isCallStackMatches($callstack, $conditions) {
667 if (!is_array($conditions)) {
668 $conditions = array($conditions);
669 }
670
671 foreach ($conditions as $condition) {
672 $condition['stack_level'] = $condition['stack_level'] ? $condition['stack_level'] : 0;
673
674 $levelData = $callstack[$condition['stack_level']];
675
676 unset($condition['stack_level']);
677
678 $levelMatches = false;
679 foreach ($condition as $key => $value) {
680 if (isset($levelData[$key]) && $levelData[$key] === $value) {
681 $levelMatches = true;
682 } else {
683 $levelMatches = false;
684 }
685 }
686
687 if ($levelMatches) return true;
688 }
689
690 return false;
691 }
692
693 /**
694 * Fail over to image URL if not found on disk
695 * In case image not available on both local and bucket
696 * try to pull image from image URL in case it is accessible by some sort of proxy.
697 *
698 * @param:
699 * $url (int/string): URL of the image.
700 * $save_to (string): Path where to save the image.
701 *
702 * @return bool|int
703 * @throws \Exception
704 */
705 public static function sync_get_attachment_if_exist($url, $save_to) {
706 if (is_int($url)) $url = wp_get_attachment_url($url);
707
708 $response = wp_remote_get($url);
709 if (!is_wp_error($response) && is_array($response)) {
710 if (!empty($response['response']['code']) && $response['response']['code'] == 200) {
711 try {
712 if (wp_mkdir_p(dirname($save_to))) {
713 return file_put_contents($save_to, $response['body']);
714 }
715 } catch (\Exception $e) {
716 throw $e;
717 }
718 }
719 }
720 return false;
721 }
722
723 /**
724 * Generate JWT token signed by current site AUTH_SALT
725 * If no AUTH_SALT defined - admin email used
726 *
727 * @param $payload
728 * @param int $ttl
729 * @return string
730 */
731 public static function generate_jwt_token($payload, $ttl = 3600) {
732 $payload = wp_parse_args($payload, [
733 'iat' => $now = time(),
734 'iss' => $site_url = get_site_url(),
735 'aud' => $site_url,
736 'exp' => $now + $ttl
737 ]);
738
739 $key = defined('AUTH_SALT') ? AUTH_SALT : get_option('admin_email');
740 return \Firebase\JWT\JWT::encode($payload, $key);
741 }
742
743 /**
744 * Verify and decode token
745 * If no AUTH_SALT defined - admin email used
746 * Throws exceptions if cannot decode
747 *
748 * @param $token
749 * @return object
750 * @throws \Exception
751 */
752 public static function verify_jwt_token($token) {
753 $key = defined('AUTH_SALT') ? AUTH_SALT : get_option('admin_email');
754 return \Firebase\JWT\JWT::decode($token, $key, ['HS256']);
755 }
756
757 /**
758 * Generate auth token for wizard iframe
759 *
760 * @param int $ttl
761 * @return string
762 */
763 public static function generate_wizard_auth_token($ttl = 3600) {
764 $payload = [
765 'is_network' => is_network_admin(),
766 'user_id' => get_current_user_id()
767 ];
768 return self::generate_jwt_token($payload, $ttl);
769 }
770
771 /**
772 * Maps a file extensions to a mimetype.
773 *
774 * @param $extension string The file extension.
775 *
776 * @return string|null
777 * @link http://svn.apache.org/repos/asf/httpd/httpd/branches/1.3.x/conf/mime.types
778 */
779 public static function mimetype_from_extension($extension) {
780 $file_type = wp_check_filetype($extension);
781 if (!empty($file_type['type'])) {
782 return $file_type['type'];
783 }
784 static $mimetypes = [
785 '7z' => 'application/x-7z-compressed',
786 'aac' => 'audio/x-aac',
787 'ai' => 'application/postscript',
788 'aif' => 'audio/x-aiff',
789 'asc' => 'text/plain',
790 'asf' => 'video/x-ms-asf',
791 'atom' => 'application/atom+xml',
792 'avi' => 'video/x-msvideo',
793 'bmp' => 'image/bmp',
794 'bz2' => 'application/x-bzip2',
795 'cer' => 'application/pkix-cert',
796 'crl' => 'application/pkix-crl',
797 'crt' => 'application/x-x509-ca-cert',
798 'css' => 'text/css',
799 'csv' => 'text/csv',
800 'cu' => 'application/cu-seeme',
801 'deb' => 'application/x-debian-package',
802 'doc' => 'application/msword',
803 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
804 'dvi' => 'application/x-dvi',
805 'eot' => 'application/vnd.ms-fontobject',
806 'eps' => 'application/postscript',
807 'epub' => 'application/epub+zip',
808 'etx' => 'text/x-setext',
809 'flac' => 'audio/flac',
810 'flv' => 'video/x-flv',
811 'gif' => 'image/gif',
812 'gz' => 'application/gzip',
813 'htm' => 'text/html',
814 'html' => 'text/html',
815 'ico' => 'image/x-icon',
816 'ics' => 'text/calendar',
817 'ini' => 'text/plain',
818 'iso' => 'application/x-iso9660-image',
819 'jar' => 'application/java-archive',
820 'jpe' => 'image/jpeg',
821 'jpeg' => 'image/jpeg',
822 'jpg' => 'image/jpeg',
823 'js' => 'text/javascript',
824 'json' => 'application/json',
825 'latex' => 'application/x-latex',
826 'log' => 'text/plain',
827 'm4a' => 'audio/mp4',
828 'm4v' => 'video/mp4',
829 'mid' => 'audio/midi',
830 'midi' => 'audio/midi',
831 'mov' => 'video/quicktime',
832 'mp3' => 'audio/mpeg',
833 'mp4' => 'video/mp4',
834 'mp4a' => 'audio/mp4',
835 'mp4v' => 'video/mp4',
836 'mpe' => 'video/mpeg',
837 'mpeg' => 'video/mpeg',
838 'mpg' => 'video/mpeg',
839 'mpg4' => 'video/mp4',
840 'oga' => 'audio/ogg',
841 'ogg' => 'audio/ogg',
842 'ogv' => 'video/ogg',
843 'ogx' => 'application/ogg',
844 'pbm' => 'image/x-portable-bitmap',
845 'pdf' => 'application/pdf',
846 'pgm' => 'image/x-portable-graymap',
847 'png' => 'image/png',
848 'pnm' => 'image/x-portable-anymap',
849 'ppm' => 'image/x-portable-pixmap',
850 'ppt' => 'application/vnd.ms-powerpoint',
851 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
852 'ps' => 'application/postscript',
853 'qt' => 'video/quicktime',
854 'rar' => 'application/x-rar-compressed',
855 'ras' => 'image/x-cmu-raster',
856 'rss' => 'application/rss+xml',
857 'rtf' => 'application/rtf',
858 'sgm' => 'text/sgml',
859 'sgml' => 'text/sgml',
860 'svg' => 'image/svg+xml',
861 'swf' => 'application/x-shockwave-flash',
862 'tar' => 'application/x-tar',
863 'tif' => 'image/tiff',
864 'tiff' => 'image/tiff',
865 'torrent' => 'application/x-bittorrent',
866 'ttf' => 'application/x-font-ttf',
867 'txt' => 'text/plain',
868 'wav' => 'audio/x-wav',
869 'webm' => 'video/webm',
870 'webp' => 'image/webp',
871 'wma' => 'audio/x-ms-wma',
872 'wmv' => 'video/x-ms-wmv',
873 'woff' => 'application/x-font-woff',
874 'wsdl' => 'application/wsdl+xml',
875 'xbm' => 'image/x-xbitmap',
876 'xls' => 'application/vnd.ms-excel',
877 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
878 'xml' => 'application/xml',
879 'xpm' => 'image/x-xpixmap',
880 'xwd' => 'image/x-xwindowdump',
881 'yaml' => 'text/yaml',
882 'yml' => 'text/yaml',
883 'zip' => 'application/zip',
884 ];
885
886 $extension = strtolower($extension);
887
888 return isset($mimetypes[$extension]) ? $mimetypes[$extension] : false;
889 }
890
891 /**
892 * @param $size
893 * @return float|int
894 */
895 public static function convert_to_byte($size) {
896 $lastCharacter = \substr($size, -1);
897 $base = \strtoupper($lastCharacter);
898 if (!\ctype_digit($lastCharacter)) {
899 switch ($base) {
900 case 'B':
901 $size = (int) $size;
902 break;
903 case 'K':
904 $size = (int) $size * 1024;
905 break;
906 case 'M':
907 $size = (int) $size * pow(1024, 2);
908 break;
909 case 'G':
910 $size = (int) $size * pow(1024, 3);
911 break;
912 }
913 }
914 return $size;
915 }
916
917 /**
918 * Get stateless data, count of stateless media
919 * @return mixed
920 */
921 public static function get_stateless_media_data_count() {
922 global $wpdb;
923
924 $stateless_media = $wpdb->get_var($wpdb->prepare("
925 SELECT COUNT(meta_id)
926 FROM " . $wpdb->postmeta . "
927 WHERE meta_key = %s
928 ", 'sm_cloud'));
929
930 return $stateless_media;
931 }
932
933 /**
934 * Get a list of instances of all available sync methods
935 */
936 public static function get_available_sync_classes() {
937 return array_filter(array_map(function ($class) {
938 if (class_exists($class)) {
939 try {
940 $class = $class::instance();
941 if (is_a($class, BackgroundSync::class)) {
942 return $class;
943 }
944 return null;
945 } catch (\Throwable $e) {
946 error_log($e->getMessage());
947 return null;
948 }
949 }
950 return null;
951 }, apply_filters('wp_stateless_sync_types', [])));
952 }
953
954 /**
955 * Process single image attachment by id
956 *
957 * @param mixed $id Attachment ID
958 * @return \WP_Post
959 * @throws FatalException
960 * @throws UnprocessableException
961 */
962 public static function process_image_by_id($id) {
963 if (ud_get_stateless_media()->is_connected_to_gs() !== true) {
964 throw new FatalException(__('Not connected to GCS', ud_get_stateless_media()->domain));
965 }
966
967 $image = get_post($id);
968
969 if (!$image || 'attachment' != $image->post_type || 'image/' != substr($image->post_mime_type, 0, 6))
970 throw new UnprocessableException(sprintf(__('Failed to process item: %s is an invalid image ID.', ud_get_stateless_media()->domain), $id));
971
972 $fullsizepath = get_attached_file($image->ID);
973
974 // If no file found
975 if (false === $fullsizepath || !file_exists($fullsizepath)) {
976
977 // Try get it and save
978 $result_code = ud_get_stateless_media()->get_client()->get_media(apply_filters('wp_stateless_file_name', $fullsizepath, true, "", ""), true, $fullsizepath);
979
980 if ($result_code !== 200) {
981 if (!Utility::sync_get_attachment_if_exist($image->ID, $fullsizepath)) {
982 throw new UnprocessableException(sprintf(__('Both local and remote files are missing. Unable to process. (%s)', ud_get_stateless_media()->domain), $image->guid));
983 }
984 }
985 }
986
987 do_action('sm:pre::synced::image', $id);
988 if (!function_exists('wp_generate_attachment_metadata')) {
989 require_once ABSPATH . '/wp-admin/includes/image.php';
990 }
991 $metadata = wp_generate_attachment_metadata($image->ID, $fullsizepath);
992
993 if (get_post_mime_type($image->ID) !== 'image/svg+xml') {
994 if (is_wp_error($metadata)) {
995 throw new UnprocessableException($metadata->get_error_message());
996 }
997
998 if (empty($metadata)) {
999 throw new UnprocessableException(sprintf(__('No metadata generated for %1$s (ID %2$s).', ud_get_stateless_media()->domain), esc_html(get_the_title($image->ID)), $image->ID));
1000 }
1001 }
1002
1003 // trigger processing filters
1004 wp_update_attachment_metadata($image->ID, $metadata);
1005 do_action('sm:synced::image', $id, $metadata);
1006
1007 return $image;
1008 }
1009
1010 /**
1011 * Process single attachment file by id
1012 *
1013 * @param mixed $id Attachment ID
1014 * @return \WP_Post
1015 * @throws FatalException
1016 * @throws UnprocessableException
1017 */
1018 public static function process_file_by_id($id) {
1019 if (ud_get_stateless_media()->is_connected_to_gs() !== true) {
1020 throw new FatalException(__('Not connected to GCS', ud_get_stateless_media()->domain));
1021 }
1022
1023 $file = get_post($id);
1024
1025 if (!$file || 'attachment' != $file->post_type) {
1026 throw new UnprocessableException(sprintf(__('Attachment not found: %s is an invalid file ID.', ud_get_stateless_media()->domain), $id));
1027 }
1028
1029 $fullsizepath = get_attached_file($file->ID);
1030 $local_file_exists = file_exists($fullsizepath);
1031
1032 if (false === $fullsizepath || !$local_file_exists) {
1033
1034 // Try get it and save
1035 $result_code = ud_get_stateless_media()->get_client()->get_media(apply_filters('wp_stateless_file_name', $fullsizepath, true, "", ""), true, $fullsizepath);
1036
1037 if ($result_code !== 200) {
1038 if (!Utility::sync_get_attachment_if_exist($file->ID, $fullsizepath)) { // Save file to local from proxy.
1039 throw new UnprocessableException(sprintf(__('File not found (%s)', ud_get_stateless_media()->domain), $file->guid));
1040 } else {
1041 $local_file_exists = true;
1042 }
1043 } else {
1044 $local_file_exists = true;
1045 }
1046 }
1047
1048 if ($local_file_exists) {
1049
1050 if (!ud_get_stateless_media()->get_client()->media_exists(apply_filters('wp_stateless_file_name', $fullsizepath, true, "", ""))) {
1051
1052 if (!function_exists('wp_generate_attachment_metadata')) {
1053 require_once ABSPATH . '/wp-admin/includes/media.php';
1054 require_once ABSPATH . '/wp-admin/includes/image.php';
1055 }
1056 $metadata = wp_generate_attachment_metadata($file->ID, $fullsizepath);
1057
1058 if (is_wp_error($metadata)) {
1059 throw new UnprocessableException($metadata->get_error_message());
1060 }
1061
1062 wp_update_attachment_metadata($file->ID, $metadata);
1063 do_action('sm:synced::nonImage', $id, $metadata);
1064 } else {
1065 // Ephemeral and Stateless modes: we don't need the local version.
1066 if (ud_get_stateless_media()->get('sm.mode') === 'ephemeral' || ud_get_stateless_media()->get('sm.mode') === 'stateless') {
1067 unlink($fullsizepath);
1068 }
1069 }
1070 }
1071
1072 return $file;
1073 }
1074 }
1075 }
1076 }
1077