PluginProbe
Jock On Air Now (JOAN) / 6.0.9
Jock On Air Now (JOAN) v6.0.9
4.7.3 4.7.4 4.7.5 4.7.6 4.7.7 4.7.8 5.0 5.1 5.2.0 5.3.0 5.4.0 5.5.0 5.5.1 5.5.2 5.5.3 5.5.4 5.5.6 5.5.7 5.5.8 5.5.9 5.6 5.6.1 5.6.2 5.6.3 5.6.4 All 77 releases
joan / joan.php

joan.php in Jock On Air Now (JOAN) 6.0.9, at joan.php

547 lines 20.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Name: JOAN - Jock On Air Now
4 * Plugin URI: https://gandenterprisesinc.com/plugins/joan
5 * Description: Display your station's current and upcoming on-air schedule in real-time with timezone awareness, Elementor & Visual Composer support, and modern code practices.
6 * Version: 6.0.9
7 * Author: G & D Enterprises, Inc.
8 * Author URI: https://gandenterprisesinc.com
9 * License: GPL v2 or later
10 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
11 * Text Domain: joan
12 * Domain Path: /languages
13 * Requires at least: 5.0
14 * Requires PHP: 7.0
15 */
16
17 defined('ABSPATH') || exit;
18
19 define('JOAN_VERSION', '6.0.9');
20 define('JOAN_PLUGIN_DIR', plugin_dir_path(__FILE__));
21 define('JOAN_PLUGIN_URL', plugin_dir_url(__FILE__));
22
23 // Locale filters: limit to admin so we don't change the whole site on the frontend
24 if ( is_admin() ) {
25 add_filter('locale', 'joan_override_locale', 1);
26 add_filter('determine_locale', 'joan_override_locale', 1);
27 }
28 /**
29 * Override locale based on user preference (works for both admin and frontend)
30 */
31 function joan_override_locale($locale) {
32 if (!function_exists('is_user_logged_in') || !function_exists('get_user_meta')) {
33 return $locale;
34 }
35
36 if (is_user_logged_in()) {
37 $user_id = get_current_user_id();
38 $user_language = get_user_meta($user_id, 'joan_language', true);
39
40 if (!empty($user_language)) {
41 global $l10n;
42 if (isset($l10n['joan'])) {
43 unset($l10n['joan']);
44 }
45
46 return $user_language;
47 }
48 }
49
50 if (!is_admin() && isset($_COOKIE['joan_visitor_locale'])) {
51 return sanitize_text_field($_COOKIE['joan_visitor_locale']);
52 }
53
54 return $locale;
55 }
56
57 // Load text domain early
58 add_action('plugins_loaded', function() {
59 load_plugin_textdomain('joan', false, dirname(plugin_basename(__FILE__)) . '/languages');
60 }, 1);
61
62 // Apply frontend-specific locale handling
63 add_action('plugins_loaded', function() {
64 if (!is_admin()) {
65 joan_apply_frontend_locale();
66 }
67 }, 2);
68
69 /**
70 * Apply frontend locale detection and switching
71 */
72 function joan_apply_frontend_locale() {
73 $chosen_locale = '';
74
75 if (is_user_logged_in()) {
76 $user_id = get_current_user_id();
77 $user_locale = get_user_meta($user_id, 'joan_language', true);
78
79 if (!empty($user_locale)) {
80 $chosen_locale = $user_locale;
81 }
82 }
83
84 if (empty($chosen_locale) && isset($_COOKIE['joan_visitor_locale'])) {
85 $chosen_locale = sanitize_text_field($_COOKIE['joan_visitor_locale']);
86 }
87 // Fallback: accept 'joan_language' cookie (set by frontend switcher JS)
88 if (empty($chosen_locale) && isset($_COOKIE['joan_language'])) {
89 $chosen_locale = sanitize_text_field($_COOKIE['joan_language']);
90 }
91
92
93 if (empty($chosen_locale)) {
94 $chosen_locale = joan_detect_browser_language();
95 }
96
97 if (!empty($chosen_locale) && $chosen_locale !== get_locale()) {
98 $mofile = JOAN_PLUGIN_DIR . 'languages/joan-' . $chosen_locale . '.mo';
99
100 if (file_exists($mofile)) {
101 global $l10n;
102 if (isset($l10n['joan'])) {
103 unset($l10n['joan']);
104 }
105
106 load_textdomain('joan', $mofile);
107
108 if (!headers_sent()) {
109 setcookie('joan_visitor_locale', $chosen_locale, time() + (30 * 24 * 60 * 60), '/', '', false, true);
110 if (!headers_sent()) {
111 setcookie('joan_language', $chosen_locale, time() + (30 * 24 * 60 * 60), '/', '', false, true);
112 }
113 }
114 }
115 }
116 }
117
118 /**
119 * Detect browser's preferred language
120 */
121 function joan_detect_browser_language() {
122 $browser_langs = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : '';
123
124 if (empty($browser_langs)) {
125 return '';
126 }
127
128 preg_match_all('/([a-z]{2,3}(?:-[A-Z]{2,3})?)\s*(?:;\s*q\s*=\s*([0-9.]+))?/i', $browser_langs, $matches);
129
130 $languages = array();
131 foreach ($matches[1] as $index => $lang) {
132 $quality = isset($matches[2][$index]) && $matches[2][$index] !== '' ? (float)$matches[2][$index] : 1.0;
133 $languages[$lang] = $quality;
134 }
135
136 arsort($languages);
137
138 $locale_map = array(
139 'en' => 'en_US',
140 'en-us' => 'en_US',
141 'en-gb' => 'en_GB',
142 'fr' => 'fr_FR',
143 'fr-fr' => 'fr_FR',
144 'de' => 'de_DE',
145 'de-de' => 'de_DE',
146 'es' => 'es_ES',
147 'es-es' => 'es_ES',
148 'pt' => 'pt_BR',
149 'pt-br' => 'pt_BR',
150 'pt-pt' => 'pt_PT',
151 'it' => 'it_IT',
152 'it-it' => 'it_IT',
153 'nl' => 'nl_NL',
154 'nl-nl' => 'nl_NL',
155 'ht' => 'ht_HT',
156 'ht-ht' => 'ht_HT',
157 'acf' => 'acf_LC',
158 'acf-lc' => 'acf_LC',
159 'gcf' => 'gcf_GP',
160 'gcf-gp' => 'gcf_GP',
161 );
162
163 $available_locales = joan_get_available_locales();
164
165 foreach ($languages as $lang => $quality) {
166 $lang_lower = strtolower($lang);
167
168 if (isset($locale_map[$lang_lower])) {
169 $locale = $locale_map[$lang_lower];
170 if (in_array($locale, $available_locales)) {
171 return $locale;
172 }
173 }
174
175 $locale_format = str_replace('-', '_', $lang);
176 if (in_array($locale_format, $available_locales)) {
177 return $locale_format;
178 }
179
180 $lang_code = substr($lang_lower, 0, 2);
181 if (isset($locale_map[$lang_code])) {
182 $locale = $locale_map[$lang_code];
183 if (in_array($locale, $available_locales)) {
184 return $locale;
185 }
186 }
187 }
188
189 return '';
190 }
191
192 /**
193 * Get available JOAN translation files
194 */
195 function joan_get_available_locales() {
196 static $cached_locales = null;
197
198 if ($cached_locales !== null) {
199 return $cached_locales;
200 }
201
202 $available = array();
203 $lang_dir = JOAN_PLUGIN_DIR . 'languages/';
204
205 if (is_dir($lang_dir)) {
206 $files = glob($lang_dir . 'joan-*.mo');
207 foreach ($files as $file) {
208 if (preg_match('/joan-([a-z]{2,3}_[A-Z]{2,3})\.mo$/i', basename($file), $matches)) {
209 $available[] = $matches[1];
210 }
211 }
212 }
213
214 $cached_locales = $available;
215
216 return $available;
217 }
218
219 add_action('init', function() {
220 $role = get_role('administrator');
221 if ($role) {
222 $role->add_cap('manage_joan_schedule');
223 }
224 });
225
226 register_activation_hook(__FILE__, function() {
227 $old_version = get_option('joan_version');
228 $migration_handled = get_option('joan_migration_handled', false);
229
230 if ($old_version && version_compare($old_version, '6.0.0', '<') && !$migration_handled) {
231 update_option('joan_needs_migration', true);
232 update_option('joan_backup_reminder', true);
233 deactivate_plugins(plugin_basename(__FILE__));
234 } else {
235 if (!$migration_handled) {
236 update_option('joan_migration_handled', true);
237 }
238 joan_ensure_table();
239 joan_ensure_columns_exist();
240 }
241
242 update_option('joan_version', JOAN_VERSION);
243 });
244
245 add_action('admin_notices', function() {
246 if (get_option('joan_backup_reminder')) {
247 joan_backup_reminder_notice();
248 }
249 if (get_option('joan_migration_handled') && !get_option('joan_post_migration_notice_dismissed')) {
250 joan_migration_success_notice();
251 }
252 });
253
254 add_action('admin_init', function() {
255 if (isset($_GET['joan_proceed_migration']) && $_GET['joan_proceed_migration'] === '1') {
256 check_admin_referer('joan_migration_proceed');
257 joan_migrate_from_old_version();
258 update_option('joan_migration_handled', true);
259 delete_option('joan_needs_migration');
260 delete_option('joan_backup_reminder');
261 wp_redirect(admin_url('admin.php?page=joan-schedule'));
262 exit;
263 }
264
265 if (isset($_GET['joan_dismiss_success']) && $_GET['joan_dismiss_success'] === '1') {
266 update_option('joan_post_migration_notice_dismissed', true);
267 wp_redirect(admin_url('admin.php?page=joan-schedule'));
268 exit;
269 }
270 });
271
272 function joan_backup_reminder_notice() {
273 $proceed_url = wp_nonce_url(
274 admin_url('admin.php?joan_proceed_migration=1'),
275 'joan_migration_proceed'
276 );
277 ?>
278 <div class="notice notice-warning">
279 <h2><?php esc_html_e('IMPORTANT: JOAN 6.0.9 Upgrade Notice', 'joan'); ?></h2>
280 <p><strong><?php esc_html_e('Version 6.0.9 requires a fresh start.', 'joan'); ?></strong> <?php esc_html_e('The plugin has been temporarily deactivated.', 'joan'); ?></p>
281 <p><?php esc_html_e('The JOAN plugin has been deactivated so you can backup your schedule.', 'joan'); ?></p>
282 <p><strong><?php esc_html_e('To backup your schedule:', 'joan'); ?></strong></p>
283 <ol>
284 <li><?php esc_html_e('Go to your JOAN admin page (if still accessible)', 'joan'); ?></li>
285 <li><?php esc_html_e('Take screenshots of your schedule', 'joan'); ?></li>
286 <li><?php esc_html_e('Write down show names, times, jock names, and image URLs', 'joan'); ?></li>
287 <li><?php esc_html_e('When ready, reactivate the plugin and choose to proceed with the upgrade', 'joan'); ?></li>
288 </ol>
289 </div>
290 <?php
291 }
292
293 function joan_migration_success_notice() {
294 ?>
295 <div class="notice notice-success is-dismissible">
296 <h2><?php esc_html_e('JOAN 6.0.9 Successfully Activated!', 'joan'); ?></h2>
297 <p><?php esc_html_e('The plugin has been upgraded and old data has been cleaned up. You can now start adding your shows using the new interface.', 'joan'); ?></p>
298 <p><a href="<?php echo admin_url('admin.php?page=joan-schedule'); ?>" class="button button-primary"><?php esc_html_e('Go to Schedule Manager', 'joan');
299
300 // Retrieve visitor language cookie (support legacy and current names)
301 function joan_get_cookie_locale() {
302 if ( isset($_COOKIE['joan_language']) ) {
303 return sanitize_text_field($_COOKIE['joan_language']);
304 }
305 if ( isset($_COOKIE['joan_visitor_locale']) ) {
306 return sanitize_text_field($_COOKIE['joan_visitor_locale']);
307 }
308 return '';
309 }
310
311 // Frontend: load only JOAN's textdomain based on user meta or cookie without changing WP global locale
312 add_action('plugins_loaded', 'joan_frontend_textdomain_from_choice', 2);
313 function joan_frontend_textdomain_from_choice() {
314 if ( is_admin() ) { return; }
315 $chosen = '';
316 if ( function_exists('is_user_logged_in') && is_user_logged_in() ) {
317 $uid = get_current_user_id();
318 $chosen = (string) get_user_meta($uid, 'joan_language', true);
319 }
320 if ($chosen === '') { $chosen = joan_get_cookie_locale(); }
321 if ($chosen === '') { return; }
322 $mofile = trailingslashit(JOAN_PLUGIN_DIR) . 'languages/joan-' . $chosen . '.mo';
323 if ( file_exists($mofile) ) {
324 global $l10n;
325 if ( isset($l10n['joan']) ) { unset($l10n['joan']); }
326 load_textdomain('joan', $mofile);
327 }
328 }
329 ?></a></p>
330 </div>
331 <?php
332 }
333
334 function joan_migrate_from_old_version() {
335 global $wpdb;
336
337 $old_tables = [
338 $wpdb->prefix . 'joan_schedule_legacy',
339 $wpdb->prefix . 'showtime_jockonair',
340 $wpdb->prefix . 'onair_schedule',
341 ];
342
343 foreach ($old_tables as $table) {
344 $wpdb->query("DROP TABLE IF EXISTS $table");
345 }
346
347 $current_table = $wpdb->prefix . 'joan_schedule';
348 $wpdb->query("DROP TABLE IF EXISTS $current_table");
349
350 joan_ensure_table();
351
352 delete_option('joan_legacy_data_imported');
353 delete_option('joan_old_version_data');
354
355 update_option('joan_time_format', '12');
356 update_option('joan_timezone', 'America/New_York');
357 update_option('joan_show_next_show', '1');
358 update_option('joan_show_jock_image', '1');
359 update_option('joan_widget_max_width', '300');
360 update_option('joan_schedule_status', 'active');
361 update_option('joan_off_air_message', __('We are currently off the air. Please check back later!', 'joan'));
362 }
363
364 if (get_option('joan_migration_handled', false)) {
365 require_once JOAN_PLUGIN_DIR . 'includes/translations.php';
366 require_once JOAN_PLUGIN_DIR . 'includes/language-switcher.php';
367 require_once JOAN_PLUGIN_DIR . 'includes/crud.php';
368 require_once JOAN_PLUGIN_DIR . 'includes/admin-menu.php';
369 require_once JOAN_PLUGIN_DIR . 'includes/shortcodes.php';
370 require_once JOAN_PLUGIN_DIR . 'includes/widget.php';
371 require_once JOAN_PLUGIN_DIR . 'includes/import-legacy.php';
372 require_once JOAN_PLUGIN_DIR . 'includes/compatibility-check.php';
373 }
374
375 function joan_enqueue_admin_assets($hook) {
376 if (!get_option('joan_migration_handled', false) || strpos($hook, 'joan') !== false) {
377 wp_enqueue_script('joan-admin', JOAN_PLUGIN_URL . 'assets/js/admin.js', ['jquery'], JOAN_VERSION, true);
378 }
379 }
380 add_action('admin_enqueue_scripts', 'joan_enqueue_admin_assets');
381
382 function joan_enqueue_assets() {
383 if (!get_option('joan_migration_handled', false)) return;
384
385 wp_enqueue_style('joan-style', JOAN_PLUGIN_URL . 'assets/css/joan.css', [], JOAN_VERSION);
386 wp_enqueue_script('joan-script', JOAN_PLUGIN_URL . 'assets/js/joan.js', ['jquery'], JOAN_VERSION, true);
387
388 $day_names = array(
389 'Sunday' => __('Sunday', 'joan'),
390 'Monday' => __('Monday', 'joan'),
391 'Tuesday' => __('Tuesday', 'joan'),
392 'Wednesday' => __('Wednesday', 'joan'),
393 'Thursday' => __('Thursday', 'joan'),
394 'Friday' => __('Friday', 'joan'),
395 'Saturday' => __('Saturday', 'joan')
396 );
397
398 wp_localize_script('joan-script', 'joan_ajax', [
399 'ajaxurl' => admin_url('admin-ajax.php'),
400 'nonce' => wp_create_nonce('joan_frontend_nonce'),
401 'settings' => [
402 'show_next_show' => get_option('joan_show_next_show', '1'),
403 'show_jock_image' => get_option('joan_show_jock_image', '1'),
404 'joan_show_local_time' => get_option('joan_show_local_time', '1'),
405 'joan_allow_timezone_selector' => get_option('joan_allow_timezone_selector', '1'),
406 'time_format' => get_option('joan_time_format', '12'),
407 'widget_max_width' => get_option('joan_widget_max_width', '300'),
408 'joan_dark_mode' => get_option('joan_dark_mode', 'auto'),
409 'joan_dark_mode_override' => get_option('joan_dark_mode_override', '0'),
410 'joan_show_day_emoji' => get_option('joan_show_day_emoji', '0'),
411 'joan_jock_field_label' => get_option('joan_jock_field_label', ''),
412 'joan_link_assignment' => get_option('joan_link_assignment', 'jock_name'),
413 'joan_image_display_mode' => get_option('joan_image_display_mode', 'constrained'),
414 'joan_center_widget_title' => get_option('joan_center_widget_title', '0'),
415 'joan_jock_only_mode' => get_option('joan_jock_only_mode', '0')
416 ],
417 'i18n' => [
418 'days' => $day_names,
419 'schedule' => __('Schedule', 'joan'),
420 'upcoming_shows' => __('Upcoming Shows', 'joan'),
421 'tooltip_sunday' => __('Helium (He) - Second most abundant element in the universe, powers the sun through nuclear fusion', 'joan'),
422 'tooltip_monday' => __('Silver (Ag) - Precious metal with highest electrical conductivity, reflects light like moonlight', 'joan'),
423 'tooltip_tuesday' => __('Iron (Fe) - Most abundant metal on Earth, essential for steel and human blood', 'joan'),
424 'tooltip_wednesday' => __('Mercury (Hg) - Only metal that is liquid at room temperature, used in thermometers', 'joan'),
425 'tooltip_thursday' => __('Gold (Au) - Noble metal that never tarnishes, symbol of value and permanence', 'joan'),
426 'tooltip_friday' => __('Copper (Cu) - Reddish metal essential for electrical wiring, naturally antibacterial', 'joan'),
427 'tooltip_saturday' => __('Lead (Pb) - Dense metal historically used for protection, now known to be toxic', 'joan'),
428 'loading' => __('Loading current show...', 'joan'),
429 'retrying' => __('Retrying...', 'joan'),
430 'refreshing' => __('Refreshing...', 'joan'),
431 'config_error' => __('Configuration error.', 'joan'),
432 'refresh_page' => __('Refresh page', 'joan'),
433 'unable_to_load' => __('Unable to load current show.', 'joan'),
434 'server_slow' => __('Server is responding slowly.', 'joan'),
435 'connection_timeout' => __('Connection timeout', 'joan'),
436 'request_blocked' => __('Request blocked or network error.', 'joan'),
437 'access_denied' => __('Access denied by server.', 'joan'),
438 'server_internal_error' => __('Server internal error.', 'joan'),
439 'server_error' => __('Server error', 'joan'),
440 'invalid_response' => __('Invalid response from server.', 'joan'),
441 'automatic_retries_stopped' => __('Automatic retries stopped.', 'joan'),
442 'off_air_default' => __('We are currently off the air. Please check back later!', 'joan'),
443 'up_next' => __('Up Next:', 'joan'),
444 'hosted_by' => __('Hosted by', 'joan'),
445 'on_air_now' => __('On Air Now', 'joan'),
446 'current_time' => __('Current time:', 'joan'),
447 'local_time' => __('Local time:', 'joan'),
448 'switch_timezone' => __('Switch timezone:', 'joan'),
449 'time_display_unavailable' => __('Time display unavailable', 'joan'),
450 'filter_by_day' => __('Filter by day:', 'joan'),
451 'all_days' => __('All Days', 'joan'),
452 'whats_on_today' => __('What\'s on today', 'joan'),
453 'no_schedule_available' => __('No schedule available.', 'joan'),
454 'no_shows_scheduled_for_today' => __('No shows scheduled for today.', 'joan'),
455 'no_shows_scheduled_for' => __('No shows scheduled for', 'joan'),
456 'no_upcoming_shows' => __('No upcoming shows scheduled.', 'joan'),
457 'schedule_suspended' => __('Schedule suspended. Special programming', 'joan'),
458 'show' => __('Show', 'joan'),
459 'day' => __('Day', 'joan'),
460 'time' => __('Time', 'joan'),
461 'jock' => __('Jock', 'joan'),
462 'image' => __('Image', 'joan'),
463 'link' => __('Link', 'joan'),
464 'n_a' => __('N/A', 'joan')
465 ]
466 ]);
467 }
468 add_action('wp_enqueue_scripts', 'joan_enqueue_assets');
469
470 function joan_ensure_table() {
471 global $wpdb;
472 $table_name = $wpdb->prefix . 'joan_schedule';
473 $charset_collate = $wpdb->get_charset_collate();
474
475 $sql = "CREATE TABLE IF NOT EXISTS $table_name (
476 id INT AUTO_INCREMENT PRIMARY KEY,
477 show_name VARCHAR(255),
478 start_day VARCHAR(10),
479 start_time TIME,
480 end_time TIME,
481 image_url TEXT,
482 dj_name VARCHAR(255),
483 link_url TEXT
484 ) $charset_collate;";
485
486 require_once ABSPATH . 'wp-admin/includes/upgrade.php';
487 dbDelta($sql);
488 }
489
490 function joan_ensure_columns_exist() {
491 global $wpdb;
492 $table = $wpdb->prefix . 'joan_schedule';
493
494 $columns = $wpdb->get_col("DESCRIBE $table", 0);
495
496 if (!in_array('link_url', $columns)) {
497 $wpdb->query("ALTER TABLE $table ADD COLUMN link_url TEXT");
498 }
499 }
500
501 register_activation_hook(__FILE__, function() {
502 joan_ensure_table();
503 joan_ensure_columns_exist();
504 });
505
506 add_filter('plugin_action_links_' . plugin_basename(__FILE__), function($links) {
507 if (get_option('joan_migration_handled', false)) {
508 $settings_link = '<a href="admin.php?page=joan-schedule">' . __('Schedule', 'joan') . '</a>';
509 array_unshift($links, $settings_link);
510 }
511 return $links;
512 });
513
514 add_action('wp_ajax_joan_widget_refresh', 'joan_handle_widget_refresh');
515 add_action('wp_ajax_nopriv_joan_widget_refresh', 'joan_handle_widget_refresh');
516
517 function joan_handle_widget_refresh() {
518
519 // Ensure JOAN textdomain matches visitor/user choice during AJAX
520 if ( function_exists('joan_load_language') ) {
521 $joan_ajax_lang = '';
522 if ( function_exists('is_user_logged_in') && is_user_logged_in() ) {
523 $joan_ajax_lang = get_user_meta(get_current_user_id(), 'joan_language', true);
524 }
525 if ($joan_ajax_lang === '' && isset($_COOKIE['joan_visitor_locale'])) {
526 $joan_ajax_lang = sanitize_text_field($_COOKIE['joan_visitor_locale']);
527 }
528 if ($joan_ajax_lang === '' && isset($_COOKIE['joan_language'])) {
529 $joan_ajax_lang = sanitize_text_field($_COOKIE['joan_language']);
530 }
531 if ($joan_ajax_lang !== '') { joan_load_language($joan_ajax_lang); }
532 }
533
534 if (!get_option('joan_migration_handled', false)) {
535 wp_die('Plugin not properly initialized');
536 }
537
538 if (!wp_verify_nonce($_POST['nonce'], 'joan_frontend_nonce')) {
539 wp_die('Security check failed');
540 }
541
542 $crud = new stdClass();
543 $crud->action = 'read';
544 $crud->read_type = 'current';
545
546 do_action('wp_ajax_show_time_curd');
547 }