PluginProbe
WP-Stateless – Google Cloud Storage / 3.2.0
WP-Stateless – Google Cloud Storage v3.2.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.2.0, at lib/classes/class-utility.php

1,092 lines 40.6 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 use \Firebase\JWT\JWT;
19 use \Firebase\JWT\Key;
20
21 if (!class_exists('wpCloud\StatelessMedia\Utility')) {
22
23 class Utility {
24
25 static $can_delete_attachment = [];
26
27 /**
28 * ChromeLogger
29 *
30 * @author potanin@UD
31 * @param $data
32 */
33 static public function log($data) {
34
35 if (!class_exists('wpCloud\StatelessMedia\Logger')) {
36 include_once(__DIR__ . '/class-logger.php');
37 }
38
39 if (!class_exists('wpCloud\StatelessMedia\Logger')) {
40 return;
41 }
42
43 if (defined('WP_STATELESS_CONSOLE_LOG') && WP_STATELESS_CONSOLE_LOG) {
44 Logger::log('[wp-stateless]', $data);
45 }
46 }
47
48 /**
49 * Override Cache Control
50 * @param $cacheControl
51 * @return mixed
52 */
53 public static function override_cache_control($cacheControl) {
54 return ud_get_stateless_media()->get('sm.cache_control');
55 }
56
57 /**
58 * wp_normalize_path was added in 3.9.0
59 *
60 * @param $path
61 * @return mixed|string
62 *
63 */
64 public static function normalize_path($path) {
65
66 if (function_exists('wp_normalize_path')) {
67 return wp_normalize_path($path);
68 }
69
70 $path = str_replace('\\', '/', $path);
71 $path = preg_replace('|/+|', '/', $path);
72 return $path;
73 }
74
75 /**
76 * Randomize file name
77 * @param $filename
78 * @return string
79 */
80 public static function randomize_filename($filename) {
81 $return = apply_filters('stateless_skip_cache_busting', null, $filename);
82 if ($return) {
83 return $return;
84 }
85
86 if (preg_match('/^[a-f0-9]{8}-/', $filename)) {
87 return $filename;
88 }
89
90 $info = pathinfo($filename);
91 $ext = empty($info['extension']) ? '' : '' . $info['extension'];
92 $_parts = array();
93 $rand = substr(md5(time()), 0, 8);
94
95 if (strpos($info['filename'], '@')) {
96 $_cleanName = explode('@', $info['filename'])[0];
97 $_retna = explode('@', $info['filename'])[1];
98 $_parts[] = $rand;
99 $_parts[] = '-';
100 $_parts[] = strtolower($_cleanName);
101 $_parts[] = '@' . strtolower($_retna);
102 } else {
103 $_parts[] = $rand;
104 $_parts[] = '-';
105 $_parts[] = strtolower($info['filename']);
106 }
107
108 $filename = join('', $_parts);
109 if (!empty($ext)) {
110 $filename .= '.' . $ext;
111 }
112
113 return $filename;
114 }
115
116 /**
117 * Get Media Item Content Disposition
118 *
119 * @param null $attachment_id
120 * @param array $metadata
121 * @param array $data
122 * @return string
123 */
124 public static function getContentDisposition($attachment_id = null, $metadata = array(), $data = array()) {
125 // return 'Content-Disposition: attachment; filename=some-file.sql';
126 return apply_filters('sm:item:contentDisposition', null, array('attachment_id' => $attachment_id, 'mime_type' => get_post_mime_type($attachment_id), 'metadata' => $metadata, 'data' => $data));
127 }
128
129 /**
130 * @param null $attachment_id
131 * @param array $metadata
132 * @param array $data
133 * @return string
134 */
135 public static function getCacheControl($attachment_id = null, $metadata = array(), $data = array()) {
136 if (!$attachment_id) {
137 return apply_filters('sm:item:cacheControl', 'private, no-cache, no-store', $attachment_id, array('attachment_id' => null, 'mime_type' => null, 'metadata' => $metadata, 'data' => $data));
138 }
139
140 $_mime_type = get_post_mime_type($attachment_id);
141
142 // Treat images as public.
143 if (strpos($_mime_type, 'image/') !== false) {
144 return apply_filters('sm:item:cacheControl', 'public, max-age=36000, must-revalidate', array('attachment_id' => $attachment_id, 'mime_type' => null, 'metadata' => $metadata, 'data' => $data));
145 }
146
147 // Treat images as public.
148 if (strpos($_mime_type, 'sql') !== false) {
149 return apply_filters('sm:item:cacheControl', 'private, no-cache, no-store', array('attachment_id' => $attachment_id, 'mime_type' => null, 'metadata' => $metadata, 'data' => $data));
150 }
151
152 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));
153 }
154
155 /**
156 * Add/Update Media to Bucket
157 * Fired for every action with image add or update
158 *
159 * $force and $args params will no be passed on media library uploads.
160 * This two will be passed on by compatibility.
161 *
162 * @action wp_generate_attachment_metadata
163 * @author peshkov@UD
164 * @param $metadata
165 * @param $attachment_id
166 * @param boolean $force Whether to force the upload incase of it's already exists.
167 * @param array $args Whether to only sync the full size image.
168 * @return bool|string
169 */
170 public static function add_media($metadata, $attachment_id, $force = false, $args = array()) {
171 $sm_mode = ud_get_stateless_media()->get('sm.mode');
172 $file = '';
173 $upload_dir = wp_upload_dir();
174 $args = wp_parse_args($args, array(
175 'is_webp' => '', // expected value ".webp";
176 ));
177
178 /* Get metadata in case if method is called directly. */
179 if (current_filter() !== 'wp_generate_attachment_metadata' && current_filter() !== 'wp_update_attachment_metadata' && current_filter() !== 'intermediate_image_sizes_advanced') {
180 $metadata = wp_get_attachment_metadata($attachment_id);
181 }
182
183 // making sure meta data isn't null.
184 if (empty($metadata)) {
185 $metadata = array();
186 }
187
188 /**
189 * To skip the sync process.
190 *
191 * Returning a non-null value
192 * will effectively short-circuit the function.
193 *
194 * $force and $args params will no be passed on non media library uploads.
195 * This two will be passed on by compatibility.
196 *
197 * @since 2.2.4
198 *
199 * @param bool $value This should return true if want to skip the sync.
200 * @param int $metadata Metadata for the attachment.
201 * @param string $attachment_id Attachment ID.
202 * @param bool $force (optional) Whether to force the sync even the file already exist in GCS.
203 * @param bool $args (optional) Whether to only sync the full size image.
204 */
205 $check = apply_filters('wp_stateless_skip_add_media', null, $metadata, $attachment_id, $force, $args);
206
207 $client = ud_get_stateless_media()->get_client();
208
209 if ((!is_wp_error($client) || ($sm_mode == 'stateless' && !wp_doing_ajax())) && !$check) {
210
211 $image_host = ud_get_stateless_media()->get_gs_host();
212 $bucketLink = apply_filters('wp_stateless_bucket_link', $image_host);
213 $fullsizepath = wp_normalize_path(wp_get_original_image_path($attachment_id));
214 $_cacheControl = self::getCacheControl($attachment_id, $metadata, null);
215 $_contentDisposition = self::getContentDisposition($attachment_id, $metadata, null);
216
217 // Ensure image upload to GCS when attachment is updated,
218 // by checking if the attachment metadata is changed.
219 if ($attachment_id && !empty($metadata) && !$force) {
220 $db_metadata = get_post_meta($attachment_id, '_wp_attachment_metadata', true);
221 if ($db_metadata != $metadata) {
222 $force = true;
223 }
224 }
225
226 /**
227 * To skip removing files from server
228 *
229 * Returning a non-null value
230 * will effectively short-circuit the function.
231 *
232 * $force and $args params will no be passed on non media library uploads.
233 * This two will be passed on by compatibility.
234 *
235 * @since 3.0
236 *
237 * @param bool $value This should return true if want to skip the sync.
238 * @param int $metadata Metadata for the attachment.
239 * @param string $attachment_id Attachment ID.
240 * @param bool $force (optional) Whether to force the sync even the file already exist in GCS.
241 * @param bool $args (optional) Whether to only sync the full size image.
242 */
243 $skip_remove_media = apply_filters('wp_stateless_skip_remove_media', false, $metadata, $attachment_id, $force, $args);
244
245 // Make non-images uploadable.
246 // empty $metadata['file'] can cause problem, so we need to generate it.
247 if (empty($metadata['file']) && $attachment_id) {
248 $mime_type = get_post_mime_type($attachment_id);
249 $file = str_replace(wp_normalize_path(trailingslashit($upload_dir['basedir'])), '', $fullsizepath);
250
251 // We shouldn't create $metadata["file"] if it's PDF file.
252 if ($mime_type != "application/pdf") {
253 $metadata["file"] = $file;
254 }
255 }
256
257 $cloud_meta = get_post_meta($attachment_id, 'sm_cloud', true);
258
259 $cloud_meta = wp_parse_args($cloud_meta, array(
260 'name' => '',
261 'bucket' => ud_get_stateless_media()->get('sm.bucket'),
262 'fileLink' => '',
263 'mediaLink' => '',
264 'cacheControl' => $_cacheControl,
265 'contentDisposition' => $_contentDisposition,
266 'sizes' => array(),
267 ));
268
269 /**
270 * Storing file size to sm_cloud first,
271 * Because assigning directly to $metadata['filesize'] don't work.
272 * Maybe filesize gets removed in first run (when file exists).
273 */
274 if (file_exists($fullsizepath)) {
275 $cloud_meta['filesize'] = filesize($fullsizepath);
276 }
277 // Getting file size from sm_cloud.
278 if (!empty($cloud_meta['filesize'])) {
279 $metadata['filesize'] = $cloud_meta['filesize'];
280 }
281
282 $image_sizes = self::get_path_and_url($metadata, $attachment_id);
283 foreach ($image_sizes as $size => $img) {
284 if ((isset($_REQUEST['size']) && $_REQUEST['size'] == $size) || empty($_REQUEST['size'])) {
285 // GCS metadata
286 $_metadata = array(
287 "width" => $img['width'],
288 "height" => $img['height'],
289 'child-of' => $attachment_id,
290 'file-hash' => md5($file),
291 'size' => $size,
292 );
293
294 // adding extra GCS meta for full size image.
295 if (!$img['is_thumb']) {
296 unset($_metadata['child-of']); // no need in full size image.
297 $_metadata['object-id'] = $attachment_id;
298 $_metadata['source-id'] = md5($attachment_id . ud_get_stateless_media()->get('sm.bucket'));
299 }
300
301 $media_args = array_filter(array(
302 'force' => $force,
303 'name' => $img['gs_name'],
304 'is_webp' => $args['is_webp'],
305 'mimeType' => $img['mime_type'],
306 'metadata' => $_metadata,
307 'absolutePath' => $img['path'],
308 'cacheControl' => $_cacheControl,
309 'contentDisposition' => $_contentDisposition,
310 ));
311
312 if ($sm_mode == 'stateless' && !wp_doing_ajax() && !wp_doing_cron()) {
313 global $gs_client;
314
315 $media_args = wp_parse_args($media_args, array(
316 'use_root' => true,
317 'force' => false,
318 'name' => false,
319 'absolutePath' => false,
320 'mimeType' => 'image/jpeg',
321 'metadata' => array(),
322 'is_webp' => '',
323 ));
324 $media_args = apply_filters('wp_stateless_add_media_args', $media_args);
325
326 //Bucket
327 $bucket = ud_get_stateless_media()->get('sm.bucket');
328
329 $bucket = $gs_client->bucket($bucket);
330 $object = $bucket->object($media_args['name']);
331
332 /**
333 * Updating object metadata, ACL, CacheControl and contentDisposition
334 * @return media object
335 */
336 try {
337 $media = $object->update(array('metadata' => $media_args['metadata']) +
338 array(
339 'cacheControl' => $_cacheControl,
340 'predefinedAcl' => 'publicRead',
341 'contentDisposition' => $_contentDisposition
342 ));
343
344 $cloud_meta = self::generate_cloud_meta($cloud_meta, $media, $size, $img, $bucketLink);
345 } catch (\Throwable $th) {
346 //throw $th;
347 }
348
349 $cloud_meta = self::generate_cloud_meta($cloud_meta, $media, $size, $img, $bucketLink);
350 } else {
351 /* Add default image */
352 $media = $client->add_media($media_args);
353
354 /* Break if we have errors. */
355 if (!is_wp_error($media)) {
356 // @note We don't add storageClass because it's same as parent...
357 $cloud_meta = self::generate_cloud_meta($cloud_meta, $media, $size, $img, $bucketLink);
358
359 /**
360 * Ephemeral and stateless mode: we don't need the local version.
361 * Except when uploading the full size image first.
362 */
363 if (self::can_delete_attachment($attachment_id, $args) && !$skip_remove_media) {
364 @unlink($img['path']);
365 }
366 }
367 }
368 }
369 }
370 // End of image sync loop
371 if (!$args['is_webp']) {
372 update_post_meta($attachment_id, 'sm_cloud', $cloud_meta);
373 }
374
375 /**
376 * Triggers when the media and it's thumbs are synced.
377 *
378 * $force and $args params will no be passed on non media library uploads.
379 * This two will be passed on by compatibility.
380 *
381 * @since 2.2.5
382 *
383 * @param int $metadata Metadata for the attachment.
384 * @param string $attachment_id Attachment ID.
385 * @param bool $force (optional) Whether to force the sync even the file already exist in GCS.
386 * @param bool $args (optional) Whether to only sync the full size image.
387 */
388 $metadata = apply_filters('wp_stateless_media_synced', $metadata, $attachment_id, $force, $args);
389 }
390
391 return $metadata;
392 }
393
394 /**
395 * Remove Media from Bucket by post ID
396 * Fired on calling function wp_delete_attachment()
397 *
398 * @todo: add error logging. peshkov@UD
399 * @see wp_delete_attachment()
400 * @action delete_attachment
401 * @author peshkov@UD
402 * @param $post_id
403 */
404 public static function remove_media($post_id) {
405 /* Get attachments metadata */
406 $metadata = wp_get_attachment_metadata($post_id);
407
408 /* Be sure we have the same bucket in settings and have GS object's name before proceed. */
409 if (isset($metadata['gs_name']) && isset($metadata['gs_bucket']) && $metadata['gs_bucket'] == ud_get_stateless_media()->get('sm.bucket')) {
410 $client = ud_get_stateless_media()->get_client();
411 if (!is_wp_error($client)) {
412
413 /* Remove default image */
414 $client->remove_media($metadata['gs_name'], $post_id);
415 $client->remove_media(get_attached_file($post_id), $post_id);
416
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 = wp_get_original_image_path($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 JWT::encode($payload, $key, 'HS256');
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 JWT::decode($token, new Key($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 = wp_get_original_image_path($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 = wp_get_original_image_path($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 $metadata = wp_get_attachment_metadata($file->ID);
1070 /**
1071 * removing thumbnails
1072 * https://github.com/udx/wp-stateless/issues/577
1073 */
1074 if (!empty($metadata['sizes'])) {
1075 $base_dir = dirname($fullsizepath);
1076 foreach ($metadata['sizes'] as $image_size => $data) {
1077 $gs_name = $base_dir . '/' . $data['file'];
1078 if (file_exists($gs_name)) {
1079 @unlink($gs_name);
1080 }
1081 }
1082 }
1083 }
1084 }
1085 }
1086
1087 return $file;
1088 }
1089 }
1090 }
1091 }
1092