PluginProbe
Jock On Air Now (JOAN) / trunk
Jock On Air Now (JOAN) vtrunk
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) trunk, at joan.php

504 lines 19.0 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.1.3
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.4
15 */
16
17 defined('ABSPATH') || exit;
18
19 // Update plugin version constant to reflect the latest release.
20 define('JOAN_VERSION', '6.1.3');
21 define('JOAN_PLUGIN_DIR', plugin_dir_path(__FILE__));
22 define('JOAN_PLUGIN_URL', plugin_dir_url(__FILE__));
23
24 // CRITICAL: Add locale filters IMMEDIATELY for both admin and frontend
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
88 if (empty($chosen_locale)) {
89 $chosen_locale = joan_detect_browser_language();
90 }
91
92 if (!empty($chosen_locale) && $chosen_locale !== get_locale()) {
93 $mofile = JOAN_PLUGIN_DIR . 'languages/joan-' . $chosen_locale . '.mo';
94
95 if (file_exists($mofile)) {
96 global $l10n;
97 if (isset($l10n['joan'])) {
98 unset($l10n['joan']);
99 }
100
101 load_textdomain('joan', $mofile);
102
103 if (!headers_sent()) {
104 setcookie('joan_visitor_locale', $chosen_locale, time() + (30 * 24 * 60 * 60), '/', '', false, true);
105 }
106 }
107 }
108 }
109
110 /**
111 * Detect browser's preferred language
112 */
113 function joan_detect_browser_language() {
114 $browser_langs = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : '';
115
116 if (empty($browser_langs)) {
117 return '';
118 }
119
120 preg_match_all('/([a-z]{2,3}(?:-[A-Z]{2,3})?)\s*(?:;\s*q\s*=\s*([0-9.]+))?/i', $browser_langs, $matches);
121
122 $languages = array();
123 foreach ($matches[1] as $index => $lang) {
124 $quality = isset($matches[2][$index]) && $matches[2][$index] !== '' ? (float)$matches[2][$index] : 1.0;
125 $languages[$lang] = $quality;
126 }
127
128 arsort($languages);
129
130 $locale_map = array(
131 'en' => 'en_US',
132 'en-us' => 'en_US',
133 'en-gb' => 'en_GB',
134 'fr' => 'fr_FR',
135 'fr-fr' => 'fr_FR',
136 'de' => 'de_DE',
137 'de-de' => 'de_DE',
138 'es' => 'es_ES',
139 'es-es' => 'es_ES',
140 'pt' => 'pt_BR',
141 'pt-br' => 'pt_BR',
142 'pt-pt' => 'pt_PT',
143 'it' => 'it_IT',
144 'it-it' => 'it_IT',
145 'nl' => 'nl_NL',
146 'nl-nl' => 'nl_NL',
147 'ht' => 'ht_HT',
148 'ht-ht' => 'ht_HT',
149 'acf' => 'acf_LC',
150 'acf-lc' => 'acf_LC',
151 'gcf' => 'gcf_GP',
152 'gcf-gp' => 'gcf_GP',
153 );
154
155 $available_locales = joan_get_available_locales();
156
157 foreach ($languages as $lang => $quality) {
158 $lang_lower = strtolower($lang);
159
160 if (isset($locale_map[$lang_lower])) {
161 $locale = $locale_map[$lang_lower];
162 if (in_array($locale, $available_locales)) {
163 return $locale;
164 }
165 }
166
167 $locale_format = str_replace('-', '_', $lang);
168 if (in_array($locale_format, $available_locales)) {
169 return $locale_format;
170 }
171
172 $lang_code = substr($lang_lower, 0, 2);
173 if (isset($locale_map[$lang_code])) {
174 $locale = $locale_map[$lang_code];
175 if (in_array($locale, $available_locales)) {
176 return $locale;
177 }
178 }
179 }
180
181 return '';
182 }
183
184 /**
185 * Get available JOAN translation files
186 */
187 function joan_get_available_locales() {
188 static $cached_locales = null;
189
190 if ($cached_locales !== null) {
191 return $cached_locales;
192 }
193
194 $available = array();
195 $lang_dir = JOAN_PLUGIN_DIR . 'languages/';
196
197 if (is_dir($lang_dir)) {
198 $files = glob($lang_dir . 'joan-*.mo');
199 foreach ($files as $file) {
200 if (preg_match('/joan-([a-z]{2,3}_[A-Z]{2,3})\.mo$/i', basename($file), $matches)) {
201 $available[] = $matches[1];
202 }
203 }
204 }
205
206 $cached_locales = $available;
207
208 return $available;
209 }
210
211 add_action('init', function() {
212 $role = get_role('administrator');
213 if ($role) {
214 $role->add_cap('manage_joan_schedule');
215 }
216 });
217
218 register_activation_hook(__FILE__, function() {
219 $old_version = get_option('joan_version');
220 $migration_handled = get_option('joan_migration_handled', false);
221
222 if ($old_version && version_compare($old_version, '6.0.0', '<') && !$migration_handled) {
223 update_option('joan_needs_migration', true);
224 update_option('joan_backup_reminder', true);
225 deactivate_plugins(plugin_basename(__FILE__));
226 } else {
227 if (!$migration_handled) {
228 update_option('joan_migration_handled', true);
229 }
230 joan_ensure_table();
231 joan_ensure_columns_exist();
232 }
233
234 update_option('joan_version', JOAN_VERSION);
235 });
236
237 add_action('admin_notices', function() {
238 if (get_option('joan_backup_reminder')) {
239 joan_backup_reminder_notice();
240 }
241 if (get_option('joan_migration_handled') && !get_option('joan_post_migration_notice_dismissed')) {
242 joan_migration_success_notice();
243 }
244 });
245
246
247 add_action('admin_init', function() {
248 if ( ! current_user_can( 'manage_options' ) ) { return; }
249 if ( isset($_GET['joan_dismiss_success']) && $_GET['joan_dismiss_success'] === '1' ) {
250 check_admin_referer('joan_dismiss_success');
251 update_option('joan_post_migration_notice_dismissed', true);
252 wp_redirect( admin_url('admin.php?page=joan-schedule') );
253 exit;
254 }
255 });
256 function joan_backup_reminder_notice() {
257 $proceed_url = wp_nonce_url(
258 admin_url('admin.php?joan_proceed_migration=1'),
259 'joan_migration_proceed'
260 );
261 ?>
262 <div class="notice notice-warning">
263 <h2><?php esc_html_e('IMPORTANT: JOAN 6.0.9 Upgrade Notice', 'joan'); ?></h2>
264 <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>
265 <p><?php esc_html_e('The JOAN plugin has been deactivated so you can backup your schedule.', 'joan'); ?></p>
266 <p><strong><?php esc_html_e('To backup your schedule:', 'joan'); ?></strong></p>
267 <ol>
268 <li><?php esc_html_e('Go to your JOAN admin page (if still accessible)', 'joan'); ?></li>
269 <li><?php esc_html_e('Take screenshots of your schedule', 'joan'); ?></li>
270 <li><?php esc_html_e('Write down show names, times, jock names, and image URLs', 'joan'); ?></li>
271 <li><?php esc_html_e('When ready, reactivate the plugin and choose to proceed with the upgrade', 'joan'); ?></li>
272 </ol>
273 </div>
274 <?php
275 }
276
277 function joan_migration_success_notice() {
278 if ( ! current_user_can( 'manage_options' ) ) { return; }
279 if ( get_option( 'joan_post_migration_notice_dismissed' ) ) { return; }
280
281 $manager_url = admin_url( 'admin.php?page=joan-schedule' );
282 $dismiss_url = wp_nonce_url( admin_url( 'admin.php?joan_dismiss_success=1' ), 'joan_dismiss_success' );
283
284 echo '<div class="notice notice-success is-dismissible">';
285 echo '<p><strong>' . esc_html__( 'JOAN has been Successfully Activated!', 'joan' ) . '</strong></p>';
286 echo '<p>' . esc_html__( 'The plugin was upgraded and old data were cleaned up. You can now begin adding your shows using the new interface.', 'joan' ) . '</p>';
287 echo '<p>'
288 . '<a class="button button-primary" href="' . esc_url( $manager_url ) . '">' . esc_html__( 'Go to Schedule Manager', 'joan' ) . '</a>'
289 . ' | '
290 . '<a class="button-link" href="' . esc_url( $dismiss_url ) . '">' . esc_html__( 'Dismiss Forever', 'joan' ) . '</a>'
291 . '</p>';
292 echo '</div>';
293 }
294 function joan_migrate_from_old_version() {
295 global $wpdb;
296
297 $old_tables = [
298 $wpdb->prefix . 'joan_schedule_legacy',
299 $wpdb->prefix . 'showtime_jockonair',
300 $wpdb->prefix . 'onair_schedule',
301 ];
302
303 foreach ($old_tables as $table) {
304 $wpdb->query("DROP TABLE IF EXISTS $table");
305 }
306
307 $current_table = $wpdb->prefix . 'joan_schedule';
308 $wpdb->query("DROP TABLE IF EXISTS $current_table");
309
310 joan_ensure_table();
311
312 delete_option('joan_legacy_data_imported');
313 delete_option('joan_old_version_data');
314
315 update_option('joan_time_format', '12');
316 update_option('joan_timezone', 'America/New_York');
317 update_option('joan_show_next_show', '1');
318 update_option('joan_show_jock_image', '1');
319 update_option('joan_widget_max_width', '300');
320 update_option('joan_schedule_status', 'active');
321 update_option('joan_off_air_message', __('We are currently off the air. Please check back later!', 'joan'));
322 }
323
324 if (get_option('joan_migration_handled', false)) {
325 require_once JOAN_PLUGIN_DIR . 'includes/translations.php';
326 require_once JOAN_PLUGIN_DIR . 'includes/language-switcher.php';
327 require_once JOAN_PLUGIN_DIR . 'includes/crud.php';
328 require_once JOAN_PLUGIN_DIR . 'includes/admin-menu.php';
329 require_once JOAN_PLUGIN_DIR . 'includes/shortcodes.php';
330 require_once JOAN_PLUGIN_DIR . 'includes/widget.php';
331 require_once JOAN_PLUGIN_DIR . 'includes/import-legacy.php';
332 require_once JOAN_PLUGIN_DIR . 'includes/compatibility-check.php';
333 }
334
335 function joan_enqueue_admin_assets($hook) {
336 if (!get_option('joan_migration_handled', false) || strpos($hook, 'joan') !== false) {
337 wp_enqueue_script('joan-admin', JOAN_PLUGIN_URL . 'assets/js/admin.js', ['jquery'], JOAN_VERSION, true);
338 }
339 }
340 add_action('admin_enqueue_scripts', 'joan_enqueue_admin_assets');
341
342 function joan_enqueue_assets() {
343 if (!get_option('joan_migration_handled', false)) return;
344
345 wp_enqueue_style('joan-style', JOAN_PLUGIN_URL . 'assets/css/joan.css', [], JOAN_VERSION);
346 wp_enqueue_script('joan-script', JOAN_PLUGIN_URL . 'assets/js/joan.js', ['jquery'], JOAN_VERSION, true);
347
348 $day_names = array(
349 'Sunday' => __('Sunday', 'joan'),
350 'Monday' => __('Monday', 'joan'),
351 'Tuesday' => __('Tuesday', 'joan'),
352 'Wednesday' => __('Wednesday', 'joan'),
353 'Thursday' => __('Thursday', 'joan'),
354 'Friday' => __('Friday', 'joan'),
355 'Saturday' => __('Saturday', 'joan')
356 );
357
358 wp_localize_script('joan-script', 'joan_ajax', [
359 'ajaxurl' => admin_url('admin-ajax.php'),
360 'nonce' => wp_create_nonce('joan_frontend_nonce'),
361 'settings' => [
362 'show_next_show' => get_option('joan_show_next_show', '1'),
363 'show_jock_image' => get_option('joan_show_jock_image', '1'),
364 'joan_show_local_time' => get_option('joan_show_local_time', '1'),
365 'joan_allow_timezone_selector' => get_option('joan_allow_timezone_selector', '1'),
366 'time_format' => get_option('joan_time_format', '12'),
367 'widget_max_width' => get_option('joan_widget_max_width', '300'),
368 'joan_dark_mode' => get_option('joan_dark_mode', 'auto'),
369 'joan_dark_mode_override' => get_option('joan_dark_mode_override', '0'),
370 'joan_show_day_emoji' => get_option('joan_show_day_emoji', '0'),
371 'joan_jock_field_label' => get_option('joan_jock_field_label', ''),
372 'joan_link_assignment' => get_option('joan_link_assignment', 'jock_name'),
373 'joan_image_display_mode' => get_option('joan_image_display_mode', 'constrained'),
374 'joan_center_widget_title' => get_option('joan_center_widget_title', '0'),
375 'joan_jock_only_mode' => get_option('joan_jock_only_mode', '0')
376 ],
377 'i18n' => [
378 'days' => $day_names,
379 'schedule' => __('Schedule', 'joan'),
380 'upcoming_shows' => __('Upcoming Shows', 'joan'),
381 'tooltip_sunday' => __('Helium (He) - Second most abundant element in the universe, powers the sun through nuclear fusion', 'joan'),
382 'tooltip_monday' => __('Silver (Ag) - Precious metal with highest electrical conductivity, reflects light like moonlight', 'joan'),
383 'tooltip_tuesday' => __('Iron (Fe) - Most abundant metal on Earth, essential for steel and human blood', 'joan'),
384 'tooltip_wednesday' => __('Mercury (Hg) - Only metal that is liquid at room temperature, used in thermometers', 'joan'),
385 'tooltip_thursday' => __('Gold (Au) - Noble metal that never tarnishes, symbol of value and permanence', 'joan'),
386 'tooltip_friday' => __('Copper (Cu) - Reddish metal essential for electrical wiring, naturally antibacterial', 'joan'),
387 'tooltip_saturday' => __('Lead (Pb) - Dense metal historically used for protection, now known to be toxic', 'joan'),
388 'loading' => __(esc_html__('Loading current show...', 'joan'), 'joan'),
389 'retrying' => __('Retrying...', 'joan'),
390 'refreshing' => __('Refreshing...', 'joan'),
391 'config_error' => __('Configuration error.', 'joan'),
392 'refresh_page' => __('Refresh page', 'joan'),
393 'unable_to_load' => __('Unable to load current show.', 'joan'),
394 'server_slow' => __('Server is responding slowly.', 'joan'),
395 'connection_timeout' => __('Connection timeout', 'joan'),
396 'request_blocked' => __('Request blocked or network error.', 'joan'),
397 'access_denied' => __('Access denied by server.', 'joan'),
398 'server_internal_error' => __('Server internal error.', 'joan'),
399 'server_error' => __('Server error', 'joan'),
400 'invalid_response' => __('Invalid response from server.', 'joan'),
401 'automatic_retries_stopped' => __('Automatic retries stopped.', 'joan'),
402 'off_air_default' => __('We are currently off the air. Please check back later!', 'joan'),
403 'up_next' => __('Up Next:', 'joan'),
404 'hosted_by' => __('Hosted by', 'joan'),
405 'on_air_now' => __('On Air Now', 'joan'),
406 'current_time' => __('Current time:', 'joan'),
407 'local_time' => __('Local time:', 'joan'),
408 'switch_timezone' => __('Switch timezone:', 'joan'),
409 'time_display_unavailable' => __('Time display unavailable', 'joan'),
410 'filter_by_day' => __('Filter by day:', 'joan'),
411 'all_days' => __('All Days', 'joan'),
412 'whats_on_today' => __('What\'s on today', 'joan'),
413 'no_schedule_available' => __('No schedule available.', 'joan'),
414 'no_shows_scheduled_for_today' => __('No shows scheduled for today.', 'joan'),
415 'no_shows_scheduled_for' => __('No shows scheduled for', 'joan'),
416 'no_upcoming_shows' => __('No upcoming shows scheduled.', 'joan'),
417 'schedule_suspended' => __('Schedule suspended. Special programming', 'joan'),
418 'show' => __('Show', 'joan'),
419 'day' => __('Day', 'joan'),
420 'time' => __('Time', 'joan'),
421 'jock' => __('Jock', 'joan'),
422 'image' => __('Image', 'joan'),
423 'link' => __('Link', 'joan'),
424 'n_a' => __('N/A', 'joan')
425 ]
426 ]);
427 }
428 add_action('wp_enqueue_scripts', 'joan_enqueue_assets');
429
430 function joan_ensure_table() {
431 global $wpdb;
432 $table_name = $wpdb->prefix . 'joan_schedule';
433 $charset_collate = $wpdb->get_charset_collate();
434
435 $sql = "CREATE TABLE IF NOT EXISTS $table_name (
436 id INT AUTO_INCREMENT PRIMARY KEY,
437 show_name VARCHAR(255),
438 start_day VARCHAR(10),
439 start_time TIME,
440 end_time TIME,
441 image_url TEXT,
442 dj_name VARCHAR(255),
443 link_url TEXT
444 ) $charset_collate;";
445
446 require_once ABSPATH . 'wp-admin/includes/upgrade.php';
447 dbDelta($sql);
448 }
449
450 function joan_ensure_columns_exist() {
451 global $wpdb;
452 $table = $wpdb->prefix . 'joan_schedule';
453
454 $columns = $wpdb->get_col("DESCRIBE $table", 0);
455
456 if (!in_array('link_url', $columns)) {
457 $wpdb->query("ALTER TABLE $table ADD COLUMN link_url TEXT");
458 }
459 }
460
461 register_activation_hook(__FILE__, function() {
462 joan_ensure_table();
463 joan_ensure_columns_exist();
464 });
465
466 add_filter('plugin_action_links_' . plugin_basename(__FILE__), function($links) {
467 if (get_option('joan_migration_handled', false)) {
468 $settings_link = '<a href="admin.php?page=joan-schedule">' . __('Schedule', 'joan') . '</a>';
469 array_unshift($links, $settings_link);
470 }
471 return $links;
472 });
473
474 add_action('wp_ajax_joan_widget_refresh', 'joan_handle_widget_refresh');
475 add_action('wp_ajax_nopriv_joan_widget_refresh', 'joan_handle_widget_refresh');
476
477 function joan_handle_widget_refresh() {
478 if (!get_option('joan_migration_handled', false)) {
479 wp_die('Plugin not properly initialized');
480 }
481
482 if (!wp_verify_nonce($_POST['nonce'], 'joan_frontend_nonce')) {
483 wp_die('Security check failed');
484 }
485
486 $crud = new stdClass();
487 $crud->action = 'read';
488 $crud->read_type = 'current';
489
490 do_action('wp_ajax_show_time_curd');
491 }
492
493 // JOAN: Permanently dismissible activation notice for 6.0.9
494 add_action('admin_init', function() {
495 if (!current_user_can('manage_options')) { return; }
496 if (isset($_GET['joan_dismiss_activation_609']) && $_GET['joan_dismiss_activation_609'] === '1') {
497 $nonce = isset($_GET['_joan_nonce']) ? $_GET['_joan_nonce'] : '';
498 if (wp_verify_nonce($nonce, 'joan_dismiss_609')) {
499 update_option('joan_hide_activation_notice_609', '1', false);
500 }
501 }
502 });
503
504