array( key, values ), "stack". * * @package BeariusCalendar */ defined( 'ABSPATH' ) || exit; /** * Settings tabs, their sections and fields. * * @return array */ function bcal_settings_schema() { $views = array(); foreach ( bcal_views() as $key => $view ) { $views[ $key ] = $view['label']; } $layouts = array(); foreach ( bcal_single_layouts() as $key => $layout ) { $layouts[ $key ] = $layout['label']; } $palettes = array(); foreach ( bcal_palettes() as $key => $palette ) { $palettes[ $key ] = $palette['label']; } $palettes['custom'] = __( 'Custom', 'bearius-calendar' ); $theme_colors = array( '' => __( '(choose a colour)', 'bearius-calendar' ) ); foreach ( bcal_theme_palette() as $slug => $color ) { $theme_colors[ $slug ] = $color['name']; } $accent_sources = array( 'custom' => __( 'My colour', 'bearius-calendar' ) ); if ( count( $theme_colors ) > 1 ) { $accent_sources['theme'] = __( 'Theme palette', 'bearius-calendar' ); } $dark_if = array( 'color_mode', array( 'auto', 'dark' ) ); $device_views = array_merge( array( '' => __( 'Same as on computers', 'bearius-calendar' ) ), $views ); $languages = bcal_admin_language_choices(); $tabs = array( 'general' => array( 'label' => __( 'General', 'bearius-calendar' ), 'fields' => array( 'admin_language' => array( 'select', __( 'Language of the plugin screens', 'bearius-calendar' ), __( 'Only the admin screens of this plugin. Visitors always see the site language.', 'bearius-calendar' ), $languages ), 'slug' => array( 'slug', __( 'Events', 'bearius-calendar' ), __( 'Archive and event addresses.', 'bearius-calendar' ) ), 'slug_single' => array( 'slug', __( 'Single event', 'bearius-calendar' ), __( 'Leave empty to use the same base as the archive.', 'bearius-calendar' ), 'optional' => true ), 'slug_category' => array( 'slug', __( 'Category', 'bearius-calendar' ), '' ), 'slug_city' => array( 'slug', __( 'City', 'bearius-calendar' ), '' ), 'slug_venue' => array( 'slug', __( 'Venue', 'bearius-calendar' ), '' ), 'slug_organizer' => array( 'slug', __( 'Organizer', 'bearius-calendar' ), '' ), 'archive_title' => array( 'text', __( 'Archive title', 'bearius-calendar' ), __( 'Leave empty for "Events calendar".', 'bearius-calendar' ) ), 'per_page' => array( 'number', __( 'Events per page', 'bearius-calendar' ), '', 1, 200 ), 'default_view' => array( 'select', __( 'Default view', 'bearius-calendar' ), __( 'On computers and wide screens.', 'bearius-calendar' ), $views ), 'default_view_tablet' => array( 'select', __( 'Default view on tablets', 'bearius-calendar' ), __( 'Screens from 641 to 1024 px.', 'bearius-calendar' ), $device_views ), 'default_view_phone' => array( 'select', __( 'Default view on phones', 'bearius-calendar' ), __( 'Screens up to 640 px. A view picked by the visitor always wins.', 'bearius-calendar' ), array_merge( array( '' => __( 'Same as on tablets', 'bearius-calendar' ) ), $views ) ), 'enabled_views' => array( 'multicheck', __( 'Views visitors can switch to', 'bearius-calendar' ), '', $views ), 'show_switcher' => array( 'checkbox', __( 'View switcher', 'bearius-calendar' ), __( 'Buttons for changing the view above the calendar.', 'bearius-calendar' ) ), 'show_filters' => array( 'checkbox', __( 'Filters', 'bearius-calendar' ), __( 'Category chips plus city and period menus.', 'bearius-calendar' ) ), 'default_duration' => array( 'duration', __( 'Default duration', 'bearius-calendar' ), __( 'Used when an event has no end time of its own. An end time set on the event always wins. Changing this updates existing events in the background.', 'bearius-calendar' ), 0, 1440, 'suffix' => _x( 'min', 'minutes, short', 'bearius-calendar' ), 'presets' => array( 0, 30, 45, 60, 90, 120, 180 ), ), 'currency' => array( 'text', __( 'Default currency', 'bearius-calendar' ), __( 'ISO code, e.g. PLN or EUR.', 'bearius-calendar' ), 'short' => true ), 'window_months' => array( 'number', __( 'Repeat events this far ahead', 'bearius-calendar' ), __( 'Later dates are added automatically as time passes.', 'bearius-calendar' ), 1, 60, 'suffix' => __( 'months', 'bearius-calendar' ) ), 'no_cache_lists' => array( 'checkbox', __( 'Keep lists out of page cache', 'bearius-calendar' ), __( 'Lists stay current. Turn off if your cache is cleared often anyway.', 'bearius-calendar' ) ), ), 'sections' => array( 'language' => array( __( 'Language', 'bearius-calendar' ), '', array( 'admin_language' ) ), 'events' => array( __( 'Events', 'bearius-calendar' ), __( 'Defaults for new and imported events.', 'bearius-calendar' ), array( 'default_duration', 'currency', 'window_months' ) ), 'lists' => array( __( 'Lists', 'bearius-calendar' ), __( 'What visitors see on the calendar pages.', 'bearius-calendar' ), array( 'default_view', 'default_view_tablet', 'default_view_phone', 'enabled_views', 'per_page', 'show_switcher', 'show_filters' ) ), 'urls' => array( __( 'Addresses', 'bearius-calendar' ), __( 'URL bases for events and their archives.', 'bearius-calendar' ), array( 'slug', 'slug_single', 'slug_category', 'slug_city', 'slug_venue', 'slug_organizer', 'archive_title' ) ), 'caching' => array( __( 'Caching', 'bearius-calendar' ), '', array( 'no_cache_lists' ) ), ), ), 'appearance' => array( 'label' => __( 'Appearance', 'bearius-calendar' ), 'fields' => array( 'palette' => array( 'palette', __( 'Colour preset', 'bearius-calendar' ), __( 'Choosing a preset fills all colours below. Adjust any of them afterwards.', 'bearius-calendar' ), $palettes ), 'accent_source' => array( 'segmented', __( 'Accent from', 'bearius-calendar' ), '', $accent_sources ), 'theme_color' => array( 'select', __( 'Theme colour', 'bearius-calendar' ), __( 'Follows the theme when its palette changes. The accent below is the fallback.', 'bearius-calendar' ), $theme_colors, 'show_if' => array( 'accent_source', array( 'theme' ) ) ), 'accent' => array( 'color', __( 'Accent', 'bearius-calendar' ), __( 'Buttons, day numbers and active filters.', 'bearius-calendar' ) ), 'surface' => array( 'color', __( 'Background', 'bearius-calendar' ), '' ), 'text' => array( 'color', __( 'Text', 'bearius-calendar' ), '' ), 'muted' => array( 'color', __( 'Secondary text', 'bearius-calendar' ), '' ), 'border' => array( 'color', __( 'Lines', 'bearius-calendar' ), '' ), 'color_mode' => array( 'segmented', __( 'Colour mode', 'bearius-calendar' ), __( '"Theme" makes the calendar transparent and uses the theme text colour.', 'bearius-calendar' ), array( 'auto' => __( 'Automatic', 'bearius-calendar' ), 'light' => __( 'Light', 'bearius-calendar' ), 'dark' => __( 'Dark', 'bearius-calendar' ), 'inherit' => __( 'Theme', 'bearius-calendar' ), ), ), 'dark_accent' => array( 'color', __( 'Accent', 'bearius-calendar' ), '', 'show_if' => $dark_if ), 'dark_surface' => array( 'color', __( 'Background', 'bearius-calendar' ), '', 'show_if' => $dark_if ), 'dark_text' => array( 'color', __( 'Text', 'bearius-calendar' ), '', 'show_if' => $dark_if ), 'dark_muted' => array( 'color', __( 'Secondary text', 'bearius-calendar' ), '', 'show_if' => $dark_if ), 'dark_border' => array( 'color', __( 'Lines', 'bearius-calendar' ), '', 'show_if' => $dark_if ), 'auto_contrast' => array( 'checkbox', __( 'Keep text readable', 'bearius-calendar' ), __( 'Checks every colour against its background and darkens or lightens text that would be hard to read (WCAG AA). Your fill colours stay as they are.', 'bearius-calendar' ) ), 'radius' => array( 'range', __( 'Corner radius', 'bearius-calendar' ), '', 0, 32, 'suffix' => 'px' ), 'show_images' => array( 'checkbox', __( 'Event images', 'bearius-calendar' ), __( 'Uses the featured image and only sizes that already exist for each file.', 'bearius-calendar' ) ), 'fallback_tile' => array( 'checkbox', __( 'Coloured tile without an image', 'bearius-calendar' ), __( 'Shows the category on an accent tile when an event has no image.', 'bearius-calendar' ) ), 'fallback_image' => array( 'image', __( 'Fallback image', 'bearius-calendar' ), __( 'For events without their own image. Used only in data for Google (Event structured data) and in previews when a link is shared, never on the calendar pages.', 'bearius-calendar' ) ), 'limit_image_sizes' => array( 'checkbox', __( 'Only core image sizes for events', 'bearius-calendar' ), __( 'Skips extra sizes from the theme and other plugins for images uploaded to events. Saves disk space on large imports.', 'bearius-calendar' ) ), ), 'sections' => array( 'preset' => array( __( 'Colours', 'bearius-calendar' ), '', array( 'palette' ) ), 'light' => array( __( 'Light mode', 'bearius-calendar' ), '', array( 'accent_source', 'theme_color', 'accent', 'surface', 'text', 'muted', 'border' ) ), 'dark' => array( __( 'Dark mode', 'bearius-calendar' ), '', array( 'color_mode', 'dark_accent', 'dark_surface', 'dark_text', 'dark_muted', 'dark_border' ) ), 'contrast' => array( __( 'Readability', 'bearius-calendar' ), '', array( 'auto_contrast' ) ), 'shape' => array( __( 'Shape and images', 'bearius-calendar' ), '', array( 'radius', 'show_images', 'fallback_tile', 'fallback_image', 'limit_image_sizes' ) ), ), ), 'single' => array( 'label' => __( 'Event page', 'bearius-calendar' ), 'fields' => array( 'single_layout' => array( 'select', __( 'Layout', 'bearius-calendar' ), '', $layouts ), 'show_related' => array( 'checkbox', __( 'More events at the same venue', 'bearius-calendar' ), '' ), 'show_ics' => array( 'checkbox', __( '"Add to calendar" button', 'bearius-calendar' ), '' ), 'show_share' => array( 'checkbox', __( '"Share" button', 'bearius-calendar' ), '' ), 'sticky_bar' => array( 'checkbox', __( 'Sticky ticket bar on phones', 'bearius-calendar' ), '' ), 'show_map_link' => array( 'checkbox', __( '"Get directions" link', 'bearius-calendar' ), __( 'A plain link. Nothing is loaded from a map provider until a visitor clicks it.', 'bearius-calendar' ) ), 'map_provider' => array( 'select', __( 'Directions open in', 'bearius-calendar' ), __( 'With Apple Maps, other devices get Google Maps.', 'bearius-calendar' ), array( 'google' => __( 'Google Maps', 'bearius-calendar' ), 'apple_ios' => __( 'Apple Maps (iPhone, iPad)', 'bearius-calendar' ), 'osm' => __( 'OpenStreetMap', 'bearius-calendar' ), ), 'show_if' => array( 'show_map_link', array( '1' ) ), ), ), 'sections' => array( 'page' => array( __( 'Page', 'bearius-calendar' ), '', array( 'single_layout', 'show_related', 'show_ics', 'show_share', 'sticky_bar' ) ), 'map' => array( __( 'Directions', 'bearius-calendar' ), '', array( 'show_map_link', 'map_provider' ) ), ), ), 'notice' => array( 'label' => __( 'Affiliate notice', 'bearius-calendar' ), 'fields' => array( 'notice_default' => array( 'textarea', __( 'Default text', 'bearius-calendar' ), __( 'Used when there is no text for the current language. Leave empty to show nothing.', 'bearius-calendar' ) ), 'notice_langs' => array( 'langs', __( 'Text per language', 'bearius-calendar' ), __( 'Languages come from Polylang or WPML when active. You can also add a language code by hand.', 'bearius-calendar' ) ), 'notice_position' => array( 'segmented', __( 'Position', 'bearius-calendar' ), '', array( 'after_content' => __( 'Below the description', 'bearius-calendar' ), 'near_ticket' => __( 'Next to the ticket button', 'bearius-calendar' ), 'both' => __( 'Both', 'bearius-calendar' ), ), ), 'notice_only_ticket' => array( 'checkbox', __( 'Only with a ticket link', 'bearius-calendar' ), __( 'Hide the notice on events without a ticket link.', 'bearius-calendar' ) ), ), 'sections' => array( 'text' => array( __( 'Text', 'bearius-calendar' ), __( 'A short note for visitors that ticket links may earn you a commission.', 'bearius-calendar' ), array( 'notice_default', 'notice_langs' ) ), 'where' => array( __( 'Placement', 'bearius-calendar' ), '', array( 'notice_position', 'notice_only_ticket' ) ), ), ), 'import' => array( 'label' => __( 'Import', 'bearius-calendar' ), 'render' => 'bcal_render_import_tab', 'fields' => array( 'import_max_image_mb' => array( 'number', __( 'Largest image to download', 'bearius-calendar' ), __( 'Larger files are skipped and listed in the import summary.', 'bearius-calendar' ), 1, 20, 'suffix' => 'MB' ), 'import_images_day' => array( 'number', __( 'Images per feed per day', 'bearius-calendar' ), __( 'Protects the server and the source site from bulk downloads.', 'bearius-calendar' ), 10, 5000 ), 'import_media_hosts' => array( 'textarea', __( 'Other addresses of your media library', 'bearius-calendar' ), __( 'One host per line, e.g. a CDN or offload bucket. Images from these hosts are matched to files already in the library instead of being downloaded again.', 'bearius-calendar' ) ), ), 'sections' => array( 'import_media' => array( __( 'Images from imports', 'bearius-calendar' ), __( 'Limits for downloading images. Images already in your library are always reused.', 'bearius-calendar' ), array( 'import_max_image_mb', 'import_images_day', 'import_media_hosts' ) ), ), ), 'advanced' => array( 'label' => __( 'Advanced', 'bearius-calendar' ), 'fields' => array( 'schema' => array( 'checkbox', __( 'Event structured data', 'bearius-calendar' ), __( 'JSON-LD for search engines. Turn off if another plugin already outputs Event markup.', 'bearius-calendar' ) ), 'rss_fields' => array( 'checkbox', __( 'Dates and places in the RSS feed', 'bearius-calendar' ), '' ), 'ics' => array( 'checkbox', __( 'Calendar files (.ics)', 'bearius-calendar' ), __( 'Download for single events and a feed for calendar apps.', 'bearius-calendar' ) ), 'credit_link' => array( 'checkbox', __( 'Show a "Powered by Bearius" link in the footer', 'bearius-calendar' ), __( 'A small link that helps others find the plugin. It is marked nofollow, so it passes no ranking signals. If several Bearius plugins have this switched on, the link still appears only once.', 'bearius-calendar' ) ), 'delete_on_uninstall' => array( 'checkbox', __( 'Clean up when deleting the plugin', 'bearius-calendar' ), __( 'Removes settings and the date index. Events themselves are never deleted.', 'bearius-calendar' ) ), ), 'sections' => array( 'data' => array( __( 'Data output', 'bearius-calendar' ), '', array( 'schema', 'rss_fields', 'ics' ) ), 'credit' => array( __( 'Bearius link', 'bearius-calendar' ), __( 'Optional. Off unless you switch it on.', 'bearius-calendar' ), array( 'credit_link' ) ), 'cleanup' => array( __( 'Uninstall', 'bearius-calendar' ), '', array( 'delete_on_uninstall' ) ), ), ), ); // The language choice only makes sense once a translation is installed. if ( count( $languages ) < 3 ) { unset( $tabs['general']['fields']['admin_language'] ); } return apply_filters( 'bcal_settings_schema', $tabs ); } /** * Choices for the language of the plugin screens: the profile language, * English and every installed translation. * * @return array locale => name */ function bcal_admin_language_choices() { $names = array( 'cs_CZ' => 'Čeština', 'da_DK' => 'Dansk', 'de_DE' => 'Deutsch', 'es_ES' => 'Español', 'fr_FR' => 'Français', 'it_IT' => 'Italiano', 'hu_HU' => 'Magyar', 'nl_NL' => 'Nederlands', 'nb_NO' => 'Norsk bokmål', 'pl_PL' => 'Polski', 'pt_BR' => 'Português do Brasil', 'pt_PT' => 'Português', 'ro_RO' => 'Română', 'sk_SK' => 'Slovenčina', 'fi' => 'Suomi', 'sv_SE' => 'Svenska', 'uk' => 'Українська', ); $choices = array( '' => __( 'Language of my profile', 'bearius-calendar' ), 'en_US' => 'English', ); foreach ( bcal_translation_locales() as $locale ) { $choices[ $locale ] = isset( $names[ $locale ] ) ? $names[ $locale ] : $locale; } return $choices; } add_action( 'admin_menu', function () { add_submenu_page( 'edit.php?post_type=' . BCAL_PT, __( 'Calendar settings', 'bearius-calendar' ), __( 'Settings', 'bearius-calendar' ), 'manage_options', 'bcal-settings', 'bcal_render_settings_page' ); } ); add_action( 'admin_init', function () { register_setting( 'bcal_settings_group', 'bcal_settings', array( 'type' => 'array', 'sanitize_callback' => 'bcal_sanitize_settings', 'default' => array(), ) ); } ); /** * Sanitizes one tab and merges it with the stored settings. * * @param mixed $input Submitted values. * @return array */ function bcal_sanitize_settings( $input ) { $current = get_option( 'bcal_settings', array() ); $current = is_array( $current ) ? $current : array(); $input = is_array( $input ) ? $input : array(); $schema = bcal_settings_schema(); $tab = isset( $input['_tab'] ) ? sanitize_key( $input['_tab'] ) : ''; $defaults = bcal_default_settings(); if ( ! isset( $schema[ $tab ] ) ) { // Saved without a settings tab: updates made by the plugin itself and the // second pass WordPress runs when the option is first created. Every // known key is sanitized with the rules of its field, unknown keys are // dropped. if ( ! $input ) { return $current; } $fields = array(); foreach ( $schema as $def ) { $fields = array_merge( $fields, $def['fields'] ); } $clean = array(); foreach ( $input as $key => $value ) { $key = (string) $key; if ( isset( $fields[ $key ] ) ) { $clean[ $key ] = bcal_sanitize_setting_field( $key, $fields[ $key ], $value, array(), $defaults ); } elseif ( array_key_exists( $key, $defaults ) ) { $clean[ $key ] = is_array( $value ) ? map_deep( $value, 'sanitize_text_field' ) : sanitize_text_field( (string) $value ); } } return $clean; } $before = bcal_settings(); foreach ( $schema[ $tab ]['fields'] as $key => $field ) { $value = isset( $input[ $key ] ) ? $input[ $key ] : null; $current[ $key ] = bcal_sanitize_setting_field( $key, $field, $value, $input, $defaults ); } if ( 'general' === $tab ) { if ( ! in_array( $current['default_view'], (array) $current['enabled_views'], true ) ) { $current['enabled_views'][] = $current['default_view']; } $current['currency'] = bcal_sanitize_currency( $current['currency'] ); foreach ( array( 'slug', 'slug_single', 'slug_category', 'slug_city', 'slug_venue', 'slug_organizer' ) as $slug_key ) { if ( $before[ $slug_key ] !== $current[ $slug_key ] ) { update_option( 'bcal_flush_rewrite', 1, false ); break; } } if ( (int) $before['window_months'] !== (int) $current['window_months'] ) { wp_schedule_single_event( time() + 30, 'bcal_daily' ); } if ( (int) $before['default_duration'] !== (int) $current['default_duration'] ) { BCal_Index::schedule_resync(); } } return $current; } /** * Sanitizes one setting with the rules of its field type. * * @param string $key Setting key. * @param array $field Field definition from the schema. * @param mixed $value Submitted value. * @param array $input All submitted values (for helper inputs). * @param array $defaults Default settings. * @return mixed */ function bcal_sanitize_setting_field( $key, $field, $value, $input, $defaults ) { $type = $field[0]; switch ( $type ) { case 'checkbox': return $value ? 1 : 0; case 'number': case 'range': case 'duration': $min = isset( $field[3] ) ? (int) $field[3] : 0; $max = isset( $field[4] ) ? (int) $field[4] : 9999; return max( $min, min( $max, (int) $value ) ); case 'color': $color = bcal_normalize_hex( (string) $value ); return $color ? $color : ( isset( $defaults[ $key ] ) ? $defaults[ $key ] : '' ); case 'slug': $slug = sanitize_title( (string) $value ); return ( '' !== $slug || ! empty( $field['optional'] ) ) ? $slug : $defaults[ $key ]; case 'select': case 'segmented': case 'palette': $options = isset( $field[3] ) ? (array) $field[3] : array(); $value = (string) $value; return array_key_exists( $value, $options ) ? $value : ( isset( $defaults[ $key ] ) ? $defaults[ $key ] : '' ); case 'multicheck': $options = isset( $field[3] ) ? (array) $field[3] : array(); return array_values( array_intersect( array_map( 'sanitize_key', (array) $value ), array_keys( $options ) ) ); case 'textarea': return sanitize_textarea_field( (string) $value ); case 'image': $image = absint( $value ); return ( $image && wp_attachment_is_image( $image ) ) ? $image : 0; case 'langs': $langs = array(); if ( is_array( $value ) ) { foreach ( $value as $lang => $text ) { $lang = sanitize_key( $lang ); $text = sanitize_textarea_field( (string) $text ); if ( '' !== $lang && '' !== $text ) { $langs[ $lang ] = $text; } } } $new_lang = isset( $input['_new_lang'] ) ? sanitize_key( $input['_new_lang'] ) : ''; $new_text = isset( $input['_new_text'] ) ? sanitize_textarea_field( (string) $input['_new_text'] ) : ''; if ( '' !== $new_lang && '' !== $new_text ) { $langs[ $new_lang ] = $new_text; } return $langs; default: return sanitize_text_field( (string) $value ); } } /** * "show_if" attribute for a field row. * * @param array $field Field definition. * @return string */ function bcal_field_show_if( $field ) { if ( empty( $field['show_if'] ) || ! is_array( $field['show_if'] ) ) { return ''; } list( $key, $values ) = $field['show_if']; return ' data-show-if="' . esc_attr( $key . ':' . implode( ',', (array) $values ) ) . '"'; } /** * Whether a row starts hidden because of its "show_if" condition. * * @param array $field Field definition. * @param array $settings Settings. * @return bool */ function bcal_field_hidden( $field, $settings ) { if ( empty( $field['show_if'] ) || ! is_array( $field['show_if'] ) ) { return false; } list( $key, $values ) = $field['show_if']; $current = isset( $settings[ $key ] ) ? (string) $settings[ $key ] : ''; return ! in_array( $current, array_map( 'strval', (array) $values ), true ); } /** * Control markup for one field. * * @param string $key Key. * @param array $field Field definition. * @param mixed $value Current value. */ function bcal_render_setting_field( $key, $field, $value ) { $name = 'bcal_settings[' . $key . ']'; $id = 'bcal-' . $key; switch ( $field[0] ) { case 'checkbox': printf( '', esc_attr( $id ), esc_attr( $name ), checked( (bool) $value, true, false ), esc_attr( $key ), esc_html( $field[1] ) ); break; case 'number': echo ''; printf( '', esc_attr( $id ), esc_attr( $name ), esc_attr( (string) $value ), isset( $field[3] ) ? (int) $field[3] : 0, isset( $field[4] ) ? (int) $field[4] : 9999, esc_attr( $key ) ); if ( ! empty( $field['suffix'] ) ) { echo '' . esc_html( $field['suffix'] ) . ''; } echo ''; break; case 'duration': echo '
' . esc_html( $field[2] ) . '
'; } echo '' . esc_html__( 'Calendar settings', 'bearius-calendar' ) . '