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

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