Plugin Directory

source: joan/trunk/joan.php

Last change on this file was 3386688, checked in by ganddser, 5 months ago

Fixed issue with android devices not respecting light mode settings stayed in dark mode regardless of user preferences.
FIXED: Elementor "JOAN - On Air Now" widget now registers properly. The earlier JOAN checked for elementor/loaded too soon, causing the widget to never appear. We removed the premature check and now register the widget whenever Elementor is active.
FIXED: Resolved an issue where show titles containing apostrophes accumulated backslashes each time they were edited. Inputs are now unslashed before saving, and existing values are unslashed for the admin interface.

File size: 19.0 KB
Line 
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.2
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
17defined('ABSPATH') || exit;
18
19// Update plugin version constant to reflect the latest release.
20define('JOAN_VERSION', '6.1.2');
21define('JOAN_PLUGIN_DIR', plugin_dir_path(__FILE__));
22define('JOAN_PLUGIN_URL', plugin_dir_url(__FILE__));
23
24// CRITICAL: Add locale filters IMMEDIATELY for both admin and frontend
25add_filter('locale', 'joan_override_locale', 1);
26add_filter('determine_locale', 'joan_override_locale', 1);
27
28/**
29 * Override locale based on user preference (works for both admin and frontend)
30 */
31function 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
58add_action('plugins_loaded', function() {
59    load_plugin_textdomain('joan', false, dirname(plugin_basename(__FILE__)) . '/languages');
60}, 1);
61
62// Apply frontend-specific locale handling
63add_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 */
72function 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 */
113function 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 */
187function 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
211add_action('init', function() {
212    $role = get_role('administrator');
213    if ($role) {
214        $role->add_cap('manage_joan_schedule');
215    }
216});
217
218register_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
237add_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
247add_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});
256function 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
277function 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}
294function 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
324if (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
335function 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}
340add_action('admin_enqueue_scripts', 'joan_enqueue_admin_assets');
341
342function 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}
428add_action('wp_enqueue_scripts', 'joan_enqueue_assets');
429
430function 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
450function 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
461register_activation_hook(__FILE__, function() {
462    joan_ensure_table();
463    joan_ensure_columns_exist();
464});
465
466add_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
474add_action('wp_ajax_joan_widget_refresh', 'joan_handle_widget_refresh');
475add_action('wp_ajax_nopriv_joan_widget_refresh', 'joan_handle_widget_refresh');
476
477function 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
494add_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
Note: See TracBrowser for help on using the repository browser.