PluginProbe
Automatic YouTube Gallery – Embed Auto-Updating YouTube Video Galleries, Feeds, Playlists & Channels / 2.3.2
Automatic YouTube Gallery – Embed Auto-Updating YouTube Video Galleries, Feeds, Playlists & Channels v2.3.2
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
← All changes | admin/admin.php +48 -465 trunk2.3.2 View file →
@@ -21,17 +21,8 @@
21 21 */
22 22 class AYG_Admin {
23 23
24 24 /**
25 - * Gallery list table instance, shared between bulk-action processing
26 - * and rendering, to avoid instantiating it twice per request.
27 - *
28 - * @since 2.8.0
29 - * @var AYG_Admin_Gallery_List_Table
30 - */
31 - private $gallery_list_table;
32 -
33 - /**
34 25 * Insert missing plugin options.
35 26 *
36 27 * @since 1.6.4
37 28 */
@@ -38,44 +29,8 @@
38 29 public function insert_missing_options() {
39 30 if ( AYG_VERSION !== get_option( 'ayg_version' ) ) {
40 31 $defaults = ayg_get_default_settings();
41 32
42 - // Insert the gallery settings
43 - $gallery_settings = get_option( 'ayg_gallery_settings' );
44 -
45 - if ( ! is_array( $gallery_settings ) || empty( $gallery_settings ) ) {
46 - $gallery_settings = $defaults['ayg_gallery_settings'];
47 - update_option( 'ayg_gallery_settings', $gallery_settings );
48 - }
49 -
50 - // Insert the strings settings
51 - if ( false == get_option( 'ayg_strings_settings' ) ) {
52 - $strings_settings = array(
53 - 'more_button_label' => ! empty( $gallery_settings['more_button_label'] ) ? $gallery_settings['more_button_label'] : $defaults['ayg_strings_settings']['more_button_label'],
54 - 'previous_button_label' => ! empty( $gallery_settings['previous_button_label'] ) ? $gallery_settings['previous_button_label'] : $defaults['ayg_strings_settings']['previous_button_label'],
55 - 'next_button_label' => ! empty( $gallery_settings['next_button_label'] ) ? $gallery_settings['next_button_label'] : $defaults['ayg_strings_settings']['next_button_label'],
56 - 'show_more_label' => $defaults['ayg_strings_settings']['show_more_label'],
57 - 'show_less_label' => $defaults['ayg_strings_settings']['show_less_label'],
58 - );
59 -
60 - add_option( 'ayg_strings_settings', $strings_settings );
61 - }
62 -
63 - // Update the player settings
64 - $player_settings = get_option( 'ayg_player_settings' );
65 -
66 - if ( ! is_array( $player_settings ) || empty( $player_settings ) ) {
67 - $player_settings = $defaults['ayg_player_settings'];
68 - update_option( 'ayg_player_settings', $player_settings );
69 - }
70 -
71 - if ( ! array_key_exists( 'player_type', $player_settings ) ) {
72 - $player_settings['player_type'] = $defaults['ayg_player_settings']['player_type'];
73 - $player_settings['player_color'] = $defaults['ayg_player_settings']['player_color'];
74 -
75 - update_option( 'ayg_player_settings', $player_settings );
76 - }
77 -
78 33 // Insert the livestream settings
79 34 if ( false == get_option( 'ayg_livestream_settings' ) ) {
80 35 add_option( 'ayg_livestream_settings', $defaults['ayg_livestream_settings'] );
81 36 }
@@ -84,10 +39,12 @@
84 39 if ( false == get_option( 'ayg_privacy_settings' ) ) {
85 40 add_option( 'ayg_privacy_settings', $defaults['ayg_privacy_settings'] );
86 41 }
87 42
88 - // Create custom database tables
89 - ayg_db_create_custom_tables();
43 + // Create a custom database table "{$wpdb->prefix}ayg_videos"
44 + if ( version_compare( AYG_VERSION, '2.1.0', '<=' ) ) {
45 + ayg_db_create_videos_table();
46 + }
90 47
91 48 // Delete the plugin cache
92 49 ayg_delete_cache();
93 50
@@ -103,14 +60,14 @@
103 60 */
104 61 public function enqueue_styles() {
105 62 wp_enqueue_style( 'wp-color-picker' );
106 63
107 - wp_enqueue_style(
108 - AYG_SLUG . '-admin',
109 - AYG_URL . 'admin/assets/css/admin.min.css',
110 - array(),
111 - AYG_VERSION,
112 - 'all'
64 + wp_enqueue_style(
65 + AYG_SLUG . '-admin',
66 + AYG_URL . 'admin/assets/css/admin.min.css',
67 + array(),
68 + AYG_VERSION,
69 + 'all'
113 70 );
114 71 }
115 72
116 73 /**
@@ -121,71 +78,27 @@
121 78 public function enqueue_scripts() {
122 79 wp_enqueue_media();
123 80 wp_enqueue_script( 'wp-color-picker' );
124 81
125 - wp_enqueue_script(
126 - AYG_SLUG . '-admin',
127 - AYG_URL . 'admin/assets/js/admin.min.js',
128 - array( 'jquery' ),
129 - AYG_VERSION,
130 - false
82 + wp_enqueue_script(
83 + AYG_SLUG . '-admin',
84 + AYG_URL . 'admin/assets/js/admin.min.js',
85 + array( 'jquery' ),
86 + AYG_VERSION,
87 + false
131 88 );
132 89
133 - wp_localize_script(
134 - AYG_SLUG . '-admin',
135 - 'ayg_admin',
90 + wp_localize_script(
91 + AYG_SLUG . '-admin',
92 + 'ayg_admin',
136 93 array(
137 94 'ajax_nonce' => wp_create_nonce( 'ayg_ajax_nonce' ),
138 - 'admin_url' => admin_url( 'admin.php' ),
139 - 'live_types' => array( 'search', 'livestream', 'video' ), // Rendered live — source stays editable (mirrors gallery-form.php).
140 - 'i18n' => array(
141 - // API key
142 - 'invalid_api_key' => __( 'Please enter your YouTube API key.', 'automatic-youtube-gallery' ),
143 -
144 - // Settings
145 - 'cache_cleared' => __( 'Cleared', 'automatic-youtube-gallery' ),
146 -
147 - // Common
148 - 'save_failed' => __( 'Save failed. Please try again.', 'automatic-youtube-gallery' ),
149 - 'delete_failed' => __( 'Delete failed. Please try again.', 'automatic-youtube-gallery' ),
150 -
151 - // Gallery: Shortcode
152 - 'shortcode_copied' => __( 'Shortcode copied! Paste it into any post, page, or widget to display your gallery.', 'automatic-youtube-gallery' ),
153 -
154 - // Gallery: Delete confirmations
155 - 'confirm_delete' => __( 'Are you sure you want to delete this gallery? This action cannot be undone.', 'automatic-youtube-gallery' ),
156 - 'confirm_bulk_delete' => __( 'Are you sure you want to delete the selected galleries? This action cannot be undone.', 'automatic-youtube-gallery' ),
157 -
158 - // Gallery Form
159 - 'unsaved_changes' => __( 'You have unsaved changes. Are you sure you want to leave?', 'automatic-youtube-gallery' ),
160 - 'update_gallery' => __( 'Update Gallery', 'automatic-youtube-gallery' ),
161 -
162 - // Gallery Form: Source validation
163 - 'source_required' => array(
164 - 'channel' => __( 'A YouTube channel ID (or) a video URL from the channel is required.', 'automatic-youtube-gallery' ),
165 - 'playlist' => __( 'A YouTube playlist ID (or) URL is required.', 'automatic-youtube-gallery' ),
166 - 'username' => __( 'A YouTube account username is required.', 'automatic-youtube-gallery' ),
167 - 'search' => __( 'A search keyword is required.', 'automatic-youtube-gallery' ),
168 - 'livestream' => __( 'A YouTube channel ID (or) a video URL from the channel is required.', 'automatic-youtube-gallery' ),
169 - 'video' => __( 'A YouTube video ID (or) URL is required.', 'automatic-youtube-gallery' ),
170 - 'videos' => __( 'At least one YouTube video ID (or) URL is required.', 'automatic-youtube-gallery' )
171 - ),
172 - 'channel_handle' => __( 'YouTube @handle URLs aren’t supported here. Please enter a channel ID, a /channel/ URL, or a video URL from the channel.', 'automatic-youtube-gallery' ),
173 -
174 - // Source hint text, swapped live as the source-type dropdown changes. Must match
175 - // the strings rendered in gallery-form.php's .ayg-source-hint (live / importable).
176 - 'source_live' => __( 'This is a live source — you can change it anytime. Updates take effect immediately the next time the gallery is viewed.', 'automatic-youtube-gallery' ),
177 - 'source_importable' => __( 'The video source can’t be changed once videos are imported. So please make sure you select the right source before saving.', 'automatic-youtube-gallery' ),
178 -
179 - // Gallery Form: Import progress
180 - 'processing' => __( 'Processing', 'automatic-youtube-gallery' ),
181 - 'import_progress' => __( 'Imported: %imported%, Updated: %updated%. Please do not close this window until you see a success or error message', 'automatic-youtube-gallery' ),
182 - 'import_complete' => __( 'Done! Imported: %imported%, Updated: %updated%, Deleted: %deleted%. Refreshing the page', 'automatic-youtube-gallery' ),
183 - 'import_failed' => __( 'Import failed. Please try again.', 'automatic-youtube-gallery' ),
184 - 'quota_exceeded' => __( 'YouTube API quota exceeded. The import has been paused — click "Update Gallery" to resume after the quota resets.', 'automatic-youtube-gallery' )
185 - )
95 + 'i18n' => array(
96 + 'invalid_api_key' => __( 'Invalid API Key', 'automatic-youtube-gallery' ),
97 + 'cleared' => __( 'Cleared', 'automatic-youtube-gallery' )
98 + )
186 99 )
187 - );
100 + );
188 101 }
189 102
190 103 /**
191 104 * Add dashboard page link on the plugins menu.
@@ -210,22 +123,19 @@
210 123 * Add "Dashboard" menu.
211 124 *
212 125 * @since 1.3.0
213 126 */
214 - public function admin_menu() {
215 - $hook = add_menu_page(
216 - __( 'Automatic YouTube Gallery', 'automatic-youtube-gallery' ),
127 + public function admin_menu() {
128 + add_menu_page(
129 + __( 'Automatic YouTube Gallery', 'automatic-youtube-gallery' ),
217 130 __( 'YouTube Gallery', 'automatic-youtube-gallery' ),
218 - 'manage_options',
219 - 'automatic-youtube-gallery',
131 + 'manage_options',
132 + 'automatic-youtube-gallery',
220 133 array( $this, 'display_dashboard_content' ),
221 - 'dashicons-format-video',
222 - 10
134 + 'dashicons-video-alt3',
135 + 10
223 136 );
224 137
225 - // Process list table bulk actions before any HTML is output to allow wp_redirect().
226 - add_action( 'load-' . $hook, array( $this, 'process_list_table_bulk_actions' ) );
227 -
228 138 add_submenu_page(
229 139 'automatic-youtube-gallery',
230 140 __( 'Dashboard', 'automatic-youtube-gallery' ),
231 141 __( 'Dashboard', 'automatic-youtube-gallery' ),
@@ -235,56 +145,22 @@
235 145 );
236 146 }
237 147
238 148 /**
239 - * Runs before the dashboard page HTML is output: hides the Screen Options tab
240 - * and processes list-table bulk actions on the list view.
241 - *
242 - * @since 2.8.0
243 - */
244 - public function process_list_table_bulk_actions() {
245 - $general_settings = ayg_get_option( 'ayg_general_settings' );
246 -
247 - // Hide the Screen Options tab entirely. The list table would otherwise
248 - // auto-populate a "Columns" section there, which we don't want on our
249 - // custom dashboard header.
250 - add_filter( 'screen_options_show_screen', '__return_false' );
251 -
252 - // No list table is rendered on the setup screen or the gallery form view.
253 - $action = isset( $_GET['action'] ) ? sanitize_text_field( $_GET['action'] ) : '';
254 - if ( empty( $general_settings['api_key'] ) || 'new' === $action || 'edit' === $action ) {
255 - return;
256 - }
257 -
258 - // Strip the GET search form's leftover bulk-action fields from the URL.
259 - add_filter( 'removable_query_args', array( $this, 'add_removable_query_args' ) );
260 -
261 - // Process bulk actions
262 - require_once AYG_DIR . 'admin/galleries.php';
263 -
264 - $this->gallery_list_table = new AYG_Admin_Gallery_List_Table();
265 - $this->gallery_list_table->process_bulk_action();
266 - }
267 -
268 - /**
269 - * Add the gallery list table's bulk-action fields to the list of query args
270 - * WordPress strips from the URL via wp_admin_canonical_url().
271 - *
272 - * @since 2.8.0
273 - * @param array $args Removable query argument names.
274 - * @return array
275 - */
276 - public function add_removable_query_args( $args ) {
277 - return array_merge( $args, array( 'action', 'action2', 'bulk_action', '_wpnonce', '_wp_http_referer' ) );
278 - }
279 -
280 - /**
281 149 * Display dashboard content.
282 150 *
283 151 * @since 1.3.0
284 152 */
285 153 public function display_dashboard_content() {
286 - require_once AYG_DIR . 'admin/templates/dashboard.php';
154 + $general_settings = get_option( 'ayg_general_settings' );
155 +
156 + $tabs = array(
157 + 'dashboard' => __( 'Build Gallery', 'automatic-youtube-gallery' )
158 + );
159 +
160 + $active_tab = isset( $_GET['tab'] ) ? sanitize_text_field( $_GET['tab'] ) : 'dashboard';
161 +
162 + require_once AYG_DIR . 'admin/templates/dashboard.php';
287 163 }
288 164
289 165 /**
290 166 * Prints admin screen notices.
@@ -291,27 +167,13 @@
291 167 *
292 168 * @since 2.0.0
293 169 */
294 170 public function admin_notices() {
295 - $screen = get_current_screen();
296 - $on_page = $screen && 'toplevel_page_automatic-youtube-gallery' === $screen->id;
171 + $general_settings = get_option( 'ayg_general_settings' );
297 172
298 - // Show on all admin pages except our own gallery form (action=new|edit).
299 - if ( $on_page ) {
300 - $action = isset( $_GET['action'] ) ? sanitize_text_field( $_GET['action'] ) : '';
301 - if ( 'new' === $action || 'edit' === $action ) {
302 - return;
303 - }
304 - }
305 -
306 - $general_settings = ayg_get_option( 'ayg_general_settings' );
307 -
308 - if ( ! empty( $general_settings['development_mode'] ) ) {
309 - // WordPress relocates non-inline notices on our custom dashboard header, hiding them.
310 - // Add the "inline" class only there so the notice stays put; leave it off elsewhere.
311 - $classes = 'notice notice-info' . ( $on_page ? ' inline' : '' );
173 + if ( isset( $general_settings['development_mode'] ) && ! empty( $general_settings['development_mode'] ) ) {
312 174 ?>
313 - <div class="<?php echo esc_attr( $classes ); ?>">
175 + <div class="notice notice-info">
314 176 <p>
315 177 <?php
316 178 printf(
317 179 __( '<strong>Automatic YouTube Gallery:</strong> You have <a href="%s">development mode</a> enabled. We do not cache API results in this mode. While this is ok when you are testing the plugin, we strongly recommend disabling this option when your site goes live.', 'automatic-youtube-gallery' ),
@@ -328,295 +190,16 @@
328 190 * Save API Key.
329 191 *
330 192 * @since 1.3.0
331 193 */
332 - public function ajax_callback_save_api_key() {
194 + public function ajax_callback_save_api_key() {
333 195 check_ajax_referer( 'ayg_ajax_nonce', 'security' );
196 +
197 + $general_settings = get_option( 'ayg_general_settings' );
198 + $general_settings['api_key'] = sanitize_text_field( $_POST['api_key'] );
334 199
335 - if ( ! current_user_can( 'manage_options' ) ) {
336 - wp_send_json_error( array( 'message' => __( 'Permission denied.', 'automatic-youtube-gallery' ) ) );
337 - }
338 -
339 - $general_settings = ayg_get_option( 'ayg_general_settings' );
340 -
341 - $general_settings['api_key'] = sanitize_text_field( $_POST['api_key'] );
342 200 update_option( 'ayg_general_settings', $general_settings );
343 201
344 - wp_send_json_success();
345 - }
346 -
347 - /**
348 - * Save a gallery (insert or update).
349 - *
350 - * @since 2.8.0
351 - */
352 - public function ajax_callback_save_gallery() {
353 - check_ajax_referer( 'ayg_ajax_nonce', 'security' );
354 -
355 - if ( ! current_user_can( 'manage_options' ) ) {
356 - wp_send_json_error( array( 'message' => __( 'Permission denied.', 'automatic-youtube-gallery' ) ) );
357 - }
358 -
359 - $gallery_id = absint( isset( $_POST['gallery_id'] ) ? $_POST['gallery_id'] : 0 );
360 - $gallery_title = sanitize_text_field( isset( $_POST['title'] ) ? $_POST['title'] : '' );
361 -
362 - $params = array();
363 - $source_locked = false;
364 - $source_type = '';
365 - $source_value = '';
366 -
367 - if ( $gallery_id > 0 ) {
368 - $existing = ayg_get_gallery( $gallery_id );
369 -
370 - if ( ! $existing ) {
371 - wp_send_json_error( array( 'message' => __( 'Gallery not found.', 'automatic-youtube-gallery' ) ) );
372 - }
373 -
374 - // Seed params from the saved row so values from disabled inputs and runtime
375 - // keys (page_token, last_video_published_at) survive the update.
376 - $saved_params = json_decode( (string) $existing->params, true );
377 - if ( is_array( $saved_params ) ) {
378 - $params = $saved_params;
379 - }
380 -
381 - // The source locks only once videos have been imported. While the gallery
382 - // still has no videos (e.g. the first import failed on a bad source), the
383 - // source stays editable so it can be corrected in place. Live sources
384 - // (search / livestream / single video) never import, so they never lock.
385 - $source_locked = ( (int) $existing->video_count > 0 && ! in_array( $existing->source_type, array( 'search', 'livestream', 'video' ), true ) );
386 -
387 - if ( $source_locked ) {
388 - $source_type = $existing->source_type;
389 - $source_value = $existing->source_value;
390 - }
391 - }
392 -
393 - // Read the source from POST for a new gallery, or for an update before the first
394 - // import. A re-edited source starts a clean import, so drop the runtime keys.
395 - if ( ! $source_locked ) {
396 - unset( $params['page_token'], $params['last_video_published_at'] );
397 -
398 - $allowed_source_types = ayg_get_source_types();
399 - $source_type = sanitize_key( isset( $_POST['type'] ) ? $_POST['type'] : 'playlist' );
400 - if ( ! array_key_exists( $source_type, $allowed_source_types ) ) {
401 - $source_type = 'playlist';
402 - }
403 -
404 - switch ( $source_type ) {
405 - case 'channel':
406 - case 'livestream':
407 - $source_value = sanitize_text_field( isset( $_POST['channel'] ) ? $_POST['channel'] : '' );
408 - break;
409 -
410 - case 'playlist':
411 - $source_value = sanitize_text_field( isset( $_POST['playlist'] ) ? $_POST['playlist'] : '' );
412 - break;
413 -
414 - case 'username':
415 - $source_value = sanitize_text_field( isset( $_POST['username'] ) ? $_POST['username'] : '' );
416 - break;
417 -
418 - case 'search':
419 - $source_value = sanitize_text_field( isset( $_POST['search'] ) ? $_POST['search'] : '' );
420 - break;
421 -
422 - case 'video':
423 - $source_value = sanitize_text_field( isset( $_POST['video'] ) ? $_POST['video'] : '' );
424 - break;
425 -
426 - case 'videos':
427 - $source_value = sanitize_textarea_field( isset( $_POST['videos'] ) ? $_POST['videos'] : '' );
428 - break;
429 -
430 - default:
431 - $source_value = '';
432 - }
433 - }
434 -
435 - $exclude = sanitize_textarea_field( isset( $_POST['exclude'] ) ? $_POST['exclude'] : '' );
436 - if ( ! empty( $exclude ) ) $exclude = array_values( array_filter( array_map( 'trim', explode( "\n", $exclude ) ) ) );
437 - $params['exclude'] = $exclude;
438 -
439 - // 'paused' is a non-numeric sentinel (stop automatic imports); every other value is seconds.
440 - $schedule = sanitize_text_field( isset( $_POST['schedule'] ) ? $_POST['schedule'] : 86400 );
441 - $params['schedule'] = ( 'paused' === $schedule ) ? 'paused' : absint( $schedule );
442 -
443 - // Display-time sort (gallery-form-only fields). Validated against whitelists; get_videos_from_db()
444 - // maps them into a safe ORDER BY. Defaults reproduce the prior hardcoded "newest first".
445 - $sort_by = sanitize_key( isset( $_POST['sort_by'] ) ? $_POST['sort_by'] : '' );
446 - $params['sort_by'] = in_array( $sort_by, array( 'date', 'title', 'duration', 'random' ), true ) ? $sort_by : 'date';
447 -
448 - $sort_order = sanitize_key( isset( $_POST['sort_order'] ) ? $_POST['sort_order'] : '' );
449 - $params['sort_order'] = ( 'asc' === $sort_order ) ? 'asc' : 'desc';
450 -
451 - // Display-time duration filter. get_videos_from_db() applies it only when a direction is set.
452 - $duration_filter = sanitize_key( isset( $_POST['duration_filter'] ) ? $_POST['duration_filter'] : '' );
453 - $params['duration_filter'] = in_array( $duration_filter, array( 'long', 'short' ), true ) ? $duration_filter : '';
454 - $params['duration'] = absint( isset( $_POST['duration'] ) ? $_POST['duration'] : 0 );
455 -
456 - // Fields stored in dedicated DB columns, not in params. 'cache' is kept (saved to params) so
457 - // the live 'search' source type can control its API cache duration.
458 - $fields = ayg_get_editor_fields();
459 - $excluded = array( 'type', 'channel', 'playlist', 'username', 'search', 'video', 'videos' );
460 -
461 - foreach ( $fields as $section ) {
462 - foreach ( $section['fields'] as $field ) {
463 - $name = $field['name'];
464 -
465 - if ( in_array( $name, $excluded, true ) ) {
466 - continue;
467 - }
468 -
469 - if ( 'checkbox' === $field['type'] ) {
470 - $params[ $name ] = isset( $_POST[ $name ] ) ? 1 : 0;
471 - } elseif ( ! isset( $_POST[ $name ] ) ) {
472 - continue;
473 - } elseif ( 'select' === $field['type'] && isset( $field['options'] ) ) {
474 - $raw = sanitize_text_field( $_POST[ $name ] );
475 - $params[ $name ] = array_key_exists( $raw, $field['options'] ) ? $raw : sanitize_text_field( $field['value'] );
476 - } else {
477 - $sanitize_callback = ! empty( $field['sanitize_callback'] ) ? $field['sanitize_callback'] : 'sanitize_text_field';
478 - $params[ $name ] = call_user_func( $sanitize_callback, $_POST[ $name ] );
479 - }
480 - }
481 - }
482 -
483 - global $wpdb;
484 - $table = $wpdb->prefix . 'ayg_galleries';
485 -
486 - $now = current_time( 'mysql' );
487 -
488 - $data = array(
489 - 'title' => $gallery_title,
490 - 'params' => wp_json_encode( $params ),
491 - 'updated_at' => $now
492 - );
493 -
494 - // Persist the source columns whenever they aren't locked (new gallery, or an
495 - // update before the first import). All columns here are strings, so the format
496 - // is left to default ('%s') instead of tracking a positional list.
497 - if ( ! $source_locked ) {
498 - $data['source_type'] = $source_type;
499 - $data['source_value'] = $source_value;
500 - }
501 -
502 - // A "paused" schedule stops automatic imports: flag the gallery paused, clear any prior
503 - // import error (so a user pause is distinguishable from a quota pause, which keeps its
504 - // message), and drop it from the cron queue. The JS save flow skips the import for this case.
505 - if ( 'paused' === $params['schedule'] ) {
506 - $data['import_status'] = 'paused';
507 - $data['import_error'] = '';
508 - $data['next_import_at'] = null;
509 - } elseif ( 0 === $params['schedule'] ) {
510 - // "Only Once" is non-recurring: ensure no future cron run stays queued. Switching here from a
511 - // recurring schedule may have left a stale next_import_at behind; clear it so a display-only
512 - // save (import skipped) can't trigger one more unexpected background import. A save that does
513 - // import will have import_batch() finalize the status / next run on completion anyway.
514 - $data['next_import_at'] = null;
515 - }
516 -
517 - if ( $gallery_id > 0 ) {
518 - if ( empty( $gallery_title ) ) {
519 - $gallery_title = sprintf( __( 'Gallery %d', 'automatic-youtube-gallery' ), $gallery_id );
520 - $data['title'] = $gallery_title;
521 - }
522 -
523 - // Drop any newly-excluded videos from this gallery's links (the videos table is left intact —
524 - // rows may be shared by other galleries) and fold the refreshed count into this same update.
525 - if ( ayg_delete_excluded_relationships( $gallery_id, $params['exclude'] ) > 0 ) {
526 - $data['video_count'] = (int) $wpdb->get_var(
527 - $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}ayg_gallery_relationships WHERE gallery_id = %s", strval( $gallery_id ) )
528 - );
529 - }
530 -
531 - $wpdb->update(
532 - $table,
533 - $data,
534 - array( 'id' => $gallery_id ),
535 - null,
536 - array( '%d' )
537 - );
538 - } else {
539 - $data['created_at'] = $now;
540 -
541 - $wpdb->insert( $table, $data );
542 -
543 - $gallery_id = (int) $wpdb->insert_id;
544 -
545 - if ( empty( $gallery_title ) ) {
546 - $gallery_title = sprintf( __( 'Gallery %d', 'automatic-youtube-gallery' ), $gallery_id );
547 -
548 - $wpdb->update(
549 - $table,
550 - array( 'title' => $gallery_title ),
551 - array( 'id' => $gallery_id ),
552 - array( '%s' ),
553 - array( '%d' )
554 - );
555 - }
556 - }
557 -
558 - // Live galleries (search / livestream / single video) render from the API and cache responses. Clear
559 - // this gallery's cache on save so the front-end reflects the latest results right after "Update Gallery".
560 - if ( in_array( $source_type, array( 'search', 'livestream', 'video' ), true ) ) {
561 - ayg_delete_cache( strval( $gallery_id ) );
562 - }
563 -
564 - wp_send_json_success( array(
565 - 'gallery_id' => $gallery_id,
566 - 'shortcode' => sprintf( '[automatic_youtube_gallery id="%d"]', $gallery_id ),
567 - ) );
568 - }
569 -
570 - /**
571 - * Import one batch of videos into a gallery via AJAX.
572 - *
573 - * The client calls this in a loop, passing back the page_token returned by
574 - * the previous call, until done = true.
575 - *
576 - * @since 2.8.0
577 - */
578 - public function ajax_callback_import_gallery() {
579 - check_ajax_referer( 'ayg_ajax_nonce', 'security' );
580 -
581 - if ( ! current_user_can( 'manage_options' ) ) {
582 - wp_send_json_error( array( 'message' => __( 'Permission denied.', 'automatic-youtube-gallery' ) ) );
583 - }
584 -
585 - $gallery_id = absint( isset( $_POST['id'] ) ? $_POST['id'] : 0 );
586 - $page_token = sanitize_text_field( isset( $_POST['page_token'] ) ? $_POST['page_token'] : '' );
587 -
588 - // The browser-driven import is always a manual run: full re-scan, uncapped, prunes deletions.
589 - $importer = new AYG_Import();
590 - $result = $importer->import_batch( $gallery_id, $page_token, true );
591 -
592 - if ( isset( $result['error'] ) ) {
593 - wp_send_json_error( array(
594 - 'message' => $result['error'],
595 - 'quota_exceeded' => ! empty( $result['quota_exceeded'] )
596 - ) );
597 - }
598 -
599 - wp_send_json_success( $result );
600 - }
601 -
602 - /**
603 - * Delete a gallery via AJAX.
604 - *
605 - * @since 2.8.0
606 - */
607 - public function ajax_callback_delete_gallery() {
608 - check_ajax_referer( 'ayg_ajax_nonce', 'security' );
609 -
610 - if ( ! current_user_can( 'manage_options' ) ) {
611 - wp_send_json_error( array( 'message' => __( 'Permission denied.', 'automatic-youtube-gallery' ) ) );
612 - }
613 -
614 - require_once AYG_DIR . 'admin/galleries.php';
615 -
616 - $id = absint( isset( $_POST['id'] ) ? $_POST['id'] : 0 );
617 - AYG_Admin_Gallery_List_Table::delete_gallery( $id );
618 -
619 - wp_send_json_success();
202 + wp_die();
620 203 }
621 204
622 205 }