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 / includes / functions.php

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

2,016 lines 70.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Helper Functions.
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 * Build gallery HTML output.
19 *
20 * @since 1.0.0
21 * @param array $args An array of gallery options.
22 * @return mixed
23 */
24 function ayg_build_gallery( $args ) {
25 $general_settings = ayg_get_option( 'ayg_general_settings' );
26 $strings_settings = ayg_get_option( 'ayg_strings_settings' );
27
28 global $post;
29
30 // Vars
31 $fields = ayg_get_editor_fields();
32 $excluded = array( 'popup' ); // Fields not part of attributes
33
34 // Defaults not backed by an editor field (kept through shortcode_atts): internal "db" source + sort.
35 $defaults = array(
36 'db' => '',
37 'sort_by' => 'date',
38 'sort_order' => 'desc',
39 'sort_seed' => '',
40 'duration_filter' => '',
41 'duration' => 0
42 );
43
44 foreach ( $fields as $key => $value ) {
45 foreach ( $value['fields'] as $field ) {
46 if ( in_array( $field['name'], $excluded ) ) {
47 continue;
48 }
49
50 $defaults[ $field['name'] ] = $field['value'];
51 }
52 }
53
54 $defaults = array_merge( $defaults, (array) $strings_settings );
55
56 // Gallery Builder mode: a numeric "id" loads the saved gallery config, layers its saved
57 // settings under $args (shortcode attributes still win, except the source), and switches
58 // the source to the internal "db" type so the existing build pipeline serves it from the DB.
59 if ( isset( $args['id'] ) && is_numeric( $args['id'] ) && (int) $args['id'] > 0 ) {
60 $config = ayg_get_gallery( (int) $args['id'] );
61
62 if ( ! $config ) {
63 return sprintf( '<div class="ayg ayg-error">%s</div>', esc_html__( 'Gallery not found.', 'automatic-youtube-gallery' ) );
64 }
65
66 $config_params = json_decode( (string) $config->params, true );
67 if ( ! is_array( $config_params ) ) {
68 $config_params = array();
69 }
70
71 // Saved settings act as defaults; explicit shortcode attributes override them.
72 $args = array_merge( $config_params, $args );
73
74 // The source always comes from the config and can't be overridden by the shortcode.
75 // Livestream, search and single video query the live API at display time (nothing is
76 // imported/stored); everything else renders from the DB. ayg_get_gallery_source() owns that
77 // mapping so the public AJAX endpoint resolves the same source from the same saved row.
78 $source = ayg_get_gallery_source( $config );
79
80 $args['type'] = $source['type'];
81 $args[ $source['field'] ] = $source['src'];
82
83 // Single video + popup mode → force the popup theme, mirroring the block and widget (which
84 // apply this before calling ayg_build_gallery()).
85 if ( 'video' === $config->source_type && ! empty( $args['popup'] ) ) {
86 $args['theme'] = 'popup';
87 }
88
89 // Gallery Builder relationships are keyed by the config ID as a string.
90 $args['uid'] = strval( $config->id );
91 }
92
93 $attributes = shortcode_atts( $defaults, $args, 'automatic_youtube_gallery' );
94
95 $attributes['post_id'] = 0;
96 if ( isset( $post->ID ) ) {
97 $attributes['post_id'] = (int) $post->ID;
98 }
99
100 $attributes['columns'] = min( 12, (int) $attributes['columns'] );
101 if ( empty( $attributes['columns'] ) ) {
102 $attributes['columns'] = 3;
103 }
104
105 $attributes['limit'] = min( 500, (int) $attributes['limit'] );
106 if ( empty( $attributes['limit'] ) ) {
107 $attributes['limit'] = 500;
108 }
109
110 $source_type = sanitize_text_field( $attributes['type'] );
111
112 // Videos per page. Database (Gallery Builder) galleries read from our own table, so 0 means
113 // "show all videos on a single page" (no pagination). The live-API sources (search, livestream)
114 // stay capped at 50 because that is YouTube's maximum number of results per request.
115 if ( 'db' === $source_type ) {
116 $attributes['per_page'] = max( 0, (int) $attributes['per_page'] );
117 } else {
118 $attributes['per_page'] = min( 50, (int) $attributes['per_page'] );
119 if ( empty( $attributes['per_page'] ) ) {
120 $attributes['per_page'] = 50;
121 }
122 }
123
124 if ( 'livestream' == $source_type ) {
125 $attributes['livestream'] = $attributes['channel'];
126 }
127
128 $attributes['lazyload'] = 0;
129 if ( ! empty( $general_settings['lazyload'] ) ) {
130 $attributes['lazyload'] = 1;
131 }
132
133 $source_url = sanitize_text_field( $attributes[ $source_type ] );
134
135 if ( isset( $args['id'] ) && ! empty( $args['id'] ) ) {
136 $attributes['id'] = absint( $args['id'] );
137 }
138
139 if ( isset( $args['uid'] ) && ! empty( $args['uid'] ) ) {
140 $attributes['uid'] = sanitize_text_field( $args['uid'] );
141 } else {
142 $attributes['uid'] = md5( $source_type . $source_url );
143 }
144
145 $attributes['uid'] = apply_filters( 'ayg_gallery_id', $attributes['uid'], $args );
146
147 // Deprecated since v2.5.8. Retained for backward compatibility.
148 $deprecated_uid = md5( $source_type . $source_url . sanitize_text_field( $attributes['theme'] ) ); // Deprecated
149 if ( isset( $args['deprecated_uid'] ) && ! empty( $args['deprecated_uid'] ) ) {
150 $deprecated_uid = sanitize_text_field( $args['deprecated_uid'] );
151 }
152
153 // Deeplinked video (premium): resolve before the query so DB-served galleries can pin it as
154 // the first item at the SQL level, keeping the per-page count exact. API-served (legacy)
155 // galleries keep the old prepend behavior below.
156 $gallery_id_from_url = get_query_var( 'ayg_gallery_id' );
157 $video_id_from_url = get_query_var( 'ayg_video_id' );
158 $deeplinked_video = false;
159
160 if ( ! empty( $video_id_from_url ) && ( $attributes['uid'] == $gallery_id_from_url || $deprecated_uid == $gallery_id_from_url ) ) {
161 $deeplinked_video = ayg_db_get_video( $video_id_from_url );
162 }
163
164 if ( ! $deeplinked_video ) {
165 $video_id_from_url = '';
166 }
167
168 $featured_video_id = ( 'db' === $source_type && ! empty( $video_id_from_url ) ) ? sanitize_text_field( $video_id_from_url ) : '';
169
170 // Random ordering: generate a per-render seed (once) so RAND(seed) stays stable across this
171 // gallery's own pagination/search AJAX calls. A seed hardcoded in the shortcode is respected.
172 if ( 'random' === $attributes['sort_by'] && empty( $attributes['sort_seed'] ) ) {
173 $attributes['sort_seed'] = wp_rand( 1, 2147483647 );
174 }
175
176 // Get Videos
177 $api_params = array(
178 'uid' => $attributes['uid'],
179 'type' => $source_type,
180 'src' => $source_url,
181 'store' => true, // Trusted context: the source comes from the rendered shortcode, not from a visitor's request.
182 'featured_video_id' => $featured_video_id, // Works only when type = "db".
183 'order' => sanitize_text_field( $attributes['order'] ), // Works only when type = "search".
184 'sort_by' => sanitize_key( $attributes['sort_by'] ), // Works only when type = "db".
185 'sort_order' => sanitize_key( $attributes['sort_order'] ), // Works only when type = "db".
186 'sort_seed' => absint( $attributes['sort_seed'] ), // Works only when type = "db" + sort_by = "random".
187 'duration_filter' => sanitize_key( $attributes['duration_filter'] ), // Works only when type = "db".
188 'duration' => absint( $attributes['duration'] ), // Works only when type = "db".
189 'limit' => $attributes['limit'], // Works only when type = "search".
190 'maxResults' => $attributes['per_page'],
191 'cache' => (int) $attributes['cache']
192 );
193
194 $api_params = apply_filters( 'ayg_youtube_api_request_params', $api_params, $args );
195
196 $youtube_api = new AYG_YouTube_API();
197 $response = $youtube_api->query( $api_params );
198
199 // Process output
200 if ( ! isset( $response->error ) ) {
201 // Store Gallery ID
202 if ( $attributes['post_id'] > 0 && isset( $attributes['deeplinking'] ) && 1 == $attributes['deeplinking'] ) {
203 $pages = ayg_get_option( 'ayg_gallery_page_ids' );
204 $page_id = $attributes['post_id'];
205
206 if ( ! in_array( $page_id, $pages ) ) {
207 $pages[] = $page_id;
208 update_option( 'ayg_gallery_page_ids', $pages );
209 }
210 }
211
212 // Gallery
213 $videos = array();
214
215 if ( ! empty( $video_id_from_url ) ) {
216 // DB-served galleries pin the deeplinked video via the SQL query itself, so the result
217 // set already starts with it. Fall back to prepending it only when the pin couldn't
218 // apply: API-served (legacy) sources, or the video is no longer part of this gallery's
219 // result set (removed from the gallery, or excluded by a duration filter).
220 $is_pinned_by_sql = ! empty( $api_params['featured_video_id'] ) && ! empty( $response->videos ) && $response->videos[0]->id == $video_id_from_url;
221
222 if ( ! $is_pinned_by_sql ) {
223 $videos[] = $deeplinked_video;
224 }
225 }
226
227 if ( isset( $response->videos ) ) {
228 if ( ! empty( $videos ) ) {
229 foreach ( $response->videos as $video ) {
230 if ( $video->id != $video_id_from_url ) {
231 $videos[] = $video;
232 }
233 }
234 } else {
235 $videos = $response->videos;
236 }
237 }
238
239 // Pagination
240 if ( isset( $response->page_info ) ) {
241 $page_info = $response->page_info;
242
243 // Sign the page tokens on their way out to the browser so the public AJAX endpoint can
244 // tell the tokens this site issued from ones a visitor made up.
245 if ( ayg_page_token_is_signed( $source_type ) ) {
246 $page_info = ayg_sign_page_tokens( $page_info, $attributes['uid'] );
247 }
248
249 $attributes = array_merge( $attributes, $api_params, $page_info );
250 }
251
252 // Theme
253 $theme = 'classic';
254
255 if ( 'video' == $source_type ) {
256 $theme = 'single';
257 } elseif ( 'livestream' == $source_type ) {
258 $theme = 'livestream';
259 } else {
260 if ( 1 == count( $videos ) ) {
261 $theme = 'single';
262 }
263 }
264
265 // Enqueue dependencies
266 wp_enqueue_style( AYG_SLUG . '-public' );
267
268 wp_enqueue_script( AYG_SLUG . '-public' );
269 if ( $attributes['theme'] == $theme && 'classic' == $attributes['theme'] ) {
270 wp_enqueue_script( AYG_SLUG . '-theme-classic' );
271 }
272
273 // Output
274 ob_start();
275 include ayg_get_template( AYG_DIR . "public/templates/theme-{$theme}.php", $attributes['theme'] );
276 return ob_get_clean();
277 } else {
278 return sprintf( '<div class="ayg ayg-error">%s</div>', wp_kses_post( $response->error_message ) );
279 }
280 }
281
282 /**
283 * Combine video attributes as a string.
284 *
285 * @since 2.5.0
286 * @param array $atts Array of video attributes.
287 * @param string Combined attributes string.
288 */
289 function ayg_combine_video_attributes( $atts ) {
290 $attributes = array();
291
292 foreach ( $atts as $key => $value ) {
293 if ( '' === $value ) {
294 $attributes[] = $key;
295 } else {
296 $attributes[] = sprintf( '%s="%s"', $key, $value );
297 }
298 }
299
300 return implode( ' ', $attributes );
301 }
302
303 /**
304 * Create custom database tables
305 *
306 * @since 2.1.0
307 */
308 function ayg_db_create_custom_tables() {
309 global $wpdb;
310
311 require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
312 $charset_collate = $wpdb->get_charset_collate();
313
314 $videos_table = $wpdb->prefix . 'ayg_videos';
315 $rel_table = $wpdb->prefix . 'ayg_gallery_relationships';
316 $old_table = $wpdb->prefix . 'ayg_galleries';
317
318 // Rename wp_ayg_galleries (old pivot) → wp_ayg_gallery_relationships.
319 // Guard: only when the old table exists AND the new name does not yet exist AND
320 // the old table is the pivot (identified by having a video_id column).
321 $old_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $old_table ) ) ) === $old_table;
322 $new_not_exist = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $rel_table ) ) ) !== $rel_table;
323
324 if ( $old_exists && $new_not_exist ) {
325 $is_old_pivot = ! empty( $wpdb->get_results( "SHOW COLUMNS FROM `$old_table` LIKE 'video_id'" ) );
326
327 if ( $is_old_pivot ) {
328 $renamed = $wpdb->query( "RENAME TABLE `$old_table` TO `$rel_table`" );
329
330 if ( false === $renamed ) {
331 // Rename failed (insufficient DB privilege on some hosts).
332 // This table holds auto-generated cache data, not user content — safe to drop.
333 $wpdb->query( "DROP TABLE IF EXISTS `$old_table`" );
334 }
335 }
336 }
337
338 // Rename columns in wp_ayg_videos (NUM → id, id → video_id).
339 // Each ALTER is guarded so the block is idempotent on re-runs.
340 $videos_table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $videos_table ) ) ) === $videos_table;
341
342 if ( $videos_table_exists ) {
343 // Rename 'id' → 'video_id' first. Must happen before NUM → id to avoid a duplicate
344 // column name error: if NUM were renamed to 'id' while the varchar 'id' still exists,
345 // MySQL would reject it with "Duplicate column name 'id'".
346 $old_id_col = $wpdb->get_row( "SHOW COLUMNS FROM `$videos_table` LIKE 'id'" );
347
348 if ( $old_id_col && false !== strpos( strtolower( $old_id_col->Type ), 'varchar' ) ) {
349 $wpdb->query( "ALTER TABLE `$videos_table` CHANGE `id` `video_id` varchar(100) NOT NULL" );
350 }
351
352 // Now safe to rename NUM → id (no column named 'id' exists at this point).
353 $has_num = ! empty( $wpdb->get_results( "SHOW COLUMNS FROM `$videos_table` LIKE 'NUM'" ) );
354
355 if ( $has_num ) {
356 $wpdb->query( "ALTER TABLE `$videos_table` CHANGE `NUM` `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT" );
357 }
358
359 // Remove duplicate rows using the new column names (idempotent).
360 $wpdb->query(
361 "DELETE v1 FROM $videos_table v1
362 INNER JOIN $videos_table v2
363 ON v1.video_id = v2.video_id AND v1.id > v2.id"
364 );
365 }
366
367 // Create/update wp_ayg_videos — dbDelta adds the 2 new columns on existing installs.
368 $sql = "CREATE TABLE $videos_table (
369 id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
370 video_id varchar(100) NOT NULL,
371 title text NOT NULL,
372 description text NOT NULL,
373 thumbnails text NOT NULL,
374 duration varchar(25) NOT NULL,
375 duration_seconds int(11) UNSIGNED NOT NULL DEFAULT 0,
376 video_type varchar(20) NOT NULL DEFAULT 'none',
377 status varchar(100) NOT NULL,
378 published_at varchar(100) NOT NULL,
379 published_at_datetime datetime NULL,
380 PRIMARY KEY (id),
381 UNIQUE KEY ayg_unique_video_id (video_id),
382 INDEX ayg_idx_published_at_datetime (published_at_datetime)
383 ) $charset_collate;";
384
385 dbDelta( $sql );
386
387 // Fallback: add UNIQUE KEY if dbDelta did not create it (edge case on existing installs).
388 $existing_keys = $wpdb->get_results( "SHOW KEYS FROM $videos_table WHERE Key_name = 'ayg_unique_video_id'" );
389
390 if ( empty( $existing_keys ) ) {
391 $wpdb->query( "ALTER TABLE $videos_table ADD UNIQUE KEY ayg_unique_video_id (video_id)" );
392 }
393
394 // Fallback: add INDEX on published_at_datetime if missing (existing installs).
395 $existing_dt_index = $wpdb->get_results( "SHOW KEYS FROM $videos_table WHERE Key_name = 'ayg_idx_published_at_datetime'" );
396
397 if ( empty( $existing_dt_index ) ) {
398 $wpdb->query( "ALTER TABLE $videos_table ADD INDEX ayg_idx_published_at_datetime (published_at_datetime)" );
399 }
400
401 // Rename NUM → id in wp_ayg_gallery_relationships (only if rename succeeded above).
402 // If the table was recreated fresh by dbDelta below, it already has 'id' — no ALTER needed.
403 $rel_table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $rel_table ) ) ) === $rel_table;
404
405 if ( $rel_table_exists ) {
406 $has_num_in_rel = ! empty( $wpdb->get_results( "SHOW COLUMNS FROM `$rel_table` LIKE 'NUM'" ) );
407
408 if ( $has_num_in_rel ) {
409 $wpdb->query( "ALTER TABLE `$rel_table` CHANGE `NUM` `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT" );
410 }
411 }
412
413 // Create/update wp_ayg_gallery_relationships (fresh creation if the rename fallback dropped the old table).
414 $sql = "CREATE TABLE $rel_table (
415 id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
416 video_id varchar(100) NOT NULL,
417 gallery_id varchar(100) NOT NULL,
418 synced_at datetime NULL,
419 PRIMARY KEY (id),
420 UNIQUE KEY ayg_unique_video_gallery (video_id, gallery_id),
421 INDEX ayg_idx_gallery_id (gallery_id)
422 ) $charset_collate;";
423
424 dbDelta( $sql );
425
426 // Fallback: add the synced_at column if dbDelta did not (existing installs).
427 $has_synced_at = ! empty( $wpdb->get_results( "SHOW COLUMNS FROM `$rel_table` LIKE 'synced_at'" ) );
428
429 if ( ! $has_synced_at ) {
430 $wpdb->query( "ALTER TABLE `$rel_table` ADD `synced_at` datetime NULL" );
431 }
432
433 // Create new wp_ayg_galleries entity table (one row per saved gallery config).
434 $galleries_table = $wpdb->prefix . 'ayg_galleries';
435
436 $sql = "CREATE TABLE $galleries_table (
437 id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
438 title text NOT NULL,
439 source_type varchar(50) NOT NULL DEFAULT 'channel',
440 source_value text NOT NULL,
441 params longtext NOT NULL,
442 import_status varchar(20) NOT NULL DEFAULT 'idle',
443 import_error text NULL,
444 import_log longtext NULL,
445 last_imported_at datetime NULL,
446 next_import_at datetime NULL,
447 video_count int(11) UNSIGNED NOT NULL DEFAULT 0,
448 created_at datetime NOT NULL,
449 updated_at datetime NOT NULL,
450 PRIMARY KEY (id),
451 INDEX idx_import (next_import_at, import_status)
452 ) $charset_collate;";
453
454 dbDelta( $sql );
455 }
456
457 /**
458 * Check whether any videos are linked to the given gallery UID.
459 *
460 * Lets the public AJAX endpoint tell a gallery this site actually displays from a UID a visitor
461 * simply made up: a gallery that has been rendered even once always has its videos in the
462 * relationship table, an invented UID never does.
463 *
464 * @since 2.9.0
465 * @param string $gallery_id Gallery UID.
466 * @return bool True when at least one video is linked to the UID.
467 */
468 function ayg_db_gallery_has_videos( $gallery_id ) {
469 global $wpdb;
470
471 $gallery_id = (string) $gallery_id;
472
473 if ( '' === $gallery_id ) {
474 return false;
475 }
476
477 $found = $wpdb->get_var(
478 $wpdb->prepare(
479 "SELECT 1 FROM {$wpdb->prefix}ayg_gallery_relationships WHERE gallery_id = %s LIMIT 1",
480 $gallery_id
481 )
482 );
483
484 return ! empty( $found );
485 }
486
487 /**
488 * Get a single video record from our custom database table "{$wpdb->prefix}ayg_videos"
489 *
490 * @since 2.1.0
491 * @param string $video_id YouTube Video ID.
492 * @return mixed
493 */
494 function ayg_db_get_video( $video_id ) {
495 global $wpdb;
496
497 $cache_key = 'ayg_'. $video_id;
498 $row = wp_cache_get( $cache_key );
499
500 if ( false === $row ) {
501 $query = $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}ayg_videos WHERE video_id = %s", $video_id );
502 $row = $wpdb->get_row( $query );
503
504 if ( $row ) {
505 if ( ! empty( $row->thumbnails ) ) {
506 $row->thumbnails = ayg_maybe_unserialize( $row->thumbnails );
507 }
508
509 // Backward compat: templates reference $video->id as the YouTube video ID.
510 $row->id = $row->video_id;
511
512 wp_cache_set( $cache_key, $row );
513 }
514 }
515
516 return $row;
517 }
518
519 /**
520 * Store videos in our custom database table "{$wpdb->prefix}ayg_videos"
521 *
522 * @since 2.1.0
523 * @param object $data YouTube API response object.
524 * @param array $attributes Array of user attributes.
525 */
526 function ayg_db_store_videos( $data, $attributes = array() ) {
527 if ( AYG_VERSION !== get_option( 'ayg_version' ) ) {
528 return false;
529 }
530
531 if ( isset( $data->kind ) && 'youtube#channelListResponse' == $data->kind ) {
532 return false;
533 }
534
535 if ( empty( $data->items ) || ! is_array( $data->items ) ) {
536 return false;
537 }
538
539 global $wpdb;
540
541 $items = $data->items;
542 $gallery_id = isset( $attributes['uid'] ) ? $attributes['uid'] : '';
543 $source_type = isset( $attributes['type'] ) ? $attributes['type'] : 'videos';
544 $store_gallery = ! empty( $gallery_id ) && 'livestream' !== $source_type;
545 $exclude = ( isset( $attributes['exclude'] ) && is_array( $attributes['exclude'] ) ) ? $attributes['exclude'] : array();
546
547 // Only videos.list responses carry contentDetails.duration and snippet.liveBroadcastContent.
548 // For other responses (playlistItems.list, search.list) the duration_seconds / video_type
549 // columns are left out entirely so existing values are never clobbered with defaults.
550 $has_video_details = isset( $data->kind ) && 'youtube#videoListResponse' === $data->kind;
551
552 $videos_table = $wpdb->prefix . 'ayg_videos';
553 $galleries_table = $wpdb->prefix . 'ayg_gallery_relationships';
554
555 $video_placeholders = array();
556 $video_values = array();
557 $gallery_placeholders = array();
558 $gallery_values = array();
559
560 foreach ( $items as $item ) {
561 $row = array();
562
563 // Video ID
564 $row['video_id'] = '';
565
566 if ( isset( $item->snippet->resourceId ) && isset( $item->snippet->resourceId->videoId ) ) {
567 $row['video_id'] = $item->snippet->resourceId->videoId;
568 } elseif ( isset( $item->contentDetails ) && isset( $item->contentDetails->videoId ) ) {
569 $row['video_id'] = $item->contentDetails->videoId;
570 } elseif ( isset( $item->id ) && isset( $item->id->videoId ) ) {
571 $row['video_id'] = $item->id->videoId;
572 } elseif ( isset( $item->id ) ) {
573 $row['video_id'] = $item->id;
574 }
575
576 if ( empty( $row['video_id'] ) ) {
577 continue;
578 }
579
580 // Skip videos on the gallery's exclude list (kept out of both tables)
581 if ( ayg_is_video_excluded( $row['video_id'], $exclude ) ) {
582 continue;
583 }
584
585 // Video title
586 $row['title'] = $item->snippet->title;
587
588 // Video description
589 $row['description'] = $item->snippet->description;
590
591 // Video thumbnails
592 $row['thumbnails'] = '';
593 if ( isset( $item->snippet->thumbnails ) ) {
594 $row['thumbnails'] = serialize( $item->snippet->thumbnails );
595 }
596
597 // Video duration
598 $row['duration'] = '';
599 if ( isset( $item->contentDetails ) && isset( $item->contentDetails->duration ) ) {
600 $row['duration'] = $item->contentDetails->duration;
601 }
602
603 if ( $has_video_details ) {
604 $row['duration_seconds'] = ayg_parse_duration_seconds( $row['duration'] );
605
606 // Raw liveBroadcastContent value ('live', 'upcoming', 'none'); shorts are not
607 // classified at import time — filter by duration_seconds <= 60 at query time.
608 $row['video_type'] = 'none';
609 if ( ! empty( $item->snippet->liveBroadcastContent ) ) {
610 $row['video_type'] = $item->snippet->liveBroadcastContent;
611 }
612 }
613
614 // Video status
615 $row['status'] = 'private';
616
617 if ( isset( $item->status ) && ( 'public' == $item->status->privacyStatus || 'unlisted' == $item->status->privacyStatus ) ) {
618 $row['status'] = 'public';
619 }
620
621 if ( isset( $item->snippet->status ) && ( 'public' == $item->snippet->status->privacyStatus || 'unlisted' == $item->snippet->status->privacyStatus ) ) {
622 $row['status'] = 'public';
623 }
624
625 if ( 'youtube#searchResult' == $item->kind ) {
626 $row['status'] = 'public';
627 }
628
629 // Video publish date
630 $row['published_at'] = $item->snippet->publishedAt;
631
632 $datetime = new DateTime( $item->snippet->publishedAt );
633 $row['published_at_datetime'] = date_format( $datetime, 'Y-m-d H:i:s' );
634
635 // Collect for bulk insert
636 if ( $has_video_details ) {
637 $video_placeholders[] = '(%s, %s, %s, %s, %s, %d, %s, %s, %s, %s)';
638
639 array_push(
640 $video_values,
641 $row['video_id'],
642 $row['title'],
643 $row['description'],
644 $row['thumbnails'],
645 $row['duration'],
646 $row['duration_seconds'],
647 $row['video_type'],
648 $row['status'],
649 $row['published_at'],
650 $row['published_at_datetime']
651 );
652 } else {
653 $video_placeholders[] = '(%s, %s, %s, %s, %s, %s, %s, %s)';
654
655 array_push(
656 $video_values,
657 $row['video_id'],
658 $row['title'],
659 $row['description'],
660 $row['thumbnails'],
661 $row['duration'],
662 $row['status'],
663 $row['published_at'],
664 $row['published_at_datetime']
665 );
666 }
667
668 if ( $store_gallery ) {
669 $gallery_placeholders[] = '(%s, %s)';
670 array_push( $gallery_values, $row['video_id'], $gallery_id );
671 }
672 }
673
674 // Bulk insert videos (2 queries total instead of N×2)
675 if ( ! empty( $video_placeholders ) ) {
676 if ( $has_video_details ) {
677 $sql = "INSERT INTO $videos_table (video_id, title, description, thumbnails, duration, duration_seconds, video_type, status, published_at, published_at_datetime)
678 VALUES " . implode( ', ', $video_placeholders ) . "
679 ON DUPLICATE KEY UPDATE
680 title = VALUES(title),
681 description = VALUES(description),
682 thumbnails = VALUES(thumbnails),
683 duration = VALUES(duration),
684 duration_seconds = VALUES(duration_seconds),
685 video_type = VALUES(video_type),
686 status = VALUES(status),
687 published_at = VALUES(published_at),
688 published_at_datetime = VALUES(published_at_datetime)";
689 } else {
690 $sql = "INSERT INTO $videos_table (video_id, title, description, thumbnails, duration, status, published_at, published_at_datetime)
691 VALUES " . implode( ', ', $video_placeholders ) . "
692 ON DUPLICATE KEY UPDATE
693 title = VALUES(title),
694 description = VALUES(description),
695 thumbnails = VALUES(thumbnails),
696 duration = VALUES(duration),
697 status = VALUES(status),
698 published_at = VALUES(published_at),
699 published_at_datetime = VALUES(published_at_datetime)";
700 }
701
702 $wpdb->query( $wpdb->prepare( $sql, $video_values ) );
703 }
704
705 // Bulk insert gallery relationships (2 queries total instead of N×2)
706 if ( ! empty( $gallery_placeholders ) ) {
707 $sql = "INSERT IGNORE INTO $galleries_table (video_id, gallery_id) VALUES " . implode( ', ', $gallery_placeholders );
708 $wpdb->query( $wpdb->prepare( $sql, $gallery_values ) );
709 }
710 }
711
712 /**
713 * Dump our plugin transients.
714 *
715 * @since 2.1.0
716 * @param string $uid Gallery uid to target. Empty string flushes all plugin transients.
717 */
718 function ayg_delete_cache( $uid = '' ) {
719 $uid = (string) $uid;
720 $keys = (array) ayg_get_option( 'ayg_transient_keys' );
721
722 // Targeted: delete only the keys prefixed with this gallery's uid, keep the rest in the registry.
723 if ( '' !== $uid ) {
724 $prefix = 'ayg_' . $uid . '_';
725 $remaining = array();
726
727 foreach ( $keys as $key ) {
728 if ( 0 === strpos( $key, $prefix ) ) {
729 delete_transient( $key );
730 } else {
731 $remaining[] = $key;
732 }
733 }
734
735 update_option( 'ayg_transient_keys', $remaining, false );
736 return;
737 }
738
739 // Full flush: every transient, plus the cached gallery page-id lookup.
740 delete_option( 'ayg_gallery_page_ids' );
741
742 foreach ( $keys as $key ) {
743 delete_transient( $key );
744 }
745
746 // Reset our DB value (autoload=no: not needed on every page load)
747 update_option( 'ayg_transient_keys', array(), false );
748 }
749
750 /**
751 * Remove a gallery's excluded videos from the relationship table.
752 *
753 * @since 2.8.0
754 * @param int $gallery_id Gallery ID.
755 * @param array $exclude Exclude entries (video IDs or URLs).
756 * @return int Number of relationships removed.
757 */
758 function ayg_delete_excluded_relationships( $gallery_id, $exclude ) {
759 global $wpdb;
760
761 $gallery_id = absint( $gallery_id );
762
763 if ( $gallery_id <= 0 || empty( $exclude ) || ! is_array( $exclude ) ) {
764 return 0;
765 }
766
767 // Resolve the exclude entries (IDs or URLs) to video IDs.
768 $video_ids = array();
769
770 foreach ( $exclude as $entry ) {
771 $video_id = ayg_get_youtube_video_id( $entry );
772
773 if ( '' !== $video_id ) {
774 $video_ids[] = $video_id;
775 }
776 }
777
778 $video_ids = array_values( array_unique( $video_ids ) );
779
780 if ( empty( $video_ids ) ) {
781 return 0;
782 }
783
784 $rel_table = $wpdb->prefix . 'ayg_gallery_relationships';
785 $placeholders = implode( ',', array_fill( 0, count( $video_ids ), '%s' ) );
786
787 return (int) $wpdb->query(
788 $wpdb->prepare(
789 "DELETE FROM $rel_table WHERE gallery_id = %s AND video_id IN ( $placeholders )",
790 array_merge( array( strval( $gallery_id ) ), $video_ids )
791 )
792 );
793 }
794
795 /**
796 * Get current address bar URL.
797 *
798 * @since 2.1.0
799 * @return string Current Page URL.
800 */
801 function ayg_get_current_url() {
802 global $wp;
803 $current_url = home_url( add_query_arg( array(), $wp->request ) );
804
805 return $current_url;
806 }
807
808 /**
809 * Get default plugin settings.
810 *
811 * @since 1.6.4
812 * @return array $defaults Array of plugin settings.
813 */
814 function ayg_get_default_settings() {
815 $defaults = array(
816 'ayg_general_settings' => array(
817 'force_load_assets' => array(
818 'css' => 'css'
819 ),
820 'api_key' => '',
821 'lazyload' => 0,
822 'development_mode' => 0
823 ),
824 'ayg_strings_settings' => array(
825 'more_button_label' => __( 'Load More', 'automatic-youtube-gallery' ),
826 'previous_button_label' => __( 'Previous', 'automatic-youtube-gallery' ),
827 'next_button_label' => __( 'Next', 'automatic-youtube-gallery' ),
828 'show_more_label' => __( 'Show More', 'automatic-youtube-gallery' ),
829 'show_less_label' => __( 'Show Less', 'automatic-youtube-gallery' )
830 ),
831 'ayg_gallery_settings' => array(
832 'theme' => 'classic',
833 'columns' => 3,
834 'per_page' => 12,
835 'thumb_ratio' => 56.25,
836 'thumb_title' => 1,
837 'thumb_title_length' => 0,
838 'thumb_excerpt' => 1,
839 'thumb_excerpt_length' => 75,
840 'pagination' => 1,
841 'pagination_type' => 'more',
842 'scroll_top_offset' => 10
843 ),
844 'ayg_player_settings' => array(
845 'player_type' => 'youtube',
846 'player_color' => '#00b3ff',
847 'player_width' => '',
848 'player_ratio' => 56.25,
849 'player_title' => 1,
850 'player_description' => 1,
851 'autoplay' => 0,
852 'autoadvance' => 1,
853 'loop' => 0,
854 'muted' => 0,
855 'controls' => 1,
856 'modestbranding' => 1,
857 'cc_load_policy' => 0,
858 'hl' => '',
859 'cc_lang_pref' => '',
860 'privacy_enhanced_mode' => 0,
861 'origin' => 0
862 ),
863 'ayg_livestream_settings' => array(
864 'fallback_message' => __( 'Sorry, but the channel is not currently streaming live content. Please check back later.', 'automatic-youtube-gallery' )
865 ),
866 'ayg_privacy_settings' => array(
867 'cookie_consent' => 0,
868 'consent_message' => __( 'Please accept YouTube cookies to play this video. By accepting you will be accessing content from YouTube, a service provided by an external third party.', 'automatic-youtube-gallery' ),
869 'button_label' => __( 'Accept', 'automatic-youtube-gallery' )
870 )
871 );
872
873 return $defaults;
874 }
875
876 /**
877 * Get editor fields.
878 *
879 * @since 1.0.0
880 * @return array Array of fields.
881 */
882 function ayg_get_editor_fields() {
883 $fields = array(
884 'source' => array(
885 'label' => __( 'Source Configuration', 'automatic-youtube-gallery' ),
886 'fields' => array(
887 array(
888 'name' => 'type',
889 'label' => __( 'Source Type', 'automatic-youtube-gallery' ),
890 'description' => '',
891 'type' => 'select',
892 'options' => ayg_get_source_types(),
893 'value' => 'playlist',
894 'sanitize_callback' => 'sanitize_key'
895 ),
896 array(
897 'name' => 'playlist',
898 'label' => __( 'YouTube Playlist ID (or) URL', 'automatic-youtube-gallery' ),
899 'description' => sprintf( '%s: https://www.youtube.com/playlist?list=XXXXXXXXXX', __( 'Example', 'automatic-youtube-gallery' ) ),
900 'type' => 'url',
901 'required' => 1,
902 'value' => '',
903 'sanitize_callback' => 'sanitize_text_field'
904 ),
905 array(
906 'name' => 'channel',
907 'label' => __( 'YouTube Channel ID (or) a Video URL from the Channel', 'automatic-youtube-gallery' ),
908 'description' => sprintf( '%s: https://www.youtube.com/channel/XXXXXXXXXX', __( 'Example', 'automatic-youtube-gallery' ) ),
909 'type' => 'url',
910 'required' => 1,
911 'value' => '',
912 'sanitize_callback' => 'sanitize_text_field'
913 ),
914 array(
915 'name' => 'username',
916 'label' => __( 'YouTube Account Username', 'automatic-youtube-gallery' ),
917 'description' => sprintf( '%s: SanRosh', __( 'Example', 'automatic-youtube-gallery' ) ),
918 'type' => 'text',
919 'required' => 1,
920 'value' => '',
921 'sanitize_callback' => 'sanitize_text_field'
922 ),
923 array(
924 'name' => 'search',
925 'label' => __( 'Search Keywords', 'automatic-youtube-gallery' ),
926 'description' => sprintf( '%s: Cartoon (space:AND , -:NOT , |:OR)', __( 'Example', 'automatic-youtube-gallery' ) ),
927 'type' => 'text',
928 'required' => 1,
929 'value' => '',
930 'sanitize_callback' => 'sanitize_text_field'
931 ),
932 array(
933 'name' => 'video',
934 'label' => __( 'YouTube Video ID (or) URL', 'automatic-youtube-gallery' ),
935 'description' => sprintf( '%s: https://www.youtube.com/watch?v=XXXXXXXXXX', __( 'Example', 'automatic-youtube-gallery' ) ),
936 'type' => 'url',
937 'required' => 1,
938 'value' => '',
939 'sanitize_callback' => 'sanitize_text_field'
940 ),
941 array(
942 'name' => 'videos',
943 'label' => __( 'YouTube Video IDs (or) URLs', 'automatic-youtube-gallery' ),
944 'description' => sprintf( '%s: https://www.youtube.com/watch?v=XXXXXXXXXX', __( 'Example', 'automatic-youtube-gallery' ) ),
945 'type' => 'textarea',
946 'required' => 1,
947 'placeholder' => __( 'Enter one video per line', 'automatic-youtube-gallery' ),
948 'value' => '',
949 'sanitize_callback' => 'sanitize_textarea_field'
950 ),
951 array(
952 'name' => 'order',
953 'label' => __( 'Search Order', 'automatic-youtube-gallery' ),
954 'description' => '',
955 'type' => 'select',
956 'options' => array(
957 'date' => __( 'Date', 'automatic-youtube-gallery' ),
958 'rating' => __( 'Rating', 'automatic-youtube-gallery' ),
959 'relevance' => __( 'Relevance', 'automatic-youtube-gallery' ),
960 'title' => __( 'Title', 'automatic-youtube-gallery' ),
961 'viewCount' => __( 'View Count', 'automatic-youtube-gallery' )
962 ),
963 'value' => 'relevance',
964 'sanitize_callback' => 'sanitize_text_field'
965 ),
966 array(
967 'name' => 'limit',
968 'label' => __( 'Search Limit', 'automatic-youtube-gallery' ),
969 'description' => __( 'Enter the number of videos to display in this gallery. Set to 0 for the maximum amount (500).', 'automatic-youtube-gallery' ),
970 'type' => 'number',
971 'min' => 0,
972 'max' => 500,
973 'value' => 0,
974 'sanitize_callback' => 'intval'
975 ),
976 array(
977 'name' => 'cache',
978 'label' => __( 'Cache Duration', 'automatic-youtube-gallery' ),
979 'description' => __( 'Specifies how frequently we should check your YouTube source for new videos/updates.', 'automatic-youtube-gallery' ),
980 'type' => 'select',
981 'options' => array(
982 '0' => ''. __( 'No Caching', 'automatic-youtube-gallery' ) . '',
983 '900' => __( '15 Minutes', 'automatic-youtube-gallery' ),
984 '1800' => __( '30 Minutes', 'automatic-youtube-gallery' ),
985 '3600' => __( '1 Hour', 'automatic-youtube-gallery' ),
986 '86400' => __( '1 Day', 'automatic-youtube-gallery' ),
987 '604800' => __( '1 Week', 'automatic-youtube-gallery' ),
988 '2419200' => __( '1 Month', 'automatic-youtube-gallery' )
989 ),
990 'value' => 86400,
991 'sanitize_callback' => 'intval'
992 )
993 )
994 ),
995 'gallery' => array(
996 'label' => __( 'Gallery Options', 'automatic-youtube-gallery' ),
997 'fields' => ayg_get_gallery_settings_fields()
998 ),
999 'player' => array(
1000 'label' => __( 'Player Options', 'automatic-youtube-gallery' ),
1001 'fields' => ayg_get_player_settings_fields()
1002 ),
1003 'search' => array(
1004 'label' => __( 'Search Form', 'automatic-youtube-gallery' ),
1005 'fields' => array(
1006 array(
1007 'name' => 'search_form',
1008 'label' => __( 'Search Form', 'automatic-youtube-gallery' ),
1009 'description' => sprintf(
1010 __( 'Check this option to enable the search form. Having issues? <a href="%s" target="_blank" rel="noopener noreferrer">Check here</a>.', 'automatic-youtube-gallery' ),
1011 'https://plugins360.com/automatic-youtube-gallery/searchform/'
1012 ),
1013 'type' => 'checkbox',
1014 'value' => 0,
1015 'sanitize_callback' => 'intval'
1016 )
1017 )
1018 )
1019 );
1020
1021 return apply_filters( 'ayg_editor_fields', $fields );
1022 }
1023
1024 /**
1025 * Fetch a single gallery row by ID.
1026 *
1027 * @since 2.8.0
1028 * @param int $gallery_id Gallery ID.
1029 * @return object|null
1030 */
1031 function ayg_get_gallery( $gallery_id ) {
1032 global $wpdb;
1033
1034 return $wpdb->get_row(
1035 $wpdb->prepare( "SELECT * FROM `{$wpdb->prefix}ayg_galleries` WHERE id = %d", absint( $gallery_id ) )
1036 );
1037 }
1038
1039 /**
1040 * Get gallery settings fields.
1041 *
1042 * @since 1.0.0
1043 * @return array $fields Array of fields.
1044 */
1045 function ayg_get_gallery_settings_fields() {
1046 $gallery_settings = ayg_get_option( 'ayg_gallery_settings' );
1047
1048 $fields = array(
1049 array(
1050 'name' => 'theme',
1051 'label' => __( 'Select Theme (Layout)', 'automatic-youtube-gallery' ),
1052 'description' => ( ayg_fs()->is_not_paying() ? sprintf( __( '<a href="%s">Upgrade Pro</a> for more themes (Popup, Inline, Slider, Playlist).', 'automatic-youtube-gallery' ), esc_url( ayg_fs()->get_upgrade_url() ) ) : '' ),
1053 'type' => 'select',
1054 'options' => array(
1055 'classic' => __( 'Classic', 'automatic-youtube-gallery' )
1056 ),
1057 'value' => $gallery_settings['theme'],
1058 'sanitize_callback' => 'sanitize_key'
1059 ),
1060 array(
1061 'name' => 'columns',
1062 'label' => __( 'Columns', 'automatic-youtube-gallery' ),
1063 'description' => __( 'Enter the number of columns you like to have in the gallery. Maximum of 12.', 'automatic-youtube-gallery' ),
1064 'type' => 'number',
1065 'min' => 0,
1066 'max' => 12,
1067 'value' => $gallery_settings['columns'],
1068 'sanitize_callback' => 'intval'
1069 ),
1070 array(
1071 'name' => 'per_page',
1072 'label' => __( 'Videos per Page', 'automatic-youtube-gallery' ),
1073 'description' => __( 'Enter the number of videos to show per page. Maximum of 50.', 'automatic-youtube-gallery' ),
1074 'type' => 'number',
1075 'min' => 0,
1076 'max' => 50,
1077 'value' => $gallery_settings['per_page'],
1078 'sanitize_callback' => 'intval'
1079 ),
1080 array(
1081 'name' => 'thumb_ratio',
1082 'label' => __( 'Image Height (Ratio)', 'automatic-youtube-gallery' ),
1083 'description' => __( 'Select the ratio value used to calculate the image height in the gallery thumbnails.', 'automatic-youtube-gallery' ),
1084 'type' => 'select',
1085 'options' => array(
1086 '56.25' => __( 'Standard (16:9) — Default', 'automatic-youtube-gallery' ),
1087 '177.78' => __( 'Shorts / Vertical (9:16)', 'automatic-youtube-gallery' ),
1088 '75' => __( 'Classic (4:3)', 'automatic-youtube-gallery' )
1089 ),
1090 'value' => $gallery_settings['thumb_ratio'],
1091 'sanitize_callback' => 'floatval'
1092 ),
1093 array(
1094 'name' => 'thumb_title',
1095 'label' => __( 'Show Video Title', 'automatic-youtube-gallery' ),
1096 'description' => __( 'Check this option to show the video title in each gallery item.', 'automatic-youtube-gallery' ),
1097 'type' => 'checkbox',
1098 'value' => $gallery_settings['thumb_title'],
1099 'sanitize_callback' => 'intval'
1100 ),
1101 array(
1102 'name' => 'thumb_title_length',
1103 'label' => __( 'Video Title Length', 'automatic-youtube-gallery' ),
1104 'description' => __( 'Enter the number of characters you like to show in the title. Set 0 to show the whole title.', 'automatic-youtube-gallery' ),
1105 'type' => 'number',
1106 'min' => 0,
1107 'max' => 500,
1108 'value' => $gallery_settings['thumb_title_length'],
1109 'sanitize_callback' => 'intval'
1110 ),
1111 array(
1112 'name' => 'thumb_excerpt',
1113 'label' => __( 'Show Video Excerpt (Short Description)', 'automatic-youtube-gallery' ),
1114 'description' => __( 'Check this option to show the short description of a video in each gallery item.', 'automatic-youtube-gallery' ),
1115 'type' => 'checkbox',
1116 'value' => $gallery_settings['thumb_excerpt'],
1117 'sanitize_callback' => 'intval'
1118 ),
1119 array(
1120 'name' => 'thumb_excerpt_length',
1121 'label' => __( 'Video Excerpt Length', 'automatic-youtube-gallery' ),
1122 'description' => __( 'Enter the number of characters you like to have in the video excerpt. Set 0 to show the whole description.', 'automatic-youtube-gallery' ),
1123 'type' => 'number',
1124 'min' => 0,
1125 'max' => 500,
1126 'value' => $gallery_settings['thumb_excerpt_length'],
1127 'sanitize_callback' => 'intval'
1128 ),
1129 array(
1130 'name' => 'pagination',
1131 'label' => __( 'Pagination', 'automatic-youtube-gallery' ),
1132 'description' => __( 'Check this option to show the pagination.', 'automatic-youtube-gallery' ),
1133 'type' => 'checkbox',
1134 'value' => $gallery_settings['pagination'],
1135 'sanitize_callback' => 'intval'
1136 ),
1137 array(
1138 'name' => 'pagination_type',
1139 'label' => __( 'Pagination Type', 'automatic-youtube-gallery' ),
1140 'type' => 'select',
1141 'options' => array(
1142 'more' => __( 'More Button', 'automatic-youtube-gallery' ),
1143 'pager' => __( 'Pager', 'automatic-youtube-gallery' )
1144
1145 ),
1146 'value' => $gallery_settings['pagination_type'],
1147 'sanitize_callback' => 'sanitize_key'
1148 )
1149 );
1150
1151 return apply_filters( 'ayg_gallery_settings_fields', $fields );
1152 }
1153
1154 /**
1155 * Build the signature that ties a gallery UID to the source and cache duration it was rendered
1156 * with.
1157 *
1158 * A gallery UID is printed into the page, so it is not a secret and cannot be trusted on its own.
1159 * What this protects is the *pairing*: a request may only ask for the source that was rendered
1160 * alongside that UID, at the cache duration that gallery was configured with.
1161 *
1162 * It exists because a UID cannot always be recomputed. ayg_build_gallery() derives it as
1163 * md5( type + source ), but a gallery can override that through the "uid" shortcode attribute or
1164 * the ayg_gallery_id filter, and neither is knowable from the request alone. Signing the whole set
1165 * at render time lets the public AJAX endpoint accept such galleries without also having to accept
1166 * whatever source — or cache duration — the caller claims goes with them.
1167 *
1168 * Including the cache duration is what lets the "Cache Duration" gallery option keep working
1169 * exactly as the site owner set it, right down to "No Caching", while still making it impossible
1170 * for a visitor to bypass the cache and force live API requests.
1171 *
1172 * Each part is length prefixed so that no two different sets of values can produce the same
1173 * string to sign.
1174 *
1175 * @since 2.9.0
1176 * @param string $uid Gallery UID.
1177 * @param string $type Gallery source type.
1178 * @param string $src Gallery source value.
1179 * @param string|int $cache Gallery cache duration in seconds.
1180 * @return string Signature.
1181 */
1182 function ayg_get_gallery_signature( $uid, $type, $src, $cache ) {
1183 $parts = array( (string) $uid, (string) $type, (string) $src, (string) (int) $cache );
1184 $data = 'ayg_gallery';
1185
1186 foreach ( $parts as $part ) {
1187 $data .= '|' . strlen( $part ) . ':' . $part;
1188 }
1189
1190 return substr( wp_hash( $data ), 0, 16 );
1191 }
1192
1193 /**
1194 * Resolve a saved gallery's source type and value.
1195 *
1196 * Livestream, search and single video galleries query the YouTube API at display time — nothing
1197 * is imported for them — so they keep their own source type. Everything else is served from the
1198 * custom tables through the internal "db" source type, keyed by the gallery ID.
1199 *
1200 * Both the renderer (ayg_build_gallery) and the public AJAX endpoint resolve a saved gallery's
1201 * source through this function, so an untrusted request can never point a gallery at a source
1202 * other than the one stored in its own row.
1203 *
1204 * @since 2.9.0
1205 * @param object $gallery Gallery row from "{$wpdb->prefix}ayg_galleries".
1206 * @return array Source "type", the matching attribute name in "field", and the
1207 * source value in "src".
1208 */
1209 function ayg_get_gallery_source( $gallery ) {
1210 $source_type = isset( $gallery->source_type ) ? $gallery->source_type : '';
1211 $source_value = isset( $gallery->source_value ) ? $gallery->source_value : '';
1212
1213 if ( 'livestream' === $source_type ) {
1214 return array(
1215 'type' => 'livestream',
1216 'field' => 'channel',
1217 'src' => $source_value
1218 );
1219 }
1220
1221 if ( 'search' === $source_type ) {
1222 return array(
1223 'type' => 'search',
1224 'field' => 'search',
1225 'src' => $source_value
1226 );
1227 }
1228
1229 if ( 'video' === $source_type ) {
1230 return array(
1231 'type' => 'video',
1232 'field' => 'video',
1233 'src' => $source_value
1234 );
1235 }
1236
1237 return array(
1238 'type' => 'db',
1239 'field' => 'db',
1240 'src' => strval( $gallery->id )
1241 );
1242 }
1243
1244 /**
1245 * Return the schedule interval options for import scheduling.
1246 *
1247 * @since 2.8.0
1248 * @return array Value (seconds as string) => label pairs.
1249 */
1250 function ayg_get_import_schedule_options() {
1251 return array(
1252 'paused' => __( '— Pause Automatic Imports —', 'automatic-youtube-gallery' ),
1253 '0' => __( 'Only Once', 'automatic-youtube-gallery' ),
1254 '3600' => __( 'Every Hour', 'automatic-youtube-gallery' ),
1255 '86400' => __( 'Every Day', 'automatic-youtube-gallery' ),
1256 '604800' => __( 'Every Week', 'automatic-youtube-gallery' ),
1257 '2592000' => __( 'Every Month', 'automatic-youtube-gallery' )
1258 );
1259 }
1260
1261 /**
1262 * Return the human-readable label for an import status key.
1263 *
1264 * @since 2.8.0
1265 * @param string $status Import status key (idle, running, paused, error, completed).
1266 * @param int|null $video_count Optional. Imported video count, used to resolve the
1267 * ambiguous "idle" status into "Pending Import" (nothing
1268 * imported yet) or "Up to Date" (waiting for the next sync).
1269 * @param bool $resuming Optional. True when an import was capped mid-run and parked
1270 * as 'idle' for cron to resume — shown as "Importing", not idle.
1271 * @param string $context Optional. 'log' for a per-run history entry, where a successful
1272 * run (stored as 'idle' for recurring galleries) reads "Completed"
1273 * like non-recurring runs, instead of the live-state "Idle".
1274 * @return string
1275 */
1276 function ayg_get_import_status_label( $status, $video_count = null, $resuming = false, $context = '' ) {
1277 if ( 'idle' === $status ) {
1278 // History rows record a finished run: a recurring gallery's success is stored as 'idle',
1279 // so surface it as "Completed" to match non-recurring runs rather than the live "Idle".
1280 if ( 'log' === $context ) {
1281 return __( 'Completed', 'automatic-youtube-gallery' );
1282 }
1283
1284 // If the import was capped mid-run and parked as 'idle' for cron to resume, show "Importing".
1285 if ( $resuming ) {
1286 return __( 'Importing', 'automatic-youtube-gallery' );
1287 }
1288
1289 // If the import has never run, show "Pending Import" instead of "Idle".
1290 if ( null !== $video_count ) {
1291 return ( (int) $video_count > 0 ) ? __( 'Up to Date', 'automatic-youtube-gallery' ) : __( 'Pending Import', 'automatic-youtube-gallery' );
1292 }
1293 }
1294
1295 $statuses = array(
1296 'idle' => __( 'Idle', 'automatic-youtube-gallery' ),
1297 'running' => __( 'Importing', 'automatic-youtube-gallery' ),
1298 'paused' => __( 'Paused', 'automatic-youtube-gallery' ),
1299 'error' => __( 'Error', 'automatic-youtube-gallery' ),
1300 'completed' => __( 'Completed', 'automatic-youtube-gallery' )
1301 );
1302
1303 return isset( $statuses[ $status ] ) ? $statuses[ $status ] : ucfirst( $status );
1304 }
1305
1306 /**
1307 * Retrieve a plugin option with fallback to default settings.
1308 *
1309 * @since 2.7.0
1310 * @param string $option The option name to retrieve.
1311 * @return mixed
1312 */
1313 function ayg_get_option( $option ) {
1314 $defaults = ayg_get_default_settings();
1315 $default = isset( $defaults[ $option ] ) ? $defaults[ $option ] : array();
1316
1317 $saved = get_option( $option, null );
1318
1319 // Option does not exist OR corrupted
1320 if ( null === $saved || ! is_array( $saved ) ) {
1321 return $default;
1322 }
1323
1324 // Merge saved values with defaults
1325 return wp_parse_args( $saved, $default );
1326 }
1327
1328 /**
1329 * Build the signature for a pagination token.
1330 *
1331 * Keyed on the site's own salts through wp_hash(), and bound to the gallery so a token issued for
1332 * one gallery cannot be replayed against another. Unlike a nonce this never expires, so tokens
1333 * printed into a page that is held by a full page cache keep working.
1334 *
1335 * @since 2.9.0
1336 * @param string $token Raw page token.
1337 * @param string $uid Gallery UID the token belongs to.
1338 * @return string Signature.
1339 */
1340 function ayg_get_page_token_signature( $token, $uid ) {
1341 return substr( wp_hash( 'ayg_page_token|' . (string) $uid . '|' . (string) $token ), 0, 16 );
1342 }
1343
1344 /**
1345 * Get video description to show on top of the player.
1346 *
1347 * @since 1.0.0
1348 * @param stdClass $video YouTube video object.
1349 * @param array $attributes Array of user attributes.
1350 * @param int $words_count Number of words to show by default.
1351 * @return string Video description.
1352 */
1353 function ayg_get_player_description( $video, $attributes = array(), $words_count = 30 ) {
1354 $description = $video->description;
1355
1356 $words_array = explode( ' ', strip_tags( $description ) );
1357 if ( count( $words_array ) > $words_count ) {
1358 $show_more_label = ! empty( $attributes['show_more_label'] ) ? $attributes['show_more_label'] : __( 'Show More', 'automatic-youtube-gallery' );
1359 $words_array[ $words_count ] = '<span class="ayg-player-description-dots">...</span></span><span class="ayg-player-description-more">' . $words_array[ $words_count ];
1360
1361 $description = '<span class="ayg-player-description-less">' . implode( ' ', $words_array ) . '</span>';
1362 $description .= '<a href="#" class="ayg-player-description-toggle-btn">' . esc_html( $show_more_label ) . '</a>';
1363 }
1364
1365 $description = nl2br( $description );
1366 $description = make_clickable( $description );
1367
1368 return apply_filters( 'ayg_player_description', $description, $video, $attributes, $words_count );
1369 }
1370
1371 /**
1372 * Get player settings fields.
1373 *
1374 * @since 1.0.0
1375 * @return array $fields Array of fields.
1376 */
1377 function ayg_get_player_settings_fields() {
1378 $player_settings = ayg_get_option( 'ayg_player_settings' );
1379
1380 $fields = array(
1381 array(
1382 'name' => 'player_width',
1383 'label' => __( 'Player Width', 'automatic-youtube-gallery' ),
1384 'description' => __( 'In pixels. Maximum width of the player. Leave this field empty to scale 100% of its enclosing container/html element.', 'automatic-youtube-gallery' ),
1385 'type' => 'text',
1386 'value' => isset( $player_settings['player_width'] ) ? $player_settings['player_width'] : '',
1387 'sanitize_callback' => 'ayg_sanitize_int'
1388 ),
1389 array(
1390 'name' => 'player_ratio',
1391 'label' => __( 'Player Height (Ratio)', 'automatic-youtube-gallery' ),
1392 'description' => __( 'Select the ratio value used to calculate the player height.', 'automatic-youtube-gallery' ),
1393 'type' => 'select',
1394 'options' => array(
1395 '56.25' => __( 'Standard (16:9) — Default', 'automatic-youtube-gallery' ),
1396 '177.78' => __( 'Shorts / Vertical (9:16)', 'automatic-youtube-gallery' ),
1397 '75' => __( 'Classic (4:3)', 'automatic-youtube-gallery' )
1398 ),
1399 'value' => $player_settings['player_ratio'],
1400 'sanitize_callback' => 'floatval'
1401 ),
1402 array(
1403 'name' => 'player_title',
1404 'label' => __( 'Show Video Title', 'automatic-youtube-gallery' ),
1405 'description' => __( 'Check this option to show the current playing video title on the bottom of the player.', 'automatic-youtube-gallery' ),
1406 'type' => 'checkbox',
1407 'value' => $player_settings['player_title'],
1408 'sanitize_callback' => 'intval'
1409 ),
1410 array(
1411 'name' => 'player_description',
1412 'label' => __( 'Show Video Description', 'automatic-youtube-gallery' ),
1413 'description' => __( 'Check this option to show the current playing video description on the bottom of the player.', 'automatic-youtube-gallery' ),
1414 'type' => 'checkbox',
1415 'value' => $player_settings['player_description'],
1416 'sanitize_callback' => 'intval'
1417 ),
1418 array(
1419 'name' => 'autoplay',
1420 'label' => __( 'Autoplay', 'automatic-youtube-gallery' ),
1421 'description' => __( 'Check this option to automatically start playing the initial video when the player loads.', 'automatic-youtube-gallery' ),
1422 'type' => 'checkbox',
1423 'value' => $player_settings['autoplay'],
1424 'sanitize_callback' => 'intval'
1425 ),
1426 array(
1427 'name' => 'autoadvance',
1428 'label' => __( 'Autoplay Next Video', 'automatic-youtube-gallery' ),
1429 'description' => __( 'Check this option to automatically play the next video in the list after the previous one ends.', 'automatic-youtube-gallery' ),
1430 'type' => 'checkbox',
1431 'value' => $player_settings['autoadvance'],
1432 'sanitize_callback' => 'intval'
1433 ),
1434 array(
1435 'name' => 'loop',
1436 'label' => __( 'Loop', 'automatic-youtube-gallery' ),
1437 'description' => __( 'Check this option to loop playback. In the case of a single video player, plays the initial video again and again. In the case of a gallery, plays the entire list and then starts again at the first video.', 'automatic-youtube-gallery' ),
1438 'type' => 'checkbox',
1439 'value' => $player_settings['loop'],
1440 'sanitize_callback' => 'intval'
1441 ),
1442 array(
1443 'name' => 'muted',
1444 'label' => __( 'Muted', 'automatic-youtube-gallery' ),
1445 'description' => __( 'Check this option to turn OFF the audio output of the video by default.', 'automatic-youtube-gallery' ),
1446 'type' => 'checkbox',
1447 'value' => isset( $player_settings['muted'] ) ? $player_settings['muted'] : 0,
1448 'sanitize_callback' => 'intval'
1449 ),
1450 array(
1451 'name' => 'controls',
1452 'label' => __( 'Show Player Controls', 'automatic-youtube-gallery' ),
1453 'description' => __( 'Uncheck this option to hide the video player controls.', 'automatic-youtube-gallery' ),
1454 'type' => 'checkbox',
1455 'value' => $player_settings['controls'],
1456 'sanitize_callback' => 'intval'
1457 ),
1458 array(
1459 'name' => 'modestbranding',
1460 'label' => __( 'Hide YouTube Logo', 'automatic-youtube-gallery' ),
1461 'description' => __( "Check this option to prevent the YouTube logo from displaying in the control bar. Note that a small YouTube text label will still display in the upper-right corner of a paused video when the user's mouse pointer hovers over the player.", 'automatic-youtube-gallery' ),
1462 'type' => 'checkbox',
1463 'value' => $player_settings['modestbranding'],
1464 'sanitize_callback' => 'intval'
1465 ),
1466 array(
1467 'name' => 'cc_load_policy',
1468 'label' => __( 'Force Closed Captions', 'automatic-youtube-gallery' ),
1469 'description' => __( 'Check this option to show captions by default, even if the user has turned captions off. The default behavior is based on user preference.', 'automatic-youtube-gallery' ),
1470 'type' => 'checkbox',
1471 'value' => $player_settings['cc_load_policy'],
1472 'sanitize_callback' => 'intval'
1473 ),
1474 array(
1475 'name' => 'hl',
1476 'label' => __( 'Player Language', 'automatic-youtube-gallery' ),
1477 'description' => sprintf(
1478 __( 'Specifies the player\'s interface language. Set the field\'s value to an <a href="%s" target="_blank">ISO 639-1 two-letter language code.</a>', 'automatic-youtube-gallery' ),
1479 'http://www.loc.gov/standards/iso639-2/php/code_list.php'
1480 ),
1481 'type' => 'text',
1482 'value' => $player_settings['hl'],
1483 'sanitize_callback' => 'sanitize_text_field'
1484 ),
1485 array(
1486 'name' => 'cc_lang_pref',
1487 'label' => __( 'Default Captions Language', 'automatic-youtube-gallery' ),
1488 'description' => sprintf(
1489 __( 'Specifies the default language that the player will use to display captions. Set the field\'s value to an <a href="%s" target="_blank">ISO 639-1 two-letter language code.</a>', 'automatic-youtube-gallery' ),
1490 'http://www.loc.gov/standards/iso639-2/php/code_list.php'
1491 ),
1492 'type' => 'text',
1493 'value' => $player_settings['cc_lang_pref'],
1494 'sanitize_callback' => 'sanitize_text_field'
1495 )
1496 );
1497
1498 return $fields;
1499 }
1500
1501
1502 /**
1503 * Get a single video page URL.
1504 *
1505 * @since 2.1.0
1506 * @param stdClass $video YouTube video object.
1507 * @param array $args An array of gallery options.
1508 * @return string Single video URL.
1509 */
1510 function ayg_get_single_video_url( $video, $attributes ) {
1511 return apply_filters( 'ayg_single_video_url', '', $video, $attributes );
1512 }
1513
1514 /**
1515 * Return the human-readable label for a source type key.
1516 *
1517 * @since 2.8.0
1518 * @param string $type Source type key (channel, playlist, username, search, livestream, video, videos).
1519 * @return string
1520 */
1521 function ayg_get_source_type_label( $type ) {
1522 $types = ayg_get_source_types();
1523 return isset( $types[ $type ] ) ? $types[ $type ] : ucfirst( $type );
1524 }
1525
1526 /**
1527 * Get source types.
1528 *
1529 * @since 2.8.0
1530 * @return array
1531 */
1532 function ayg_get_source_types() {
1533 return array(
1534 'channel' => __( 'Channel', 'automatic-youtube-gallery' ),
1535 'playlist' => __( 'Playlist', 'automatic-youtube-gallery' ),
1536 'username' => __( 'Username', 'automatic-youtube-gallery' ),
1537 'search' => __( 'Search Keywords', 'automatic-youtube-gallery' ),
1538 'livestream' => __( 'Livestream', 'automatic-youtube-gallery' ),
1539 'video' => __( 'Single Video', 'automatic-youtube-gallery' ),
1540 'videos' => __( 'Custom Videos List', 'automatic-youtube-gallery' )
1541 );
1542 }
1543
1544 /**
1545 * Get filtered php template file path.
1546 *
1547 * @since 1.0.0
1548 * @param array $template PHP file path.
1549 * @param string $theme Automatic YouTube Gallery Theme.
1550 * @return string Filtered file path.
1551 */
1552 function ayg_get_template( $template, $theme = '' ) {
1553 return apply_filters( 'ayg_load_template', $template, $theme );
1554 }
1555
1556 /**
1557 * Get unique ID.
1558 *
1559 * @since 1.0.0
1560 * @return string Unique ID.
1561 */
1562 function ayg_get_uniqid() {
1563 global $ayg_uniqid;
1564
1565 if ( ! $ayg_uniqid ) {
1566 $ayg_uniqid = 0;
1567 }
1568
1569 return uniqid() . ++$ayg_uniqid;
1570 }
1571
1572 /**
1573 * Get YouTube domain.
1574 *
1575 * @since 2.3.0
1576 * @return string YouTube embed domain.
1577 */
1578 function ayg_get_youtube_domain() {
1579 $player_settings = ayg_get_option( 'ayg_player_settings' );
1580
1581 $domain = 'https://www.youtube.com';
1582 if ( isset( $player_settings['privacy_enhanced_mode'] ) && ! empty( $player_settings['privacy_enhanced_mode'] ) ) {
1583 $domain = 'https://www.youtube-nocookie.com';
1584 }
1585
1586 return $domain;
1587 }
1588
1589 /**
1590 * Get the YouTube embed URL.
1591 *
1592 * @since 2.5.0
1593 * @param string $video_id YouTube video ID.
1594 * @param array $attributes Array of user attributes.
1595 * @return string Player embed URL.
1596 */
1597 function ayg_get_youtube_embed_url( $video_id, $attributes = array() ) {
1598 $player_settings = ayg_get_option( 'ayg_player_settings' );
1599
1600 $player_website = 'https://www.youtube.com';
1601 if ( isset( $player_settings['privacy_enhanced_mode'] ) && ! empty( $player_settings['privacy_enhanced_mode'] ) ) {
1602 $player_website = 'https://www.youtube-nocookie.com';
1603 }
1604
1605 if ( empty( $video_id ) ) {
1606 return '';
1607 }
1608
1609 $url = $player_website . '/embed/' . $video_id . '?enablejsapi=1&playsinline=1&rel=0';
1610
1611 if ( isset( $player_settings['origin'] ) && ! empty( $player_settings['origin'] ) ) {
1612 $site_url_parts = parse_url( site_url() );
1613 $origin = $site_url_parts['scheme'] . '://' . $site_url_parts['host'];
1614
1615 $url = add_query_arg( 'origin', $origin, $url );
1616 }
1617
1618 if ( ! is_array( $attributes ) ) {
1619 $attributes = (array) $attributes;
1620 }
1621
1622 $autoplay = isset( $attributes['autoplay'] ) ? (int) $attributes['autoplay'] : 0;
1623 if ( 1 == $autoplay ) {
1624 $url = add_query_arg( 'autoplay', 1, $url );
1625 }
1626
1627 $loop = isset( $attributes['loop'] ) ? (int) $attributes['loop'] : 0;
1628 if ( 1 == $loop ) {
1629 $url = add_query_arg( 'playlist', $video_id, $url );
1630 $url = add_query_arg( 'loop', 1, $url );
1631 }
1632
1633 $muted = isset( $attributes['muted'] ) ? (int) $attributes['muted'] : 0;
1634 if ( 1 == $muted ) {
1635 $url = add_query_arg( 'mute', 1, $url );
1636 }
1637
1638 $controls = isset( $attributes['controls'] ) ? (int) $attributes['controls'] : 1;
1639 if ( 0 == $controls ) {
1640 $url = add_query_arg( 'controls', 0, $url );
1641 }
1642
1643 $modestbranding = isset( $attributes['modestbranding'] ) ? (int) $attributes['modestbranding'] : 0;
1644 if ( 1 == $modestbranding ) {
1645 $url = add_query_arg( 'modestbranding', 1, $url );
1646 }
1647
1648 $cc_load_policy = isset( $attributes['cc_load_policy'] ) ? (int) $attributes['cc_load_policy'] : 0;
1649 if ( 1 == $cc_load_policy ) {
1650 $url = add_query_arg( 'cc_load_policy', 1, $url );
1651 }
1652
1653 if ( isset( $attributes['hl'] ) && ! empty( $attributes['hl'] ) ) {
1654 $url = add_query_arg( 'hl', sanitize_text_field( $attributes['hl'] ), $url );
1655 }
1656
1657 if ( isset( $attributes['cc_lang_pref'] ) && ! empty( $attributes['cc_lang_pref'] ) ) {
1658 $url = add_query_arg( 'cc_lang_pref', sanitize_text_field( $attributes['cc_lang_pref'] ), $url );
1659 }
1660
1661 return apply_filters( 'ayg_youtube_embed_url', $url, $video_id, $attributes );
1662 }
1663
1664 /**
1665 * Resolve a YouTube video ID from a bare ID or a YouTube URL.
1666 *
1667 * @since 2.8.0
1668 * @param string $string A video ID or YouTube URL.
1669 * @return string The 11-char video ID, or '' if none found.
1670 */
1671 function ayg_get_youtube_video_id( $string ) {
1672 $string = trim( (string) $string );
1673
1674 if ( '' === $string ) {
1675 return '';
1676 }
1677
1678 // Already a bare video ID.
1679 if ( preg_match( '~^[A-Za-z0-9_-]{11}$~', $string ) ) {
1680 return $string;
1681 }
1682
1683 // Common YouTube URL forms.
1684 if ( preg_match( '~(?:v=|/(?:embed|v|shorts|live)/|youtu\.be/)([A-Za-z0-9_-]{11})~', $string, $matches ) ) {
1685 return $matches[1];
1686 }
1687
1688 return '';
1689 }
1690
1691 /**
1692 * Inserts a new associative array after another associative array key.
1693 *
1694 * @since 2.1.0
1695 * @param string $key The associative array key to insert after.
1696 * @param array $array An array to insert in to.
1697 * @param array $new_array An array to insert.
1698 * @return array Updated array.
1699 */
1700 function ayg_insert_array_after( $key, $array, $new_array ) {
1701 if ( array_key_exists( $key, $array ) ) {
1702 $new = array();
1703
1704 foreach ( $array as $k => $value ) {
1705 $new[ $k ] = $value;
1706
1707 if ( $k === $key ) {
1708 foreach ( $new_array as $new_key => $new_value ) {
1709 $new[ $new_key ] = $new_value;
1710 }
1711 }
1712 }
1713
1714 return $new;
1715 }
1716
1717 return $array;
1718 }
1719
1720 /**
1721 * Detect if the client is using an iOS device (iPhone, iPad, or iPod).
1722 *
1723 * @return bool True if the user agent string suggests an iOS device, false otherwise.
1724 */
1725 function ayg_is_ios() {
1726 if ( empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
1727 return false;
1728 }
1729
1730 $ua = strtolower( $_SERVER['HTTP_USER_AGENT'] );
1731
1732 if (
1733 strpos( $ua, 'iphone' ) !== false ||
1734 strpos( $ua, 'ipad' ) !== false ||
1735 strpos( $ua, 'ipod' ) !== false
1736 ) {
1737 return true;
1738 }
1739
1740 return false;
1741 }
1742
1743 /**
1744 * Check whether a video ID matches a gallery's exclude list.
1745 *
1746 * @since 2.8.0
1747 * @param string $video_id YouTube video ID.
1748 * @param array $exclude Exclude entries (video IDs or URLs).
1749 * @return bool True if the video is excluded.
1750 */
1751 function ayg_is_video_excluded( $video_id, $exclude ) {
1752 if ( empty( $video_id ) || empty( $exclude ) || ! is_array( $exclude ) ) {
1753 return false;
1754 }
1755
1756 foreach ( $exclude as $entry ) {
1757 if ( false !== strpos( $entry, $video_id ) ) {
1758 return true;
1759 }
1760 }
1761
1762 return false;
1763 }
1764
1765 /**
1766 * Unserialize a stored value without letting it instantiate arbitrary classes.
1767 *
1768 * Our serialized columns only ever hold the thumbnail set from an API response — plain data plus
1769 * stdClass objects — so nothing else is allowed through. This keeps a row that was tampered with
1770 * or carried over from another install from building an object of some other class.
1771 *
1772 * @since 2.9.0
1773 * @param string $value Serialized value.
1774 * @return mixed Unserialized value, or the value unchanged when it isn't serialized.
1775 */
1776 function ayg_maybe_unserialize( $value ) {
1777 if ( ! is_serialized( $value ) ) {
1778 return $value;
1779 }
1780
1781 // is_serialized() trims the value before testing it, so trim here as well — otherwise a padded
1782 // value passes the test and then fails to unserialize. Mirrors core's maybe_unserialize().
1783 $value = trim( $value );
1784
1785 // PHP 7.0+ can restrict this natively.
1786 if ( version_compare( PHP_VERSION, '7.0', '>=' ) ) {
1787 return @unserialize( $value, array( 'allowed_classes' => array( 'stdClass' ) ) );
1788 }
1789
1790 // PHP 5.6 has no "allowed_classes" option, so screen the payload instead: "O:" introduces a
1791 // plain object and "C:" a Serializable one, so refuse the value outright if it names any class
1792 // other than stdClass. Same protection as the call above, without raising the minimum PHP
1793 // version and cutting those sites off from plugin updates.
1794 if ( preg_match_all( '/(?:^|[;{])([OC]):\d+:"([^"]*)"/', $value, $matches, PREG_SET_ORDER ) ) {
1795 foreach ( $matches as $match ) {
1796 if ( 'O' !== $match[1] || 'stdClass' !== $match[2] ) {
1797 return false;
1798 }
1799 }
1800 }
1801
1802 return @unserialize( $value );
1803 }
1804
1805 /**
1806 * Does this source type hand out signed pagination tokens?
1807 *
1808 * Only the sources that page through the live YouTube API do, because for those every distinct
1809 * page token means another API call. The rest either never reach the API at all (the Gallery
1810 * Builder "db" source) or page through a list already known to the site ("videos"), where the
1811 * page number is simply clamped to the real range and so cannot force extra calls.
1812 *
1813 * @since 2.9.0
1814 * @param string $source_type Gallery source type.
1815 * @return bool True when tokens of this source type are signed.
1816 */
1817 function ayg_page_token_is_signed( $source_type ) {
1818 return in_array( $source_type, array( 'playlist', 'channel', 'username', 'search' ), true );
1819 }
1820
1821 /**
1822 * Parse an ISO 8601 duration (e.g. PT4M13S) into total seconds.
1823 *
1824 * @since 2.8.0
1825 * @param string $iso8601 ISO 8601 duration string from the YouTube API.
1826 * @return int Duration in seconds.
1827 */
1828 function ayg_parse_duration_seconds( $iso8601 ) {
1829 if ( empty( $iso8601 ) ) {
1830 return 0;
1831 }
1832
1833 try {
1834 $interval = new DateInterval( $iso8601 );
1835 } catch ( Exception $e ) {
1836 return 0;
1837 }
1838
1839 return ( $interval->d * DAY_IN_SECONDS ) + ( $interval->h * HOUR_IN_SECONDS ) + ( $interval->i * MINUTE_IN_SECONDS ) + $interval->s;
1840 }
1841
1842 /**
1843 * Sanitize the array inputs.
1844 *
1845 * @since 2.7.0
1846 * @param array $value Input array.
1847 * @return array Sanitized array.
1848 */
1849 function ayg_sanitize_array( $value ) {
1850 return ! empty( $value ) ? array_map( 'sanitize_text_field', $value ) : array();
1851 }
1852
1853 /**
1854 * Sanitize the integer inputs, accepts empty values.
1855 *
1856 * @since 1.0.0
1857 * @param string|int $value Input value.
1858 * @return string|int Sanitized value.
1859 */
1860 function ayg_sanitize_int( $value ) {
1861 $value = intval( $value );
1862 return ( 0 == $value ) ? '' : $value;
1863 }
1864
1865 /**
1866 * Sign a pagination token before it is handed to the browser.
1867 *
1868 * Page tokens travel out to the visitor and are posted back to the public AJAX endpoint, where
1869 * each distinct token means another live call to the YouTube Data API. Signing them means only
1870 * the tokens this site actually issued are ever acted on, so a visitor cannot invent an endless
1871 * stream of new ones and burn through the site's daily API quota.
1872 *
1873 * @since 2.9.0
1874 * @param string $token Raw page token from the API response.
1875 * @param string $uid Gallery UID the token belongs to.
1876 * @return string Signed token, or an empty string when there is no token.
1877 */
1878 function ayg_sign_page_token( $token, $uid ) {
1879 $token = (string) $token;
1880
1881 if ( '' === $token ) {
1882 return '';
1883 }
1884
1885 return $token . '.' . ayg_get_page_token_signature( $token, $uid );
1886 }
1887
1888 /**
1889 * Sign the pagination tokens in a page info array.
1890 *
1891 * @since 2.9.0
1892 * @param array $page_info Page info array from the API response.
1893 * @param string $uid Gallery UID the tokens belong to.
1894 * @return array The same array with its tokens signed.
1895 */
1896 function ayg_sign_page_tokens( $page_info, $uid ) {
1897 foreach ( array( 'next_page_token', 'prev_page_token' ) as $key ) {
1898 if ( ! empty( $page_info[ $key ] ) ) {
1899 $page_info[ $key ] = ayg_sign_page_token( $page_info[ $key ], $uid );
1900 }
1901 }
1902
1903 return $page_info;
1904 }
1905
1906 /**
1907 * Trims text to a certain number of characters.
1908 *
1909 * @since 2.0.0
1910 * @param string $text Text to trim.
1911 * @param int $num_characters Number of characters.
1912 * @param string $append String to append to the end of the excerpt.
1913 * @return string Trimmed text.
1914 */
1915 function ayg_trim_words( $text, $num_characters, $append = '...' ) {
1916 $num_characters++;
1917
1918 $original_text = $text;
1919 $text = ( $num_characters > 1 ) ? wp_strip_all_tags( $original_text, true ) : nl2br( $original_text );
1920
1921 if ( $num_characters > 1 && mb_strlen( $text ) > $num_characters ) {
1922 $subex = mb_substr( $text, 0, $num_characters - 5 );
1923 $exwords = explode( ' ', $subex );
1924 $excut = - ( mb_strlen( $exwords[ count( $exwords ) - 1 ] ) );
1925
1926 if ( $excut < 0 ) {
1927 $text = mb_substr( $subex, 0, $excut );
1928 } else {
1929 $text = $subex;
1930 }
1931
1932 $text .= $append;
1933 }
1934
1935 return apply_filters( 'ayg_trim_words', $text, $original_text, $num_characters, $append );
1936 }
1937
1938 /**
1939 * Verify a pagination token that came back from the browser.
1940 *
1941 * @since 2.9.0
1942 * @param string $token Signed token as posted by the browser.
1943 * @param string $uid Gallery UID the token is claimed to belong to.
1944 * @return string|false The raw token when the signature matches, false when it does not. An
1945 * empty token is the first page and needs no signature.
1946 */
1947 function ayg_verify_page_token( $token, $uid ) {
1948 $token = (string) $token;
1949
1950 if ( '' === $token ) {
1951 return '';
1952 }
1953
1954 // The signature is appended last, and a YouTube page token can itself contain almost anything,
1955 // so split on the final separator rather than the first.
1956 $position = strrpos( $token, '.' );
1957
1958 if ( false === $position ) {
1959 return false;
1960 }
1961
1962 $raw = substr( $token, 0, $position );
1963 $signature = substr( $token, $position + 1 );
1964
1965 if ( '' === $raw || ! hash_equals( ayg_get_page_token_signature( $raw, $uid ), $signature ) ) {
1966 return false;
1967 }
1968
1969 return $raw;
1970 }
1971
1972 /**
1973 * Gallery HTML output.
1974 *
1975 * @since 1.0.0
1976 * @param array $video YouTube video object.
1977 * @param array $attributes Array of user attributes.
1978 */
1979 function the_ayg_gallery_thumbnail( $video, $attributes ) {
1980 include ayg_get_template( AYG_DIR . 'public/templates/thumbnail.php' );
1981 }
1982
1983 /**
1984 * Pagination HTML output.
1985 *
1986 * @since 1.0.0
1987 * @param array $attributes Array of user attributes.
1988 */
1989 function the_ayg_pagination( $attributes ) {
1990 if ( ! empty( $attributes['pagination'] ) ) {
1991 include ayg_get_template( AYG_DIR . 'public/templates/pagination.php' );
1992 }
1993 }
1994
1995 /**
1996 * Search Form HTML output.
1997 *
1998 * @since 2.5.7
1999 * @param array $attributes Array of user attributes.
2000 */
2001 function the_ayg_search_form( $attributes ) {
2002 if ( ! empty( $attributes['search_form'] ) ) {
2003 include ayg_get_template( AYG_DIR . 'public/templates/search-form.php' );
2004 }
2005 }
2006
2007 /**
2008 * Gallery HTML output.
2009 *
2010 * @since 2.5.0
2011 * @param array $video YouTube video object.
2012 * @param array $attributes Array of user attributes.
2013 */
2014 function the_ayg_player( $video, $attributes ) {
2015 include ayg_get_template( AYG_DIR . 'public/templates/player.php' );
2016 }