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
← All changes | lib/classes/class-bootstrap.php +2120 -1462 2.2.1trunk View file →
@@ -1,1462 +1,2120 @@
1 -<?php
2 -/**
3 - * Bootstrap
4 - *
5 - * @since 0.2.0
6 - */
7 -namespace wpCloud\StatelessMedia {
8 -
9 - if( !class_exists( 'wpCloud\StatelessMedia\Bootstrap' ) ) {
10 -
11 - final class Bootstrap extends \UsabilityDynamics\WP\Bootstrap_Plugin {
12 -
13 - /**
14 - * Google Storage Client
15 - * Use $this->get_client()
16 - *
17 - * @var \wpCloud\StatelessMedia\GS_CLient
18 - */
19 - private $client;
20 -
21 - /**
22 - * Plugin core version.
23 - *
24 - * @static
25 - * @property $version
26 - * @type {Object}
27 - */
28 - public static $version = '2.1';
29 -
30 - /**
31 - * Singleton Instance Reference.
32 - *
33 - * @protected
34 - * @static
35 - * @property $instance
36 - * @type \wpCloud\StatelessMedia\Bootstrap object
37 - */
38 - protected static $instance = null;
39 -
40 -
41 - /**
42 - * Constructor
43 - * Attention: MUST NOT BE CALLED DIRECTLY! USE get_instance() INSTEAD!
44 - *
45 - * @author peshkov@UD
46 - */
47 - protected function __construct( $args ) {
48 - parent::__construct( $args );
49 -
50 - //** Define our Admin Notices handler object */
51 - $this->errors = new Errors( array_merge( $args, array(
52 - 'type' => $this->type
53 - ) ) );
54 -
55 - // Initialize compatibility modules.
56 - add_action( 'plugins_loaded', function(){
57 - new Module();
58 - });
59 - }
60 -
61 - /**
62 - * Instantiate class.
63 - */
64 - public function init() {
65 -
66 - /**
67 - * Copied from wp-property
68 - * Duplicates UsabilityDynamics\WP\Bootstrap_Plugin::load_textdomain();
69 - *
70 - * There is a bug with localization in lib-wp-bootstrap 1.1.3 and lower.
71 - * So we load textdomain here again, in case old version lib-wp-bootstrap is being loaded
72 - * by another plugin.
73 - *
74 - * @since 1.9.1
75 - */
76 - load_plugin_textdomain($this->domain, false, dirname(plugin_basename($this->boot_file)) . '/static/languages/');
77 -
78 - // Parse feature falgs, set constants.
79 - $this->parse_feature_flags();
80 -
81 - new SyncNonMedia();
82 -
83 - /**
84 - * Define settings and UI.
85 - *
86 - * Example:
87 - *
88 - * Get option
89 - * $this->get( 'sm.client_id' )
90 - *
91 - * Manually Update/Add option
92 - * $this->set( 'sm.client_id', 'zxcvv12adffse' );
93 - */
94 - $this->settings = new Settings();
95 -
96 - // Invoke REST API
97 - add_action( 'rest_api_init', array( $this, 'api_init' ) );
98 -
99 - /**
100 - * Register SM metaboxes
101 - */
102 - add_action( 'admin_init', array( $this, 'register_metaboxes' ) );
103 -
104 - /**
105 - * Init hook
106 - */
107 - add_action( 'admin_init', array( $this, 'admin_init' ) );
108 -
109 - /**
110 - * Add custom actions to media rows
111 - */
112 - add_filter( 'media_row_actions', array( $this, 'add_custom_row_actions' ), 10, 3 );
113 -
114 - /**
115 - * Handle switch blog properly.
116 - */
117 - add_action( 'switch_blog', array( $this, 'on_switch_blog' ), 10, 2 );
118 -
119 - /**
120 - * Filter for getting stateless settings
121 - */
122 - add_filter( 'stateless::get_settings', array( $this, 'get_settings' ), 10);
123 -
124 - /**
125 - * Init AJAX jobs
126 - */
127 - new Ajax();
128 -
129 - /**
130 - * Load WP-CLI Commands
131 - */
132 - if( defined( 'WP_CLI' ) && WP_CLI ) {
133 - include_once($this->path('lib/cli/class-sm-cli-command.php', 'dir'));
134 - }
135 -
136 - $this->is_network_detected();
137 -
138 - /**
139 - * Add scripts
140 - */
141 - add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
142 -
143 - /**
144 - * Hashify file name if option is enabled
145 - */
146 - if ( $this->get( 'sm.hashify_file_name' ) == 'true' ) {
147 - add_filter('sanitize_file_name', array( 'wpCloud\StatelessMedia\Utility', 'randomize_filename' ), 10);
148 - }
149 -
150 - /* Initialize plugin only if Mode is not 'disabled'. */
151 - if ( $this->get( 'sm.mode' ) !== 'disabled' ) {
152 -
153 - /**
154 - * Override Cache Control is option is enabled
155 - */
156 - $cacheControl = trim($this->get( 'sm.cache_control' ));
157 - if ( !empty($cacheControl) ) {
158 - add_filter( 'sm:item:cacheControl', array( $this, 'override_cache_control' ) );
159 - }
160 -
161 - /**
162 - * Determine if we have issues with connection to Google Storage Bucket
163 - * if SM is not disabled.
164 - */
165 - $is_connected = $this->is_connected_to_gs();
166 -
167 - if ( is_wp_error( $is_connected ) ) {
168 - $this->errors->add( $is_connected->get_error_message() );
169 - }
170 -
171 - if ( $googleSDKVersionConflictError = get_transient( "wp_stateless_google_sdk_conflict" ) ) {
172 - $this->errors->add( $googleSDKVersionConflictError, 'warning' );
173 - }
174 -
175 - // To prevent fatal errors for users who use PHP 5.5 or less.
176 - if( version_compare(PHP_VERSION, '5.5', '<') ) {
177 - $this->errors->add( sprintf( __( 'The plugin requires PHP %s or higher. You current PHP version %s is too old.', ud_get_stateless_media()->domain ), '<b>5.5</b>', '<b>' . PHP_VERSION . '</b>' ) );
178 - }
179 -
180 - /** Temporary fix to WP 4.4 srcset feature **/
181 - //add_filter( 'max_srcset_image_width', function(){return 1;} );
182 -
183 - /**
184 - * Carry on only if we do not have errors.
185 - */
186 - if( !$this->has_errors() ) {
187 -
188 - if( $this->get( 'sm.mode' ) === 'cdn' || $this->get( 'sm.mode' ) === 'stateless' ) {
189 - add_filter( 'wp_get_attachment_image_attributes', array( $this, 'wp_get_attachment_image_attributes' ), 20, 3 );
190 - add_filter( 'wp_get_attachment_url', array( $this, 'wp_get_attachment_url' ), 20, 2 );
191 - add_filter( 'attachment_url_to_postid', array( $this, 'attachment_url_to_postid' ), 20, 2 );
192 -
193 - if ( $this->get( 'sm.body_rewrite' ) == 'true' || $this->get( 'sm.body_rewrite' ) == 'enable_editor' ) {
194 - add_filter( 'the_content', array( $this, 'the_content_filter' ), 99 );
195 - }
196 -
197 - if ( $this->get( 'sm.body_rewrite' ) == 'true' || $this->get( 'sm.body_rewrite' ) == 'enable_meta' ) {
198 - add_filter( 'get_post_metadata', array( $this, 'post_metadata_filter' ), 2, 4 );
199 - }
200 -
201 - add_filter( 'wp_stateless_bucket_link', array( $this, 'wp_stateless_bucket_link' ) );
202 - }
203 -
204 - if ( $root_dir = $this->get( 'sm.root_dir' ) ) {
205 - if ( trim( $root_dir, '/ ' ) !== '' ) { // Remove any forward slash and empty space.
206 - add_filter( 'wp_stateless_file_name', array( $this, 'handle_root_dir' ) );
207 - }
208 - }
209 -
210 - /**
211 - * Rewrite Image URLS
212 - */
213 - add_filter( 'image_downsize', array( $this, 'image_downsize' ), 99, 3 );
214 - add_filter( 'wp_calculate_image_srcset', array($this, 'wp_calculate_image_srcset'), 10, 5 );
215 -
216 - /**
217 - * Extends metadata by adding GS information.
218 - */
219 - add_filter( 'wp_get_attachment_metadata', array( $this, 'wp_get_attachment_metadata' ), 10, 2 );
220 -
221 - /**
222 - * Add/Edit Media
223 - *
224 - * Once added or edited we can get into Attachment ID then get all image sizes and sync them with GS
225 - * We can't use this. That's prevent removing this filter.
226 - */
227 - add_filter( 'wp_update_attachment_metadata', array( 'wpCloud\StatelessMedia\Utility', 'add_media' ), 999, 2 );
228 -
229 - /**
230 - * Upload the full size image first.
231 - *
232 - */
233 - if(!defined('WP_STATELESS_MEDIA_DISABLE_FULL_IMAGE_FIRST') || WP_STATELESS_MEDIA_DISABLE_FULL_IMAGE_FIRST != true){
234 - add_filter( 'intermediate_image_sizes_advanced', array( $this, 'before_intermediate_image_sizes' ), 10, 2 );
235 - }
236 -
237 - /**
238 - * Add Media
239 - *
240 - * Once added we can get into Attachment ID then get all image sizes and sync them with GS
241 - */
242 - // add_filter( 'wp_generate_attachment_metadata', array( $this, 'add_media' ), 100, 2 );
243 -
244 - if ( $this->get( 'sm.delete_remote' ) == 'true' ) {
245 - /**
246 - * On physical file deletion we remove any from GS
247 - */
248 - add_filter('delete_attachment', array($this, 'remove_media'));
249 - }
250 -
251 - // Trigger module initialization and registration.
252 - do_action('sm::module::init', $this->get( 'sm' ));
253 - }
254 -
255 - }
256 - }
257 -
258 - /**
259 - * Rebuild srcset from gs_link.
260 - * Using calculations returned from WordPress wp_calculate_image_srcset()
261 - *
262 - */
263 - public function wp_calculate_image_srcset($sources, $size_array, $image_src, $image_meta, $attachment_id){
264 - if (empty($image_meta['gs_link'])) {
265 - $image_meta = wp_get_attachment_metadata($attachment_id);
266 - }
267 -
268 - if(is_array($sources) && !empty( $image_meta['gs_link'] )){
269 - $gs_name = $image_meta['gs_name'];
270 - // getting position of root_dir in gs_name.
271 - $root_dir_pos = strpos($gs_name, $image_meta['file']);
272 - // removing rood_dir from gs_name so we can compare to replace url with gs_link.
273 - if($root_dir_pos !== false){
274 - $gs_name = substr($gs_name, $root_dir_pos);
275 - }
276 -
277 - foreach ($sources as $width => &$image) {
278 - if (!isset($image_meta['gs_name']) || empty($image_meta['gs_name'])) {
279 - continue;
280 - }
281 -
282 - // If srcset includes original image src, replace it
283 - if (substr_compare($image['url'], $gs_name, -strlen($gs_name)) === 0) {
284 - $image['url'] = $image_meta['gs_link'];
285 - // Replace all sizes
286 - } elseif (isset($image_meta['sizes']) && is_array($image_meta['sizes'])) {
287 - foreach ($image_meta['sizes'] as $key => $meta) {
288 - if (!isset($meta['gs_name']) || empty($meta['gs_name'])) {
289 - continue;
290 - }
291 -
292 - $thumb_gs_name = $meta['gs_name'];
293 - // removing rood_dir from gs_name because
294 - if($root_dir_pos !== false){
295 - $thumb_gs_name = substr($thumb_gs_name, $root_dir_pos);
296 - }
297 -
298 - if (substr_compare($image['url'], $thumb_gs_name, -strlen($thumb_gs_name)) === 0) {
299 - $image['url'] = $meta['gs_link'];
300 - break;
301 - }
302 - }
303 - }
304 - }
305 - }
306 -
307 - return $sources;
308 - }
309 -
310 - /**
311 - * Return gs host.
312 - * If custom domain is set it's return bucket name as host,
313 - * else return storage.googleapis.com as host and append bucket name at the end.
314 - * @param none
315 - * @return Host name to use
316 - */
317 - public function get_gs_host($sm = array()) {
318 - $sm = $sm?$sm: $this->get( 'sm');
319 - $image_host = 'https://storage.googleapis.com/';
320 - $image_host .= $sm['bucket'];
321 -
322 - $custom_domain = $sm['custom_domain'];
323 - $is_ssl = strpos($custom_domain, 'https://');
324 - $custom_domain = str_replace(array('http://', 'https://'), '', $custom_domain);
325 - $custom_domain = trim($custom_domain, '/');
326 -
327 - if ( !empty($sm['bucket']) && !empty($custom_domain) && $custom_domain !== 'storage.googleapis.com' && ( $is_ssl === 0 || $custom_domain != $sm['bucket'] ) ) {
328 - $image_host = $is_ssl === 0 ? 'https://' : 'http://'; // bucketname will be host
329 - $image_host .= $custom_domain;
330 - }
331 -
332 - return apply_filters( 'get_gs_host', $image_host, $image_host, $sm['bucket'], $is_ssl, $sm );
333 - }
334 -
335 - /**
336 - * Filter for wp_stateless_bucket_link if custom domain is set.
337 - * It's get attachment url and remove "storage.googleapis.com" from url.
338 - * So that custom url can be used.
339 - * @param $new_blog
340 - * @param $prev_blog_id
341 - */
342 - public function wp_stateless_bucket_link($fileLink) {
343 - $bucketname = $this->get( 'sm.bucket' );
344 - $custom_domain = $this->get( 'sm.custom_domain' );
345 - $is_ssl = strpos($custom_domain, 'https://') === 0;
346 - $fileLink_is_ssl = strpos($fileLink, 'https://') === 0;
347 - $custom_domain = str_replace(array('http://', 'https://'), '', $custom_domain);
348 - $custom_domain = trim($custom_domain, '/');
349 -
350 - if ( $custom_domain !== 'storage.googleapis.com' && $custom_domain == $bucketname && strpos($fileLink, $bucketname) > 8 ) {
351 - $fileLink = ($is_ssl ? 'https://' : 'http://') . substr($fileLink, strpos($fileLink, $bucketname));
352 - }
353 - elseif( $custom_domain !== 'storage.googleapis.com' && $custom_domain == $bucketname && $fileLink_is_ssl !== $is_ssl){
354 - if($is_ssl)
355 - $fileLink = str_replace(array('http://', 'https://'), 'https://', $fileLink);
356 - else
357 - $fileLink = str_replace(array('http://', 'https://'), 'http://', $fileLink);
358 - }
359 - return $fileLink;
360 - }
361 -
362 - /**
363 - * Return settings page url.
364 - * @param $path
365 - */
366 - public function get_settings_page_url( $path = '' ) {
367 - $url = get_admin_url( get_current_blog_id(), ( is_network_admin() ? 'network/settings.php' : 'upload.php' ) );
368 - return $url . $path;
369 - }
370 -
371 - /**
372 - * Get new blog settings once switched blog.
373 - * @param $new_blog
374 - * @param $prev_blog_id
375 - */
376 - public function on_switch_blog( $new_blog, $prev_blog_id ) {
377 - $this->settings->refresh();
378 - }
379 -
380 - /**
381 - * Get settings handler.
382 - * Filling array if some settings missing.
383 - *
384 - * @param $settings
385 - * @return
386 - */
387 - public function get_settings($settings) {
388 -
389 - $settings_list = array(
390 - 'mode',
391 - 'body_rewrite',
392 - 'body_rewrite_types',
393 - 'bucket',
394 - 'root_dir',
395 - 'key_json',
396 - 'cache_control',
397 - 'delete_remote',
398 - 'custom_domain',
399 - 'organize_media',
400 - 'hashify_file_name'
401 - );
402 -
403 - foreach ($settings_list as $setting) {
404 -
405 - /** If setting is already exist, just skip it */
406 - if( isset( $settings[ $setting ] ) ) {
407 - continue;
408 - }
409 -
410 - $value = $this->get( 'sm.' . $setting );
411 -
412 - /** Decode json to array */
413 - if( $value && is_string( $value ) && $setting === 'key_json' ) {
414 - $value = json_decode( $value, true );
415 - $setting = 'key';
416 - }
417 -
418 - $settings[ $setting ] = $value;
419 - }
420 -
421 - return $settings;
422 - }
423 -
424 - /**
425 - * Remove all settings.
426 - */
427 - public function reset($network = false) {
428 - $this->settings->reset($network);
429 - }
430 -
431 - /**
432 - * @param $actions
433 - * @param $post
434 - * @param $detached
435 - * @return mixed
436 - */
437 - public function add_custom_row_actions( $actions, $post, $detached ) {
438 -
439 - if ( !current_user_can( 'upload_files' ) ) return $actions;
440 -
441 - if ( $post && 'attachment' == $post->post_type && 'image/' == substr( $post->post_mime_type, 0, 6 ) ) {
442 - $actions['sm_sync'] = '<a href="javascript:;" data-id="'.$post->ID.'" data-type="image" class="sm_inline_sync">' . __('Regenerate and Sync with GCS', ud_get_stateless_media()->domain) . '</a>';
443 - }
444 -
445 - if ( $post && 'attachment' == $post->post_type && 'image/' != substr( $post->post_mime_type, 0, 6 ) ) {
446 - $actions['sm_sync'] = '<a href="javascript:;" data-id="'.$post->ID.'" data-type="other" class="sm_inline_sync">' . __('Sync with GCS', ud_get_stateless_media()->domain) . '</a>';
447 - }
448 -
449 - return $actions;
450 -
451 - }
452 -
453 - /**
454 - * Register metaboxes
455 - */
456 - public function register_metaboxes() {
457 - add_meta_box(
458 - 'sm-attachment-metabox',
459 - __( 'Google Cloud Storage', ud_get_stateless_media()->domain ),
460 - array($this, 'attachment_meta_box_callback'),
461 - 'attachment',
462 - 'side',
463 - 'low'
464 - );
465 - }
466 -
467 - /**
468 - * Define REST API.
469 - *
470 - * // https://usabilitydynamics-sandbox-uds-io-stateless-testing.c.rabbit.ci/wp-json/wp-stateless/v1
471 - *
472 - * @author potanin@UD
473 - */
474 - public function api_init() {
475 -
476 - $route_namespace = 'wp-stateless/v1';
477 - $api_namespace = 'wpCloud\StatelessMedia\API';
478 -
479 - register_rest_route( $route_namespace, '/status', array(
480 - 'methods' => 'GET',
481 - 'callback' => array( $api_namespace, 'status' ),
482 - ) );
483 -
484 - register_rest_route( $route_namespace, '/jobs', array(
485 - 'methods' => 'GET',
486 - 'callback' => array( $api_namespace, 'jobs' ),
487 - ) );
488 -
489 - /**
490 - * Return stateless settings.
491 - *
492 - * Request parameter: none
493 - *
494 - * Response:
495 - * ok: Whether API is up or not
496 - * message: Describe what is done or error message on error.
497 - * settings: array of stateless settings
498 - *
499 - */
500 - register_rest_route( $route_namespace, '/getSettings', array( 'methods' => 'GET', 'callback' => array( $api_namespace, 'getSettings' ), ) );
501 -
502 - /**
503 - * Essentially for scrolling through media library to build our index.
504 - *
505 - * Request parameter: none
506 - *
507 - * Response:
508 - * ok: Whether API is up or not
509 - * message: Describe what is done or error message on error.
510 - * settings: array of media files
511 - *
512 - */
513 - register_rest_route( $route_namespace, '/getMediaLibrary', array( 'methods' => 'GET', 'callback' => array( $api_namespace, 'getMediaLibrary' ), ) );
514 -
515 - /**
516 - * Get detailed information of media file
517 - *
518 - * Request parameter: none
519 - *
520 - * Response:
521 - * ok: Whether API is up or not
522 - * message: Describe what is done or error message on error.
523 - * settings: media file array
524 - *
525 - */
526 - register_rest_route( $route_namespace, '/getMediaItem', array( 'methods' => 'GET', 'callback' => array( $api_namespace, 'getMediaItem' ), ) );
527 -
528 - }
529 -
530 - /**
531 - * @param $post
532 - */
533 - public function attachment_meta_box_callback( $post ) {
534 - ob_start();
535 -
536 - $sm_cloud = get_post_meta( $post->ID, 'sm_cloud', 1 );
537 -
538 - if ( is_array( $sm_cloud ) && !empty( $sm_cloud[ 'fileLink' ] ) ) { ?>
539 -
540 - <?php if( !empty( $sm_cloud[ 'cacheControl' ] ) ) { ?>
541 - <div class="misc-pub-cache-control hidden">
542 - <?php _e( 'Cache Control:', ud_get_stateless_media()->domain ); ?> <strong><span><?php echo $sm_cloud[ 'cacheControl' ]; ?></span> </strong>
543 - </div>
544 - <?php } ?>
545 -
546 - <div class="misc-pub-gs-file-link" style="margin-bottom: 15px;">
547 - <label>
548 - <?php _e( 'Storage Bucket URL:', ud_get_stateless_media()->domain ); ?> <a href="<?php echo $sm_cloud[ 'fileLink' ]; ?>" target="_blank" class="sm-view-link"><?php _e( '[view]' ); ?></a>
549 - <input type="text" class="widefat urlfield" readonly="readonly" value="<?php echo esc_attr($sm_cloud[ 'fileLink' ]); ?>" />
550 - </label>
551 - </div>
552 -
553 - <?php
554 -
555 - if ( !empty( $sm_cloud[ 'bucket' ] ) ) {
556 - ?>
557 - <div class="misc-pub-gs-bucket" style="margin-bottom: 15px;">
558 - <label>
559 - <?php _e( 'Storage Bucket:', ud_get_stateless_media()->domain ); ?>
560 - <input type="text" class="widefat urlfield" readonly="readonly" value="gs://<?php echo esc_attr($sm_cloud[ 'bucket' ]); ?>" />
561 - </label>
562 - </div>
563 - <?php
564 - }
565 -
566 - if ( current_user_can( 'upload_files' ) ) {
567 - if ( $post && 'attachment' == $post->post_type && 'image/' == substr( $post->post_mime_type, 0, 6 ) ) {
568 - ?>
569 - <a href="javascript:;" data-type="image" data-id="<?php echo $post->ID; ?>"
570 - class="button-secondary sm_inline_sync"><?php _e('Regenerate and Sync with GCS', ud_get_stateless_media()->domain); ?></a>
571 - <?php
572 - }
573 -
574 - if ( $post && 'attachment' == $post->post_type && 'image/' != substr( $post->post_mime_type, 0, 6 ) ) {
575 - ?>
576 - <a href="javascript:;" data-type="other" data-id="<?php echo $post->ID; ?>"
577 - class="button-secondary sm_inline_sync"><?php _e('Sync with GCS', ud_get_stateless_media()->domain); ?></a>
578 - <?php
579 - }
580 - }
581 - }
582 -
583 - echo apply_filters( 'sm::attachment::meta', ob_get_clean(), $post->ID );
584 - }
585 -
586 - /**
587 - * @param $current_path
588 - * @return string
589 - */
590 - public function handle_root_dir( $current_path ) {
591 - $root_dir = $this->get( 'sm.root_dir' );
592 - $root_dir = trim( $root_dir, '/ ' ); // Remove any forward slash and empty space.
593 -
594 - if ( !empty( $root_dir ) ) {
595 - return $root_dir . '/' . $current_path;
596 - }
597 -
598 - return $current_path;
599 - }
600 -
601 - /**
602 - * @param $content
603 - * @return mixed
604 - */
605 - public function the_content_filter( $content ) {
606 -
607 - if ( $upload_data = wp_upload_dir() ) {
608 -
609 - if ( !empty( $upload_data['baseurl'] ) && !empty( $content ) ) {
610 - $baseurl = preg_replace('/https?:\/\//','',$upload_data['baseurl']);
611 - $root_dir = trim( $this->get( 'sm.root_dir' ), '/ ' ); // Remove any forward slash and empty space.
612 - $root_dir = !empty( $root_dir ) ? $root_dir . '/' : false;
613 - $image_host = $this->get_gs_host();
614 - $file_ext = $this->replaceable_file_types();
615 - $content = preg_replace( '/(href|src)=(\'|")(https?:\/\/'.str_replace('/', '\/', $baseurl).')\/(.+?)('.$file_ext.')(\'|")/i',
616 - '$1=$2'.$image_host.'/'.($root_dir?$root_dir:'').'$4$5$6', $content);
617 - }
618 - }
619 -
620 - return $content;
621 - }
622 -
623 - /**
624 - * Return file types supported by File URL Replacement.
625 - *
626 - */
627 - public function replaceable_file_types(){
628 - $types = $this->get('sm.body_rewrite_types');
629 -
630 - // Removing extra space.
631 - $types = trim($types);
632 - $types = preg_replace("/\s{2,}/", ' ', $types);
633 -
634 - $types_arr = explode(' ', $types);
635 - return '\.' . implode('|\.', $types_arr);
636 - }
637 -
638 - /**
639 - * Copied from https://developer.wordpress.org/reference/functions/get_metadata/
640 - *
641 - * @param $value null unless other filter hooked in this function.
642 - * @param $object_id post id
643 - * @param $meta_key
644 - * @param $single
645 - * @return mixed
646 - */
647 - public function post_metadata_filter($value, $object_id, $meta_key, $single){
648 - if(empty($value)){
649 - $meta_type = 'post';
650 - $transient_key = "stateless_{$meta_type}_meta";
651 -
652 - $meta_cache = wp_cache_get($object_id, $transient_key);
653 - if(empty($meta_cache)){
654 - $meta_cache = wp_cache_get($object_id, $meta_type . '_meta');
655 -
656 - if ( !$meta_cache ) {
657 - $meta_cache = update_meta_cache( $meta_type, array( $object_id ) );
658 - $meta_cache = $meta_cache[$object_id];
659 - }
660 -
661 - foreach($meta_cache as $key => $meta){
662 - $meta_cache[$key] = array_map('maybe_unserialize', $meta_cache[$key]);
663 - }
664 -
665 - $meta_cache = $this->convert_to_gs_link($meta_cache);
666 - wp_cache_set($object_id, $meta_cache, $transient_key);
667 - }
668 -
669 - if ( ! $meta_key ) {
670 - return $meta_cache;
671 - }
672 -
673 - if ( isset($meta_cache[$meta_key]) ) {
674 - if ( $single )
675 - return $meta_cache[$meta_key][0];
676 - else
677 - return $meta_cache[$meta_key];
678 - }
679 -
680 - // in case no metadata is found return what was passed in $value.
681 - // $value most of the time is null.
682 - return $value;
683 - }
684 -
685 - return $this->convert_to_gs_link($value);
686 -
687 - }
688 -
689 - /**
690 - * Rplace all image link with gs link and return only if meta modified.
691 - * @param $meta
692 - * @return mixed or null when not changed.
693 - */
694 - public function convert_to_gs_link($meta){
695 - $updated = $meta;
696 - if ( $meta && $upload_data = wp_upload_dir() ) {
697 - if ( !empty( $upload_data['baseurl'] ) && !empty( $meta ) ) {
698 - $baseurl = preg_replace('/https?:\/\//','',$upload_data['baseurl']);
699 - $root_dir = trim( $this->get( 'sm.root_dir' ), '/ ' ); // Remove any forward slash and empty space.
700 - $root_dir = !empty( $root_dir ) ? $root_dir . '/': false;
701 - $image_host = $this->get_gs_host().'/'.($root_dir?$root_dir:'');
702 - $file_ext = $this->replaceable_file_types();
703 - $updated = $this->_convert_to_gs_link($meta, $image_host, $baseurl, $file_ext);
704 - }
705 - }
706 -
707 - if($updated == $meta){
708 - return null; // Not changed.
709 - }
710 - return $updated;
711 - }
712 -
713 - /**
714 - * Rplace all image link with gs link
715 - * @param $meta
716 - * @return mixed
717 - */
718 - public function _convert_to_gs_link($meta, $image_host, $baseurl, $file_ext){
719 - if(is_array($meta)){
720 - foreach ($meta as $key => $value) {
721 - $meta[$key] = $this->_convert_to_gs_link($value, $image_host, $baseurl, $file_ext);
722 - }
723 - return $meta;
724 - } elseif (is_object($meta) && $meta instanceof \stdClass ) {
725 - foreach (get_object_vars($meta) as $key => $value) {
726 - $meta->{$key} = $this->_convert_to_gs_link($value, $image_host, $baseurl, $file_ext);
727 - }
728 - return $meta;
729 - } elseif(is_string($meta)){
730 - return preg_replace( '/(https?:\/\/'.str_replace('/', '\/', $baseurl).')\/(.+?)(' . $file_ext . ')/i', $image_host.'$2$3', $meta);
731 - }
732 -
733 - return $meta;
734 - }
735 -
736 -
737 - /**
738 - * @param $links
739 - * @param $file
740 - * @return mixed
741 - */
742 - public function plugin_action_links( $links, $file ) {
743 -
744 - if ($file == plugin_basename( dirname( __DIR__ ) . '/wp-stateless-media.php' ) ) {
745 - $settings_link = '<a href="'. '' .'">'.__( 'Settings' , 'ssd').'</a>';
746 - array_unshift( $links, $settings_link );
747 - }
748 -
749 - if ($file == plugin_basename( dirname( __DIR__ ) . '/wp-stateless.php' ) ) {
750 - $settings_link = '<a href="'. '' .'">'.__( 'Settings' , 'ssd').'</a>';
751 - array_unshift( $links, $settings_link );
752 - }
753 -
754 - return $links;
755 - }
756 -
757 - /**
758 - * Determines if plugin is loaded via mu-plugins
759 - * or Network Enabled.
760 - *
761 - * @author peshkov@UD
762 - */
763 - public function is_network_detected() {
764 - /* Plugin is loaded via mu-plugins. */
765 -
766 - if( strpos( Utility::normalize_path( $this->root_path ), Utility::normalize_path( WPMU_PLUGIN_DIR ) ) !== false ) {
767 - return true;
768 - }
769 -
770 - if( is_multisite() ) {
771 - /* Looks through network enabled plugins to see if our one is there. */
772 - foreach (wp_get_active_network_plugins() as $path) {
773 - // Trying again using readlink in case it's a symlink file.
774 - // boot_file is already solved.
775 - // wp_normalize_path is helpfull in windows.
776 - if (wp_normalize_path($this->boot_file) == wp_normalize_path($path) || (is_link($path) AND $this->boot_file == readlink($path))) {
777 - return true;
778 - }
779 - }
780 - }
781 - return false;
782 - }
783 -
784 - /**
785 - * Initialization.
786 - * Register scripts and styles
787 - */
788 - public function admin_init() {
789 - $this->show_notice_stateless_cache_busting();
790 - wp_register_style( 'wp-stateless', $this->path( 'static/styles/wp-stateless.css', 'url' ), array(), self::$version );
791 -
792 - /* Attachment or upload page */
793 - wp_register_script( 'wp-stateless-uploads-js', $this->path( 'static/scripts/wp-stateless-uploads.js', 'url' ), array( 'jquery' ), self::$version );
794 -
795 - /* Setup wizard styles and scripts. */
796 - wp_register_style( 'wp-stateless-bootstrap', $this->path( 'static/styles/bootstrap.min.css', 'url' ), array(), '3.3.7' );
797 - wp_register_style( 'bootstrap-grid-v4', $this->path( 'static/styles/bootstrap-grid.min.css', 'url' ), array(), '3.3.7' );
798 - wp_register_style( 'wp-stateless-setup-wizard', $this->path( 'static/styles/wp-stateless-setup-wizard.css', 'url' ), array(), self::$version );
799 -
800 - wp_register_script( 'async.min', ud_get_stateless_media()->path( 'static/scripts/async.js', 'url' ), array(), ud_get_stateless_media()->version );
801 - wp_register_script( 'jquery.history', ud_get_stateless_media()->path( 'static/scripts/jquery.history.js', 'url' ), array( 'jquery' ), ud_get_stateless_media()->version, true );
802 - wp_register_script( 'wp-stateless-validation', ud_get_stateless_media()->path( 'static/scripts/jquery.validation.js', 'url' ), array( 'jquery' ), ud_get_stateless_media()->version, true );
803 - wp_register_script( 'wp-stateless-loading', ud_get_stateless_media()->path( 'static/scripts/jquery.loading.js', 'url' ), array( 'jquery' ), ud_get_stateless_media()->version, true );
804 - wp_register_script( 'wp-stateless-comboBox', ud_get_stateless_media()->path( 'static/scripts/jquery.wp-stateless-combo-box.js', 'url' ), array( 'jquery' ), ud_get_stateless_media()->version, true );
805 - wp_register_script( 'wp-stateless-setup', ud_get_stateless_media()->path( 'static/scripts/wp-stateless-setup.js', 'url' ), array( 'jquery-ui-core', 'wp-api', 'jquery.history' ), ud_get_stateless_media()->version, true );
806 - wp_localize_script( 'wp-stateless-setup', 'stateless_l10n', $this->get_l10n_data() );
807 -
808 - wp_register_script( 'wp-stateless-setup-wizard-js', ud_get_stateless_media()->path( 'static/scripts/wp-stateless-setup-wizard.js', 'url' ), array( 'jquery', 'wp-api', 'async.min', 'wp-stateless-setup', 'wp-stateless-comboBox', 'wp-stateless-validation', 'wp-stateless-loading' ), ud_get_stateless_media()->version, true );
809 -
810 -
811 - /* Stateless settings page */
812 - wp_register_script( 'wp-stateless-settings', ud_get_stateless_media()->path( 'static/scripts/wp-stateless-settings.js', 'url' ), array(), ud_get_stateless_media()->version );
813 - wp_localize_script( 'wp-stateless-settings', 'stateless_l10n', $this->get_l10n_data() );
814 -
815 - wp_register_style( 'wp-stateless-settings', $this->path( 'static/styles/wp-stateless-settings.css', 'url' ), array(), self::$version );
816 -
817 - // Sync tab
818 - if ( wp_script_is( 'jquery-ui-widget', 'registered' ) ){
819 - wp_register_script( 'jquery-ui-progressbar', ud_get_stateless_media()->path('static/scripts/jquery-ui/jquery.ui.progressbar.min.js', 'url'), array( 'jquery-ui-core', 'jquery-ui-widget' ), '1.8.6' );
820 - }
821 - else{
822 - wp_register_script( 'jquery-ui-progressbar', ud_get_stateless_media()->path( 'static/scripts/jquery-ui/jquery.ui.progressbar.min.1.7.2.js', 'url' ), array( 'jquery-ui-core' ), '1.7.2' );
823 - }
824 - wp_register_script( 'wp-stateless-angular', ud_get_stateless_media()->path( 'static/scripts/angular.min.js', 'url' ), array(), '1.5.0', true );
825 - wp_register_script( 'wp-stateless', ud_get_stateless_media()->path( 'static/scripts/wp-stateless.js', 'url' ), array( 'jquery-ui-core', 'wp-stateless-settings' ), ud_get_stateless_media()->version, true );
826 -
827 - wp_localize_script( 'wp-stateless', 'stateless_l10n', $this->get_l10n_data() );
828 - wp_localize_script('wp-stateless', 'wp_stateless_configs', array(
829 - 'WP_DEBUG' => defined('WP_DEBUG') ? WP_DEBUG : false,
830 - ));
831 -
832 - $settings = ud_get_stateless_media()->get('sm');
833 - if(defined('WP_STATELESS_MEDIA_JSON_KEY') && WP_STATELESS_MEDIA_JSON_KEY){
834 - $settings['key_json'] = "Currently configured via a constant.";
835 - }
836 - wp_localize_script('wp-stateless', 'wp_stateless_settings', $settings);
837 - wp_localize_script('wp-stateless', 'wp_stateless_compatibility', Module::get_modules());
838 - wp_register_style( 'jquery-ui-regenthumbs', ud_get_stateless_media()->path( 'static/scripts/jquery-ui/redmond/jquery-ui-1.7.2.custom.css', 'url' ), array(), '1.7.2' );
839 -
840 - }
841 -
842 - public function get_l10n_data($value=''){
843 - include ud_get_stateless_media()->path( 'l10n.php', 'dir');
844 - return $l10n;
845 - }
846 -
847 - /**
848 - *
849 - * @todo: it should not be loaded everywhere. peshkov@UD
850 - * @param $hook
851 - */
852 - public function admin_enqueue_scripts( $hook ) {
853 -
854 -
855 - switch( $hook ) {
856 -
857 - case 'options-media.php':
858 - //wp_enqueue_script( 'wp-api' );
859 -
860 - wp_enqueue_style( 'wp-stateless');
861 - wp_enqueue_script( 'wp-stateless-setup' );
862 - break;
863 -
864 - case 'upload.php':
865 -
866 - wp_enqueue_style( 'wp-stateless');
867 - wp_enqueue_script( 'wp-stateless-uploads-js' );
868 -
869 - break;
870 -
871 - case 'post.php':
872 -
873 - global $post;
874 -
875 - if ( $post->post_type == 'attachment' ) {
876 - wp_enqueue_style( 'wp-stateless');
877 - wp_enqueue_script( 'wp-stateless-uploads-js' );
878 - }
879 -
880 - break;
881 -
882 - case 'media_page_stateless-setup':
883 - wp_enqueue_style( 'wp-stateless');
884 - wp_enqueue_style( 'wp-stateless-bootstrap' );
885 - wp_enqueue_style( 'wp-stateless-setup-wizard' );
886 -
887 - wp_enqueue_script( 'async.min' );
888 - wp_enqueue_script( 'jquery.history' );
889 - wp_enqueue_script( 'wp-stateless-validation' );
890 - wp_enqueue_script( 'wp-stateless-loading' );
891 - wp_enqueue_script( 'wp-stateless-comboBox' );
892 - wp_enqueue_script( 'wp-stateless-setup' );
893 - wp_enqueue_script( 'wp-stateless-setup-wizard-js' );
894 - break;
895 - case 'media_page_stateless-settings':
896 - wp_enqueue_style( 'wp-stateless');
897 - wp_enqueue_script( 'wp-stateless-settings' );
898 - wp_enqueue_style( 'bootstrap-grid-v4' );
899 - wp_enqueue_style( 'wp-stateless-settings' );
900 -
901 - // Sync tab
902 - wp_enqueue_script( 'jquery-ui-progressbar' );
903 - wp_enqueue_script( 'wp-stateless-angular' );
904 - wp_enqueue_script( 'wp-stateless' );
905 - wp_enqueue_style( 'jquery-ui-regenthumbs' );
906 -
907 - $data = array(
908 - 'key' => 'stateless-cache-busting',
909 - 'class' => 'notice',
910 - 'title' => sprintf( __( "Stateless mode enables and requires the Cache-Busting option.", ud_get_stateless_media()->domain ) ),
911 - 'message' => sprintf( __("WordPress looks at local files to prevent files with the same filenames.
912 - Since Stateless mode bypasses this check, there is a potential for files to be stored with the same file name. We enforce the Cache-Busting option to prevent this.
913 - Override with the <a href='%s' target='_blank'>%s</a> constant.", ud_get_stateless_media()->domain),"https://github.com/wpCloud/wp-stateless/wiki/Constants#wp_stateless_media_cache_busting", "WP_STATELESS_MEDIA_CACHE_BUSTING" ),
914 - );
915 - echo "<script id='template-stateless-cache-busting' type='text/html'>";
916 - include ud_get_stateless_media()->path( '/static/views/error-notice.php', 'dir' );
917 - echo "</script>";
918 - default: break;
919 - }
920 -
921 - }
922 -
923 - /**
924 - * Add Attributes to media HTML
925 - *
926 - * @author potanin@UD
927 - * @param $attr
928 - * @param $attachment
929 - * @param $size
930 - * @return mixed
931 - */
932 - public function wp_get_attachment_image_attributes( $attr, $attachment, $size = null ) {
933 -
934 - $sm_cloud = get_post_meta( $attachment->ID, 'sm_cloud', true );
935 - if( is_array( $sm_cloud ) && !empty( $sm_cloud[ 'name' ] ) ) {
936 - $attr[ 'class' ] = $attr[ 'class' ] . ' wp-stateless-item';
937 - $attr[ 'data-image-size' ] = is_array( $size ) ? implode( 'x', $size ) : $size;
938 - $attr[ 'data-stateless-media-bucket' ] = isset( $sm_cloud[ 'bucket' ] ) ? $sm_cloud[ 'bucket' ] : false;
939 - $attr[ 'data-stateless-media-name' ] = $sm_cloud[ 'name' ];
940 - }
941 -
942 - return $attr;
943 -
944 - }
945 -
946 - /**
947 - * Adds filter link to Media Library table.
948 - *
949 - * @param $views
950 - * @return mixed
951 - */
952 - public function views_upload( $views ) {
953 - $views['stateless'] = '<a href="#">' . __( 'Stateless Media' ) . '</a>';
954 - return $views;
955 - }
956 -
957 - /**
958 - * Replace media URL
959 - *
960 - * @param bool $false
961 - * @param integer $id
962 - * @param string $size
963 - * @return mixed $false
964 - */
965 - public function image_downsize( $false = false, $id, $size ) {
966 -
967 - if ( !isset( $this->client ) || !$this->client || is_wp_error( $this->client ) ) {
968 - return $false;
969 - }
970 -
971 - /**
972 - * Check if enabled
973 - */
974 - if ( $this->get( 'sm.mode' ) !== 'cdn' && $this->get( 'sm.mode' ) !== 'stateless' ) {
975 - return $false;
976 - }
977 -
978 - /** Start determine remote file */
979 - $img_url = wp_get_attachment_url($id);
980 - $meta = wp_get_attachment_metadata($id);
981 - $width = $height = 0;
982 - $is_intermediate = false;
983 -
984 - //** try for a new style intermediate size */
985 - if ( $intermediate = image_get_intermediate_size( $id, $size ) ) {
986 - if(!empty( $intermediate['gs_link'] )){
987 - $img_url = $intermediate['gs_link'];
988 - }
989 - else if(!empty( $intermediate['url'] )){
990 - $img_url = $intermediate['url'];
991 - }
992 - else{
993 - $img_url = dirname($img_url) . $intermediate['file'];
994 - }
995 -
996 - $width = $intermediate['width'];
997 - $height = $intermediate['height'];
998 - $is_intermediate = true;
999 - }
1000 - //die( '<pre>' . print_r( $intermediate, true ) . '</pre>' );
1001 -
1002 - /**
1003 - * maybe try to get images info from sm_cloud
1004 - * this case may happen when no local files
1005 - * @author korotkov@ud
1006 - */
1007 - if ( !$width && !$height ) {
1008 - $sm_cloud = get_post_meta( $id, 'sm_cloud', true );
1009 - if ( is_string($size) && !empty( $sm_cloud['sizes'] ) && !empty( $sm_cloud['sizes'][$size] ) ) {
1010 - global $_wp_additional_image_sizes;
1011 -
1012 - $img_url = !empty( $sm_cloud['sizes'][$size]['fileLink'] ) ? $sm_cloud['sizes'][$size]['fileLink'] : $img_url;
1013 -
1014 - if ( !empty( $_wp_additional_image_sizes[ $size ] ) ) {
1015 - $width = !empty( $_wp_additional_image_sizes[ $size ]['width'] ) ? $_wp_additional_image_sizes[ $size ]['width'] : $width;
1016 - $height = !empty( $_wp_additional_image_sizes[ $size ]['height'] ) ? $_wp_additional_image_sizes[ $size ]['height'] : $height;
1017 - }
1018 -
1019 - $is_intermediate = true;
1020 - }
1021 - }
1022 -
1023 - if ( !$width && !$height && isset( $meta['width'], $meta['height'] ) ) {
1024 -
1025 - //** any other type: use the real image */
1026 - $width = $meta['width'];
1027 - $height = $meta['height'];
1028 - }
1029 -
1030 -
1031 - if ( $img_url) {
1032 -
1033 - //** we have the actual image size, but might need to further constrain it if content_width is narrower */
1034 - list( $width, $height ) = image_constrain_size_for_editor( $width, $height, $size );
1035 - $img_url = apply_filters('wp_stateless_bucket_link', $img_url);
1036 - return array( $img_url, $width, $height, $is_intermediate );
1037 - }
1038 -
1039 -
1040 - /**
1041 - * All other cases work as usually
1042 - */
1043 - return $false;
1044 -
1045 - }
1046 -
1047 - /**
1048 - * Extends metadata by adding GS information.
1049 - * Note: must not be called directly. It's used only on hook
1050 - *
1051 - * @action wp_get_attachment_metadata
1052 - * @param $metadata
1053 - * @param $attachment_id
1054 - * @return $metadata
1055 - */
1056 - public function wp_get_attachment_metadata( $metadata, $attachment_id ) {
1057 - /* Determine if the media file has GS data at all. */
1058 - $sm_cloud = get_post_meta( $attachment_id, 'sm_cloud', true );
1059 - // If metadata not passed the get metadata from post meta.
1060 - if(empty($metadata)){
1061 - $metadata = get_post_meta( $attachment_id, '_wp_attachment_metadata', true );
1062 - }
1063 -
1064 - if( is_array( $metadata ) && is_array( $sm_cloud ) && !empty( $sm_cloud[ 'fileLink' ] ) ) {
1065 - $metadata[ 'gs_link' ] = apply_filters('wp_stateless_bucket_link', $sm_cloud[ 'fileLink' ]);
1066 - $metadata[ 'gs_name' ] = isset( $sm_cloud[ 'name' ] ) ? $sm_cloud[ 'name' ] : false;
1067 - $metadata[ 'gs_bucket' ] = isset( $sm_cloud[ 'bucket' ] ) ? $sm_cloud[ 'bucket' ] : false;
1068 - if( !empty( $metadata[ 'sizes' ] ) && is_array( $metadata[ 'sizes' ] ) ) {
1069 - foreach( $metadata[ 'sizes' ] as $k => $v ) {
1070 - if( !empty( $sm_cloud[ 'sizes' ][ $k ][ 'name' ] ) ) {
1071 - $metadata['sizes'][$k]['gs_name'] = $sm_cloud[ 'sizes' ][ $k ][ 'name' ];
1072 - $metadata['sizes'][$k]['gs_link'] = apply_filters('wp_stateless_bucket_link', $sm_cloud[ 'sizes' ][ $k ][ 'fileLink' ]);
1073 - }
1074 - }
1075 - }
1076 - }
1077 -
1078 - return $metadata;
1079 - }
1080 -
1081 - /**
1082 - * Returns client object
1083 - * or WP_Error on failure.
1084 - *
1085 - * @author peshkov@UD
1086 - * @return object $this->client. \wpCloud\StatelessMedia\GS_Client or \WP_Error
1087 - */
1088 - public function get_client() {
1089 -
1090 - if( null === $this->client ) {
1091 -
1092 - $key_json = get_site_option( 'sm_key_json' );
1093 - if ( empty($key_json) ) {
1094 - $key_json = $this->get( 'sm.key_json' );
1095 - }
1096 -
1097 - /* Try to initialize GS Client */
1098 - $this->client = GS_Client::get_instance( array(
1099 - 'bucket' => $this->get( 'sm.bucket' ),
1100 - 'key_json' => $key_json
1101 - ) );
1102 - }
1103 -
1104 - return $this->client;
1105 -
1106 - }
1107 -
1108 - /**
1109 - * Determines if we can connect to Google Storage Bucket.
1110 - *
1111 - * @author peshkov@UD
1112 - */
1113 - public function is_connected_to_gs() {
1114 -
1115 - $trnst = get_transient( 'sm::is_connected_to_gs' );
1116 -
1117 - if ( empty($trnst) || false === $trnst || !isset( $trnst[ 'hash' ] ) || $trnst[ 'hash' ] != md5( serialize( $this->get( 'sm' ) ) ) ) {
1118 - $trnst = array(
1119 - 'success' => 'true',
1120 - 'error' => '',
1121 - 'hash' => md5( serialize( $this->get( 'sm' ) ) ),
1122 - );
1123 - $client = $this->get_client();
1124 -
1125 - if ( is_wp_error( $client ) ) {
1126 - $trnst[ 'success' ] = 'false';
1127 - $trnst[ 'error' ] = $client->get_error_message();
1128 - } else {
1129 - $connected = $client->is_connected();
1130 - if( $connected !== true ) {
1131 - $trnst[ 'success' ] = 'false';
1132 - $trnst[ 'error' ] = sprintf( __( 'Could not connect to Google Storage bucket. Please, be sure that bucket with name <b>%s</b> exists.', $this->domain ), $this->get( 'sm.bucket' ) );
1133 -
1134 - if( is_callable(array($connected, 'getHandlerContext')) && $handlerContext = $connected->getHandlerContext() ){
1135 - if(!empty($handlerContext['error'])){
1136 - $handlerContext['error'];
1137 - $trnst[ 'error' ] = "Could not connect to Google Storage bucket. " . make_clickable($handlerContext['error']);
1138 - }
1139 - }
1140 -
1141 - if( is_callable(array($connected, 'getErrors')) && $error = $connected->getErrors() ){
1142 - $error = reset($error);
1143 - if($error['reason'] == 'accessNotConfigured')
1144 - $trnst[ 'error' ] = "Could not connect to Google Storage bucket. " . make_clickable($error['message']);
1145 - }
1146 - }
1147 - }
1148 - set_transient( 'sm::is_connected_to_gs', $trnst, 4 * HOUR_IN_SECONDS );
1149 - }
1150 -
1151 - if( isset( $trnst[ 'success' ] ) && $trnst[ 'success' ] == 'false' ) {
1152 - return new \WP_Error( 'error', ( !empty( $trnst[ 'error' ] ) ? $trnst[ 'error' ] : __( 'There is an Error on connection to Google Storage.', $this->domain ) ) );
1153 - }
1154 -
1155 - return true;
1156 - }
1157 -
1158 - /**
1159 - * Flush all plugin transients
1160 - *
1161 - */
1162 - public function flush_transients() {
1163 - delete_transient( 'sm::is_connected_to_gs' );
1164 - }
1165 -
1166 - /**
1167 - * Plugin Activation
1168 - *
1169 - */
1170 - public function activate() {
1171 - add_action( 'activated_plugin', array($this, 'redirect_to_splash'), 99 );
1172 - $this->run_upgrade_process();
1173 - }
1174 -
1175 - /**
1176 - * Run Install Process.
1177 - * Triggered on plugins_loaded instead of register_activation_hook action.
1178 - * Works on even manual plugin update.
1179 - *
1180 - * @param string $old_version Old version.
1181 - * @author alim@UD
1182 - */
1183 - public function run_install_process()
1184 - {
1185 - // calling the upgrade function because it's same as this point for fresh install or updates.
1186 - $this->run_upgrade_process();
1187 - }
1188 -
1189 - /**
1190 - * Run Upgrade Process:
1191 - * Triggered on plugins_loaded instead of register_activation_hook action.
1192 - * Works on even manual plugin update.
1193 - *
1194 - * @author alim@UD
1195 - */
1196 - public function run_upgrade_process()
1197 - {
1198 - // Creating database on new installation.
1199 - $this->create_db();
1200 - /**
1201 - * Maybe Upgrade current Version
1202 - */
1203 - Upgrader::call( $this->args[ 'version' ] );
1204 - }
1205 -
1206 - /**
1207 - * Create database on plugin activation.
1208 - * @param $force whether to create db even if option exists. For debug purpose only.
1209 - */
1210 - public function create_db($force = false) {
1211 - global $wpdb;
1212 - $sm_sync_db_version = get_option( 'sm_sync_db_version' );
1213 -
1214 - if( $sm_sync_db_version && $force == false ) {
1215 - return;
1216 - }
1217 -
1218 - $table_name = $wpdb->prefix . 'sm_sync';
1219 - $charset_collate = $wpdb->get_charset_collate();
1220 -
1221 - // `expire` timestamp NULL DEFAULT NULL,
1222 - $sql = "CREATE TABLE $table_name (
1223 - `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT ,
1224 - `file` varchar(255) NOT NULL ,
1225 - `status` varchar(10) NOT NULL ,
1226 - PRIMARY KEY (`id`)
1227 - ) $charset_collate;";
1228 -
1229 - require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
1230 - dbDelta( $sql );
1231 -
1232 - add_option( 'sm_sync_db_version', $this->args[ 'version' ] );
1233 - }
1234 -
1235 - public function redirect_to_splash($plugin =''){
1236 - $this->settings = new Settings();
1237 -
1238 - if(defined( 'WP_CLI' ) || $this->settings->get('sm.key_json') || isset($_POST['checked']) && count($_POST['checked']) > 1){
1239 - return;
1240 - }
1241 -
1242 - if(
1243 - !$this->settings->get('sm.key_json') &&
1244 - defined('WP_STATELESS_MEDIA_HIDE_SETUP_ASSISTANT') && WP_STATELESS_MEDIA_HIDE_SETUP_ASSISTANT == true &&
1245 - defined('WP_STATELESS_MEDIA_HIDE_SETTINGS_PANEL') && WP_STATELESS_MEDIA_HIDE_SETTINGS_PANEL == true
1246 - ) {
1247 - return;
1248 - }
1249 -
1250 - if( !$this->settings->get('sm.key_json') && defined('WP_STATELESS_MEDIA_HIDE_SETUP_ASSISTANT') && WP_STATELESS_MEDIA_HIDE_SETUP_ASSISTANT == true ) {
1251 - $url = $this->get_settings_page_url('?page=stateless-settings');
1252 - exit( wp_redirect($url));
1253 - }
1254 -
1255 - if( $plugin == plugin_basename( $this->boot_file ) ) {
1256 - $url = $this->get_settings_page_url('?page=stateless-setup&step=splash-screen');
1257 - if(json_decode($this->settings->get('sm.key_json'))){
1258 - $url = $this->get_settings_page_url('?page=stateless-settings');
1259 - }
1260 - exit( wp_redirect($url));
1261 - }
1262 - }
1263 -
1264 - /**
1265 - * Plugin Deactivation
1266 - *
1267 - */
1268 - public function deactivate() {}
1269 -
1270 - public function show_notice_stateless_cache_busting(){
1271 - $this->errors->add( array(
1272 - 'key' => 'stateless_cache_busting',
1273 - 'button' => 'View Settings',
1274 - 'button_link' => admin_url('upload.php?page=stateless-settings'),
1275 - 'title' => sprintf( __( "Stateless mode now requires the Cache-Busting option.", ud_get_stateless_media()->domain ) ),
1276 - 'message' => sprintf( __("WordPress looks at local files to prevent files with the same filenames.
1277 - Since Stateless mode bypasses this check, there is a potential for files to be stored with the same file name. We enforce the Cache-Busting option to prevent this.
1278 - Override with the <a href='%s' target='_blank'>%s</a> constant.", ud_get_stateless_media()->domain),"https://github.com/wpCloud/wp-stateless/wiki/Constants#wp_stateless_media_cache_busting", "WP_STATELESS_MEDIA_CACHE_BUSTING" ),
1279 - ), 'notice' );
1280 - }
1281 -
1282 - /**
1283 - * Filter for wp_get_attachment_url();
1284 - *
1285 - * @param string $url
1286 - * @param string $post_id
1287 - * @return mixed|null|string
1288 - */
1289 - public function wp_get_attachment_url( $url = '', $post_id = '' ) {
1290 - $sm_cloud = get_post_meta( $post_id, 'sm_cloud', 1 );
1291 - if( is_array( $sm_cloud ) && !empty( $sm_cloud[ 'fileLink' ] ) ) {
1292 - $_url = parse_url($sm_cloud[ 'fileLink' ]);
1293 - $url = !isset($_url['scheme']) ? ( 'https:' . $sm_cloud[ 'fileLink' ] ) : $sm_cloud[ 'fileLink' ];
1294 - return apply_filters('wp_stateless_bucket_link', $url);
1295 - }
1296 - return $url;
1297 - }
1298 -
1299 - /**
1300 - * Filter for attachment_url_to_postid()
1301 - *
1302 - * @param int|false $post_id originally found post ID (or false if not found)
1303 - * @param string $url the URL to find the post ID for
1304 - * @return int|false found post ID from cloud storage URL
1305 - */
1306 - public function attachment_url_to_postid( $post_id, $url ) {
1307 - global $wpdb;
1308 -
1309 - if ( ! $post_id ) {
1310 - $post_id = get_transient("stateless_url_to_postid_" . md5($url));
1311 -
1312 - if(defined('WP_STATELESS_LEGACY_URL_TO_POSTID')){
1313 - // User can use this constant if they change the Bucket Folder (root_dir) after uploading image.
1314 - // This can be little slow at first run.
1315 - if(empty($post_id)){
1316 - $query = "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = 'sm_cloud' AND meta_value LIKE '%s'";
1317 - $post_id = $wpdb->get_var( $wpdb->prepare( $query, '%' . $url . '%' ) );
1318 -
1319 - if($post_id){
1320 - set_transient("stateless_url_to_postid_" . md5($url), $post_id);
1321 - }
1322 - }
1323 - return $post_id;
1324 - }
1325 -
1326 - if(empty($post_id)){
1327 - $gs_base_url = $this->get_gs_host();
1328 - $gs_url = $this->get_gs_host() . '/' . $this->get( 'sm.root_dir' );
1329 - $site_url = parse_url($gs_url);
1330 - $image_path = parse_url( $url );
1331 -
1332 - //force the protocols to match if needed
1333 - if( isset( $image_path['scheme'] ) && ( $image_path['scheme'] !== $site_url['scheme'] ) ) {
1334 - $url = str_replace( $image_path['scheme'], $site_url['scheme'], $url );
1335 - }
1336 -
1337 - if( 0 === strpos( $url, $gs_url . '/' ) ) {
1338 - $url = substr( $url, strlen( $gs_url . '/' ) );
1339 - }
1340 - else if( 0 === strpos( $url, $gs_base_url . '/' ) ) {
1341 - // In case user added Bucket Folder (root_dir) after uploading image.
1342 - $url = substr( $url, strlen( $gs_base_url . '/' ) );
1343 - }
1344 -
1345 - $sql = $wpdb->prepare(
1346 - "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_wp_attached_file' AND meta_value = %s",
1347 - $url
1348 - );
1349 - $post_id = $wpdb->get_var( $sql );
1350 -
1351 - if($post_id){
1352 - set_transient("stateless_url_to_postid_" . md5($url), $post_id);
1353 - }
1354 - }
1355 - }
1356 -
1357 - return $post_id;
1358 - }
1359 -
1360 - /**
1361 - * Upload the full size image first.
1362 - *
1363 - */
1364 - public function before_intermediate_image_sizes($sizes, $metadata = array()){
1365 - if(empty($metadata)){
1366 - return $sizes;
1367 - }
1368 -
1369 - try{
1370 - $attachment_id = attachment_url_to_postid($metadata['file']);
1371 - $this->add_media(null, $attachment_id, false, array('no_thumb' => true));
1372 - }
1373 - catch(Exception $e){
1374 -
1375 - }
1376 -
1377 - return $sizes;
1378 - }
1379 -
1380 - /**
1381 - * Change Upload BaseURL when CDN Used.
1382 - *
1383 - * @param $data
1384 - * @return mixed
1385 - */
1386 - public function upload_dir( $data ) {
1387 - $data[ 'basedir' ] = $this->get_gs_host();
1388 - $data[ 'baseurl' ] = $this->get_gs_host();
1389 - $data[ 'url' ] = $data[ 'baseurl' ] . $data[ 'subdir' ];
1390 -
1391 - return $data;
1392 -
1393 - }
1394 -
1395 - /**
1396 - * Set Feature Flag constants by parsing composer.json
1397 - *
1398 - * @todo Make sure settings from DB can override these.
1399 - *
1400 - * @author potanin@UD
1401 - * @return array|mixed|null|object
1402 - */
1403 - public function parse_feature_flags( ) {
1404 -
1405 - try {
1406 -
1407 - $_raw = file_get_contents( Utility::normalize_path( $this->root_path ) . 'composer.json' );
1408 -
1409 - $_parsed = json_decode( $_raw );
1410 -
1411 - // @todo Catch poorly formatted JSON.
1412 - if( !is_object( $_parsed ) ) {
1413 - // throw new Error( "unable to parse." );
1414 - }
1415 -
1416 - foreach( (array) $_parsed->extra->featureFlags as $_feature ) {
1417 -
1418 - if( !defined( $_feature->constant ) ) {
1419 - define( $_feature->constant, $_feature->enabled );
1420 -
1421 - if( $_feature->enabled ) {
1422 - Utility::log( 'Feature flag ' . $_feature->name . ', [' . $_feature->constant . '] enabled.' );
1423 - } else {
1424 - Utility::log( 'Feature flag ' . $_feature->name . ', [' . $_feature->constant . '] disabled.' );
1425 - }
1426 - }
1427 -
1428 - }
1429 -
1430 - } catch ( Exception $e ) {
1431 - Utility::log( 'Unable to parse [composer.json] feature flags. Error: [' . $e->getMessage() . ']' );
1432 - // echo 'Caught exception: ', $e->getMessage(), "\n";
1433 - }
1434 -
1435 -
1436 - return isset( $_parsed ) ? $_parsed : null;
1437 -
1438 -
1439 - }
1440 -
1441 - /**
1442 - * Determine if Utility class contains missed function
1443 - * in other case, just return NULL to prevent ERRORS
1444 - *
1445 - * @author peshkov@UD
1446 - * @param $name
1447 - * @param $arguments
1448 - * @return mixed|null
1449 - */
1450 - public function __call( $name, $arguments ) {
1451 - if( is_callable( array( "wpCloud\\StatelessMedia\\Utility", $name ) ) ) {
1452 - return call_user_func_array( array( "wpCloud\\StatelessMedia\\Utility", $name ), $arguments );
1453 - } else {
1454 - return NULL;
1455 - }
1456 - }
1457 -
1458 - }
1459 -
1460 - }
1461 -
1462 -}
1 +<?php
2 +
3 +/**
4 + * Bootstrap
5 + *
6 + * @since 0.2.0
7 + */
8 +
9 +namespace wpCloud\StatelessMedia {
10 +
11 + use Google\Cloud\Storage\StorageClient;
12 + use Google\Auth\HttpHandler\HttpHandlerFactory;
13 + use wpCloud\StatelessMedia\Sync\FileSync;
14 + use wpCloud\StatelessMedia\Sync\ImageSync;
15 + use wpCloud\StatelessMedia\Sync\NonLibrarySync;
16 + use wpCloud\StatelessMedia\Batch\BatchTaskManager;
17 +
18 + if (!class_exists('wpCloud\StatelessMedia\Bootstrap')) {
19 +
20 + final class Bootstrap extends \UsabilityDynamics\WP\Bootstrap_Plugin {
21 +
22 + const REQUIRED_PHP_VERSION = '5.5';
23 +
24 + /**
25 + * Google Storage Client
26 + * Use $this->get_client()
27 + *
28 + * @var \wpCloud\StatelessMedia\GS_CLient
29 + */
30 + private $client;
31 +
32 + /**
33 + * Plugin core version.
34 + *
35 + * @static
36 + * @property $version
37 + * @type {Object}
38 + */
39 + public static $version = '3.0';
40 +
41 + /**
42 + * Singleton Instance Reference.
43 + *
44 + * @protected
45 + * @static
46 + * @property $instance
47 + * @type \wpCloud\StatelessMedia\Bootstrap object
48 + */
49 + protected static $instance = null;
50 +
51 + /**
52 + * Constructor
53 + * Attention: MUST NOT BE CALLED DIRECTLY! USE get_instance() INSTEAD!
54 + * @param $args
55 + * @author peshkov@UD
56 + */
57 + protected function __construct($args) {
58 + /**
59 + * Need to be loaded before plugin initialization.
60 + */
61 + if ( !(defined('WP_STATELESS_COMPATIBILITY_GAE') && !WP_STATELESS_COMPATIBILITY_GAE) ) {
62 + AppEngine::instance();
63 + }
64 +
65 + parent::__construct($args);
66 +
67 + self::$version = $args['version'] ?? self::$version;
68 +
69 + /**
70 + * Add custom args to api ping request
71 + */
72 + add_filter('ud-api-client-ping-args', function ($args, $_, $__) {
73 + $args['multisite'] = is_multisite();
74 + $args['stateless_media'] = Utility::get_stateless_media_data_count();
75 + return $args;
76 + }, 10, 3);
77 +
78 + //** Define our Admin Notices handler object */
79 + $this->errors = new Errors(array_merge($args, array(
80 + 'type' => $this->type
81 + )));
82 +
83 + // Initialize compatibility modules.
84 + add_action('plugins_loaded', function () {
85 + Addons::instance();
86 + new Module();
87 + DynamicImageSupport::instance();
88 + }, 20);
89 +
90 + /**
91 + * Define settings and UI.
92 + *
93 + * Example:
94 + *
95 + * Get option
96 + * $this->get( 'sm.client_id' )
97 + *
98 + * Manually Update/Add option
99 + * $this->set( 'sm.client_id', 'zxcvv12adffse' );
100 + */
101 + $this->settings = new Settings($this);
102 + Status::instance();
103 + }
104 +
105 + /**
106 + * Prevent loading of textdomain
107 + */
108 + public function load_textdomain() {
109 + }
110 +
111 + /**
112 + * Instantiate class.
113 + */
114 + public function init() {
115 + // Parse feature flags, set constants.
116 + $this->parse_feature_flags();
117 + $sm_mode = $this->get('sm.mode');
118 +
119 + /**
120 + * Send admin email
121 + */
122 + add_action('wp_stateless_send_admin_email', array($this, 'send_admin_email'), 10, 3);
123 +
124 + // Should be created unconditionally and as early as possible to handle batch migration requests
125 + Migrator::instance();
126 + BatchTaskManager::instance();
127 +
128 + new SyncNonMedia();
129 +
130 + ImageSync::instance();
131 + FileSync::instance();
132 + NonLibrarySync::instance();
133 +
134 + // Invoke REST API
135 + add_action('rest_api_init', array($this, 'api_init'));
136 +
137 + // Register meta boxes and fields for media edit page
138 + add_filter('rwmb_meta_boxes', array($this, 'attachment_meta_box_callback'));
139 +
140 + // Register meta boxes and fields for media modal page
141 + add_filter('attachment_fields_to_edit', array($this, 'attachment_modal_meta_box_callback'), 11, 2);
142 +
143 + // Get the REST API root
144 + add_filter('wp_stateless_rest_api_root', array($this, 'get_rest_api_root'));
145 +
146 + /**
147 + * Init hook
148 + */
149 + add_action('admin_init', array($this, 'admin_init'));
150 +
151 + /**
152 + * Handle switch blog properly.
153 + */
154 + add_action('switch_blog', array($this, 'on_switch_blog'), 10, 2);
155 +
156 + /**
157 + * Filter for getting stateless settings
158 + */
159 + add_filter('stateless::get_settings', array($this, 'get_settings'), 10);
160 +
161 + /**
162 + * Init AJAX jobs
163 + */
164 + new Ajax();
165 +
166 + /**
167 + * Load WP-CLI Commands
168 + */
169 + if (defined('WP_CLI') && WP_CLI) {
170 + include_once($this->path('lib/cli/class-sm-cli-command.php', 'dir'));
171 + }
172 +
173 + /**
174 + * Add scripts
175 + */
176 + add_action('admin_enqueue_scripts', array($this, 'admin_enqueue_scripts'));
177 +
178 + /**
179 + * Delete table when blog is deleted.
180 + */
181 + add_action('wp_delete_site', array($this, 'wp_delete_site'));
182 +
183 + /**
184 + * To prevent fatal errors for users who use PHP 5.5 or less.
185 + */
186 + if (version_compare(PHP_VERSION, self::REQUIRED_PHP_VERSION, '<')) {
187 + $this->errors->add(sprintf(__('The plugin requires PHP %s or higher. You current PHP version %s is too old.', ud_get_stateless_media()->domain), '<b>' . self::REQUIRED_PHP_VERSION . '</b>', '<b>' . PHP_VERSION . '</b>'));
188 + }
189 +
190 + /**
191 + * Add the currently processing nag
192 + */
193 + foreach (Utility::get_available_sync_classes() as $process) {
194 + if ($process->is_running()) {
195 + $this->errors->add([
196 + 'title' => __('Media Library Synchronization Underway', ud_get_stateless_media()->domain),
197 + 'message' => __('WP-Stateless is synchronizing your media library in accordance with the Mode setting. You can view progress or stop the process via the WP-Stateless Sync settings area.', ud_get_stateless_media()->domain),
198 + 'button' => __('View Synchronization', ud_get_stateless_media()->domain),
199 + 'button_link' => admin_url('upload.php?page=stateless-settings&tab=stless_sync_tab'),
200 + 'key' => 'processing-in-progress'
201 + ], 'message');
202 + break;
203 + }
204 + }
205 +
206 + /* Initialize plugin only if Mode is not 'disabled'. */
207 + if (($sm_mode !== 'disabled' && $sm_mode !== 'stateless') || ($sm_mode === 'stateless' && (wp_doing_ajax() || wp_doing_cron()))) {
208 +
209 + /**
210 + * Determine if we have issues with connection to Google Storage Bucket
211 + * if SM is not disabled.
212 + */
213 + $is_connected = $this->is_connected_to_gs();
214 +
215 + if (is_wp_error($is_connected)) {
216 + $this->errors->add($is_connected->get_error_message(), 'warning', false);
217 + }
218 +
219 + if ($googleSDKVersionConflictError = get_transient("wp_stateless_google_sdk_conflict")) {
220 + $this->errors->add($googleSDKVersionConflictError, 'warning', false);
221 + }
222 +
223 + /**
224 + * Carry on only if we do not have errors.
225 + */
226 + if (!$this->has_errors()) {
227 +
228 + if (in_array($sm_mode, array('cdn', 'ephemeral', 'stateless'))) {
229 + /**
230 + * init main filters
231 + */
232 + $this->_init_filters('main');
233 + }
234 +
235 + if ($sm_mode === 'ephemeral' || $sm_mode === 'stateless') {
236 + // Store attachment id in a static variable on 'intermediate_image_sizes_advanced' filter.
237 + // Utility::store_can_delete_attachment();
238 + if (function_exists('is_wp_version_compatible') && is_wp_version_compatible('5.3-RC4-46673')) {
239 + add_filter('intermediate_image_sizes_advanced', array('wpCloud\StatelessMedia\Utility', 'store_can_delete_attachment'), 10, 3);
240 + }
241 + }
242 +
243 + if ($sm_mode === 'stateless') {
244 + /**
245 + * Replacing local path to gs:// for using it on StreamWrapper
246 + */
247 + add_filter('upload_dir', array($this, 'filter_upload_dir'), 99);
248 +
249 + /**
250 + * Stateless mode working only with GD library
251 + */
252 + add_filter('wp_image_editors', array($this, 'select_wp_image_editors'));
253 +
254 + /**
255 + * Init GS client
256 + */
257 + global $gs_client;
258 + if ($gs_client = $this->init_gs_client()) {
259 + StreamWrapper::register($gs_client);
260 + }
261 + }
262 +
263 + if ($this->get('sm.delete_remote') == 'true') {
264 + /**
265 + * On physical file deletion we remove any from GS
266 + * We need priority grater than default (10) for ShortPixel plugin to work properly.
267 + */
268 + add_filter('delete_attachment', array($this, 'remove_media'), 11);
269 + }
270 +
271 + /**
272 + * init client's filters
273 + */
274 + $this->_init_filters('client');
275 + }
276 + } elseif ($sm_mode == 'stateless') {
277 + /**
278 + * Determine if we have issues with connection to Google Storage Bucket
279 + * if SM is not disabled.
280 + */
281 + $is_connected = $this->is_connected_to_gs();
282 +
283 + if (is_wp_error($is_connected)) {
284 + $this->errors->add($is_connected->get_error_message(), 'warning', false);
285 + }
286 +
287 + if ($googleSDKVersionConflictError = get_transient("wp_stateless_google_sdk_conflict")) {
288 + $this->errors->add($googleSDKVersionConflictError, 'warning', false);
289 + }
290 +
291 + /**
292 + * Carry on only if we do not have errors.
293 + */
294 + if (!$this->has_errors()) {
295 + /**
296 + * Replacing local path to gs:// for using it on StreamWrapper
297 + */
298 + add_filter('upload_dir', array($this, 'filter_upload_dir'), 99);
299 +
300 + /**
301 + * Stateless mode working only with GD library
302 + */
303 + add_filter('wp_image_editors', array($this, 'select_wp_image_editors'));
304 +
305 + /**
306 + * Init GS client
307 + */
308 + global $gs_client;
309 + if ($gs_client = $this->init_gs_client()) {
310 + StreamWrapper::register($gs_client);
311 + }
312 +
313 + /**
314 + * init client's filters
315 + */
316 + $this->_init_filters('client');
317 +
318 + /**
319 + * init main filters
320 + */
321 + $this->_init_filters('main');
322 + }
323 + }
324 + }
325 +
326 + /**
327 + * Init additional filters which uses on all modes
328 + * @param string $type
329 + */
330 + private function _init_filters($type = '') {
331 + switch ($type) {
332 + case 'main':
333 + add_filter('wp_get_attachment_image_attributes', array($this, 'wp_get_attachment_image_attributes'), 20, 3);
334 + add_filter('wp_get_attachment_url', array($this, 'wp_get_attachment_url'), 20, 2);
335 + add_filter('get_attached_file', array($this, 'get_attached_file'), 9, 2);
336 + add_filter('attachment_url_to_postid', array($this, 'attachment_url_to_postid'), 20, 2);
337 +
338 + if ($this->get('sm.body_rewrite') == 'true' || $this->get('sm.body_rewrite') == 'enable_editor') {
339 + add_filter('the_content', array($this, 'the_content_filter'), 99);
340 + }
341 +
342 + if ($this->get('sm.body_rewrite') == 'true' || $this->get('sm.body_rewrite') == 'enable_meta') {
343 + add_filter('get_post_metadata', array($this, 'post_metadata_filter'), 2, 4);
344 + }
345 +
346 + add_filter('wp_stateless_bucket_link', array($this, 'wp_stateless_bucket_link'));
347 + break;
348 + case 'client':
349 + /**
350 + * Add custom actions to media rows
351 + */
352 + add_filter('media_row_actions', array($this, 'add_custom_row_actions'), 10, 3);
353 +
354 + /**
355 + * Hashify file name if option is enabled
356 + */
357 + if ($this->get('sm.hashify_file_name') == 'true') {
358 + add_filter('sanitize_file_name', array('wpCloud\StatelessMedia\Utility', 'randomize_filename'), 10);
359 + }
360 +
361 + /**
362 + * Override Cache Control if option is enabled
363 + */
364 + add_filter('sm:item:cacheControl', array($this->settings, 'override_cache_control'));
365 +
366 + add_filter('wp_stateless_file_name', array($this, 'handle_root_dir'), 10, 4);
367 +
368 + /**
369 + * Extends metadata by adding GS information.
370 + */
371 + add_filter('wp_get_attachment_metadata', array($this, 'wp_get_attachment_metadata'), 10, 2);
372 +
373 + /**
374 + * Add/Edit Media
375 + *
376 + * Once added or edited we can get into Attachment ID then get all image sizes and sync them with GS
377 + * We can't use this. That's prevent removing this filter.
378 + */
379 + add_filter('wp_update_attachment_metadata', array('wpCloud\StatelessMedia\Utility', 'add_media'), 999, 2);
380 +
381 + /**
382 + * Delete original in the end of generating metadata
383 + * it does not work with PDF
384 + */
385 + // add_filter('wp_generate_attachment_metadata', function ($metadata, $attachment_id, $state) {
386 + // if (!in_array(ud_get_stateless_media()->get('sm.mode'), array('ephemeral'))) return $metadata;
387 + // @unlink(get_attached_file($attachment_id, true));
388 + // return $metadata;
389 + // }, 99, 3);
390 +
391 + /**
392 + * Rewrite Image URLS
393 + */
394 + add_filter('image_downsize', array($this, 'image_downsize'), 99, 3);
395 + add_filter('wp_calculate_image_srcset', array($this, 'wp_calculate_image_srcset'), 10, 5);
396 +
397 + /**
398 + * Trigger module initialization and registration.
399 + */
400 + do_action('sm::module::init', $this->get('sm'));
401 +
402 + break;
403 + case 'default':
404 + break;
405 + }
406 + }
407 +
408 + /**
409 + * The default $editors value:
410 + *
411 + * array( 'WP_Image_Editor_Imagick', 'WP_Image_Editor_GD' )
412 + * @param $editors
413 + * @return array
414 + */
415 + public function select_wp_image_editors($editors) {
416 + return array('WP_Image_Editor_GD');
417 + }
418 +
419 + /**
420 + * @param callable|null $httpHandler
421 + * @return StorageClient
422 + * @throws \Exception
423 + */
424 + public function init_gs_client(?callable $httpHandler = null) {
425 + // May be Loading Google SDK....
426 + if (!class_exists('HttpHandlerFactory')) {
427 + include_once(ud_get_stateless_media()->path('lib/Google/vendor/autoload.php', 'dir'));
428 + }
429 +
430 + $httpHandler = $httpHandler ? $httpHandler : HttpHandlerFactory::build();
431 +
432 + $json_key = json_decode($this->settings->get('sm.key_json'), true);
433 +
434 + if (!empty($json_key)) {
435 + return new StorageClient(
436 + [
437 + 'keyFile' => $json_key,
438 + 'httpHandler' => function ($request, $options) use ($httpHandler) {
439 + $xGoogApiClientHeader = $request->getHeaderLine('x-goog-api-client');
440 + $request = $request->withHeader('x-goog-api-client', $xGoogApiClientHeader);
441 +
442 + return call_user_func_array($httpHandler, [$request, $options]);
443 + },
444 + 'authHttpHandler' => HttpHandlerFactory::build(),
445 + ]
446 + );
447 + }
448 + }
449 +
450 + /**
451 + * Replacing root dir with GCS path
452 + * @param $uploads
453 + * @return array
454 + */
455 + public function filter_upload_dir($uploads) {
456 + global $default_dir;
457 + if ($default_dir) return $uploads;
458 + //Bucket
459 + $bucket = $this->get('sm.bucket');
460 +
461 + //Bucket folder path
462 + $root_dir = $this->get('sm.root_dir');
463 + $root_dir = apply_filters("wp_stateless_handle_root_dir", $root_dir);
464 +
465 + /**
466 + * Subdir not uses on Stateless mode
467 + */
468 + $uploads['subdir'] = '';
469 +
470 + $basedir = rtrim(sprintf('gs://%s/%s', $bucket, $root_dir), '/');
471 + $baseurl = rtrim(sprintf('https://storage.googleapis.com/%s/%s', $bucket, $root_dir), '/');
472 +
473 + $uploads = array(
474 + 'url' => rtrim($baseurl . $uploads['subdir'], '/'),
475 + 'path' => $basedir . $uploads['subdir'],
476 + 'subdir' => $uploads['subdir'],
477 + 'basedir' => $basedir,
478 + 'baseurl' => $baseurl,
479 + 'error' => false,
480 + );
481 + return $uploads;
482 + }
483 +
484 + /**
485 + * Rebuild srcset from gs_link.
486 + * Using calculations returned from WordPress wp_calculate_image_srcset()
487 + *
488 + * @param $sources
489 + * @param $size_array
490 + * @param $image_src
491 + * @param $image_meta
492 + * @param $attachment_id
493 + * @return array
494 + */
495 + public function wp_calculate_image_srcset($sources, $size_array, $image_src, $image_meta, $attachment_id) {
496 + $sm_mode = $this->get('sm.mode');
497 +
498 + /**
499 + * In Backup mode using local URL
500 + */
501 + if ("backup" == $sm_mode) {
502 + return $sources;
503 + }
504 +
505 + if (empty($image_meta['gs_link'])) {
506 + $image_meta = wp_get_attachment_metadata($attachment_id);
507 + }
508 +
509 + if (is_array($sources) && !empty($image_meta['gs_link'])) {
510 + $gs_name = $image_meta['gs_name'];
511 + // getting position of root_dir in gs_name.
512 + $root_dir_pos = strpos($gs_name, $image_meta['file']);
513 + // removing rood_dir from gs_name so we can compare to replace url with gs_link.
514 + if ($root_dir_pos !== false) {
515 + $gs_name = substr($gs_name, $root_dir_pos);
516 + }
517 +
518 + if (!isset($gs_name) || empty($gs_name)) {
519 + return [];
520 + }
521 +
522 + foreach ($sources as $width => &$image) {
523 +
524 + // If srcset includes original image src, replace it
525 + if (substr_compare($image['url'], $gs_name, -strlen($gs_name)) === 0) {
526 + $image['url'] = $image_meta['gs_link'];
527 + // Replace all sizes
528 + } elseif (isset($image_meta['sizes']) && is_array($image_meta['sizes'])) {
529 + $found = false;
530 + foreach ($image_meta['sizes'] as $key => $meta) {
531 + if (!isset($meta['gs_name']) || empty($meta['gs_name'])) {
532 + continue;
533 + }
534 +
535 + $thumb_gs_name = $meta['gs_name'];
536 + // removing rood_dir from gs_name
537 + if ($root_dir_pos !== false) {
538 + $thumb_gs_name = substr($thumb_gs_name, $root_dir_pos);
539 + }
540 +
541 + if (substr_compare($image['url'], $thumb_gs_name, -strlen($thumb_gs_name)) === 0) {
542 + $image['url'] = $meta['gs_link'];
543 + $found = true;
544 + break;
545 + }
546 + }
547 +
548 + // if no size found and mode is ephemeral or stateless and nothing to show for srcset item - unset that item
549 + if (!$found && ($sm_mode === 'ephemeral' || $sm_mode === 'stateless')) {
550 + $image = null;
551 + }
552 + } else {
553 + // if mode is stateless and nothing to show for srcset item - unset that item
554 + if ($sm_mode === 'ephemeral' || $sm_mode === 'stateless') {
555 + $image = null;
556 + }
557 + }
558 + }
559 + } elseif (is_array($sources) && $sm_mode === 'stateless') {
560 + foreach ($sources as $width => &$image) {
561 + // Set default src
562 + $image['url'] = $image_src;
563 + }
564 + }
565 +
566 + return is_array($sources) ? array_filter($sources) : $sources;
567 + }
568 +
569 + /**
570 + * Return gs host.
571 + * If custom domain is set it's return bucket name as host,
572 + * else return storage.googleapis.com as host and append bucket name at the end.
573 + *
574 + * @param array $sm
575 + * @return mixed|void
576 + */
577 + public function get_gs_host($sm = array()) {
578 + $sm = $sm ? $sm : $this->get('sm');
579 + $image_host = 'https://storage.googleapis.com/';
580 + $image_host .= $sm['bucket'];
581 +
582 + $custom_domain = $sm['custom_domain'];
583 + $is_ssl = strpos($custom_domain, 'https://');
584 + $custom_domain = str_replace(array('http://', 'https://'), '', $custom_domain);
585 + $custom_domain = trim($custom_domain, '/');
586 +
587 + // checking whether the provided domain is valid.
588 + // if the custom domain is same as the bucket name
589 + // or the custom domain is using https.
590 + if (!empty($sm['bucket']) && !empty($custom_domain) && $custom_domain !== 'storage.googleapis.com' && ($is_ssl === 0 || $custom_domain == $sm['bucket'])) {
591 + $image_host = $is_ssl === 0 ? 'https://' : 'http://'; // bucketname will be host
592 + $image_host .= $custom_domain;
593 + }
594 +
595 + return apply_filters('get_gs_host', $image_host, $image_host, $sm['bucket'], $is_ssl, $sm);
596 + }
597 +
598 + /**
599 + * Return gs:// path.
600 + *
601 + * @param array $sm
602 + * @return mixed|void
603 + */
604 + public function get_gs_path() {
605 + $path = 'gs://' . $this->get('sm.bucket');
606 +
607 + return apply_filters('get_gs_path', $path);
608 + }
609 +
610 + /**
611 + * Filter for wp_stateless_bucket_link if custom domain is set.
612 + * It's get attachment url and remove "storage.googleapis.com" from url.
613 + * So that custom url can be used.
614 + *
615 + * @param $fileLink
616 + * @return mixed|string
617 + */
618 + public function wp_stateless_bucket_link($fileLink) {
619 + $bucketname = $this->get('sm.bucket');
620 + $custom_domain = $this->get('sm.custom_domain');
621 + $is_ssl = strpos($custom_domain, 'https://') === 0;
622 + $fileLink_is_ssl = strpos($fileLink, 'https://') === 0;
623 + $custom_domain = str_replace(array('http://', 'https://'), '', $custom_domain);
624 + $custom_domain = trim($custom_domain, '/');
625 +
626 + if (!empty($bucketname) && $custom_domain !== 'storage.googleapis.com' && $custom_domain == $bucketname && strpos($fileLink, $bucketname) > 8) {
627 + $fileLink = ($is_ssl ? 'https://' : 'http://') . substr($fileLink, strpos($fileLink, $bucketname));
628 + } elseif ($custom_domain !== 'storage.googleapis.com' && $custom_domain == $bucketname && $fileLink_is_ssl !== $is_ssl) {
629 + if ($is_ssl)
630 + $fileLink = str_replace(array('http://', 'https://'), 'https://', $fileLink);
631 + else
632 + $fileLink = str_replace(array('http://', 'https://'), 'http://', $fileLink);
633 + }
634 + return $fileLink;
635 + }
636 +
637 + /**
638 + * Return settings page url.
639 + *
640 + * @param string $path
641 + * @return string
642 + */
643 + public function get_settings_page_url($path = '') {
644 + $url = get_admin_url(get_current_blog_id(), (is_network_admin() ? 'network/settings.php' : 'upload.php'));
645 + return $url . $path;
646 + }
647 +
648 + /**
649 + * Return docs page url.
650 + *
651 + * @param string $path
652 + * @return string
653 + */
654 + public function get_docs_page_url($path = '') {
655 + $path = ltrim($path, '/');
656 +
657 + return 'https://stateless.udx.io/' . $path;
658 + }
659 +
660 + /**
661 + * Get new blog settings once switched blog.
662 + * @param $new_blog
663 + * @param $prev_blog_id
664 + */
665 + public function on_switch_blog($new_blog, $prev_blog_id) {
666 + $this->settings->refresh();
667 + }
668 +
669 + /**
670 + * Get settings handler.
671 + * Filling array if some settings missing.
672 + *
673 + * @param $settings
674 + * @return
675 + */
676 + public function get_settings($settings) {
677 +
678 + $settings_list = array(
679 + 'mode',
680 + 'body_rewrite',
681 + 'body_rewrite_types',
682 + 'bucket',
683 + 'root_dir',
684 + 'key_json',
685 + 'cache_control',
686 + 'delete_remote',
687 + 'custom_domain',
688 + 'organize_media',
689 + 'hashify_file_name'
690 + );
691 +
692 + foreach ($settings_list as $setting) {
693 +
694 + /** If setting is already exist, just skip it */
695 + if (isset($settings[$setting])) {
696 + continue;
697 + }
698 +
699 + $value = $this->get('sm.' . $setting);
700 +
701 + /** Decode json to array */
702 + if ($value && is_string($value) && $setting === 'key_json') {
703 + $value = json_decode($value, true);
704 + $setting = 'key';
705 + }
706 +
707 + $settings[$setting] = $value;
708 + }
709 +
710 + return $settings;
711 + }
712 +
713 + /**
714 + * Remove all settings.
715 + * @param bool $network
716 + */
717 + public function reset($network = false) {
718 + $this->settings->reset($network);
719 + }
720 +
721 + /**
722 + * @param $actions
723 + * @param $post
724 + * @param $detached
725 + * @return mixed
726 + */
727 + public function add_custom_row_actions($actions, $post, $detached) {
728 +
729 + if (!current_user_can('upload_files')) return $actions;
730 +
731 + $sm_cloud = apply_filters('wp_stateless_get_file', [], $post->ID);
732 +
733 +
734 + $sm_mode = $this->get('sm.mode');
735 + if (!empty($sm_cloud) && $sm_mode === 'stateless') return $actions;
736 +
737 + if ($post && 'attachment' == $post->post_type && 'image/' == substr($post->post_mime_type, 0, 6)) {
738 + $actions['sm_sync'] = '<a href="javascript:;" data-id="' . $post->ID . '" data-type="image" class="sm_inline_sync">' . __('Regenerate and Sync with GCS', ud_get_stateless_media()->domain) . '</a>';
739 + }
740 +
741 + if ($post && 'attachment' == $post->post_type && 'image/' != substr($post->post_mime_type, 0, 6)) {
742 + $actions['sm_sync'] = '<a href="javascript:;" data-id="' . $post->ID . '" data-type="other" class="sm_inline_sync">' . __('Sync with GCS', ud_get_stateless_media()->domain) . '</a>';
743 + }
744 +
745 + return $actions;
746 + }
747 +
748 + /**
749 + * Define REST API.
750 + *
751 + * @author korotkov@UD
752 + */
753 + public function api_init() {
754 +
755 + $route_namespace = 'wp-stateless/v1';
756 + $api_namespace = 'wpCloud\StatelessMedia\API';
757 +
758 + register_rest_route($route_namespace, '/status', array(
759 + 'methods' => \WP_REST_Server::READABLE,
760 + 'callback' => array($api_namespace, 'status'),
761 + 'permission_callback' => '__return_true'
762 + ));
763 +
764 + register_rest_route($route_namespace, '/getSettings', array(
765 + 'methods' => \WP_REST_Server::READABLE,
766 + 'callback' => array($api_namespace, 'getSettings'),
767 + 'permission_callback' => array($api_namespace, 'authCheck')
768 + ));
769 +
770 + register_rest_route($route_namespace, '/updateSettings', array(
771 + 'methods' => \WP_REST_Server::CREATABLE,
772 + 'callback' => array($api_namespace, 'updateSettings'),
773 + 'permission_callback' => array($api_namespace, 'authCheck')
774 + ));
775 +
776 + register_rest_route($route_namespace, '/sync/getProcesses', array(
777 + 'methods' => \WP_REST_Server::READABLE,
778 + 'callback' => array($api_namespace, 'syncGetProcesses'),
779 + 'permission_callback' => array($api_namespace, 'authCheck')
780 + ));
781 +
782 + register_rest_route($route_namespace, '/sync/getProcess/(?P<id>\S+)', array(
783 + 'methods' => \WP_REST_Server::READABLE,
784 + 'callback' => array($api_namespace, 'syncGetProcess'),
785 + 'permission_callback' => array($api_namespace, 'authCheck')
786 + ));
787 +
788 + register_rest_route($route_namespace, '/sync/run', array(
789 + 'methods' => \WP_REST_Server::CREATABLE,
790 + 'callback' => array($api_namespace, 'syncRun'),
791 + 'permission_callback' => array($api_namespace, 'authCheck')
792 + ));
793 +
794 + register_rest_route($route_namespace, '/sync/stop', array(
795 + 'methods' => \WP_REST_Server::CREATABLE,
796 + 'callback' => array($api_namespace, 'syncStop'),
797 + 'permission_callback' => array($api_namespace, 'authCheck')
798 + ));
799 +
800 + register_rest_route($route_namespace, '/batch/state', array(
801 + 'methods' => \WP_REST_Server::READABLE,
802 + 'callback' => array($api_namespace, 'batchState'),
803 + 'permission_callback' => array($api_namespace, 'authCheck')
804 + ));
805 +
806 + register_rest_route($route_namespace, '/batch/action', array(
807 + 'methods' => \WP_REST_Server::CREATABLE,
808 + 'callback' => array($api_namespace, 'batchAction'),
809 + 'permission_callback' => array($api_namespace, 'authCheck')
810 + ));
811 + }
812 +
813 + /**
814 + * Metabox for media modal page
815 + * @param $form_fields
816 + * @param $post
817 + * @return array
818 + */
819 + public function attachment_modal_meta_box_callback($form_fields, $post) {
820 + // Do not show on media edit page, only on modal
821 + // Do not show if we are not in Media Library
822 + if ( isset($_GET['post']) || wp_doing_ajax() ) {
823 + return $form_fields;
824 + }
825 +
826 + $link = get_edit_post_link($post->ID);
827 +
828 + $form_field['label'] = '';
829 + $form_field['input'] = 'html';
830 + $form_field['html'] = sprintf("<script>jQuery('.sm_edit_links').remove();jQuery('.actions').prepend('<a class=\"sm_edit_links\" href=\"%s#sm-attachment-metabox\">%s</a> | ')</script>", $link, __("View stateless meta", ud_get_stateless_media()->domain));
831 + $form_field['show_in_modal'] = true;
832 +
833 + $form_fields['sm_html'] = $form_field;
834 + return $form_fields;
835 + }
836 +
837 + /**
838 + * Metabox for media edit page
839 + * @param $meta_boxes
840 + * @return array
841 + */
842 + public function attachment_meta_box_callback($meta_boxes) {
843 + $post_id = false;
844 + if (isset($_GET['post'])) {
845 + $post_id = intval($_GET['post']);
846 + } elseif (isset($_POST['post_ID'])) {
847 + $post_id = intval($_POST['post_ID']);
848 + } elseif (isset($_GET['item'])) {
849 + $post_id = intval($_GET['item']);
850 + } elseif (isset($_POST['item'])) {
851 + $post_id = intval($_POST['item']);
852 + }
853 +
854 + return $this->_prepare_data_for_metabox($meta_boxes, $post_id);
855 + }
856 +
857 + /**
858 + * Prepare data for metabox fields
859 + * @param $meta_boxes
860 + * @param $post_id
861 + * @return array
862 + */
863 + private function _prepare_data_for_metabox($meta_boxes, $post_id) {
864 + $post = get_post($post_id);
865 + $sm_cloud = apply_filters('wp_stateless_get_file', [], $post_id, true);
866 + $sm_mode = $this->get('sm.mode');
867 +
868 + if (empty($post)) {
869 + return $meta_boxes;
870 + }
871 +
872 + $sizes = $this->get_image_sizes();
873 +
874 + $fields = array();
875 +
876 + if ($sm_mode !== 'stateless' || empty($sm_cloud)) {
877 + $fields[] = array(
878 + 'name' => __('Regenerate', ud_get_stateless_media()->domain),
879 + 'id' => 'storage_bucket_url',
880 + 'type' => 'custom_html',
881 + 'std' => $this->_prepare_generate_link($post, false, '', true, $sm_cloud),
882 + 'tab' => 'thumbnails',
883 + );
884 + }
885 +
886 + if (is_array($sm_cloud) && !empty($sm_cloud['fileLink'])) {
887 +
888 + $fields[] = array(
889 + 'type' => 'heading',
890 + 'name' => 'Files',
891 + 'tab' => 'thumbnails',
892 + );
893 +
894 + $fields[] = array(
895 + 'name' => __('Original', ud_get_stateless_media()->domain),
896 + 'id' => 'storage_bucket_url',
897 + 'type' => 'custom_html',
898 + 'media_modal' => true,
899 + 'std' => '<label><input type="text" class="widefat urlfield" readonly="readonly" value="' . esc_attr($sm_cloud['fileLink']) . '" />
900 + <a href="' . $sm_cloud['fileLink'] . '" target="_blank" class="sm-view-link"><i class="dashicons dashicons-external"></i></a>&nbsp;&nbsp;&nbsp;' . $this->_prepare_generate_link($post, true, '', false, $sm_cloud) . ' </label>',
901 + 'tab' => 'thumbnails',
902 + );
903 +
904 + if (!empty($sm_cloud['sizes']) && is_array($sm_cloud['sizes'])) {
905 + foreach ($sm_cloud['sizes'] as $size_label => $size) {
906 +
907 + $fields[] = array(
908 + 'name' => __(sprintf("%s x %s", ($size['width'] ?: $sizes[$size_label]['width']), ($size['height'] ?: $sizes[$size_label]['height'])), ud_get_stateless_media()->domain),
909 + 'id' => 'storage_bucket_url' . $size_label,
910 + 'type' => 'custom_html',
911 + 'media_modal' => true,
912 + 'std' => '<label><input type="text" class="widefat urlfield" readonly="readonly" value="' . esc_attr($size['fileLink']) . '" />
913 + <a href="' . $size['fileLink'] . '" target="_blank" class="sm-view-link"><i class="dashicons dashicons-external"></i></a>&nbsp;&nbsp;&nbsp;' . $this->_prepare_generate_link($post, true, $size_label, false, $sm_cloud) . ' </label>',
914 + 'tab' => 'thumbnails',
915 + );
916 + }
917 + }
918 +
919 + if (!empty($sm_cloud['cacheControl'])) {
920 + $fields[] = array(
921 + 'name' => __('Cache Control', ud_get_stateless_media()->domain),
922 + 'id' => 'cache_control',
923 + 'type' => 'custom_html',
924 + 'std' => '<label><input type="text" class="widefat urlfield" readonly="readonly" value="' . $sm_cloud['cacheControl'] . '" /></label>',
925 + 'tab' => 'meta',
926 + );
927 + }
928 +
929 + if (!empty($sm_cloud['bucket'])) {
930 + $fields[] = array(
931 + 'name' => __('Storage Bucket', ud_get_stateless_media()->domain),
932 + 'id' => 'storage_bukcet',
933 + 'type' => 'custom_html',
934 + 'std' => '<label><input type="text" class="widefat urlfield" readonly="readonly" value="gs://' . esc_attr($sm_cloud['bucket']) . '" />
935 + <a href="https://console.cloud.google.com/storage/browser/' . esc_attr($sm_cloud['bucket']) . '" target="_blank" class="sm-view-link"><i class="dashicons dashicons-external"></i></a></label>',
936 + 'tab' => 'meta',
937 + );
938 + }
939 + }
940 +
941 + $meta_boxes[] = apply_filters('sm::attachment::meta', array(
942 + 'id' => 'sm-attachment-metabox',
943 + 'title' => __('Stateless', ud_get_stateless_media()->domain),
944 + 'post_types' => 'attachment',
945 + //'media_modal' => true,
946 + //set context `side` for left column
947 + 'context' => 'normal',
948 + 'priority' => 'low',
949 + 'tabs' => array(
950 + 'thumbnails' => array(
951 + 'label' => __('Thumbnails', ud_get_stateless_media()->domain),
952 + 'icon' => 'dashicons-format-gallery',
953 + ),
954 + 'meta' => array(
955 + 'label' => __('Meta', ud_get_stateless_media()->domain),
956 + 'icon' => 'dashicons-admin-site',
957 + ),
958 + ),
959 + // Tab style: 'default', 'box' or 'left'. Optional
960 + 'tab_style' => 'left',
961 + // Show meta box wrapper around tabs? true (default) or false. Optional
962 + 'tab_wrapper' => true,
963 + 'fields' => $fields
964 + ), $post->ID);
965 +
966 + return $meta_boxes;
967 + }
968 +
969 + /**
970 + * Preparing link for sync
971 + * @param $post
972 + * @param bool $use_icon
973 + * @param string $size
974 + * @param bool $button
975 + * @param array $sm_cloud
976 + * @return string
977 + */
978 + private function _prepare_generate_link($post, $use_icon = false, $size = '', $button = false, $sm_cloud = array()) {
979 + $sync = '';
980 +
981 + $sm_mode = $this->get('sm.mode');
982 +
983 + if (current_user_can('upload_files') && $sm_mode !== 'disabled' && ($sm_mode !== 'stateless' || empty($sm_cloud))) {
984 + if ($post && 'attachment' == $post->post_type && 'image/' == substr($post->post_mime_type, 0, 6)) {
985 + $sync = '<a href="javascript:;" data-type="image" data-id="' . $post->ID . '" data-size="' . $size . '" data-reload_page="' . $button . '"
986 + class="sm_inline_sync ' . ($button ? 'button button-primary button-large' : '') . '">' . ($use_icon ? "<i class='dashicons dashicons-image-rotate'></i>" : __('Regenerate and Sync with GCS', ud_get_stateless_media()->domain)) . '</a>';
987 + }
988 + if ($post && 'attachment' == $post->post_type && 'image/' != substr($post->post_mime_type, 0, 6)) {
989 + $sync = '<a href="javascript:;" data-type="other" data-id="' . $post->ID . '" data-size="' . $size . '" data-reload_page="' . $button . '"
990 + class="sm_inline_sync ' . ($button ? 'button button-primary button-large' : '') . '">' . ($use_icon ? "<i class='dashicons dashicons-image-rotate'></i>" : __('Sync with GCS', ud_get_stateless_media()->domain)) . '</a>';
991 + }
992 + } elseif ($button && $sm_mode !== 'stateless') {
993 + $sync = __('You do not have access to sync or Stateless mode is Disabled', ud_get_stateless_media()->domain);
994 + }
995 + return $sync;
996 + }
997 +
998 + /**
999 + * @param $current_path
1000 + * @param $use_root boolean: whether to use the root dir or not.
1001 + * 0 will be passed from various compatibilities so that the root dir is not used.
1002 + * false will passed from some compatibilities to use the value as local path.
1003 + * @param $attachment_id
1004 + * @param $size
1005 + * @return string
1006 + */
1007 + public function handle_root_dir($current_path, $use_root = true, $attachment_id = '', $size = '') {
1008 + //non media files
1009 + if ($use_root === 0) {
1010 + $non_media = ud_stateless_db()->get_non_library_file_name($current_path);
1011 +
1012 + if ($non_media) {
1013 + return $non_media;
1014 + }
1015 + }
1016 +
1017 + $root_dir = $this->get('sm.root_dir');
1018 + $root_dir_regex = '~^' . apply_filters("wp_stateless_handle_root_dir", $root_dir, true) . '/~';
1019 + /**
1020 + * Retrieve Y/M and other tags from current path
1021 + */
1022 + $path_elements = apply_filters('wp_stateless_unhandle_root_dir', $current_path);
1023 + $root_dir = apply_filters("wp_stateless_handle_root_dir", $root_dir, false, $path_elements);
1024 +
1025 + $upload_dir = wp_upload_dir();
1026 + $current_path = str_replace(wp_normalize_path(trailingslashit($upload_dir['basedir'])), '', wp_normalize_path($current_path));
1027 + $current_path = str_replace(wp_normalize_path(trailingslashit($upload_dir['baseurl'])), '', wp_normalize_path($current_path));
1028 + $current_path = str_replace(trailingslashit($this->get_gs_host()), '', $current_path);
1029 +
1030 + /**
1031 + * Using only filename. Other parts of path included to $root_dir.
1032 + * excluding compatibility.
1033 + */
1034 + if ($use_root) {
1035 + $current_path = basename($current_path);
1036 + }
1037 +
1038 + if (!$use_root) {
1039 + // removing the root dir if already exists in the beginning.
1040 + $raw_name = preg_replace($root_dir_regex, '', $current_path);
1041 +
1042 + if ($raw_name && is_multisite() && ($blog_id = get_current_blog_id()) != 1) {
1043 + $folder = "sites/{$blog_id}/";
1044 + if (strpos($raw_name, $folder) === 0) return $raw_name;
1045 +
1046 + return "$folder$raw_name";
1047 + }
1048 +
1049 + return $raw_name;
1050 + }
1051 +
1052 + // skip adding root dir if it's already added.
1053 + if (!empty($root_dir) && !preg_match($root_dir_regex, $current_path)) {
1054 + return $root_dir . '/' . trim($current_path, '/ ');
1055 + }
1056 +
1057 + return $current_path;
1058 + }
1059 +
1060 + /**
1061 + * @param $content
1062 + * @return mixed
1063 + */
1064 + public function the_content_filter($content) {
1065 +
1066 + if ($upload_data = wp_upload_dir()) {
1067 +
1068 + if (!empty($upload_data['url']) && !empty($content)) {
1069 + $url = preg_replace('/https?:\/\//', '', $upload_data['url']);
1070 +
1071 + $root_dir = trim($this->get('sm.root_dir'), '/ '); // Remove any forward slash and empty space.
1072 + $root_dir = apply_filters("wp_stateless_handle_root_dir", $root_dir);
1073 + $root_dir = !empty($root_dir) ? $root_dir . '/' : false;
1074 + $image_host = $this->get_gs_host();
1075 + $file_ext = $this->replaceable_file_types();
1076 + $content = preg_replace(
1077 + '/(href|src)=(\'|")(https?:\/\/' . str_replace('/', '\/', $url) . ')\/(.+?)(' . $file_ext . ')(\'|")/i',
1078 + '$1=$2' . $image_host . '/' . ($root_dir ? $root_dir : '') . '$4$5$6',
1079 + $content
1080 + );
1081 + }
1082 + }
1083 +
1084 + return $content;
1085 + }
1086 +
1087 + /**
1088 + * Return file types supported by File URL Replacement.
1089 + * @return string
1090 + */
1091 + public function replaceable_file_types() {
1092 + $types = $this->get('sm.body_rewrite_types');
1093 +
1094 + // Removing extra space.
1095 + $types = trim($types);
1096 + $types = preg_replace("/\s{2,}/", ' ', $types);
1097 +
1098 + $types_arr = explode(' ', $types);
1099 + return '\.' . implode('|\.', $types_arr);
1100 + }
1101 +
1102 + /**
1103 + * Copied from https://developer.wordpress.org/reference/functions/get_metadata/
1104 + *
1105 + * @param $value null unless other filter hooked in this function.
1106 + * @param $object_id post id
1107 + * @param $meta_key
1108 + * @param $single
1109 + * @return mixed
1110 + */
1111 + public function post_metadata_filter($value, $object_id, $meta_key, $single) {
1112 + if (empty($value)) {
1113 + $meta_type = 'post';
1114 + $transient_key = "stateless_{$meta_type}_meta";
1115 +
1116 + $meta_cache = wp_cache_get($object_id, $transient_key);
1117 + if (empty($meta_cache)) {
1118 + $meta_cache = wp_cache_get($object_id, $meta_type . '_meta');
1119 +
1120 + if (!$meta_cache) {
1121 + $meta_cache = update_meta_cache($meta_type, array($object_id));
1122 + $meta_cache = $meta_cache[$object_id];
1123 + }
1124 +
1125 + foreach ($meta_cache as $key => $meta) {
1126 + $meta_cache[$key] = array_map('maybe_unserialize', $meta_cache[$key]);
1127 + }
1128 +
1129 + $meta_cache = $this->convert_to_gs_link($meta_cache);
1130 + wp_cache_set($object_id, $meta_cache, $transient_key);
1131 + }
1132 +
1133 + if (!$meta_key) {
1134 + return $meta_cache;
1135 + }
1136 +
1137 + if (isset($meta_cache[$meta_key])) {
1138 + return $meta_cache[$meta_key];
1139 + }
1140 +
1141 + // in case no metadata is found return what was passed in $value.
1142 + // $value most of the time is null.
1143 + return $value;
1144 + }
1145 +
1146 + return $this->convert_to_gs_link($value);
1147 + }
1148 +
1149 + /**
1150 + * Replace all image link with gs link and return only if meta modified.
1151 + *
1152 + * @param $meta
1153 + * @param $return
1154 + * @return mixed or null when not changed.
1155 + */
1156 + public function convert_to_gs_link($meta, $return = false) {
1157 + $updated = $meta;
1158 + if ($meta && $upload_data = wp_upload_dir()) {
1159 + if (!empty($upload_data['url']) && !empty($meta)) {
1160 + $url = preg_replace('/https?:\/\//', '', $upload_data['url']);
1161 + $root_dir = trim($this->get('sm.root_dir'), '/ '); // Remove any forward slash and empty space.
1162 + $root_dir = apply_filters("wp_stateless_handle_root_dir", $root_dir);
1163 + $root_dir = !empty($root_dir) ? $root_dir . '/' : false;
1164 + $image_host = $this->get_gs_host() . '/' . ($root_dir ? $root_dir : '');
1165 + $file_ext = $this->replaceable_file_types();
1166 + $updated = $this->_convert_to_gs_link($meta, $image_host, $url, $file_ext);
1167 + }
1168 + }
1169 +
1170 + if ($updated == $meta && !$return) {
1171 + return null; // Not changed.
1172 + }
1173 + return $updated;
1174 + }
1175 +
1176 + /**
1177 + * Replace all image link with gs link
1178 + *
1179 + * @param $meta
1180 + * @param $image_host
1181 + * @param $url
1182 + * @param $file_ext
1183 + * @return array|null|string|string[]
1184 + */
1185 + public function _convert_to_gs_link($meta, $image_host, $url, $file_ext) {
1186 + if (is_array($meta)) {
1187 + foreach ($meta as $key => $value) {
1188 + $meta[$key] = $this->_convert_to_gs_link($value, $image_host, $url, $file_ext);
1189 + }
1190 + return $meta;
1191 + } elseif (is_object($meta) && $meta instanceof \stdClass) {
1192 + foreach (get_object_vars($meta) as $key => $value) {
1193 + $meta->{$key} = $this->_convert_to_gs_link($value, $image_host, $url, $file_ext);
1194 + }
1195 + return $meta;
1196 + } elseif (is_string($meta)) {
1197 + return preg_replace('/(https?:\/\/' . str_replace('/', '\/', $url) . ')\/(.+?)(' . $file_ext . ')/i', $image_host . '$2$3', $meta);
1198 + }
1199 +
1200 + return $meta;
1201 + }
1202 +
1203 + /**
1204 + * Determines if plugin is loaded via mu-plugins
1205 + * or Network Enabled.
1206 + *
1207 + * @param bool $is_multisite
1208 + * @return bool
1209 + * @author peshkov@UD
1210 + */
1211 + public function is_network_detected($is_multisite = false) {
1212 + /* Plugin is loaded via mu-plugins. */
1213 +
1214 + if (strpos(Utility::normalize_path($this->root_path), Utility::normalize_path(WPMU_PLUGIN_DIR)) !== false) {
1215 + return true;
1216 + }
1217 +
1218 + if (is_multisite()) {
1219 + if ($is_multisite) return true;
1220 + /* Looks through network enabled plugins to see if our one is there. */
1221 + foreach (wp_get_active_network_plugins() as $path) {
1222 + // Trying again using readlink in case it's a symlink file.
1223 + // boot_file is already solved.
1224 + // wp_normalize_path is helpfull in windows.
1225 + if (wp_normalize_path($this->boot_file) == wp_normalize_path($path) || (is_link($path) && $this->boot_file == readlink($path))) {
1226 + return true;
1227 + }
1228 + }
1229 + }
1230 + return false;
1231 + }
1232 +
1233 + /**
1234 + * Initialization.
1235 + * Register scripts and styles
1236 + */
1237 + public function admin_init() {
1238 + $this->show_notice_stateless_cache_busting();
1239 + wp_register_style('wp-stateless', $this->path('static/styles/wp-stateless.css', 'url'), array(), self::$version);
1240 +
1241 + /**
1242 + * select2 styles
1243 + */
1244 + wp_register_style('wp-stateless-select2', $this->path('static/styles/select2.min.css', 'url'), array(), self::$version);
1245 +
1246 + /* Attachment or upload page */
1247 + wp_register_script('wp-stateless-uploads-js', $this->path('static/scripts/wp-stateless-uploads.js', 'url'), array('jquery'), self::$version);
1248 + wp_localize_script('wp-stateless-uploads-js', 'stateless_upload', [
1249 + 'inline_sync_nonce' => wp_create_nonce('sm_inline_sync'),
1250 + ]);
1251 +
1252 + /* Setup wizard styles. */
1253 + // #152
1254 + // wp_register_style('wp-stateless-setup-wizard', $this->path('static/styles/wp-stateless-setup-wizard.css', 'url'), array(), self::$version);
1255 +
1256 + wp_register_script('wp-stateless-select2', ud_get_stateless_media()->path('static/scripts/select2.min.js', 'url'), array('jquery'), self::$version, true);
1257 +
1258 + /* Stateless settings page */
1259 + wp_register_script('wp-stateless-settings', ud_get_stateless_media()->path('static/scripts/wp-stateless-settings.js', 'url'), array('clipboard'), self::$version);
1260 + wp_localize_script('wp-stateless-settings', 'stateless_l10n', $this->get_l10n_data());
1261 +
1262 + wp_register_style('wp-stateless-settings', $this->path('static/styles/wp-stateless-settings.css', 'url'), array(), self::$version);
1263 + wp_register_style('wp-stateless-addons', $this->path('static/styles/wp-stateless-addons.css', 'url'), array(), self::$version);
1264 + wp_register_style('wp-stateless-status', $this->path('static/styles/wp-stateless-status.css', 'url'), array(), self::$version);
1265 +
1266 + // Sync tab
1267 + wp_register_script('wp-stateless', ud_get_stateless_media()->path('static/scripts/wp-stateless.js', 'url'), array('jquery-ui-core', 'wp-stateless-settings', 'wp-api-request'), self::$version, true);
1268 +
1269 + wp_localize_script('wp-stateless', 'stateless_l10n', $this->get_l10n_data());
1270 + wp_localize_script('wp-stateless', 'wp_stateless_configs', array(
1271 + 'WP_DEBUG' => defined('WP_DEBUG') ? WP_DEBUG : false,
1272 + 'REST_API_TOKEN' => Utility::generate_jwt_token(['user_id' => get_current_user_id()], DAY_IN_SECONDS),
1273 + 'api_root' => apply_filters( 'wp_stateless_rest_api_root', '' ),
1274 + 'ajaxurl' => admin_url( 'admin-ajax.php' ),
1275 + 'stateless_check_ajax_nonce' => wp_create_nonce('stateless_check_ajax'),
1276 + 'text_ok' => __('Ok', ud_get_stateless_media()->domain),
1277 + 'text_fail' => __('Fail', ud_get_stateless_media()->domain),
1278 + ));
1279 +
1280 + $settings = ud_get_stateless_media()->get('sm');
1281 + $settings['wildcards'] = $this->settings->wildcards;
1282 + $settings['network_admin'] = is_network_admin();
1283 + $settings['is_multisite'] = is_multisite();
1284 + if (defined('WP_STATELESS_MEDIA_JSON_KEY') && WP_STATELESS_MEDIA_JSON_KEY) {
1285 + $settings['key_json'] = "Currently configured via a constant.";
1286 + }
1287 + wp_localize_script('wp-stateless', 'wp_stateless_settings', $settings);
1288 +
1289 + // Batch processes tab
1290 + wp_register_script('wp-stateless-batch', ud_get_stateless_media()->path('static/scripts/wp-stateless-batch.js', 'url'), array('wp-api-request'), self::$version, true);
1291 + wp_localize_script('wp-stateless-batch', 'wp_stateless_batch', array(
1292 + 'REST_API_TOKEN' => Utility::generate_jwt_token(['user_id' => get_current_user_id()], DAY_IN_SECONDS),
1293 + 'is_running' => BatchTaskManager::instance()->is_running(),
1294 + 'api_root' => apply_filters( 'wp_stateless_rest_api_root', '' ),
1295 + ));
1296 + }
1297 +
1298 + /**
1299 + * Get_l10n_data
1300 + *
1301 + * @param string $value
1302 + * @return mixed
1303 + */
1304 + public function get_l10n_data($value = '') {
1305 + include ud_get_stateless_media()->path('l10n.php', 'dir');
1306 + return $l10n;
1307 + }
1308 +
1309 + /**
1310 + * Admin Scripts
1311 + *
1312 + * @param $hook
1313 + */
1314 + public function admin_enqueue_scripts($hook) {
1315 +
1316 + switch ($hook) {
1317 +
1318 + case 'options-media.php':
1319 + wp_enqueue_style('wp-stateless');
1320 + break;
1321 +
1322 + case 'upload.php':
1323 + wp_enqueue_style('wp-stateless');
1324 + wp_enqueue_script('wp-stateless-uploads-js');
1325 +
1326 + break;
1327 +
1328 + case 'post.php':
1329 +
1330 + global $post;
1331 +
1332 + if ($post->post_type == 'attachment') {
1333 + wp_enqueue_style('wp-stateless');
1334 + wp_enqueue_script('wp-stateless-uploads-js');
1335 + }
1336 +
1337 + break;
1338 +
1339 + // #152
1340 + // case 'media_page_stateless-setup':
1341 + case 'settings_page_stateless-setup':
1342 + wp_enqueue_style('wp-stateless');
1343 + // #152
1344 + // wp_enqueue_style('wp-stateless-setup-wizard');
1345 + break;
1346 + case 'media_page_stateless-settings':
1347 + case 'settings_page_stateless-settings':
1348 + wp_enqueue_style('wp-stateless');
1349 + wp_enqueue_style('wp-stateless-select2');
1350 + wp_enqueue_script('wp-stateless-settings');
1351 + wp_enqueue_script('wp-stateless-select2');
1352 + wp_enqueue_style('wp-stateless-settings');
1353 + wp_enqueue_style('wp-stateless-addons');
1354 + wp_enqueue_style('wp-stateless-status');
1355 + wp_enqueue_style( 'media-views' );
1356 +
1357 + // Sync tab
1358 + wp_enqueue_script('wp-stateless');
1359 +
1360 + // Data updates
1361 + wp_enqueue_script('jquery-ui-dialog');
1362 + wp_enqueue_style('wp-jquery-ui-dialog');
1363 + wp_enqueue_script('wp-stateless-batch');
1364 +
1365 + wp_enqueue_style('wp-pointer');
1366 + wp_enqueue_script('wp-pointer');
1367 +
1368 + $data = array(
1369 + 'key' => 'stateless-cache-busting',
1370 + 'class' => 'notice',
1371 + 'title' => sprintf(__("Stateless and Ephemeral modes enables and requires the Cache-Busting option.", ud_get_stateless_media()->domain)),
1372 + 'message' => sprintf(__("WordPress looks at local files to prevent files with the same filenames.
1373 + Since Stateless mode bypasses this check, there is a potential for files to be stored with the same file name. We enforce the Cache-Busting option to prevent this.
1374 + Override with the <a href='%s' target='_blank'>%s</a> constant.", ud_get_stateless_media()->domain), ud_get_stateless_media()->get_docs_page_url('docs/constants/#wpstatelessmediacachebusting'), "WP_STATELESS_MEDIA_CACHE_BUSTING"),
1375 + );
1376 + echo "<script id='template-stateless-cache-busting' type='text/html'>";
1377 + include ud_get_stateless_media()->path('/static/views/error-notice.php', 'dir');
1378 + echo "</script>";
1379 +
1380 + break;
1381 + default:
1382 + break;
1383 + }
1384 + }
1385 +
1386 + /**
1387 + * Add Attributes to media HTML
1388 + *
1389 + * @author potanin@UD
1390 + * @param $attr
1391 + * @param $attachment
1392 + * @param $size
1393 + * @return mixed
1394 + */
1395 + public function wp_get_attachment_image_attributes($attr, $attachment, $size = null) {
1396 +
1397 + $sm_cloud = apply_filters('wp_stateless_get_file', [], $attachment->ID);
1398 + if (is_array($sm_cloud) && !empty($sm_cloud['name'])) {
1399 + $attr['class'] = $attr['class'] . ' wp-stateless-item';
1400 + $attr['data-image-size'] = is_array($size) ? implode('x', $size) : $size;
1401 + $attr['data-stateless-media-bucket'] = isset($sm_cloud['bucket']) ? $sm_cloud['bucket'] : false;
1402 + $attr['data-stateless-media-name'] = $sm_cloud['name'];
1403 + }
1404 +
1405 + return $attr;
1406 + }
1407 +
1408 + /**
1409 + * Adds filter link to Media Library table.
1410 + *
1411 + * @param $views
1412 + * @return mixed
1413 + */
1414 + public function views_upload($views) {
1415 + $views['stateless'] = '<a href="#">' . __('Stateless Media') . '</a>';
1416 + return $views;
1417 + }
1418 +
1419 + /**
1420 + * Replace media URL
1421 + *
1422 + * @param bool $false
1423 + * @param integer $id
1424 + * @param string $size
1425 + * @return mixed $false
1426 + */
1427 + public function image_downsize($false, $id, $size) {
1428 +
1429 + if ((!isset($this->client) || !$this->client || is_wp_error($this->client)) && $this->get('sm.mode') !== 'stateless') {
1430 + return $false;
1431 + }
1432 +
1433 + /**
1434 + * Check if enabled
1435 + */
1436 + if (!in_array($this->get('sm.mode'), array('cdn', 'stateless', 'ephemeral'))) {
1437 + return $false;
1438 + }
1439 +
1440 + /** Start determine remote file */
1441 + $img_url = wp_get_attachment_url($id);
1442 + $meta = wp_get_attachment_metadata($id);
1443 + $width = $height = 0;
1444 + $is_intermediate = false;
1445 +
1446 + //** try for a new style intermediate size */
1447 + if ($intermediate = image_get_intermediate_size($id, $size)) {
1448 + if (!empty($intermediate['gs_link'])) {
1449 + $img_url = $intermediate['gs_link'];
1450 + } else if (!empty($intermediate['url'])) {
1451 + $img_url = $intermediate['url'];
1452 + } else {
1453 + $img_url = dirname($img_url) . $intermediate['file'];
1454 + }
1455 +
1456 + $width = $intermediate['width'];
1457 + $height = $intermediate['height'];
1458 + $is_intermediate = true;
1459 + }
1460 +
1461 + /**
1462 + * maybe try to get images info from sm_cloud
1463 + * this case may happen when no local files
1464 + * @author korotkov@ud
1465 + */
1466 + if (!$width && !$height) {
1467 + $sm_cloud = apply_filters('wp_stateless_get_file', [], $id, true);
1468 + if (is_string($size) && !empty($sm_cloud['sizes']) && !empty($sm_cloud['sizes'][$size])) {
1469 + global $_wp_additional_image_sizes;
1470 +
1471 + $img_url = !empty($sm_cloud['sizes'][$size]['fileLink']) ? $sm_cloud['sizes'][$size]['fileLink'] : $img_url;
1472 +
1473 + if (!empty($_wp_additional_image_sizes[$size])) {
1474 + $width = !empty($_wp_additional_image_sizes[$size]['width']) ? $_wp_additional_image_sizes[$size]['width'] : $width;
1475 + $height = !empty($_wp_additional_image_sizes[$size]['height']) ? $_wp_additional_image_sizes[$size]['height'] : $height;
1476 + }
1477 +
1478 + $is_intermediate = true;
1479 + }
1480 + }
1481 +
1482 + if (!$width && !$height && isset($meta['width'], $meta['height'])) {
1483 +
1484 + //** any other type: use the real image */
1485 + $width = $meta['width'];
1486 + $height = $meta['height'];
1487 + }
1488 +
1489 +
1490 + if ($img_url) {
1491 +
1492 + //** we have the actual image size, but might need to further constrain it if content_width is narrower */
1493 + list($width, $height) = image_constrain_size_for_editor($width, $height, $size);
1494 + $img_url = apply_filters('wp_stateless_bucket_link', $img_url);
1495 + return array($img_url, $width, $height, $is_intermediate);
1496 + }
1497 +
1498 +
1499 + /**
1500 + * All other cases work as usually
1501 + */
1502 + return $false;
1503 + }
1504 +
1505 + /**
1506 + * Extends metadata by adding GS information.
1507 + * Note: must not be called directly. It's used only on hook
1508 + *
1509 + * @param $metadata
1510 + * @param $attachment_id
1511 + * @return array|mixed
1512 + */
1513 + public function wp_get_attachment_metadata($metadata, $attachment_id) {
1514 + global $default_dir;
1515 + $default_dir = false;
1516 + /* Determine if the media file has GS data at all. */
1517 + $sm_cloud = apply_filters('wp_stateless_get_file', [], $attachment_id, true);
1518 + // If metadata not passed the get metadata from post meta.
1519 + if (empty($metadata)) {
1520 + $metadata = get_post_meta($attachment_id, '_wp_attachment_metadata', true);
1521 + }
1522 +
1523 + if (empty($metadata)) {
1524 + $metadata = [];
1525 + }
1526 +
1527 + if (is_array($metadata) && is_array($sm_cloud) && !empty($sm_cloud['fileLink'])) {
1528 + $metadata['gs_link'] = apply_filters('wp_stateless_bucket_link', $sm_cloud['fileLink']);
1529 + $metadata['gs_name'] = isset($sm_cloud['name']) ? $sm_cloud['name'] : false;
1530 + $metadata['gs_bucket'] = isset($sm_cloud['bucket']) ? $sm_cloud['bucket'] : false;
1531 + if (!empty($metadata['sizes']) && is_array($metadata['sizes'])) {
1532 + foreach ($metadata['sizes'] as $k => $v) {
1533 + if (!empty($sm_cloud['sizes'][$k]['name'])) {
1534 + $metadata['sizes'][$k]['gs_name'] = $sm_cloud['sizes'][$k]['name'];
1535 + $metadata['sizes'][$k]['gs_link'] = apply_filters('wp_stateless_bucket_link', $sm_cloud['sizes'][$k]['fileLink']);
1536 + }
1537 + }
1538 + }
1539 + }
1540 + if (is_multisite() && !empty($metadata['file'])) {
1541 + if ($this->get('sm.mode') == 'stateless') {
1542 + $default_dir = true;
1543 + $uploads = wp_get_upload_dir();
1544 + $default_dir = false;
1545 +
1546 + $file_path_fix = $uploads['basedir'] . "/{$metadata['file']}";
1547 + if (file_exists($file_path_fix)) {
1548 + $metadata['file'] = "{$metadata['file']}";
1549 + }
1550 + } else {
1551 + $uploads = wp_get_upload_dir();
1552 + $blog_id = get_current_blog_id();
1553 + $file_path_fix = $uploads['basedir'] . "/sites/$blog_id/{$metadata['file']}";
1554 + if (file_exists($file_path_fix)) {
1555 + $metadata['file'] = "sites/$blog_id/{$metadata['file']}";
1556 + }
1557 + }
1558 + } elseif ( empty($sm_cloud) && $this->get('sm.mode') == 'stateless' && isset($metadata['file']) ) {
1559 + $default_dir = true;
1560 + $uploads = wp_get_upload_dir();
1561 + $default_dir = false;
1562 +
1563 + $file_path_fix = $uploads['basedir'] . "/{$metadata['file']}";
1564 + if (file_exists($file_path_fix)) {
1565 + $metadata['file'] = "{$metadata['file']}";
1566 + }
1567 + }
1568 +
1569 + return $metadata;
1570 + }
1571 +
1572 + /**
1573 + *
1574 + * @param $file
1575 + * @param $attachment_id
1576 + * @return string
1577 + */
1578 + public function get_attached_file($file, $attachment_id) {
1579 + global $default_dir;
1580 +
1581 + $sm_cloud = apply_filters('wp_stateless_get_file', [], $attachment_id);
1582 + $_file = get_post_meta($attachment_id, '_wp_attached_file', true);
1583 +
1584 + /* Determine if the media file has GS data at all. */
1585 + if (is_multisite() && $_file) {
1586 + $blog_id = get_current_blog_id();
1587 + $uploads = wp_get_upload_dir();
1588 + $_file = apply_filters('wp_stateless_file_name', $_file, false);
1589 + $file_path_fix = $uploads['basedir'] . "/sites/$blog_id/$_file";
1590 +
1591 + if (file_exists($file_path_fix)) {
1592 + return $file_path_fix;
1593 + }
1594 + } elseif (empty($sm_cloud) && $this->get('sm.mode') == 'stateless' && $_file) {
1595 + $default_dir = true;
1596 + $uploads = wp_get_upload_dir();
1597 + $default_dir = false;
1598 + return $uploads['basedir'] . '/' . $_file;
1599 + }
1600 +
1601 + return $file;
1602 + }
1603 +
1604 + /**
1605 + * Returns client object
1606 + * or WP_Error on failure.
1607 + *
1608 + * @author peshkov@UD
1609 + * @return object $this->client. \wpCloud\StatelessMedia\GS_Client or \WP_Error
1610 + */
1611 + public function get_client() {
1612 +
1613 + if (null === $this->client) {
1614 +
1615 + $key_json = $this->get('sm.key_json');
1616 + if (empty($key_json)) {
1617 + $key_json = get_site_option('sm_key_json');
1618 + }
1619 +
1620 + /* Try to initialize GS Client */
1621 + $this->client = GS_Client::get_instance(array(
1622 + 'bucket' => $this->get('sm.bucket'),
1623 + 'key_json' => $key_json
1624 + ));
1625 + }
1626 +
1627 + return $this->client;
1628 + }
1629 +
1630 + /**
1631 + * Determines if we can connect to Google Storage Bucket.
1632 + *
1633 + * @author peshkov@UD
1634 + */
1635 + public function is_connected_to_gs() {
1636 + $trnst = get_transient('sm::is_connected_to_gs');
1637 +
1638 + if (empty($trnst) || false === $trnst || !isset($trnst['hash']) || $trnst['hash'] != md5(serialize($this->get('sm')))) {
1639 + $trnst = array(
1640 + 'success' => 'true',
1641 + 'error' => '',
1642 + 'hash' => md5(serialize($this->get('sm'))),
1643 + );
1644 + $client = $this->get_client();
1645 +
1646 + if (is_wp_error($client)) {
1647 + $trnst['success'] = 'false';
1648 + $trnst['error'] = $client->get_error_message();
1649 + } else {
1650 + $connected = $client->is_connected();
1651 + if ($connected !== true) {
1652 + $trnst['success'] = 'false';
1653 + $trnst['error'] = sprintf('Could not connect to Google Storage bucket. Please be sure that bucket with name <b>%s</b> exists and the access credentials are correct.', esc_html($this->get('sm.bucket')));
1654 +
1655 + if (is_callable(array($connected, 'getHandlerContext')) && $handlerContext = $connected->getHandlerContext()) {
1656 + if (!empty($handlerContext['error'])) {
1657 + $handlerContext['error'];
1658 + $trnst['error'] = "Could not connect to Google Storage bucket. " . make_clickable($handlerContext['error']);
1659 + }
1660 + }
1661 +
1662 + if (is_callable(array($connected, 'getErrors')) && $error = $connected->getErrors()) {
1663 + $error = reset($error);
1664 + if ($error['reason'] == 'accessNotConfigured')
1665 + $trnst['error'] = "Could not connect to Google Storage bucket. " . make_clickable($error['message']);
1666 + }
1667 + }
1668 + }
1669 + set_transient('sm::is_connected_to_gs', $trnst, 4 * HOUR_IN_SECONDS);
1670 + }
1671 +
1672 + if (isset($trnst['success']) && $trnst['success'] == 'false') {
1673 + return new \WP_Error('error', (!empty($trnst['error']) ? $trnst['error'] : 'There is an Error on connection to Google Storage.'));
1674 + }
1675 +
1676 + return true;
1677 + }
1678 +
1679 + /**
1680 + * Flush all plugin transients
1681 + *
1682 + */
1683 + public function flush_transients() {
1684 + delete_transient('sm::is_connected_to_gs');
1685 + }
1686 +
1687 + /**
1688 + * Plugin Activation
1689 + *
1690 + */
1691 + public function activate() {
1692 + add_action('activated_plugin', array($this, 'redirect_to_splash'), 99);
1693 + $this->run_upgrade_process();
1694 + }
1695 +
1696 + /**
1697 + * Run Install Process.
1698 + * Triggered on plugins_loaded instead of register_activation_hook action.
1699 + * Works on even manual plugin update.
1700 + *
1701 + * @author alim@UD
1702 + */
1703 + public function run_install_process() {
1704 + // calling the upgrade function because it's same as this point for fresh install or updates.
1705 + $this->run_upgrade_process();
1706 + }
1707 +
1708 + /**
1709 + * Run Upgrade Process:
1710 + * Triggered on plugins_loaded instead of register_activation_hook action.
1711 + * Works on even manual plugin update.
1712 + *
1713 + * @author alim@UD
1714 + */
1715 + public function run_upgrade_process() {
1716 + // Creating database on new installation.
1717 + ud_stateless_db()->create_db();
1718 +
1719 + Migrator::instance()->migrate();
1720 +
1721 + /**
1722 + * Maybe Upgrade current Version
1723 + */
1724 + Upgrader::call($this->args['version']);
1725 + }
1726 +
1727 + /**
1728 + * Delete table when blog is deleted.
1729 + *
1730 + * @param $old_site
1731 + */
1732 + public function wp_delete_site($old_site) {
1733 + ud_stateless_db()->clear_db($old_site->id);
1734 + }
1735 +
1736 + /**
1737 + * Redirect_to_splash
1738 + *
1739 + * @param string $plugin
1740 + */
1741 + public function redirect_to_splash($plugin = '') {
1742 + // $this->settings = new Settings();
1743 +
1744 + if (defined('WP_CLI') || $this->settings->get('sm.key_json') || isset($_POST['checked']) && count($_POST['checked']) > 1) {
1745 + return;
1746 + }
1747 +
1748 + if (
1749 + !$this->settings->get('sm.key_json') &&
1750 + defined('WP_STATELESS_MEDIA_HIDE_SETUP_ASSISTANT') && WP_STATELESS_MEDIA_HIDE_SETUP_ASSISTANT == true &&
1751 + defined('WP_STATELESS_MEDIA_HIDE_SETTINGS_PANEL') && WP_STATELESS_MEDIA_HIDE_SETTINGS_PANEL == true
1752 + ) {
1753 + return;
1754 + }
1755 +
1756 + if (!$this->settings->get('sm.key_json') && defined('WP_STATELESS_MEDIA_HIDE_SETUP_ASSISTANT') && WP_STATELESS_MEDIA_HIDE_SETUP_ASSISTANT == true) {
1757 + $url = $this->get_settings_page_url('?page=stateless-settings');
1758 + exit(wp_redirect($url));
1759 + }
1760 +
1761 + if ($plugin == plugin_basename($this->boot_file)) {
1762 + // #152
1763 + // $url = $this->get_settings_page_url('?page=stateless-setup&step=splash-screen');
1764 + $url = $this->get_settings_page_url('?page=stateless-settings');
1765 +
1766 + if (json_decode($this->settings->get('sm.key_json'))) {
1767 + $url = $this->get_settings_page_url('?page=stateless-settings');
1768 + }
1769 +
1770 + exit( wp_redirect($url) );
1771 + }
1772 + }
1773 +
1774 + /**
1775 + * Plugin Deactivation
1776 + *
1777 + */
1778 + public function deactivate() {
1779 + }
1780 +
1781 + /**
1782 + * Show_notice_stateless_cache_busting
1783 + *
1784 + */
1785 + public function show_notice_stateless_cache_busting() {
1786 + $this->errors->add(array(
1787 + 'key' => 'stateless_cache_busting',
1788 + 'button' => 'View Settings',
1789 + 'button_link' => admin_url('upload.php?page=stateless-settings'),
1790 + 'title' => sprintf(__("Stateless mode now requires the Cache-Busting option.", ud_get_stateless_media()->domain)),
1791 + 'message' => sprintf(__("WordPress looks at local files to prevent files with the same filenames.
1792 + Since Stateless mode bypasses this check, there is a potential for files to be stored with the same file name. We enforce the Cache-Busting option to prevent this.
1793 + Override with the <a href='%s' target='_blank'>%s</a> constant.", ud_get_stateless_media()->domain), ud_get_stateless_media()->get_docs_page_url('docs/constants/#wpstatelessmediacachebusting'), "WP_STATELESS_MEDIA_CACHE_BUSTING"),
1794 + ), 'notice');
1795 + }
1796 +
1797 + /**
1798 + * Filter for wp_get_attachment_url();
1799 + * @param string $url
1800 + * @param string $post_id
1801 + * @return mixed|null|string
1802 + */
1803 + public function wp_get_attachment_url($url = '', $post_id = '') {
1804 + global $default_dir;
1805 + $sm_cloud = apply_filters('wp_stateless_get_file', [], $post_id);
1806 + if (is_array($sm_cloud) && !empty($sm_cloud['fileLink'])) {
1807 + $_url = parse_url($sm_cloud['fileLink']);
1808 + $url = !isset($_url['scheme']) ? ('https:' . $sm_cloud['fileLink']) : $sm_cloud['fileLink'];
1809 + $url = apply_filters('wp_stateless_bucket_link', $url);
1810 + } elseif (is_multisite() && empty($sm_cloud)) {
1811 + $_file = get_post_meta($post_id, '_wp_attached_file', true);
1812 + if ($_file) {
1813 + if ($this->get('sm.mode') == 'stateless') {
1814 + $default_dir = true;
1815 + $uploads = wp_get_upload_dir();
1816 + $default_dir = false;
1817 + $url = $uploads['baseurl'] . '/' . $_file;
1818 + } else {
1819 + $uploads = wp_get_upload_dir();
1820 + $default_dir = false;
1821 + $_file = apply_filters('wp_stateless_file_name', $_file, false);
1822 + $blog_id = get_current_blog_id();
1823 + $file_path_fix = $uploads['basedir'] . "/sites/$blog_id/$_file";
1824 +
1825 + if (file_exists($file_path_fix)) {
1826 + $url = $uploads['baseurl'] . "/sites/$blog_id/$_file";
1827 + }
1828 + }
1829 + }
1830 + } elseif (empty($sm_cloud) && $this->get('sm.mode') == 'stateless') {
1831 + $_file = get_post_meta($post_id, '_wp_attached_file', true);
1832 + if ($_file) {
1833 + $default_dir = true;
1834 + $uploads = wp_get_upload_dir();
1835 + $default_dir = false;
1836 +
1837 + $url = $uploads['baseurl'] . '/' . $_file;
1838 + }
1839 + }
1840 +
1841 + return apply_filters('wp_stateless_attachment_url', $url, $post_id);
1842 + }
1843 +
1844 + /**
1845 + * Filter for attachment_url_to_postid()
1846 + *
1847 + * @param int|false $post_id originally found post ID (or false if not found)
1848 + * @param string $url the URL to find the post ID for
1849 + * @return int|false found post ID from cloud storage URL
1850 + */
1851 + public function attachment_url_to_postid($post_id, $url) {
1852 + global $wpdb;
1853 +
1854 + if (!$post_id) {
1855 + $post_id = get_transient("stateless_url_to_postid_" . md5($url));
1856 +
1857 + if (defined('WP_STATELESS_LEGACY_URL_TO_POSTID')) {
1858 + // User can use this constant if they change the Bucket Folder (root_dir) after uploading image.
1859 + // This can be little slow at first run.
1860 + if (empty($post_id)) {
1861 + if ( !$this->get('sm.use_postmeta') ) {
1862 + $query = 'SELECT post_id FROM ' . ud_stateless_db()->files . ' WHERE file_link = %s';
1863 + $post_id = $wpdb->get_var($wpdb->prepare($query, $url));
1864 + }
1865 +
1866 + if (empty($post_id)) {
1867 + $query = "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = 'sm_cloud' AND meta_value LIKE '%s'";
1868 + $post_id = $wpdb->get_var($wpdb->prepare($query, '%' . $url . '%'));
1869 + }
1870 +
1871 + if ($post_id) {
1872 + set_transient("stateless_url_to_postid_" . md5($url), $post_id);
1873 + }
1874 + }
1875 + return $post_id;
1876 + }
1877 +
1878 + if (empty($post_id)) {
1879 + $gs_base_url = $this->get_gs_host();
1880 + $root_dir = $this->get('sm.root_dir');
1881 + $path_elements = apply_filters('wp_stateless_unhandle_root_dir', $url);
1882 + $root_dir = apply_filters("wp_stateless_handle_root_dir", $root_dir, false, $path_elements);
1883 + $gs_url = $this->get_gs_host() . '/' . $root_dir;
1884 + $site_url = parse_url($gs_url);
1885 + $image_path = parse_url($url);
1886 +
1887 + //force the protocols to match if needed
1888 + if (isset($image_path['scheme']) && ($image_path['scheme'] !== $site_url['scheme'])) {
1889 + $url = str_replace($image_path['scheme'], $site_url['scheme'], $url);
1890 + }
1891 +
1892 + if (0 === strpos($url, $gs_url . '/')) {
1893 + $url = substr($url, strlen($gs_url . '/'));
1894 + } else if (0 === strpos($url, $gs_base_url . '/')) {
1895 + // In case user added Bucket Folder (root_dir) after uploading image.
1896 + $url = substr($url, strlen($gs_base_url . '/'));
1897 + }
1898 +
1899 + /**
1900 + * If `uploads_use_yearmonth_folders` is set - adding year and month to url
1901 + */
1902 + $organize_media = get_option('uploads_use_yearmonth_folders');
1903 + $path = '';
1904 + if ($organize_media == '1' && isset($path_elements['%date_year/date_month%'])) {
1905 + $path .= $path_elements['%date_year/date_month%'] . '/';
1906 + }
1907 + $url = $path . $url;
1908 +
1909 + $sql = $wpdb->prepare(
1910 + "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_wp_attached_file' AND meta_value = %s",
1911 + $url
1912 + );
1913 + $post_id = $wpdb->get_var($sql);
1914 +
1915 + if ($post_id) {
1916 + set_transient("stateless_url_to_postid_" . md5($url), $post_id);
1917 + }
1918 + }
1919 + }
1920 +
1921 + return $post_id;
1922 + }
1923 +
1924 + /**
1925 + * Change Upload BaseURL when CDN Used.
1926 + *
1927 + * @param $data
1928 + * @return mixed
1929 + */
1930 + public function upload_dir($data) {
1931 + $data['basedir'] = $this->get_gs_host();
1932 + $data['baseurl'] = $this->get_gs_host();
1933 + $data['url'] = $data['baseurl'] . $data['subdir'];
1934 +
1935 + return $data;
1936 + }
1937 +
1938 + /**
1939 + * Set Feature Flag constants by parsing composer.json
1940 + *
1941 + * @todo Make sure settings from DB can override these.
1942 + *
1943 + * @author potanin@UD
1944 + * @return array|mixed|null|object
1945 + */
1946 + public function parse_feature_flags() {
1947 +
1948 + try {
1949 +
1950 + $_raw = file_get_contents(Utility::normalize_path($this->root_path) . 'composer.json');
1951 +
1952 + $_parsed = json_decode($_raw);
1953 +
1954 + // @todo Catch poorly formatted JSON.
1955 + if (!is_object($_parsed)) {
1956 + // throw new Error( "unable to parse." );
1957 + }
1958 +
1959 + foreach ((array) $_parsed->extra->featureFlags as $_feature) {
1960 +
1961 + if (!defined($_feature->constant)) {
1962 + define($_feature->constant, $_feature->enabled);
1963 +
1964 + if ($_feature->enabled) {
1965 + Utility::log('Feature flag ' . $_feature->name . ', [' . $_feature->constant . '] enabled.');
1966 + } else {
1967 + Utility::log('Feature flag ' . $_feature->name . ', [' . $_feature->constant . '] disabled.');
1968 + }
1969 + }
1970 + }
1971 + } catch (\Exception $e) {
1972 + Utility::log('Unable to parse [composer.json] feature flags. Error: [' . $e->getMessage() . ']');
1973 + // echo 'Caught exception: ', $e->getMessage(), "\n";
1974 + }
1975 +
1976 + return isset($_parsed) ? $_parsed : null;
1977 + }
1978 +
1979 + /**
1980 + * Determine if Utility class contains missed function
1981 + * in other case, just return NULL to prevent ERRORS
1982 + *
1983 + * @author peshkov@UD
1984 + * @param $name
1985 + * @param $arguments
1986 + * @return mixed|null
1987 + */
1988 + public function __call($name, $arguments) {
1989 + if (is_callable(array("wpCloud\\StatelessMedia\\Utility", $name))) {
1990 + return call_user_func_array(array("wpCloud\\StatelessMedia\\Utility", $name), $arguments);
1991 + } else {
1992 + return NULL;
1993 + }
1994 + }
1995 +
1996 + /**
1997 + * Get all thumbnail sizes
1998 + *
1999 + * @global $_wp_additional_image_sizes
2000 + * @uses get_intermediate_image_sizes()
2001 + *
2002 + * @param boolean [$unset_disabled = true]
2003 + * @return array
2004 + */
2005 + function get_image_sizes($unset_disabled = true) {
2006 + $wais = &$GLOBALS['_wp_additional_image_sizes'];
2007 +
2008 + $sizes = array();
2009 +
2010 + foreach (get_intermediate_image_sizes() as $_size) {
2011 + if (in_array($_size, array('thumbnail', 'medium', 'medium_large', 'large'))) {
2012 + $sizes[$_size] = array(
2013 + 'width' => get_option("{$_size}_size_w"),
2014 + 'height' => get_option("{$_size}_size_h"),
2015 + 'crop' => (bool) get_option("{$_size}_crop"),
2016 + );
2017 + } elseif (isset($wais[$_size])) {
2018 + $sizes[$_size] = array(
2019 + 'width' => $wais[$_size]['width'],
2020 + 'height' => $wais[$_size]['height'],
2021 + 'crop' => $wais[$_size]['crop'],
2022 + );
2023 + }
2024 +
2025 + // size registered, but has 0 width and height
2026 + if ($unset_disabled && ($sizes[$_size]['width'] == 0) && ($sizes[$_size]['height'] == 0))
2027 + unset($sizes[$_size]);
2028 + }
2029 +
2030 + return $sizes;
2031 + }
2032 +
2033 + /**
2034 + * Get the email for notifications
2035 + *
2036 + * @return string
2037 + */
2038 + public function get_notification_email() {
2039 + $type = $this->get( 'sm.status_email_type' );
2040 +
2041 + switch ($type) {
2042 + case 'true':
2043 + // Emails to Admin
2044 + return get_site_option('admin_email');
2045 + case 'custom':
2046 + // Emails to Custom Email
2047 + return $this->get( 'sm.status_email_address' );
2048 + }
2049 +
2050 + // Emails disabled
2051 + return '';
2052 + }
2053 +
2054 + /**
2055 + * Send admin email
2056 + *
2057 + * @param string $subject
2058 + * @param string $message
2059 + * @param string $email
2060 + */
2061 + public function send_admin_email($subject, $message, $email = '') {
2062 + $admin_email = empty($email) ? $this->get_notification_email() : $email;
2063 +
2064 + $admin_email = explode(',', $admin_email);
2065 + $admin_email = array_map( function($item) {
2066 + return strip_tags( trim($item) );
2067 + }, $admin_email);
2068 +
2069 + if ( empty($admin_email) || empty($subject) || empty($message) ) {
2070 + return;
2071 + }
2072 +
2073 + wp_mail( $admin_email, $subject, $message);
2074 + }
2075 +
2076 + /**
2077 + * Check if we are in specific mode
2078 + *
2079 + * @param string|array $mode
2080 + * @return bool
2081 + */
2082 + public function is_mode($mode) {
2083 + if ( !is_array($mode) ) {
2084 + $mode = [$mode];
2085 + }
2086 +
2087 + return in_array($this->get('sm.mode'), $mode);
2088 + }
2089 +
2090 + /**
2091 + * Get default cache control value
2092 + *
2093 + * @return string
2094 + */
2095 + public function get_default_cache_control() {
2096 + return Settings::DEFAULT_CACHE_CONTROL;
2097 + }
2098 +
2099 + /**
2100 + * Override REST API root for headless CMS
2101 + *
2102 + * @return string
2103 + */
2104 + public function get_rest_api_root($rest_api_root) {
2105 + $rest_api_root = sanitize_url( get_rest_url() );
2106 + $rest_api_root .= 'wp-stateless/v1/';
2107 +
2108 + if ( $this->get('sm.use_api_siteurl') == 'WP_SITEURL' ) {
2109 + $home = get_home_url();
2110 + $site = get_site_url();
2111 +
2112 + return str_replace( $home, $site, $rest_api_root );
2113 + }
2114 +
2115 +
2116 + return $rest_api_root;
2117 + }
2118 + }
2119 + }
2120 +}