add_rewrite_rules(); } flush_rewrite_rules(); } /** * Enqueue admin assets. */ public function enqueue_assets( string $hook ) : void { $allowed_hooks = [ 'toplevel_page_webcalendar', 'webcalendar_page_webcalendar-categories', 'webcalendar_page_webcalendar-remote', 'webcalendar_page_webcalendar-holidays', 'webcalendar_page_webcalendar-shared', 'webcalendar_page_webcalendar-layers', 'webcalendar_page_webcalendar-submissions', 'webcalendar_page_webcalendar-places', 'webcalendar_page_webcalendar-extra-fields' ]; if ( !in_array( $hook, $allowed_hooks, true ) ) { return; } wp_enqueue_media(); $asset_file = WEBCALENDAR_WP_PATH . 'build/index.asset.php'; if ( !file_exists( $asset_file ) ) { return; } $assets = (require $asset_file); wp_enqueue_script( 'webcalendar-admin', WEBCALENDAR_WP_URL . 'build/index.js', $assets['dependencies'], $assets['version'], true ); // The admin bundle carries the bulk of the plugin's translatable JS. // Without this, WordPress never loads the JSON catalogue for it and // every __() in the React admin stays in English. wp_set_script_translations( 'webcalendar-admin', 'agenticdaisy-calendar', WEBCALENDAR_WP_PATH . 'languages' ); // Build layer data for admin color matching (mirrors Plugin.php frontend logic) $layers_data = []; if ( is_user_logged_in() ) { $layer_service = \WebCalendar\WP\Container::get_instance()->get_layer_service(); $layers = $layer_service->getLayersForUser( wp_get_current_user()->user_login ); foreach ( $layers as $layer ) { $layer_name = $layer->layerUser(); $wp_layer_user = get_user_by( 'login', $layer->layerUser() ); if ( $wp_layer_user ) { $layer_name = $wp_layer_user->display_name; } else { $resource_repo = \WebCalendar\WP\Container::get_instance()->get_resource_repository(); $resource = $resource_repo->findByLogin( $layer->layerUser() ); if ( $resource !== null ) { $layer_name = $resource->name(); } } $layers_data[] = [ 'layerUser' => $layer->layerUser(), 'color' => $layer->color(), 'showDuplicates' => $layer->showDuplicates(), 'name' => $layer_name, ]; } } wp_localize_script( 'webcalendar-admin', 'webcalApiSettings', [ 'root' => esc_url_raw( rest_url() ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'timeFormat' => webcal_time_format(), 'dateFormat' => webcal_date_format(), 'startOfWeek' => $this->start_of_week(), 'mapProvider' => get_option( 'webcal_map_provider', 'osm' ), 'googleMapsApiKey' => get_option( 'webcal_google_maps_api_key', '' ), 'currentUser' => wp_get_current_user()->user_login, 'canEditOthers' => current_user_can( 'webcal_can_edit_others' ), 'licenseTier' => \WebCalendar\WP\Container::get_instance()->get_license()->get_tier(), 'upgradeUrl' => esc_url_raw( $this->get_upgrade_url() ), 'layers' => $layers_data, 'features' => webcal_feature_flags(), ] ); // wp-components is declared as a dependency rather than merely enqueued, // so WordPress loads it before our overrides. // // The React admin is built from @wordpress/components, and listing // wp-components among the *script* dependencies does not load its // stylesheet. WordPress 7.x enqueues that stylesheet by itself, which // is why this was invisible: on 6.6 -- the version this plugin declares // as its minimum -- it is registered and never enqueued, so every // component renders unstyled. The event editor is the visible casualty: // .components-modal__screen-overlay falls back to position: static and // lays out in document flow instead of covering the viewport, leaving // the admin menu (z-index: 9990) painted over the modal. wp_enqueue_style( 'webcalendar-admin', WEBCALENDAR_WP_URL . 'build/index.css', ['wp-components'], $assets['version'] ); // Premium admin bundles. Each built file is stripped from the free build // via @fs_premium_only, so enqueue_premium_bundle() no-ops there. $premium_admin = [ 'webcalendar_page_webcalendar-remote' => 'premium-remote-calendars', 'webcalendar_page_webcalendar-submissions' => 'premium-submissions', ]; if ( isset( $premium_admin[$hook] ) ) { $this->enqueue_premium_bundle( $premium_admin[$hook] ); } } /** * Enqueue a premium UI bundle if it exists in this build. * * Premium bundles (build/premium-*.js) are stripped from the auto-generated * free build via the @fs_premium_only header, so file_exists() is false * there and nothing is enqueued. The bundle depends on webcalendar-admin so * React, webcalApiSettings, and apiFetch middleware are already available. * * @param string $name Bundle base name, e.g. "premium-remote-calendars". */ private function enqueue_premium_bundle( string $name ) : void { $asset_path = WEBCALENDAR_WP_PATH . 'build/' . $name . '.asset.php'; $js_path = WEBCALENDAR_WP_PATH . 'build/' . $name . '.js'; if ( !file_exists( $asset_path ) || !file_exists( $js_path ) ) { return; } $asset = (require $asset_path); wp_enqueue_script( 'webcal-' . $name, WEBCALENDAR_WP_URL . 'build/' . $name . '.js', array_merge( $asset['dependencies'], ['webcalendar-admin'] ), $asset['version'], true ); } /** * Add the menu page. */ public function add_menu_page() : void { add_menu_page( __( 'WebCalendar', 'agenticdaisy-calendar' ), __( 'WebCalendar', 'agenticdaisy-calendar' ), 'webcal_can_view_calendar', 'webcalendar', [$this, 'render_calendar_page'], 'dashicons-calendar-alt' ); // Rename the auto-created first submenu item to "Calendar". add_submenu_page( 'webcalendar', __( 'Calendar', 'agenticdaisy-calendar' ), __( 'Calendar', 'agenticdaisy-calendar' ), 'webcal_can_view_calendar', 'webcalendar', [$this, 'render_calendar_page'] ); add_submenu_page( 'webcalendar', __( 'Categories', 'agenticdaisy-calendar' ), __( 'Categories', 'agenticdaisy-calendar' ), 'webcal_can_manage_categories', 'webcalendar-categories', [$this, 'render_categories_page'] ); add_submenu_page( 'webcalendar', __( 'Custom Fields', 'agenticdaisy-calendar' ), __( 'Custom Fields', 'agenticdaisy-calendar' ), 'webcal_can_manage_categories', 'webcalendar-extra-fields', [$this, 'render_extra_fields_page'] ); add_submenu_page( 'webcalendar', __( 'Venues & Organizers', 'agenticdaisy-calendar' ), __( 'Venues & Organizers', 'agenticdaisy-calendar' ), 'webcal_can_manage_categories', 'webcalendar-places', [$this, 'render_places_page'] ); // Submissions submenu with pending count badge if ( webcal_feature_available( 'frontend_submission' ) && function_exists( 'webcal_fs' ) ) { } } /** * Add the Settings submenu at a later priority so it appears last. */ public function add_settings_submenu() : void { add_submenu_page( 'webcalendar', __( 'Settings', 'agenticdaisy-calendar' ), __( 'Settings', 'agenticdaisy-calendar' ), 'webcal_can_manage_settings', 'webcalendar-settings', [$this, 'render_settings_page'] ); } /** * Render the full calendar page. */ public function render_calendar_page() : void { ?>

'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => '12', ] ); register_setting( 'webcalendar_settings', 'webcal_config_DATE_FORMAT', [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => '', ] ); register_setting( 'webcalendar_settings', 'webcal_calendar_page', [ 'type' => 'integer', 'sanitize_callback' => 'absint', 'default' => 0, ] ); register_setting( 'webcalendar_settings', 'webcal_single_event_pages', [ 'type' => 'string', 'sanitize_callback' => [$this, 'sanitize_single_event_pages'], 'default' => 'off', ] ); register_setting( 'webcalendar_settings', 'webcal_map_provider', [ 'type' => 'string', 'sanitize_callback' => [$this, 'sanitize_map_provider'], 'default' => 'osm', ] ); register_setting( 'webcalendar_settings', 'webcal_google_maps_api_key', [ 'type' => 'string', 'sanitize_callback' => [$this, 'sanitize_google_maps_key'], 'default' => '', ] ); register_setting( 'webcalendar_settings', 'webcal_import_max_events', [ 'type' => 'integer', 'sanitize_callback' => [$this, 'sanitize_import_max_events'], 'default' => 5000, ] ); register_setting( 'webcalendar_settings', 'webcal_delete_data_on_uninstall', [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => 'no', ] ); register_setting( 'webcalendar_settings', 'webcal_enable_mcp', [ 'type' => 'string', 'sanitize_callback' => [$this, 'sanitize_enable_mcp'], 'default' => 'yes', ] ); register_setting( 'webcalendar_settings', 'webcal_mcp_read_only', [ 'type' => 'string', 'sanitize_callback' => [$this, 'sanitize_yes_no'], 'default' => 'no', ] ); register_setting( 'webcalendar_settings', 'webcal_mcp_audience', [ 'type' => 'string', 'sanitize_callback' => [$this, 'sanitize_mcp_audience'], 'default' => 'admins', ] ); register_setting( 'webcalendar_settings', 'webcal_frontend_submission', [ 'type' => 'string', 'sanitize_callback' => [$this, 'sanitize_yes_no'], 'default' => 'no', ] ); register_setting( 'webcalendar_settings', 'webcal_submission_requires_approval', [ 'type' => 'string', 'sanitize_callback' => [$this, 'sanitize_yes_no'], 'default' => 'yes', ] ); register_setting( 'webcalendar_settings', 'webcal_submission_show_location', [ 'type' => 'string', 'sanitize_callback' => [$this, 'sanitize_yes_no'], 'default' => 'yes', ] ); register_setting( 'webcalendar_settings', 'webcal_popup_share_links', [ 'type' => 'string', 'sanitize_callback' => [$this, 'sanitize_yes_no'], 'default' => 'yes', ] ); register_setting( 'webcalendar_settings', 'webcal_rate_limit', [ 'type' => 'integer', 'sanitize_callback' => 'absint', 'default' => 100, ] ); register_setting( 'webcalendar_settings', 'webcal_rate_limit_period', [ 'type' => 'integer', 'sanitize_callback' => 'absint', 'default' => 60, ] ); // Dev override fields only where the override can apply: WP_DEBUG on // and a local or development environment (License::dev_override_allowed()). if ( $this->dev_override_allowed() ) { register_setting( 'webcalendar_settings', 'webcal_license_tier', [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => 'free', ] ); register_setting( 'webcalendar_settings', 'webcal_license_dev_override', [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => 'no', ] ); } add_settings_section( 'webcalendar_general', __( 'General Settings', 'agenticdaisy-calendar' ), [$this, 'render_general_section'], 'webcalendar-settings' ); add_settings_section( 'webcalendar_maps', __( 'Maps', 'agenticdaisy-calendar' ), [$this, 'render_maps_section'], 'webcalendar-settings' ); add_settings_section( 'webcalendar_license', __( 'License Settings', 'agenticdaisy-calendar' ), [$this, 'render_license_section'], 'webcalendar-settings' ); add_settings_field( 'webcal_time_format', __( 'Time Format', 'agenticdaisy-calendar' ), [$this, 'render_time_format_field'], 'webcalendar-settings', 'webcalendar_general' ); add_settings_field( 'webcal_date_format', __( 'Date Format', 'agenticdaisy-calendar' ), [$this, 'render_date_format_field'], 'webcalendar-settings', 'webcalendar_general' ); add_settings_field( 'webcal_single_event_pages', __( 'Single Event Pages', 'agenticdaisy-calendar' ), [$this, 'render_single_event_pages_field'], 'webcalendar-settings', 'webcalendar_general' ); add_settings_field( 'webcal_calendar_page', __( 'Calendar Page', 'agenticdaisy-calendar' ), [$this, 'render_calendar_page_field'], 'webcalendar-settings', 'webcalendar_general' ); add_settings_field( 'webcal_import_max_events', __( 'Import Event Limit', 'agenticdaisy-calendar' ), [$this, 'render_import_max_events_field'], 'webcalendar-settings', 'webcalendar_general' ); add_settings_field( 'webcal_rate_limit', __( 'API Rate Limit', 'agenticdaisy-calendar' ), [$this, 'render_rate_limit_field'], 'webcalendar-settings', 'webcalendar_general' ); add_settings_field( 'webcal_delete_data_on_uninstall', __( 'Delete Data on Uninstall', 'agenticdaisy-calendar' ), [$this, 'render_delete_data_field'], 'webcalendar-settings', 'webcalendar_general' ); add_settings_field( 'webcal_enable_mcp', __( 'AI/MCP Integration', 'agenticdaisy-calendar' ), [$this, 'render_enable_mcp_field'], 'webcalendar-settings', 'webcalendar_general' ); add_settings_field( 'webcal_mcp_read_only', __( 'AI Read-Only Mode', 'agenticdaisy-calendar' ), [$this, 'render_mcp_read_only_field'], 'webcalendar-settings', 'webcalendar_general' ); add_settings_field( 'webcal_mcp_audience', __( 'AI Access', 'agenticdaisy-calendar' ), [$this, 'render_mcp_audience_field'], 'webcalendar-settings', 'webcalendar_general' ); add_settings_section( 'webcalendar_submission', __( 'Frontend Event Submission', 'agenticdaisy-calendar' ), [$this, 'render_submission_section'], 'webcalendar-settings' ); add_settings_field( 'webcal_frontend_submission', __( 'Enable Frontend Submission', 'agenticdaisy-calendar' ), [$this, 'render_frontend_submission_field'], 'webcalendar-settings', 'webcalendar_submission' ); add_settings_field( 'webcal_submission_requires_approval', __( 'Require Approval', 'agenticdaisy-calendar' ), [$this, 'render_submission_approval_field'], 'webcalendar-settings', 'webcalendar_submission' ); add_settings_field( 'webcal_submission_show_location', __( 'Location Field', 'agenticdaisy-calendar' ), [$this, 'render_submission_location_field'], 'webcalendar-settings', 'webcalendar_submission' ); add_settings_field( 'webcal_popup_share_links', __( 'Share Links in Popups', 'agenticdaisy-calendar' ), [$this, 'render_popup_share_links_field'], 'webcalendar-settings', 'webcalendar_general' ); add_settings_field( 'webcal_map_provider', __( 'Map Provider', 'agenticdaisy-calendar' ), [$this, 'render_map_provider_field'], 'webcalendar-settings', 'webcalendar_maps' ); add_settings_field( 'webcal_google_maps_api_key', __( 'Google Maps API Key', 'agenticdaisy-calendar' ), [$this, 'render_google_maps_key_field'], 'webcalendar-settings', 'webcalendar_maps' ); add_settings_field( 'webcal_license_status', __( 'License Status', 'agenticdaisy-calendar' ), [$this, 'render_license_status_field'], 'webcalendar-settings', 'webcalendar_license' ); // Dev override fields only where the override can apply. if ( $this->dev_override_allowed() ) { add_settings_field( 'webcal_license_dev_override', __( 'Dev Override', 'agenticdaisy-calendar' ), [$this, 'render_dev_override_field'], 'webcalendar-settings', 'webcalendar_license' ); add_settings_field( 'webcal_license_tier', __( 'Dev Tier', 'agenticdaisy-calendar' ), [$this, 'render_license_tier_field'], 'webcalendar-settings', 'webcalendar_license' ); } add_settings_section( 'webcalendar_feed', __( 'iCal Subscription Feed', 'agenticdaisy-calendar' ), [$this, 'render_feed_section'], 'webcalendar-settings' ); add_settings_field( 'webcal_feed_urls', __( 'Feed URLs', 'agenticdaisy-calendar' ), [$this, 'render_feed_urls_field'], 'webcalendar-settings', 'webcalendar_feed' ); add_settings_section( 'webcalendar_system_info', __( 'System Info', 'agenticdaisy-calendar' ), [$this, 'render_system_info_section'], 'webcalendar-settings' ); } /** * Render general section description. */ public function render_general_section() : void { echo '

' . esc_html__( 'Configure basic calendar behavior.', 'agenticdaisy-calendar' ) . '

'; // The timezone itself lives in WordPress General Settings, but its effect // on stored event times is a calendar concern, so surface it here too. ( new TimezoneCheck() )->render_notice(); } /** * Render maps section description. */ public function render_maps_section() : void { echo '

' . esc_html__( 'Configure how event location maps are displayed on single event pages and calendar popups.', 'agenticdaisy-calendar' ) . '

'; } /** * Render the map provider dropdown. */ public function render_map_provider_field() : void { $value = get_option( 'webcal_map_provider', 'osm' ); $value = ( is_scalar( $value ) ? (string) $value : 'osm' ); ?>

__( 'Please enter a Google Maps API key, or switch to OpenStreetMap.', 'agenticdaisy-calendar' ), ] ); } /** * Render Google Maps API key field. */ public function render_google_maps_key_field() : void { $raw_val = get_option( 'webcal_google_maps_api_key', '' ); $value = ( is_scalar( $raw_val ) ? (string) $raw_val : '' ); ?>

Google Cloud Console' ); ?>

>

>

>


' . esc_html__( 'Allow visitors to submit events from the frontend. Requires Business plan or higher.', 'agenticdaisy-calendar' ) . '

'; return; } echo '

' . esc_html__( 'Allow logged-in users to submit events from the frontend using the [webcalendar-submit] shortcode or Submit Event block.', 'agenticdaisy-calendar' ) . '

'; } /** * Render the frontend submission enable checkbox. */ public function render_frontend_submission_field() : void { $available = webcal_feature_available( 'frontend_submission' ); $raw_val = get_option( 'webcal_frontend_submission', 'no' ); $checked = is_scalar( $raw_val ) && (string) $raw_val === 'yes'; ?> >

>

'webcal_calendar_page', 'selected' => ( is_scalar( $raw ) ? absint( $raw ) : 0 ), 'show_option_none' => esc_html__( 'Automatic (the page showing the calendar)', 'agenticdaisy-calendar' ), 'option_none_value' => '0', ] ); ?>

' . esc_html__( 'Manage your WebCalendar license and features.', 'agenticdaisy-calendar' ) . '

'; } /** * Render the license status field. */ public function render_license_status_field() : void { $license = \WebCalendar\WP\Container::get_instance()->get_license(); $tier = $license->get_tier(); $tier_labels = [ 'free' => __( 'Free', 'agenticdaisy-calendar' ), 'starter' => __( 'Starter', 'agenticdaisy-calendar' ), 'business' => __( 'Business', 'agenticdaisy-calendar' ), 'agency' => __( 'Agency', 'agenticdaisy-calendar' ), ]; $label = $tier_labels[$tier] ?? ucfirst( $tier ); echo '' . esc_html( $label ) . ''; if ( $license->is_freemius_active() ) { $fs = $license->get_freemius(); if ( $fs !== null ) { $account_url = $fs->get_account_url(); echo ' — ' . esc_html__( 'Manage Account', 'agenticdaisy-calendar' ) . ''; } } if ( $tier === 'free' ) { $upgrade_url = $this->get_upgrade_url(); echo '

' . esc_html__( 'Upgrade to unlock more features.', 'agenticdaisy-calendar' ) . '

'; } } /** * Sanitize the single event pages setting to an allowed value. * * @param mixed $value Raw input. * @return string 'off', 'public', or 'all'. */ public function sanitize_single_event_pages( $value ) : string { $value = ( is_scalar( $value ) ? (string) $value : 'off' ); return ( in_array( $value, ['off', 'public', 'all'], true ) ? $value : 'off' ); } /** * Render the dev override checkbox (WP_DEBUG only). */ public function render_dev_override_field() : void { $value = get_option( 'webcal_license_dev_override', 'no' ); $checked = is_scalar( $value ) && (string) $value === 'yes'; ?> >

get_license(); if ( $license->is_freemius_active() ) { $fs = $license->get_freemius(); if ( $fs !== null ) { return $fs->get_upgrade_url(); } } return \WebCalendar\WP\License\License::PRICING_URL; } /** * Whether the dev tier override can apply on this site, so its fields are * not offered where they would do nothing. */ private function dev_override_allowed() : bool { return \WebCalendar\WP\Container::get_instance()->get_license()->dev_override_allowed(); } /** * Render time format field. */ public function render_time_format_field() : void { $value = webcal_time_format(); ?>

render_status_panel(); ?>
get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i', $wpdb->prefix . 'webcal_entry' ) ); $category_count = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i', $wpdb->prefix . 'webcal_categories' ) ); $user_count = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(DISTINCT cal_login) FROM %i', $wpdb->prefix . 'webcal_entry_user' ) ); // Only count rows with a URL — internal shared/nonuser calendars // (cal_url IS NULL) are not "remote" and the Remote Calendars admin // page filters them out. /** @psalm-suppress TooManyArguments -- wpdb::prepare is variadic; Psalm stub is fixed-arity. */ $remote_count = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE cal_url IS NOT NULL AND cal_url <> %s', $wpdb->prefix . 'webcal_nonuser_cals', '' ) ); // phpcs:enable $next_sync = wp_next_scheduled( 'webcal_sync_remote_calendars' ); $last_sync_raw = get_option( 'webcal_last_sync_time', '' ); $last_sync = ( is_string( $last_sync_raw ) ? $last_sync_raw : '' ); ?>
render_status_card( __( 'Total Events', 'agenticdaisy-calendar' ), number_format_i18n( $event_count ), 'calendar-alt' ); $this->render_status_card( __( 'Categories', 'agenticdaisy-calendar' ), number_format_i18n( $category_count ), 'category' ); $this->render_status_card( __( 'Active Users', 'agenticdaisy-calendar' ), number_format_i18n( $user_count ), 'groups' ); $this->render_status_card( __( 'Remote Calendars', 'agenticdaisy-calendar' ), number_format_i18n( $remote_count ), 'update' ); if ( $remote_count > 0 ) { if ( !empty( $last_sync ) ) { $sync_label = __( 'Last Sync', 'agenticdaisy-calendar' ); $sync_display = sprintf( /* translators: %s is a human-readable time difference like "2 hours ago". */ __( '%s ago', 'agenticdaisy-calendar' ), human_time_diff( intval( $last_sync ), time() ) ); } elseif ( $next_sync !== false ) { $sync_label = __( 'Next Sync', 'agenticdaisy-calendar' ); $sync_display = sprintf( /* translators: %s is a human-readable time until next sync like "45 minutes". */ __( 'in %s', 'agenticdaisy-calendar' ), human_time_diff( time(), $next_sync ) ); } else { $sync_label = __( 'Sync', 'agenticdaisy-calendar' ); $sync_display = __( 'Not scheduled', 'agenticdaisy-calendar' ); } $this->render_status_card( $sync_label, $sync_display, 'clock' ); } ?>
' . esc_html__( 'Subscribe to your calendar from external apps like Google Calendar, Apple Calendar, or Outlook.', 'agenticdaisy-calendar' ) . '

'; } /** * Render the system info section showing dependency versions. */ public function render_system_info_section() : void { $health = new SiteHealth(); $fields = $health->get_version_fields(); echo ''; echo ''; foreach ( $fields as $field ) { echo ''; echo ''; echo ''; echo ''; } echo ''; echo '
' . esc_html( $field['label'] ) . '' . esc_html( $field['value'] ) . '
'; } /** * Render the feed URLs field with public/personal URLs and regenerate button. */ public function render_feed_urls_field() : void { $public_url = rest_url( 'webcal/v1/feed' ); $token_manager = new \WebCalendar\WP\FeedTokenManager(); $user_id = get_current_user_id(); $token = $token_manager->get_token( $user_id ); $personal_url = add_query_arg( 'token', $token, $public_url ); $webcal_public = preg_replace( '/^https?:/', 'webcal:', $public_url ); $webcal_personal = preg_replace( '/^https?:/', 'webcal:', $personal_url ); ?> regenerate_token( $user_id ); $redirect_url = add_query_arg( 'webcal_token_regenerated', '1', admin_url( 'admin.php?page=webcalendar-settings' ) ); wp_safe_redirect( (string) $redirect_url ); exit; } }