plugin_name = $plugin_name; $this->version = $version; // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Only reading for tab display $this->current_tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'dashboard'; // phpcs:enable WordPress.Security.NonceVerification.Recommended } /** * Enqueue admin styles */ public function enqueue_styles($hook) { if (strpos($hook, '3task-calendar') === false) { return; } wp_enqueue_style('wp-color-picker'); wp_enqueue_style( $this->plugin_name . '-admin', THREECAL_PLUGIN_URL . 'admin/css/admin.css', array(), threecal_asset_version('admin/css/admin.css') ); } /** * Enqueue admin scripts */ public function enqueue_scripts($hook) { if (strpos($hook, '3task-calendar') === false) { return; } wp_enqueue_media(); wp_enqueue_script('wp-color-picker'); wp_enqueue_script( $this->plugin_name . '-admin', THREECAL_PLUGIN_URL . 'admin/js/admin.js', array('jquery', 'wp-color-picker'), threecal_asset_version('admin/js/admin.js'), true ); wp_localize_script($this->plugin_name . '-admin', 'threecal_admin', array( 'ajax_url' => admin_url('admin-ajax.php'), 'admin_url' => admin_url(), 'nonce' => wp_create_nonce('threecal_admin_nonce'), 'strings' => array( 'confirm_delete' => __('Are you sure you want to delete this item?', '3task-calendar'), 'confirm_delete_multiple' => __('Are you sure you want to delete the selected items?', '3task-calendar'), 'select_image' => __('Select Image', '3task-calendar'), 'use_image' => __('Use Image', '3task-calendar'), 'prefill_no_url' => __('Please enter the address first.', '3task-calendar'), 'prefill_wait' => __('Reading the website …', '3task-calendar'), 'prefill_error' => __('The website could not be read.', '3task-calendar'), /* translators: %s: list of field names */ 'prefill_done' => __('Taken from the website: %s.', '3task-calendar'), 'prefill_nothing_new' => __('All fields were already filled in, nothing changed.', '3task-calendar'), /* translators: %s: name and address of the location */ 'prefill_location' => __('Location on the website: %s. Add it under Locations and choose it here.', '3task-calendar'), /* translators: %s: image address */ 'prefill_image' => __('Image on the website: %s', '3task-calendar'), 'saving' => __('Saving...', '3task-calendar'), 'saved' => __('Saved!', '3task-calendar'), 'error' => __('An error occurred.', '3task-calendar') ) )); } /** * Add menu pages. */ public function add_menu_pages() { add_menu_page( __( '3task Calendar', '3task-calendar' ), __( '3task Calendar', '3task-calendar' ), 'edit_threecal_events', '3task-calendar', array( $this, 'render_admin_page' ), 'dashicons-calendar-alt', 26 ); } /** * Get available tabs. * * @return array Tabs configuration. */ private function get_tabs() { $counts = $this->get_tab_counts(); return array( 'dashboard' => array( 'title' => __( 'Dashboard', '3task-calendar' ), 'icon' => 'layout-dashboard', ), 'events' => array( 'title' => __( 'Events', '3task-calendar' ), 'icon' => 'calendar-event', 'count' => $counts['events'], ), 'categories' => array( 'title' => __( 'Categories', '3task-calendar' ), 'icon' => 'category', 'count' => $counts['categories'], ), 'locations' => array( 'title' => __( 'Locations', '3task-calendar' ), 'icon' => 'map-pin', 'count' => $counts['locations'], ), 'posts' => array( 'title' => __( 'Posts', '3task-calendar' ), 'icon' => 'article', ), 'import' => array( 'title' => __( 'Import', '3task-calendar' ), 'icon' => 'file-import', ), 'design' => array( 'title' => __( 'Design', '3task-calendar' ), 'icon' => 'palette', ), 'settings' => array( 'title' => __( 'Settings', '3task-calendar' ), 'icon' => 'settings', ), 'help' => array( 'title' => __( 'Help', '3task-calendar' ), 'icon' => 'help-circle', ), ); } /** * Counts for the tab badges. * * @return array */ private function get_tab_counts() { global $wpdb; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Counts from custom tables. $events = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}threecal_events WHERE parent_id IS NULL OR parent_id = 0" ); $categories = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}threecal_categories" ); $locations = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}threecal_locations" ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching return compact( 'events', 'categories', 'locations' ); } /** * Render main admin page with tabs. */ public function render_admin_page() { $tabs = $this->get_tabs(); ?>

render_tab_content(); ?>
current_tab ) { case 'events': $this->render_events(); break; case 'categories': $this->render_categories(); break; case 'locations': $this->render_locations(); break; case 'design': $this->render_design(); break; case 'posts': ThreeCal_Post_Dates::render_settings_page(); break; case 'import': $this->render_import(); break; case 'settings': $this->render_settings(); break; case 'help': $this->render_help(); break; default: $this->render_dashboard(); } } /** * Render dashboard tab. */ private function render_dashboard() { $stats = $this->get_event_stats(); $counts = $this->get_tab_counts(); $upcoming = ThreeCal_Event::get_upcoming( 5 ); $cats = ThreeCal_Event::get_categories_for_events( wp_list_pluck( $upcoming, 'id' ) ); $pending = ThreeCal_Submit::pending_count(); if ( $pending ) { echo '

' . esc_html( sprintf( /* translators: %s: number of submitted events */ _n( '%s event was submitted by a visitor and waits for your check.', '%s events were submitted by visitors and wait for your check.', $pending, '3task-calendar' ), number_format_i18n( $pending ) ) ) . ' ' . esc_html__( 'Show submitted events', '3task-calendar' ) . '

'; } $new_url = admin_url( 'admin.php?page=3task-calendar&tab=events&action=new' ); ?>

' . esc_html__( 'You do not have permission to change the calendar settings.', '3task-calendar' ) . '

'; return; } $themes = ThreeCal_Themes::all(); if ( isset( $_POST['threecal_save_design'] ) ) { check_admin_referer( 'threecal_design' ); $settings = (array) get_option( 'threecal_settings', array() ); $theme = isset( $_POST['default_theme'] ) ? sanitize_key( wp_unslash( $_POST['default_theme'] ) ) : 'default'; $settings['default_theme'] = isset( $themes[ $theme ] ) ? $theme : 'default'; $accent = isset( $_POST['accent_color'] ) ? sanitize_hex_color( wp_unslash( $_POST['accent_color'] ) ) : ''; $settings['accent_color'] = $accent ? $accent : ''; update_option( 'threecal_settings', $settings ); echo '

' . esc_html__( 'Settings saved!', '3task-calendar' ) . '

'; } $current = ThreeCal_Themes::normalize( '' ); $settings = (array) get_option( 'threecal_settings', array() ); $accent = isset( $settings['accent_color'] ) ? $settings['accent_color'] : ''; $renderer = new ThreeCal_Calendar_Renderer(); wp_enqueue_style( 'threecal-public', THREECAL_PLUGIN_URL . 'public/css/threecal.css', array(), threecal_asset_version( 'public/css/threecal.css' ) ); wp_enqueue_style( 'threecal-mini', THREECAL_PLUGIN_URL . 'public/css/threecal-mini.css', array(), threecal_asset_version( 'public/css/threecal-mini.css' ) ); ?>

$theme ) : ?>

render_edit_event(); return; } // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Only reading for navigation, no data modification. if ( isset( $_GET['action'] ) && $_GET['action'] === 'new' ) { // phpcs:enable WordPress.Security.NonceVerification.Recommended $this->render_new_event(); return; } // List events (repetitions of a series are managed through their first event). // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display filter only. $scope = isset( $_GET['scope'] ) ? sanitize_key( wp_unslash( $_GET['scope'] ) ) : 'upcoming'; $scope = in_array( $scope, array( 'upcoming', 'past', 'all', 'submitted' ), true ) ? $scope : 'upcoming'; /** * Filters the events shown in the admin list. * * @param array $events Event rows. * @param string $scope upcoming, past or all. */ $events = apply_filters( 'threecal_admin_events', $this->get_events( $scope ), $scope ); // Dates that come from posts are kept in the posts, not in this list. $from_posts = 0; foreach ( $events as $index => $row ) { $row_settings = json_decode( (string) $row->settings, true ); if ( ! empty( $row_settings['post_id'] ) ) { $from_posts++; unset( $events[ $index ] ); } } $events = array_values( $events ); $row_settings_all = array(); foreach ( $events as $row ) { $row_settings_all[ $row->id ] = json_decode( (string) $row->settings, true ); } $cats = ThreeCal_Event::get_categories_for_events( wp_list_pluck( $events, 'id' ) ); $rules = ThreeCal_Event::recurrence_options(); $scopes = array( 'upcoming' => __( 'Upcoming', '3task-calendar' ), 'past' => __( 'Past', '3task-calendar' ), 'all' => __( 'All', '3task-calendar' ), ); $pending_count = ThreeCal_Submit::pending_count(); if ( $pending_count || 'submitted' === $scope ) { /* translators: %s: number of submitted events */ $scopes['submitted'] = sprintf( __( 'Submitted (%s)', '3task-calendar' ), number_format_i18n( $pending_count ) ); } $status_labels = array( 'published' => __( 'Published', '3task-calendar' ), 'draft' => __( 'Draft', '3task-calendar' ), 'cancelled' => __( 'Cancelled', '3task-calendar' ), ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Message param is for display only. $message = isset( $_GET['message'] ) ? sanitize_key( wp_unslash( $_GET['message'] ) ) : ''; ?>

$type_settings ) { $type_object = get_post_type_object( $type_key ); if ( $type_object ) { echo ' ' . esc_html( $type_object->labels->name ) . ''; } } ?>

$label ) : ?> >

' . esc_html__( 'You do not have permission to change the calendar settings.', '3task-calendar' ) . '

'; return; } if ( isset( $_POST['submit'] ) ) { check_admin_referer( 'threecal_settings' ); // Merge with the stored settings so fields that are not part of this form stay untouched. $settings = (array) get_option( 'threecal_settings', array() ); $settings['date_format'] = isset( $_POST['date_format'] ) ? sanitize_text_field( wp_unslash( $_POST['date_format'] ) ) : ''; $settings['time_format'] = isset( $_POST['time_format'] ) ? sanitize_text_field( wp_unslash( $_POST['time_format'] ) ) : ''; $settings['week_starts_on'] = ( isset( $_POST['week_starts_on'] ) && 0 === absint( $_POST['week_starts_on'] ) ) ? 0 : 1; $settings['show_event_time'] = ! empty( $_POST['show_event_time'] ); $settings['show_event_location'] = ! empty( $_POST['show_event_location'] ); $settings['enable_schema'] = ! empty( $_POST['enable_schema'] ); $settings['external_links'] = ( isset( $_POST['external_links'] ) && 'follow' === $_POST['external_links'] ) ? 'follow' : 'nofollow'; $settings['ics_reminder'] = isset( $_POST['ics_reminder'] ) && in_array( absint( $_POST['ics_reminder'] ), self::reminder_choices_minutes(), true ) ? absint( $_POST['ics_reminder'] ) : 0; $settings['submit_category'] = isset( $_POST['submit_category'] ) ? absint( $_POST['submit_category'] ) : 0; $settings['submit_notify'] = ! empty( $_POST['submit_notify'] ); $settings['delete_data_on_uninstall'] = ! empty( $_POST['delete_data_on_uninstall'] ); update_option( 'threecal_settings', $settings ); echo '

' . esc_html__( 'Settings saved!', '3task-calendar' ) . '

'; } $settings = wp_parse_args( (array) get_option( 'threecal_settings', array() ), array( 'date_format' => '', 'time_format' => '', 'week_starts_on' => 1, 'show_event_time' => true, 'show_event_location' => true, 'enable_schema' => true, 'external_links' => 'nofollow', 'ics_reminder' => 0, 'submit_category' => 0, 'submit_notify' => true, 'delete_data_on_uninstall' => false, ) ); ?>

/>
/>


render_schema_doktor(); ?> render_llms(); ?> llms_snippet(); ?>

schema_report( 20 ); $labels = ThreeCal::schema_reason_labels(); $pages = threecal()->calendar_pages( 5 ); ?>

✓ ✗

$page ) : ?>

label */ public static function reminder_choices() { return array( 0 => __( 'No reminder', '3task-calendar' ), 30 => __( '30 minutes before', '3task-calendar' ), 60 => __( '1 hour before', '3task-calendar' ), 120 => __( '2 hours before', '3task-calendar' ), 1440 => __( '1 day before', '3task-calendar' ), 2880 => __( '2 days before', '3task-calendar' ), ); } /** * Allowed reminder values. Since 1.7.0 * * @return int[] */ public static function reminder_choices_minutes() { return array_keys( self::reminder_choices() ); } /** * Render help tab */ private function render_help() { ?>

[threecal]

[threecal view="month" category="1" location="2" theme="default" show_filters="true" show_legend="true" week_starts_on="1"]
view month
category 0
location 0
theme default
show_filters true
show_legend true
week_starts_on

[threecal_events]

[threecal_events category="1" location="2" limit="10" view="list" show_past="false" show_pagination="true" columns="3"]
category 0
location 0
limit 10
view list
show_past false
show_pagination true
columns 3

[threecal_post_date]

[threecal_events view="poster" category="1" columns="4"]

[threecal_events view="poster" month="current"]

[threecal_events view="poster" running="40"]

[threecal_upcoming]

[threecal_upcoming limit="5" category="1" show_date="true" show_time="true" show_location="true"]
limit 5
category 0
show_date true
show_time true
show_location true

[threecal_event id="123" show_map="true" show_description="true"]
id
show_map true
show_description true

[threecal_mini]

[threecal_mini category="1" show_nav="true" show_today="true" week_starts_on="1"]
category 0
show_nav true
show_today true
week_starts_on

  1. Widgets', '3task-calendar' ); ?>
  2. [threecal_upcoming limit="3"]


[threecal_mini]:
[threecal_upcoming limit="3"]:

' . esc_html__( 'support forum', '3task-calendar' ) . '' ); ?>

get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}threecal_events WHERE parent_id IS NULL OR parent_id = 0" ); $published_events = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}threecal_events WHERE status = 'published' AND (parent_id IS NULL OR parent_id = 0)" ); $draft_events = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}threecal_events WHERE status = 'draft' AND (parent_id IS NULL OR parent_id = 0)" ); $total_categories = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}threecal_categories" ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching return array( 'total_events' => $total_events ? $total_events : 0, 'published_events' => $published_events ? $published_events : 0, 'draft_events' => $draft_events ? $draft_events : 0, 'total_categories' => $total_categories ? $total_categories : 0, ); } /** * Get events */ private function get_events( $scope = 'all' ) { global $wpdb; $now = current_time( 'mysql' ); // Events of which only the month is known count as upcoming until their month is over. $month_start = substr( $now, 0, 7 ) . '-01 00:00:00'; $month_like = '%"' . ThreeCal_Event::PRECISION_KEY . '":"month"%'; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table for events. if ( 'upcoming' === $scope ) { $results = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}threecal_events WHERE (parent_id IS NULL OR parent_id = 0) AND (COALESCE(end_date, start_date) >= %s OR (recurrence_rule IS NOT NULL AND recurrence_rule <> '' AND COALESCE(recurrence_end, start_date) >= %s) OR (start_date >= %s AND settings LIKE %s)) ORDER BY start_date ASC", $now, $now, $month_start, $month_like ) ); } elseif ( 'past' === $scope ) { $results = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}threecal_events WHERE (parent_id IS NULL OR parent_id = 0) AND COALESCE(end_date, start_date) < %s AND (recurrence_rule IS NULL OR recurrence_rule = '' OR COALESCE(recurrence_end, start_date) < %s) AND NOT (start_date >= %s AND COALESCE(settings, '') LIKE %s) ORDER BY start_date DESC", $now, $now, $month_start, $month_like ) ); } elseif ( 'submitted' === $scope ) { $results = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}threecal_events WHERE (parent_id IS NULL OR parent_id = 0) AND status = 'draft' AND settings LIKE %s ORDER BY start_date ASC", '%"' . ThreeCal_Submit::KEY . '":%' ) ); } else { $results = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}threecal_events WHERE parent_id IS NULL OR parent_id = 0 ORDER BY start_date ASC" ); } // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching return $results ? $results : array(); } /** * Get categories */ private function get_categories() { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table for categories. $results = $wpdb->get_results( " SELECT c.*, COUNT(e.id) as event_count FROM {$wpdb->prefix}threecal_categories c LEFT JOIN {$wpdb->prefix}threecal_events e ON c.id = e.category_id GROUP BY c.id ORDER BY c.name ASC " ); return $results ? $results : array(); } /** * Get locations */ private function get_locations() { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table for locations. $results = $wpdb->get_results( " SELECT * FROM {$wpdb->prefix}threecal_locations ORDER BY name ASC " ); return $results ? $results : array(); } /** * Render mini calendar */ private function render_mini_calendar() { $current_date = current_time('Y-m-d'); $events = $this->get_upcoming_events(5); if ( empty( $events ) ) { echo '

' . esc_html__( 'No upcoming events.', '3task-calendar' ) . '

'; return; } echo '
'; foreach ($events as $event) { echo '
'; echo '' . esc_html($event->title) . '
'; echo '' . esc_html( wp_date( 'M j, Y', strtotime( $event->start_date ) ) ) . ''; echo '
'; } echo '
'; } /** * Get upcoming events */ private function get_upcoming_events( $limit = 5 ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table for events. $results = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}threecal_events WHERE start_date >= %s AND status = 'published' ORDER BY start_date ASC LIMIT %d", current_time( 'Y-m-d' ), $limit ) ); return $results ? $results : array(); } /** * Register settings */ public function register_settings() { register_setting('threecal_settings', 'threecal_settings', array( 'sanitize_callback' => array($this, 'sanitize_settings') )); // Handle form submissions $this->handle_form_submissions(); } /** * Sanitize settings */ public function sanitize_settings($input) { $input = is_array($input) ? $input : array(); $themes = ThreeCal_Themes::all(); $theme = isset($input['default_theme']) ? sanitize_key($input['default_theme']) : 'default'; $view = isset($input['default_view']) ? sanitize_key($input['default_view']) : 'month'; $accent = isset($input['accent_color']) ? sanitize_hex_color($input['accent_color']) : ''; // Every key the plugin reads. A key missing here is lost on the next save. $sanitized = array( 'date_format' => sanitize_text_field($input['date_format'] ?? ''), 'time_format' => sanitize_text_field($input['time_format'] ?? ''), 'week_starts_on' => (isset($input['week_starts_on']) && 0 === absint($input['week_starts_on'])) ? 0 : 1, 'default_view' => in_array($view, array('month', 'list'), true) ? $view : 'month', 'default_theme' => isset($themes[$theme]) ? $theme : 'default', 'accent_color' => $accent ? $accent : '', 'show_event_time' => isset($input['show_event_time']) ? !empty($input['show_event_time']) : true, 'show_event_location' => isset($input['show_event_location']) ? !empty($input['show_event_location']) : true, 'show_event_description' => isset($input['show_event_description']) ? !empty($input['show_event_description']) : true, 'events_per_page' => max(1, absint($input['events_per_page'] ?? 10)), 'enable_event_popup' => isset($input['enable_event_popup']) ? !empty($input['enable_event_popup']) : true, 'enable_schema' => isset($input['enable_schema']) ? !empty($input['enable_schema']) : true, 'external_links' => (isset($input['external_links']) && 'follow' === $input['external_links']) ? 'follow' : 'nofollow', 'ics_reminder' => isset($input['ics_reminder']) && in_array(absint($input['ics_reminder']), self::reminder_choices_minutes(), true) ? absint($input['ics_reminder']) : 0, 'submit_category' => absint($input['submit_category'] ?? 0), 'submit_notify' => isset($input['submit_notify']) ? !empty($input['submit_notify']) : true, 'delete_data_on_uninstall' => !empty($input['delete_data_on_uninstall']), ); /** * Filters the sanitized settings, so extensions can keep their own keys. * * @param array $sanitized Sanitized settings. * @param array $input Raw settings. */ return apply_filters('threecal_sanitize_settings', $sanitized, $input); } /** * Handle form submissions */ private function handle_form_submissions() { // Sample file for the CSV import. if ( isset( $_GET['threecal_import_sample'], $_GET['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'threecal_import_sample' ) ) { $this->send_import_sample(); } // Save event. if ( isset( $_POST['threecal_save_event'] ) && check_admin_referer( 'threecal_save_event' ) ) { $this->save_event(); } // Delete event. if ( isset( $_GET['action'] ) && 'delete' === $_GET['action'] && isset( $_GET['id'], $_GET['_wpnonce'] ) ) { $nonce = sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ); $id = absint( $_GET['id'] ); if ( wp_verify_nonce( $nonce, 'threecal_delete_' . $id ) ) { $this->delete_event( $id ); } } // Cancel, restore or remove a single date of a series. if ( isset( $_GET['threecal_date_action'], $_GET['series'], $_GET['date'], $_GET['_wpnonce'] ) ) { $series_id = absint( $_GET['series'] ); $nonce = sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ); if ( wp_verify_nonce( $nonce, 'threecal_series_' . $series_id ) ) { $this->series_date_action( $series_id, sanitize_key( wp_unslash( $_GET['threecal_date_action'] ) ), sanitize_text_field( wp_unslash( $_GET['date'] ) ) ); } } // Save location. if ( isset( $_POST['threecal_save_location'] ) && check_admin_referer( 'threecal_save_location' ) ) { $this->save_location(); } // Delete location. if ( isset( $_GET['action'] ) && 'delete_location' === $_GET['action'] && isset( $_GET['id'], $_GET['_wpnonce'] ) ) { $nonce = sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ); $id = absint( $_GET['id'] ); if ( wp_verify_nonce( $nonce, 'threecal_delete_location_' . $id ) ) { $this->delete_location( $id ); } } // Save category. if ( isset( $_POST['threecal_save_category'] ) && check_admin_referer( 'threecal_save_category' ) ) { $this->save_category(); } // Delete category. if ( isset( $_GET['action'] ) && 'delete_category' === $_GET['action'] && isset( $_GET['id'], $_GET['_wpnonce'] ) ) { $nonce = sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ); $id = absint( $_GET['id'] ); if ( wp_verify_nonce( $nonce, 'threecal_delete_category_' . $id ) ) { $this->delete_category( $id ); } } } /** * Save event */ private function save_event() { if ( ! current_user_can( 'create_threecal_events' ) && ! current_user_can( 'edit_threecal_events' ) ) { wp_die( esc_html__( 'Permission denied.', '3task-calendar' ) ); } // Nonce is already verified in handle_form_submissions() via check_admin_referer(). // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in handle_form_submissions. $event_id = isset( $_POST['event_id'] ) ? absint( $_POST['event_id'] ) : 0; if ( $event_id > 0 ) { $event = ThreeCal_Event::get( $event_id ); if ( ! $event ) { wp_die( esc_html__( 'Event not found.', '3task-calendar' ) ); } } else { $event = new ThreeCal_Event(); } // Remember where a repetition came from, so its date can be excluded from the series. $original_parent = (int) $event->parent_id; $original_day = substr( (string) $event->start_date, 0, 10 ); $event->title = isset( $_POST['event_title'] ) ? sanitize_text_field( wp_unslash( $_POST['event_title'] ) ) : ''; $event->description = isset( $_POST['event_description'] ) ? wp_kses_post( wp_unslash( $_POST['event_description'] ) ) : ''; $event->start_date = isset( $_POST['event_start_date'] ) ? sanitize_text_field( wp_unslash( $_POST['event_start_date'] ) ) : ''; $event->end_date = isset( $_POST['event_end_date'] ) ? sanitize_text_field( wp_unslash( $_POST['event_end_date'] ) ) : ''; $event->all_day = isset( $_POST['event_all_day'] ); $event->location_id = isset( $_POST['event_location'] ) ? absint( $_POST['event_location'] ) : 0; $event->url = isset( $_POST['event_url'] ) ? esc_url_raw( wp_unslash( $_POST['event_url'] ) ) : ''; $event->featured_image = isset( $_POST['event_featured_image'] ) ? absint( $_POST['event_featured_image'] ) : 0; $event->color = isset( $_POST['event_color'] ) ? ( sanitize_hex_color( wp_unslash( $_POST['event_color'] ) ) ?: '#3788d8' ) : '#3788d8'; $event->status = isset( $_POST['event_status'] ) ? sanitize_key( wp_unslash( $_POST['event_status'] ) ) : 'published'; $event->set_organizer( isset( $_POST['event_organizer_name'] ) ? sanitize_text_field( wp_unslash( $_POST['event_organizer_name'] ) ) : '', isset( $_POST['event_organizer_url'] ) ? esc_url_raw( wp_unslash( $_POST['event_organizer_url'] ), array( 'http', 'https' ) ) : '' ); if ( ! in_array( $event->status, array( 'draft', 'published', 'cancelled' ), true ) ) { $event->status = 'draft'; } // Normalise dates (datetime-local sends Y-m-d\TH:i) and reject invalid input. $event->start_date = $this->normalize_datetime( $event->start_date ); $event->end_date = $event->end_date ? $this->normalize_datetime( $event->end_date ) : ''; // Precision: the day is known, or only the month ("July 2027, date to be announced"). $precision = ( isset( $_POST['event_date_precision'] ) && 'month' === sanitize_key( wp_unslash( $_POST['event_date_precision'] ) ) ) ? 'month' : 'day'; if ( 'month' === $precision ) { $event->start_date = self::normalize_month( isset( $_POST['event_start_month'] ) ? sanitize_text_field( wp_unslash( $_POST['event_start_month'] ) ) : '' ); if ( '' === $event->title || '' === $event->start_date ) { wp_die( esc_html__( 'Please enter a title and a valid month, for example 2027-07.', '3task-calendar' ), '', array( 'back_link' => true ) ); } $rule = isset( $_POST['event_recurrence'] ) ? sanitize_key( wp_unslash( $_POST['event_recurrence'] ) ) : ''; if ( '' !== $rule && ! $event->parent_id ) { wp_die( esc_html__( 'An event of which only the month is known cannot repeat. Set "Repeat" to "Does not repeat".', '3task-calendar' ), '', array( 'back_link' => true ) ); } } if ( '' === $event->title || '' === $event->start_date ) { wp_die( esc_html__( 'Please enter a title and a valid start date.', '3task-calendar' ), '', array( 'back_link' => true ) ); } $categories = isset( $_POST['event_categories'] ) ? array_map( 'absint', (array) $_POST['event_categories'] ) : array(); /** * Filters whether the current user may save this event. * * @param bool $allowed Whether saving is allowed. * @param ThreeCal_Event $event The event with the submitted values (ID 0 for a new one). * @param int[] $categories Submitted category IDs. */ if ( ! apply_filters( 'threecal_user_can_save_event', true, $event, $categories ) ) { wp_die( esc_html__( 'Permission denied.', '3task-calendar' ), '', array( 'back_link' => true ) ); } // An end before the start is treated as "no end". if ( $event->end_date && $event->end_date < $event->start_date ) { $event->end_date = ''; } // Repeat pattern. Editing a single repetition detaches it from its series. if ( $event->parent_id ) { $event->parent_id = 0; $event->recurrence_rule = ''; $event->recurrence_end = ''; } else { $rule = isset( $_POST['event_recurrence'] ) ? sanitize_key( wp_unslash( $_POST['event_recurrence'] ) ) : ''; $event->recurrence_rule = array_key_exists( $rule, ThreeCal_Event::recurrence_options() ) ? $rule : ''; $until = isset( $_POST['event_recurrence_end'] ) ? $this->normalize_datetime( sanitize_text_field( wp_unslash( $_POST['event_recurrence_end'] ) ) ) : ''; $event->recurrence_end = ( $event->recurrence_rule && $until ) ? substr( $until, 0, 10 ) . ' 23:59:59' : ''; } // Month only: first of the month, all day, no end, no repeat. Day: as before. $event->set_date_precision( $precision ); if ( $event->save() ) { // Set categories. $event->set_categories( $categories ); // phpcs:enable WordPress.Security.NonceVerification.Missing // Create or update the repetitions of a series. $event->regenerate_series(); /** * Fires after an event has been saved from the admin form. * * @param ThreeCal_Event $event The saved event. * @param int[] $categories Category IDs. */ do_action( 'threecal_admin_event_saved', $event, $categories ); // A detached repetition becomes an exception of its series. if ( $original_parent && $original_day ) { $parent = ThreeCal_Event::get( $original_parent ); if ( $parent ) { $exdates = (array) $parent->get_setting( 'exdates', array() ); $exdates[] = $original_day; $parent->set_setting( 'exdates', array_values( array_unique( $exdates ) ) ); $parent->save(); } } // Redirect $redirect = add_query_arg(array( 'page' => '3task-calendar', 'tab' => 'events', 'action' => 'edit', 'event' => $event->id, 'message' => 'saved' ), admin_url('admin.php')); wp_safe_redirect( $redirect ); exit; } else { wp_die( esc_html__( 'Error saving event.', '3task-calendar' ) ); } } /** * Convert a submitted date to Y-m-d H:i:s or return an empty string. * * @param string $value Submitted value. * @return string */ private function normalize_datetime( $value ) { $value = trim( str_replace( 'T', ' ', (string) $value ) ); foreach ( array( 'Y-m-d H:i:s', 'Y-m-d H:i', 'Y-m-d' ) as $format ) { $date = DateTime::createFromFormat( '!' . $format, $value ); if ( $date && $date->format( $format ) === $value ) { return $date->format( 'Y-m-d H:i:s' ); } } return ''; } /** * Convert a month (YYYY-MM) to the first day of that month as Y-m-d H:i:s. * * @since 1.6.0 * @param string $value Submitted value. * @return string Empty string for invalid input. */ public static function normalize_month( $value ) { return ThreeCal_Event::parse_month( $value ); } /** * Delete event */ private function delete_event($id) { if (!current_user_can('delete_threecal_events')) { wp_die( esc_html__( 'Permission denied.', '3task-calendar' ) ); } $event = ThreeCal_Event::get($id); /** * Filters whether the current user may delete this event. * * @param bool $allowed Whether deleting is allowed. * @param ThreeCal_Event $event The event. */ if ( $event && ! apply_filters( 'threecal_user_can_delete_event', true, $event ) ) { wp_die( esc_html__( 'Permission denied.', '3task-calendar' ), '', array( 'back_link' => true ) ); } if ($event && $event->delete()) { wp_safe_redirect( add_query_arg( array( 'page' => '3task-calendar', 'tab' => 'events', 'message' => 'deleted' ), admin_url('admin.php'))); exit; } } /** * Cancel, restore or remove a single date of a series. * * @param int $series_id ID of the series (its first event). * @param string $action cancel, restore or remove. * @param string $day Date (Y-m-d). */ private function series_date_action( $series_id, $action, $day ) { if ( ! current_user_can( 'edit_threecal_events' ) ) { wp_die( esc_html__( 'Permission denied.', '3task-calendar' ) ); } $series = ThreeCal_Event::get( $series_id ); if ( ! $series || $series->parent_id || ! $series->recurrence_rule || ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $day ) || ! in_array( $action, array( 'cancel', 'restore', 'remove' ), true ) ) { wp_die( esc_html__( 'This date could not be changed.', '3task-calendar' ), '', array( 'back_link' => true ) ); } /** This filter is documented in admin/class-admin.php */ if ( ! apply_filters( 'threecal_user_can_save_event', true, $series, wp_list_pluck( ThreeCal_Event::get_categories( $series->id ), 'id' ) ) ) { wp_die( esc_html__( 'Permission denied.', '3task-calendar' ), '', array( 'back_link' => true ) ); } $target = $series->id; $split = null; foreach ( $series->get_split_events() as $candidate ) { if ( substr( (string) $candidate->start_date, 0, 10 ) === $day ) { $split = $candidate; break; } } if ( $split ) { // A former first date that is a separate event now. if ( 'remove' === $action ) { $split->delete(); $ids = array_diff( array_map( 'absint', (array) $series->get_setting( 'split_dates', array() ) ), array( (int) $split->id ) ); $series->set_setting( 'split_dates', array_values( $ids ) ); $series->save(); } else { $split->status = ( 'cancel' === $action && 'published' === $series->status ) ? 'cancelled' : $series->status; $split->save(); } } elseif ( substr( (string) $series->start_date, 0, 10 ) === $day ) { // The first date is the series itself: the next date takes the series over. $next = $series->split_off_first_date(); if ( 'remove' === $action ) { $series->delete(); } elseif ( 'cancel' === $action ) { $series->status = 'cancelled'; $series->save(); } $target = $next ? $next : ( 'remove' === $action ? 0 : $series->id ); } elseif ( 'remove' === $action ) { $series->remove_date( $day ); } else { $series->set_date_cancelled( $day, 'cancel' === $action ); } $args = array( 'page' => '3task-calendar', 'tab' => 'events', 'message' => 'date_' . $action, ); if ( $target ) { $args['action'] = 'edit'; $args['event'] = $target; } wp_safe_redirect( add_query_arg( $args, admin_url( 'admin.php' ) ) ); exit; } /** * Save location */ private function save_location() { if ( ! current_user_can( 'manage_threecal_locations' ) ) { wp_die( esc_html__( 'Permission denied.', '3task-calendar' ) ); } // Nonce is already verified in handle_form_submissions() via check_admin_referer(). // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in handle_form_submissions. $location_id = isset( $_POST['location_id'] ) ? absint( $_POST['location_id'] ) : 0; if ( $location_id > 0 ) { $location = ThreeCal_Location::get( $location_id ); if ( ! $location ) { wp_die( esc_html__( 'Location not found.', '3task-calendar' ) ); } } else { $location = new ThreeCal_Location(); } $location->name = isset( $_POST['location_name'] ) ? sanitize_text_field( wp_unslash( $_POST['location_name'] ) ) : ''; $location->address = isset( $_POST['location_address'] ) ? sanitize_text_field( wp_unslash( $_POST['location_address'] ) ) : ''; $location->city = isset( $_POST['location_city'] ) ? sanitize_text_field( wp_unslash( $_POST['location_city'] ) ) : ''; $location->postal_code = isset( $_POST['location_postal_code'] ) ? sanitize_text_field( wp_unslash( $_POST['location_postal_code'] ) ) : ''; $location->country = isset( $_POST['location_country'] ) ? sanitize_text_field( wp_unslash( $_POST['location_country'] ) ) : 'DE'; $location->phone = isset( $_POST['location_phone'] ) ? sanitize_text_field( wp_unslash( $_POST['location_phone'] ) ) : ''; $location->email = isset( $_POST['location_email'] ) ? sanitize_email( wp_unslash( $_POST['location_email'] ) ) : ''; $location->website = isset( $_POST['location_website'] ) ? esc_url_raw( wp_unslash( $_POST['location_website'] ) ) : ''; $location->description = isset( $_POST['location_description'] ) ? wp_kses_post( wp_unslash( $_POST['location_description'] ) ) : ''; $location->featured_image = isset( $_POST['location_featured_image'] ) ? absint( $_POST['location_featured_image'] ) : 0; $location->latitude = ( isset( $_POST['location_latitude'] ) && '' !== trim( sanitize_text_field( wp_unslash( $_POST['location_latitude'] ) ) ) ) ? floatval( $_POST['location_latitude'] ) : null; $location->longitude = ( isset( $_POST['location_longitude'] ) && '' !== trim( sanitize_text_field( wp_unslash( $_POST['location_longitude'] ) ) ) ) ? floatval( $_POST['location_longitude'] ) : null; // phpcs:enable WordPress.Security.NonceVerification.Missing if ( $location->save() ) { wp_safe_redirect( add_query_arg( array( 'page' => '3task-calendar', 'tab' => 'locations', 'message' => 'saved', ), admin_url( 'admin.php' ) ) ); exit; } } /** * Delete location */ private function delete_location($id) { if (!current_user_can('manage_threecal_locations')) { wp_die( esc_html__( 'Permission denied.', '3task-calendar' ) ); } $location = ThreeCal_Location::get($id); if ($location && $location->delete()) { wp_safe_redirect( add_query_arg( array( 'page' => '3task-calendar', 'tab' => 'locations', 'message' => 'deleted' ), admin_url('admin.php'))); exit; } } /** * Save category */ private function save_category() { if ( ! current_user_can( 'manage_threecal_categories' ) ) { wp_die( esc_html__( 'Permission denied.', '3task-calendar' ) ); } // Nonce is already verified in handle_form_submissions() via check_admin_referer(). // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in handle_form_submissions. $category_id = isset( $_POST['category_id'] ) ? absint( $_POST['category_id'] ) : 0; if ( $category_id > 0 ) { $category = ThreeCal_Category::get( $category_id ); if ( ! $category ) { wp_die( esc_html__( 'Category not found.', '3task-calendar' ) ); } } else { $category = new ThreeCal_Category(); } $category->name = isset( $_POST['category_name'] ) ? sanitize_text_field( wp_unslash( $_POST['category_name'] ) ) : ''; $category->slug = isset( $_POST['category_slug'] ) ? sanitize_title( wp_unslash( $_POST['category_slug'] ) ) : ''; $category->description = isset( $_POST['category_description'] ) ? sanitize_textarea_field( wp_unslash( $_POST['category_description'] ) ) : ''; $category->color = isset( $_POST['category_color'] ) ? ( sanitize_hex_color( wp_unslash( $_POST['category_color'] ) ) ?: '#3788d8' ) : '#3788d8'; $category->parent_id = isset( $_POST['category_parent'] ) ? absint( $_POST['category_parent'] ) : 0; $category->sort_order = isset( $_POST['category_sort_order'] ) ? absint( $_POST['category_sort_order'] ) : 0; // phpcs:enable WordPress.Security.NonceVerification.Missing if ( $category->save() ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in handle_form_submissions. ThreeCal_Category::set_schema_excluded( $category->id, ! empty( $_POST['category_no_schema'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in handle_form_submissions. ThreeCal_Category::set_highlighted( $category->id, ! empty( $_POST['category_highlight'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in handle_form_submissions. ThreeCal_Category::set_follow( $category->id, ! empty( $_POST['category_follow'] ) ); wp_safe_redirect( add_query_arg( array( 'page' => '3task-calendar', 'tab' => 'categories', 'message' => 'saved', ), admin_url( 'admin.php' ) ) ); exit; } } /** * Delete category */ private function delete_category($id) { if (!current_user_can('manage_threecal_categories')) { wp_die( esc_html__( 'Permission denied.', '3task-calendar' ) ); } $category = ThreeCal_Category::get($id); if ($category && $category->delete()) { ThreeCal_Category::set_schema_excluded($id, false); wp_safe_redirect( add_query_arg( array( 'page' => '3task-calendar', 'tab' => 'categories', 'message' => 'deleted' ), admin_url('admin.php'))); exit; } } /** * Transient that holds a checked import between preview and import. * * @since 1.6.0 * @param int $user_id User ID. * @param string $token Random token of the preview. * @return string */ private static function import_transient( $user_id, $token ) { return 'threecal_import_' . (int) $user_id . '_' . $token; } /** * Importer with the rights of the current user: categories and locations * are only created by users who may manage them. * * @since 1.6.0 * @return ThreeCal_Importer */ private function importer() { return new ThreeCal_Importer( array( 'create_categories' => current_user_can( 'manage_threecal_categories' ), 'create_locations' => current_user_can( 'manage_threecal_locations' ), ) ); } /** * Check the uploaded CSV file. * * @since 1.6.0 * @return array|WP_Error Array( temporary path, file name ). */ private function uploaded_csv() { // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce checked in render_import() before this is called. if ( empty( $_FILES['threecal_csv'] ) || ! is_array( $_FILES['threecal_csv'] ) ) { return new WP_Error( 'threecal_import_nofile', __( 'Please choose a CSV file.', '3task-calendar' ) ); } $error = isset( $_FILES['threecal_csv']['error'] ) ? (int) $_FILES['threecal_csv']['error'] : UPLOAD_ERR_NO_FILE; $size = isset( $_FILES['threecal_csv']['size'] ) ? (int) $_FILES['threecal_csv']['size'] : 0; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Temporary path from PHP, checked with is_uploaded_file() below. $tmp = isset( $_FILES['threecal_csv']['tmp_name'] ) ? (string) $_FILES['threecal_csv']['tmp_name'] : ''; $name = isset( $_FILES['threecal_csv']['name'] ) ? sanitize_file_name( wp_unslash( $_FILES['threecal_csv']['name'] ) ) : ''; // phpcs:enable WordPress.Security.NonceVerification.Missing if ( UPLOAD_ERR_NO_FILE === $error ) { return new WP_Error( 'threecal_import_nofile', __( 'Please choose a CSV file.', '3task-calendar' ) ); } if ( UPLOAD_ERR_INI_SIZE === $error || UPLOAD_ERR_FORM_SIZE === $error || $size > ThreeCal_Importer::MAX_BYTES ) { /* translators: %s: maximum file size, for example "2 MB" */ return new WP_Error( 'threecal_import_size', sprintf( __( 'The file is larger than %s.', '3task-calendar' ), size_format( ThreeCal_Importer::MAX_BYTES ) ) ); } if ( UPLOAD_ERR_OK !== $error || '' === $tmp || ! is_uploaded_file( $tmp ) ) { return new WP_Error( 'threecal_import_upload', __( 'The upload did not work. Please try again.', '3task-calendar' ) ); } if ( ! in_array( strtolower( pathinfo( $name, PATHINFO_EXTENSION ) ), array( 'csv', 'txt' ), true ) ) { return new WP_Error( 'threecal_import_type', __( 'Please upload a file ending in .csv.', '3task-calendar' ) ); } return array( $tmp, $name ); } /** * Send the sample CSV for the import. * * @since 1.6.0 */ private function send_import_sample() { if ( ! current_user_can( 'edit_threecal_events' ) ) { wp_die( esc_html__( 'Permission denied.', '3task-calendar' ) ); } nocache_headers(); header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="3task-calendar-import-sample.csv"' ); echo ThreeCal_Importer::sample_csv(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSV download, not HTML. exit; } /** * Render import tab: upload a CSV file, check the preview, then import. * * @since 1.6.0 */ private function render_import() { if ( ! current_user_can( 'edit_threecal_events' ) ) { echo '

' . esc_html__( 'Permission denied.', '3task-calendar' ) . '

'; return; } $user_id = get_current_user_id(); $plan = null; $result = null; $token = ''; $error = ''; $file = ''; if ( isset( $_POST['threecal_import_preview'] ) ) { check_admin_referer( 'threecal_import_upload' ); $status = ( isset( $_POST['threecal_import_status'] ) && 'publish' === sanitize_key( wp_unslash( $_POST['threecal_import_status'] ) ) ) ? 'publish' : 'draft'; $upload = $this->uploaded_csv(); $parsed = is_wp_error( $upload ) ? $upload : ThreeCal_Importer::read_file( $upload[0] ); if ( is_wp_error( $parsed ) ) { $error = $parsed->get_error_message(); } else { $file = $upload[1]; $plan = $this->importer()->analyze( $parsed, $status ); $token = wp_generate_password( 24, false ); // Only the checked plan is kept, on the server and for this user. set_transient( self::import_transient( $user_id, $token ), array( 'user' => $user_id, 'file' => $file, 'plan' => $plan, ), 30 * MINUTE_IN_SECONDS ); } } elseif ( isset( $_POST['threecal_import_run'] ) ) { $token = isset( $_POST['threecal_import_token'] ) ? preg_replace( '/[^A-Za-z0-9]/', '', sanitize_text_field( wp_unslash( $_POST['threecal_import_token'] ) ) ) : ''; check_admin_referer( 'threecal_import_run_' . $token ); $stored = '' !== $token ? get_transient( self::import_transient( $user_id, $token ) ) : false; if ( ! is_array( $stored ) || (int) $stored['user'] !== $user_id || empty( $stored['plan'] ) ) { $error = __( 'The preview has expired. Please upload the file again.', '3task-calendar' ); } else { delete_transient( self::import_transient( $user_id, $token ) ); $plan = $stored['plan']; $file = $stored['file']; $result = $this->importer()->run( $plan ); } } $labels = array( 'new' => __( 'New', '3task-calendar' ), 'update' => __( 'Will be updated', '3task-calendar' ), 'error' => __( 'Error', '3task-calendar' ), ); $sample_url = wp_nonce_url( admin_url( 'admin.php?page=3task-calendar&tab=import&threecal_import_sample=1' ), 'threecal_import_sample' ); $base_url = admin_url( 'admin.php?page=3task-calendar&tab=import' ); ?>


import_key
title *
start *
end
all_day
url
description
category
location_name, address, postal_code, city, country
status
organizer_name, organizer_url