PluginProbe
Automatic YouTube Gallery – Embed Auto-Updating YouTube Video Galleries, Feeds, Playlists & Channels / trunk
Automatic YouTube Gallery – Embed Auto-Updating YouTube Video Galleries, Feeds, Playlists & Channels vtrunk
2.9.1 2.9.0 trunk 1.0.0 1.1.0 1.2.0 1.3.0 1.4.0 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 2.0.0 2.1.0 2.2.0 2.3.2 2.3.3 2.3.5 2.3.6 2.3.8 2.3.9 2.4.3 All 37 releases
automatic-youtube-gallery / public / public.php

public.php in Automatic YouTube Gallery – Embed Auto-Updating YouTube Video Galleries, Feeds, Playlists & Channels trunk, at public/public.php

507 lines 19.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * The public-facing functionality of the plugin.
5 *
6 * @link https://plugins360.com
7 * @since 1.0.0
8 *
9 * @package Automatic_YouTube_Gallery
10 */
11
12 // Exit if accessed directly
13 if ( ! defined( 'WPINC' ) ) {
14 die;
15 }
16
17 /**
18 * AYG_Public class.
19 *
20 * @since 1.0.0
21 */
22 class AYG_Public {
23
24 /**
25 * Get things started.
26 *
27 * @since 1.0.0
28 */
29 public function __construct() {
30 add_shortcode( 'automatic_youtube_gallery', array( $this, 'shortcode_automatic_youtube_gallery' ) );
31 }
32
33 /**
34 * Enqueue styles for the public-facing side of the site.
35 *
36 * @since 1.0.0
37 */
38 public function register_styles() {
39 $general_settings = ayg_get_option( 'ayg_general_settings' );
40
41 // Register Styles
42 wp_register_style(
43 AYG_SLUG . '-public',
44 AYG_URL . 'public/assets/css/public.min.css',
45 array(),
46 AYG_VERSION,
47 'all'
48 );
49
50 // Enqueue Styles
51 if ( ! empty( $general_settings['force_load_assets']['css'] ) ) {
52 wp_enqueue_style( AYG_SLUG . '-public' );
53 }
54 }
55
56 /**
57 * Enqueue scripts for the public-facing side of the site.
58 *
59 * @since 1.0.0
60 */
61 public function register_scripts() {
62 $general_settings = ayg_get_option( 'ayg_general_settings' );
63 $gallery_settings = ayg_get_option( 'ayg_gallery_settings' );
64 $player_settings = ayg_get_option( 'ayg_player_settings' );
65 $privacy_settings = ayg_get_option( 'ayg_privacy_settings' );
66 $strings_settings = ayg_get_option( 'ayg_strings_settings' );
67
68 $player_type = isset( $player_settings['player_type'] ) ? sanitize_text_field( $player_settings['player_type'] ) : 'youtube';
69
70 // YouTube rejects embeds rendered inside wp-admin with "Error 153" (missing/unacceptable
71 // referrer). Force the Plyr.js player in editor previews so the poster image is shown
72 // instead of the failed native embed. Front-end output is unaffected.
73 if ( is_admin() ) {
74 $player_type = 'custom';
75 }
76
77 $scroll_top_offset = ( isset( $gallery_settings['scroll_top_offset'] ) && ! empty( $gallery_settings['scroll_top_offset'] ) ) ? (int) $gallery_settings['scroll_top_offset'] : 10;
78 $scroll_top_offset = apply_filters( 'ayg_gallery_scrolltop_offset', $scroll_top_offset ); // Backward compatibility to 2.4.3
79 $scroll_top_offset = apply_filters( 'ayg_gallery_scroll_top_offset', $scroll_top_offset );
80
81 $show_more_label = ! empty( $strings_settings['show_more_label'] ) ? sanitize_text_field( $strings_settings['show_more_label'] ) : __( 'Show More', 'automatic-youtube-gallery' );
82 $show_less_label = ! empty( $strings_settings['show_less_label'] ) ? sanitize_text_field( $strings_settings['show_less_label'] ) : __( 'Show Less', 'automatic-youtube-gallery' );
83
84 $script_args = array(
85 'plugin_url' => AYG_URL,
86 'plugin_version' => AYG_VERSION,
87 'ajax_url' => admin_url( 'admin-ajax.php' ),
88 'ajax_nonce' => wp_create_nonce( 'ayg_ajax_nonce' ),
89 'current_page_url' => get_permalink(),
90 'current_gallery_id' => get_query_var( 'ayg_gallery_id' ),
91 'player_type' => $player_type,
92 'player_color' => isset( $player_settings['player_color'] ) ? sanitize_text_field( $player_settings['player_color'] ) : '#00b3ff',
93 'privacy_enhanced_mode' => isset( $player_settings['privacy_enhanced_mode'] ) ? (int) $player_settings['privacy_enhanced_mode'] : 0,
94 'origin' => '',
95 'cookieconsent' => 0,
96 'top_offset' => $scroll_top_offset,
97 'i18n' => array(
98 'show_more' => $show_more_label,
99 'show_less' => $show_less_label
100 )
101 );
102
103 if ( isset( $player_settings['origin'] ) && ! empty( $player_settings['origin'] ) ) {
104 $url_parts = parse_url( site_url() );
105 $script_args['origin'] = $url_parts['scheme'] . '://' . $url_parts['host'];
106 }
107
108 if ( ! isset( $_COOKIE['ayg_gdpr_consent'] ) ) {
109 if ( ! empty( $privacy_settings['cookie_consent'] ) && ! empty( $privacy_settings['consent_message'] ) && ! empty( $privacy_settings['button_label'] ) ) {
110 $script_args['cookieconsent'] = 1;
111 $script_args['cookieconsent_message'] = wp_kses_post( trim( $privacy_settings['consent_message'] ) );
112 $script_args['cookieconsent_button_label'] = esc_html( $privacy_settings['button_label'] );
113 }
114 }
115
116 // Register Scripts
117 $deps = array( 'jquery' );
118
119 wp_register_script(
120 AYG_SLUG . '-plyr',
121 AYG_URL . 'vendor/plyr/plyr.polyfilled.js',
122 array(),
123 '3.7.8',
124 array( 'strategy' => 'defer' )
125 );
126
127 if ( empty( $general_settings['force_load_assets']['js'] ) ) {
128 if ( isset( $player_settings['player_type'] ) && 'custom' == $player_settings['player_type'] ) {
129 $deps[] = AYG_SLUG . '-plyr';
130 }
131 }
132
133 wp_register_script(
134 AYG_SLUG . '-public',
135 AYG_URL . 'public/assets/js/public.min.js',
136 $deps,
137 AYG_VERSION,
138 array( 'strategy' => 'defer' )
139 );
140
141 wp_localize_script(
142 AYG_SLUG . '-public',
143 'ayg_config',
144 $script_args
145 );
146
147 wp_register_script(
148 AYG_SLUG . '-theme-classic',
149 AYG_URL . 'public/assets/js/theme-classic.min.js',
150 array( 'jquery' ),
151 AYG_VERSION,
152 array( 'strategy' => 'defer' )
153 );
154
155 // Enqueue Scripts
156 if ( ! empty( $general_settings['force_load_assets']['js'] ) ) {
157 wp_enqueue_script( AYG_SLUG . '-public' );
158 }
159 }
160
161 /**
162 * Enqueue block assets inside the block editor (iframe).
163 *
164 * Hooked to enqueue_block_assets with an is_admin() guard so styles and scripts
165 * are injected inside the iframed block editor (WP 6.3+ / WP 7.0 always) only,
166 * and not duplicated on the front end where wp_enqueue_scripts already handles them.
167 *
168 * @since 2.7.2
169 */
170 public function enqueue_block_assets() {
171 if ( ! is_admin() ) {
172 return;
173 }
174
175 $this->enqueue_editor_assets();
176 }
177
178 /**
179 * Enqueue the plugin's public styles and scripts in any editor context.
180 *
181 * Called by enqueue_block_assets() (WordPress block editor, guarded by is_admin())
182 * and hooked directly to Elementor actions so assets are also available in the
183 * Elementor editor panel and its frontend live-preview iframe:
184 * - elementor/editor/after_enqueue_scripts (admin context)
185 * - elementor/preview/enqueue_scripts (frontend context, is_admin() = false)
186 *
187 * @since 1.6.1
188 */
189 public function enqueue_editor_assets() {
190 // Styles
191 $this->register_styles();
192 wp_enqueue_style( AYG_SLUG . '-public' );
193
194 // Scripts
195 $this->register_scripts();
196
197 wp_enqueue_script( AYG_SLUG . '-public' );
198 wp_enqueue_script( AYG_SLUG . '-theme-classic' );
199 }
200
201 /**
202 * Process the shortcode [automatic_youtube_gallery].
203 *
204 * @since 1.0.0
205 * @param array $attributes An associative array of attributes.
206 * @param string $content Enclosing content.
207 * @return string Shortcode HTML output.
208 */
209 public function shortcode_automatic_youtube_gallery( $attributes, $content = null ) {
210 if ( ! empty( $content ) ) {
211 $attributes['content'] = $content;
212 }
213
214 return ayg_build_gallery( $attributes );
215 }
216
217 /**
218 * Load more videos.
219 *
220 * Registered for both wp_ajax_ and wp_ajax_nopriv_, so every value in $_POST arrives from an
221 * unauthenticated visitor. The gallery being paginated is therefore resolved server side
222 * rather than taken from the request — see resolve_gallery_request().
223 *
224 * @since 1.0.0
225 */
226 public function ajax_callback_load_videos() {
227 // Security check
228 check_ajax_referer( 'ayg_ajax_nonce', 'security' );
229
230 // Proceed safe
231 $json = array();
232 $attributes = array_map( 'sanitize_text_field', $_POST );
233
234 // Work out which gallery this request belongs to, from the site's own data.
235 $request = $this->resolve_gallery_request( $attributes );
236
237 // Only galleries this site actually displays may reach the YouTube API from here. A saved
238 // Gallery Builder record qualifies, and so does a legacy shortcode gallery that has been
239 // rendered at least once — its videos are already in the relationship table. Anything else
240 // describes a gallery that does not exist here, or a UID that could not be tied to the
241 // source sent with it, so it is refused before a single unit of API quota is spent on it.
242 if ( ! $request['gallery'] && ! ayg_db_gallery_has_videos( $request['uid'] ) ) {
243 wp_send_json_error( array(
244 'message' => __( 'No videos found matching your query.', 'automatic-youtube-gallery' )
245 ) );
246 }
247
248 $source_type = $request['type'];
249
250 // The resolved values replace whatever was posted, so the thumbnails rendered below — and
251 // the deeplink URLs built from them — belong to the gallery we resolved.
252 $attributes['uid'] = $request['uid'];
253 $attributes['type'] = $source_type;
254
255 // Videos per page and search limit are normalised exactly as ayg_build_gallery() does, so a
256 // paginated request can't ask for a page size the initial render would never produce.
257 $per_page = isset( $attributes['per_page'] ) ? (int) $attributes['per_page'] : 0;
258
259 if ( 'db' === $source_type ) {
260 $per_page = max( 0, $per_page ); // 0 = every video on a single page (DB served galleries only).
261 } else {
262 $per_page = min( 50, $per_page ); // YouTube returns at most 50 results per request.
263
264 if ( $per_page < 1 ) {
265 $per_page = 50;
266 }
267 }
268
269 $limit = isset( $attributes['limit'] ) ? min( 500, (int) $attributes['limit'] ) : 500;
270
271 if ( $limit < 1 ) {
272 $limit = 500;
273 }
274
275 // Page token. For the sources that page through the live API, every distinct token costs
276 // another API call, so only tokens this site actually issued are accepted — see
277 // ayg_sign_page_token(). A search request is answered from our own tables instead, so its
278 // page number is just a number and needs no signature.
279 $page_token = isset( $attributes['pageToken'] ) ? $attributes['pageToken'] : '';
280
281 if ( empty( $attributes['searchTerm'] ) && ayg_page_token_is_signed( $source_type ) ) {
282 $page_token = ayg_verify_page_token( $page_token, $request['uid'] );
283
284 if ( false === $page_token ) {
285 wp_send_json_error( array(
286 'message' => __( 'No videos found matching your query.', 'automatic-youtube-gallery' )
287 ) );
288 }
289 }
290
291 $api_params = array(
292 'uid' => $request['uid'],
293 'type' => $source_type,
294 'src' => $request['src'],
295 'store' => true, // Safe: the uid above is resolved server side, never posted.
296 'featured_video_id' => isset( $attributes['featured_video_id'] ) ? $attributes['featured_video_id'] : '', // Works only when type=db (deeplinked video pinned first)
297 'order' => isset( $attributes['order'] ) ? $attributes['order'] : 'date', // Works only when type=search
298 'sort_by' => isset( $attributes['sort_by'] ) ? $attributes['sort_by'] : 'date', // Works only when type=db
299 'sort_order' => isset( $attributes['sort_order'] ) ? $attributes['sort_order'] : 'desc', // Works only when type=db
300 'sort_seed' => isset( $attributes['sort_seed'] ) ? (int) $attributes['sort_seed'] : 0, // Works only when type=db + sort_by=random
301 'duration_filter' => isset( $attributes['duration_filter'] ) ? $attributes['duration_filter'] : '', // Works only when type=db
302 'duration' => isset( $attributes['duration'] ) ? (int) $attributes['duration'] : 0, // Works only when type=db
303 'limit' => $limit,
304 'maxResults' => $per_page,
305 'cache' => (int) apply_filters( 'ayg_ajax_cache_duration', $request['cache'], $attributes ),
306 'pageToken' => $page_token
307 );
308
309 if ( ! empty( $attributes['searchTerm'] ) ) {
310 $api_params['searchTerm'] = $attributes['searchTerm'];
311 }
312
313 $youtube_api = new AYG_YouTube_API();
314 $response = $youtube_api->query( $api_params );
315
316 if ( ! isset( $response->error ) ) {
317 if ( isset( $response->page_info ) ) {
318 $json = $response->page_info;
319
320 // Sign the tokens handed back to the browser, exactly as the initial render does,
321 // so the next page request can be verified the same way.
322 if ( ayg_page_token_is_signed( $source_type ) ) {
323 $json = ayg_sign_page_tokens( $json, $request['uid'] );
324 }
325
326 $json['message'] = sprintf(
327 _n( '%s video found matching your query.', '%s videos found matching your query.', $json['videos_found'], 'automatic-youtube-gallery' ),
328 number_format_i18n( $json['videos_found'] )
329 );
330 }
331
332 if ( isset( $response->videos ) ) {
333 $videos = $response->videos;
334 $columns = isset( $attributes['columns'] ) ? min( 12, max( 1, (int) $attributes['columns'] ) ) : 3;
335
336 ob_start();
337 foreach ( $videos as $index => $video ) {
338 $classes = array();
339 $classes[] = 'ayg-video';
340 $classes[] = 'ayg-video-' . $video->id;
341 $classes[] = 'ayg-col';
342 $classes[] = 'ayg-col-' . $columns;
343 if ( $columns > 3 ) $classes[] = 'ayg-col-sm-3';
344 if ( $columns > 2 ) $classes[] = 'ayg-col-xs-2';
345
346 echo'<div class="' . implode( ' ', $classes ) . '">';
347 the_ayg_gallery_thumbnail( $video, $attributes );
348 echo '</div>';
349 }
350 $json['html'] = ob_get_clean();
351 }
352
353 wp_send_json_success( $json );
354 } else {
355 $json['message'] = $response->error_message;
356 wp_send_json_error( $json );
357 }
358 }
359
360 /**
361 * Work out which gallery an AJAX request belongs to, without trusting the request.
362 *
363 * The gallery UID decides which gallery any videos fetched by the request are linked to, and a
364 * UID on its own proves nothing — it is printed in the page for anyone to read. So it is only
365 * accepted when it is demonstrably tied to the source posted with it.
366 *
367 * A numeric UID may be a Gallery Builder gallery ID — but it may equally be a legacy shortcode
368 * gallery that was given a numeric "uid" attribute ( e.g. [automatic_youtube_gallery
369 * type="channel" channel="UC..." uid="5"] ). So a numeric UID is only treated as a Builder
370 * gallery when the request is genuinely tied to that gallery's own saved source — signed for
371 * it, or, for pages cached before this version, posting that exact source. Otherwise it is
372 * resolved as the legacy gallery it is, so a legacy "uid" that happens to match a Builder ID is
373 * never misrouted to the wrong gallery.
374 *
375 * For a Builder gallery the source type and value come from the saved row, the one thing a
376 * shortcode cannot override. Its other settings, cache duration included, can be overridden per
377 * shortcode, so the posted cache is honoured when the signature proves it.
378 *
379 * A legacy shortcode gallery has no saved row, so its UID must prove itself in one of two ways:
380 *
381 * 1. It is the source's own fingerprint. ayg_build_gallery() derives the UID as
382 * md5( source type + source ), so for the great majority of galleries the posted UID
383 * already proves which source it belongs to and needs nothing else.
384 * 2. It carries a valid signature. A gallery can set its own UID through the "uid" shortcode
385 * attribute or the ayg_gallery_id filter, and neither can be recomputed here. For those the
386 * render signs the UID together with its source, and that pairing is verified here.
387 *
388 * Either way the caller cannot combine one gallery's UID with another source, which is what
389 * the whole fix rests on.
390 *
391 * @since 2.9.0
392 * @access private
393 * @param array $attributes Sanitized $_POST values.
394 * @return array Resolved "gallery" row (null for legacy galleries), "uid",
395 * "type" and "src". The UID is an empty string when the request
396 * could not be tied to a gallery.
397 */
398 private function resolve_gallery_request( $attributes ) {
399 $requested_uid = isset( $attributes['uid'] ) ? (string) $attributes['uid'] : '';
400 $source_type = isset( $attributes['type'] ) ? $attributes['type'] : '';
401 $source_url = isset( $attributes['src'] ) ? $attributes['src'] : '';
402 $signature = isset( $attributes['signature'] ) ? $attributes['signature'] : '';
403 $cache = isset( $attributes['cache'] ) ? (int) $attributes['cache'] : 0;
404
405 // A numeric UID might be a Gallery Builder gallery — look it up, but only commit to that
406 // reading when the request is actually tied to the gallery's own source ( see the method
407 // note ). This is what stops a legacy gallery using a numeric "uid" attribute from being
408 // misrouted to a Builder gallery that happens to share the number.
409 $gallery = ( '' !== $requested_uid && ctype_digit( $requested_uid ) ) ? ayg_get_gallery( (int) $requested_uid ) : null;
410
411 if ( $gallery ) {
412 $source = ayg_get_gallery_source( $gallery );
413
414 // Signed for this gallery's own source ( normal, post-2.9.0 render ), or — for pages
415 // cached before this version, which carry no signature — posting that exact source.
416 $is_signed = ( '' !== $signature && hash_equals( ayg_get_gallery_signature( $requested_uid, $source['type'], $source['src'], $cache ), $signature ) );
417 $matches_source = ( $source['type'] === $source_type && $source['src'] === $source_url );
418
419 if ( $is_signed || $matches_source ) {
420 return array(
421 'gallery' => $gallery,
422 'uid' => strval( $gallery->id ),
423 'type' => $source['type'],
424 'src' => $source['src'],
425 'cache' => $is_signed ? max( 0, $cache ) : $this->get_gallery_cache_duration( $gallery )
426 );
427 }
428
429 // Not this Builder gallery after all — fall through and resolve as a legacy gallery.
430 }
431
432 // Legacy shortcode gallery.
433 //
434 // Route 1 — the UID is the fingerprint of the source posted with it. Accepting this
435 // without a signature also means pages rendered before this version, including ones held
436 // in a full page cache, keep paginating normally.
437 $is_derived = ( '' !== $requested_uid && hash_equals( md5( $source_type . $source_url ), $requested_uid ) );
438
439 // Route 2 — the UID, source and cache duration were all signed together when the gallery
440 // was rendered, so the whole set is known to be one this site issued. This is the route a
441 // legacy gallery with a custom numeric "uid" takes.
442 $is_signed = ( '' !== $signature && hash_equals( ayg_get_gallery_signature( $requested_uid, $source_type, $source_url, $cache ), $signature ) );
443
444 return array(
445 'gallery' => null,
446 'uid' => ( $is_derived || $is_signed ) ? $requested_uid : '',
447 'type' => $source_type,
448 'src' => $source_url,
449 // The gallery's own "Cache Duration" setting is honoured in full — including "No
450 // Caching" — because a signed request proves the value is the one the shortcode was
451 // rendered with rather than one the caller picked. Only an unsigned request (a page
452 // rendered before this version) falls back to the default.
453 'cache' => $is_signed ? max( 0, $cache ) : DAY_IN_SECONDS
454 );
455 }
456
457 /**
458 * Read a Gallery Builder gallery's saved cache duration.
459 *
460 * Taken from the gallery's own row rather than the request, so there is nothing for a caller
461 * to influence.
462 *
463 * @since 2.9.0
464 * @access private
465 * @param object $gallery Gallery row.
466 * @return int Cache duration in seconds.
467 */
468 private function get_gallery_cache_duration( $gallery ) {
469 $cache_duration = DAY_IN_SECONDS; // Matches the "Cache Duration" field default.
470
471 $params = json_decode( (string) $gallery->params, true );
472
473 if ( is_array( $params ) && isset( $params['cache'] ) ) {
474 $cache_duration = (int) $params['cache'];
475 }
476
477 return max( 0, $cache_duration );
478 }
479
480 /**
481 * Set cookie for accepting the privacy consent.
482 *
483 * @since 2.0.0
484 */
485 public function set_gdpr_cookie() {
486 // Security check
487 check_ajax_referer( 'ayg_ajax_nonce', 'security' );
488
489 // Proceed safe
490 setcookie( 'ayg_gdpr_consent', 1, time() + ( 30 * 24 * 60 * 60 ), COOKIEPATH, COOKIE_DOMAIN );
491 wp_send_json_success();
492 }
493
494 /**
495 * [SMUSH] Skip YouTube iframes from lazy loading.
496 *
497 * @since 1.5.0
498 * @param bool $skip Should skip? Default: false.
499 * @param string $src Iframe url.
500 * @return bool
501 */
502 public function smush( $skip, $src ) {
503 return false !== strpos( $src, 'youtube' );
504 }
505
506 }
507