templates = $templates; } /** * Hook admin-side functionality. */ public function init() { add_action( 'admin_menu', array( $this, 'register_menu' ) ); add_action( 'admin_init', array( $this, 'maybe_handle_actions' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) ); add_action( 'wp_ajax_absd_um_preview', array( $this, 'ajax_preview' ) ); } /** * Default option values. * * @return array */ public static function get_defaults() { return array( 'maintenance_mode' => false, 'template' => 'template-1', 'logo_id' => 0, 'background_id' => 0, 'overlay_opacity' => 70, 'heading' => __( 'We\'ll be back soon!', 'absd-under-maintenance' ), 'description' => __( 'We are currently working on improving our website.', 'absd-under-maintenance' ), 'small_text' => __( 'Thank you for your patience.', 'absd-under-maintenance' ), 'footer_text' => sprintf( /* translators: %s: current year */ __( '© %s All rights reserved.', 'absd-under-maintenance' ), gmdate( 'Y' ) ), 'show_icon' => true, 'icon' => 'wrench', 'icon_custom_id' => 0, 'retry_text' => __( 'Please try again later.', 'absd-under-maintenance' ), 'show_contact' => false, 'contact_text' => '', 'accent_color' => '#c9a15a', 'private_access' => false, 'private_token' => '', ); } /** * Get current settings merged with defaults. * * @return array */ public function get_settings() { if ( null !== $this->cache ) { return $this->cache; } $saved = get_option( self::OPTION_KEY, array() ); if ( ! is_array( $saved ) ) { $saved = array(); } $this->cache = wp_parse_args( $saved, self::get_defaults() ); return $this->cache; } /** * Get a single setting value. * * @param string $key Setting key. * @param mixed $fallback Fallback if not set. * @return mixed */ public function get( $key, $fallback = '' ) { $settings = $this->get_settings(); return isset( $settings[ $key ] ) ? $settings[ $key ] : $fallback; } /** * Update settings (merges with existing). * * @param array $new_values New values to merge in. * @return void */ public function update( array $new_values ) { $settings = $this->get_settings(); $settings = array_merge( $settings, $new_values ); $this->cache = $settings; update_option( self::OPTION_KEY, $settings ); } /** * Register the admin menu page. */ public function register_menu() { add_menu_page( __( 'ABSD Under Maintenance', 'absd-under-maintenance' ), __( 'Under Maintenance', 'absd-under-maintenance' ), self::CAPABILITY, 'absd-under-maintenance', array( $this, 'render_settings_page' ), 'dashicons-hammer', 80 ); } /** * Enqueue admin CSS/JS only on our settings page. * * @param string $hook Current admin page hook. */ public function enqueue_admin_assets( $hook ) { if ( 'toplevel_page_absd-under-maintenance' !== $hook ) { return; } wp_enqueue_media(); wp_enqueue_style( 'wp-color-picker' ); wp_enqueue_script( 'wp-color-picker' ); wp_enqueue_style( 'absd-um-admin', ABSD_UM_URL . 'admin/css/admin.css', array( 'wp-color-picker' ), ABSD_UM_VERSION ); wp_enqueue_script( 'absd-um-admin', ABSD_UM_URL . 'admin/js/admin.js', array( 'jquery', 'wp-color-picker' ), ABSD_UM_VERSION, true ); wp_localize_script( 'absd-um-admin', 'absdUmAdmin', array( 'chooseLogo' => __( 'Choose Logo', 'absd-under-maintenance' ), 'chooseBackground' => __( 'Choose Background Image', 'absd-under-maintenance' ), 'useImage' => __( 'Use this image', 'absd-under-maintenance' ), 'copied' => __( 'Copied!', 'absd-under-maintenance' ), 'ajaxUrl' => admin_url( 'admin-ajax.php' ), ) ); } /** * Handle form submissions (save settings, regenerate token, revoke token). * Hooked on admin_init so redirects can still happen. */ public function maybe_handle_actions() { // admin_init also fires during admin-ajax.php requests (including // our own live-preview endpoint below, which serializes this same // form and so always carries absd_um_action=save_settings). The // real save/regenerate/revoke actions only ever arrive via a normal // full-page form POST, never AJAX — bail here so an AJAX request // can never be misread as a settings save. if ( wp_doing_ajax() ) { return; } if ( ! isset( $_POST['absd_um_action'] ) ) { return; } if ( ! current_user_can( self::CAPABILITY ) ) { wp_die( esc_html__( 'You do not have permission to perform this action.', 'absd-under-maintenance' ) ); } if ( ! isset( $_POST['absd_um_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['absd_um_nonce'] ) ), self::NONCE_ACTION ) ) { wp_die( esc_html__( 'Security check failed. Please try again.', 'absd-under-maintenance' ) ); } $action = sanitize_key( wp_unslash( $_POST['absd_um_action'] ) ); switch ( $action ) { case 'save_settings': $this->handle_save(); break; case 'regenerate_token': $this->handle_regenerate_token(); break; case 'revoke_token': $this->handle_revoke_token(); break; } } /** * Sanitize a raw (already unslashed) settings input array against the * known fields. Shared by the save handler and the live preview AJAX * endpoint — neither persists anything by itself. * * @param array $input Raw, unslashed input (e.g. from $_POST). * @return array Sanitized settings. */ public function sanitize_input( array $input ) { $sanitized = array( 'maintenance_mode' => ! empty( $input['maintenance_mode'] ), 'template' => $this->templates->exists( sanitize_key( $input['template'] ?? '' ) ) ? sanitize_key( $input['template'] ) : $this->templates->get_default_id(), 'logo_id' => isset( $input['logo_id'] ) ? absint( $input['logo_id'] ) : 0, 'background_id' => isset( $input['background_id'] ) ? absint( $input['background_id'] ) : 0, 'overlay_opacity' => isset( $input['overlay_opacity'] ) ? min( 100, max( 0, absint( $input['overlay_opacity'] ) ) ) : 70, 'heading' => isset( $input['heading'] ) ? sanitize_text_field( $input['heading'] ) : '', 'description' => isset( $input['description'] ) ? sanitize_textarea_field( $input['description'] ) : '', 'small_text' => isset( $input['small_text'] ) ? sanitize_text_field( $input['small_text'] ) : '', 'footer_text' => isset( $input['footer_text'] ) ? wp_kses_post( $input['footer_text'] ) : '', 'show_icon' => ! empty( $input['show_icon'] ), 'icon' => $this->templates->icon_exists( sanitize_key( $input['icon'] ?? '' ) ) ? sanitize_key( $input['icon'] ) : $this->templates->get_default_icon_id(), 'icon_custom_id' => isset( $input['icon_custom_id'] ) ? absint( $input['icon_custom_id'] ) : 0, 'retry_text' => isset( $input['retry_text'] ) ? sanitize_text_field( $input['retry_text'] ) : '', 'show_contact' => ! empty( $input['show_contact'] ), 'contact_text' => isset( $input['contact_text'] ) ? sanitize_text_field( $input['contact_text'] ) : '', 'accent_color' => isset( $input['accent_color'] ) ? sanitize_hex_color( $input['accent_color'] ) : '#c9a15a', 'private_access' => ! empty( $input['private_access'] ), ); if ( empty( $sanitized['accent_color'] ) ) { $sanitized['accent_color'] = '#c9a15a'; } return $sanitized; } /** * Persist the settings form. */ private function handle_save() { // Nonce already verified in maybe_handle_actions() before this method is called. $input = wp_unslash( $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing $sanitized = $this->sanitize_input( $input ); $this->update( $sanitized ); // If private access was just enabled and there is no token yet, create one. if ( $sanitized['private_access'] && empty( $this->get( 'private_token' ) ) ) { $this->update( array( 'private_token' => absd_um_generate_token() ) ); } wp_safe_redirect( add_query_arg( array( 'page' => 'absd-under-maintenance', 'absd_saved' => '1' ), admin_url( 'admin.php' ) ) ); exit; } /** * AJAX: render the maintenance page with the current (unsaved) form * values so the admin can see a live preview. Nothing is persisted. */ public function ajax_preview() { if ( ! current_user_can( self::CAPABILITY ) ) { wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'absd-under-maintenance' ) ), 403 ); } if ( ! isset( $_POST['absd_um_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['absd_um_nonce'] ) ), self::NONCE_ACTION ) ) { wp_send_json_error( array( 'message' => __( 'Security check failed. Please reload the page.', 'absd-under-maintenance' ) ), 403 ); } $input = wp_unslash( $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified above. $sanitized = $this->sanitize_input( $input ); $maintenance = ABSD_Under_Maintenance::instance()->maintenance; $html = $maintenance->render_template_preview( $sanitized, $sanitized['template'] ); wp_send_json_success( array( 'html' => $html ) ); } /** * Regenerate the private access token. */ private function handle_regenerate_token() { $this->update( array( 'private_token' => absd_um_generate_token() ) ); wp_safe_redirect( add_query_arg( array( 'page' => 'absd-under-maintenance', 'absd_token' => 'regenerated' ), admin_url( 'admin.php' ) ) ); exit; } /** * Revoke (clear) the private access token and disable private access. */ private function handle_revoke_token() { $this->update( array( 'private_token' => '', 'private_access' => false, ) ); wp_safe_redirect( add_query_arg( array( 'page' => 'absd-under-maintenance', 'absd_token' => 'revoked' ), admin_url( 'admin.php' ) ) ); exit; } /** * Render the settings page (delegates to the view file). */ public function render_settings_page() { if ( ! current_user_can( self::CAPABILITY ) ) { wp_die( esc_html__( 'You do not have permission to access this page.', 'absd-under-maintenance' ) ); } $settings = $this->get_settings(); $templates = $this->templates->get_templates(); $icons = $this->templates->get_icons(); include ABSD_UM_PATH . 'admin/views/settings-page.php'; } }