From 80960cfaf2b4329e09bef4014a1080f0b4203d67 Mon Sep 17 00:00:00 2001 From: Amirreza Nazemi Date: Mon, 27 Oct 2025 17:59:05 +0330 Subject: [PATCH 1/6] feat(settings): add complete admin settings page with tabs and base layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Includes: - New settings page registered under WP Admin → Settings. - Four tabs: General, Functions, Integrations, and Views. - Added ayout.php for consistent form and sidebar structure. - Implemented Flexbox-based field layout replacing legacy tables. - Enqueued custom settings.css for unified design and responsive grid. --- anything-shortcodes.php | 2 +- assets/css/settings.css | 330 +++++++++++++++++++++++ includes/settings/settings.php | 238 ++++++++++++++++ includes/settings/views/functions.php | 42 +++ includes/settings/views/general.php | 37 +++ includes/settings/views/integrations.php | 112 ++++++++ includes/settings/views/layout.php | 59 ++++ includes/settings/views/views.php | 140 ++++++++++ 8 files changed, 959 insertions(+), 1 deletion(-) create mode 100644 assets/css/settings.css create mode 100644 includes/settings/settings.php create mode 100644 includes/settings/views/functions.php create mode 100644 includes/settings/views/general.php create mode 100644 includes/settings/views/integrations.php create mode 100644 includes/settings/views/layout.php create mode 100644 includes/settings/views/views.php diff --git a/anything-shortcodes.php b/anything-shortcodes.php index 3221dae..54471af 100644 --- a/anything-shortcodes.php +++ b/anything-shortcodes.php @@ -151,7 +151,7 @@ public function load_textdomain() { */ public function load_dependencies() { require_once ANYS_INCLUDES_PATH . 'utilities.php'; - require_once ANYS_INCLUDES_PATH . 'settings-page.php'; + require_once ANYS_INCLUDES_PATH . 'settings/settings.php'; require_once ANYS_INCLUDES_PATH . 'register-shortcodes.php'; require_once ANYS_INCLUDES_PATH . 'nav-menu.php'; } diff --git a/assets/css/settings.css b/assets/css/settings.css new file mode 100644 index 0000000..5b3385b --- /dev/null +++ b/assets/css/settings.css @@ -0,0 +1,330 @@ +/** + * Anything Shortcodes Admin Styles + * + * Provides layout and visuals for the plugin settings page. + * + * @since NEXT + */ + +/* --- Global layout --- */ +.anys-page-header { + display: flex; + justify-content: space-between; + align-items: center; + /* margin-bottom: 12px; */ + background-color: white; + padding: 1rem 2rem; +} + +.anys-title { + font-size: 24px; + margin: 0; + font-weight: 600; +} + +.anys-settings-grid { + display: grid; + grid-template-columns: 1fr 280px; + gap: 24px; + margin-top: 20px; + padding-inline: 2rem; +} + +.anys-main { + border-radius: 6px; +} + +/* --- CTA (PRO banner) --- */ +.anys-pro-cta { + background: #DAFFD9; + border: 1px solid transparent; + color: #1D2327; + font-weight: 500; + padding: 16px 16px; + border-radius: 3px; + text-decoration: none; + font-size: 13px; + transition: all 0.2s ease; +} + +.anys-sidebar { + background: #ffffff; + border: 1px solid #e0e0e0; + border-radius: 6px; + align-self: start; +} + +.anys-pro-cta:hover { + background: #d8f3e3d6; + color: #1D2327; + border: 1px solid #b8e0c3; +} + +/* --- Quick Links card --- */ +.anys-card { + border-top: 2px solid #f0f0f0; + padding: 1rem; +} + + +.anys-sidebar h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + padding: 0.8rem 1rem; +} + +.anys-links { + list-style: none; + margin: 0; + padding: 0; +} + +.anys-links li { + margin: 8px 0; +} + +.anys-links a { + color: #000000; + text-decoration: none; + font-size: 13px; +} + +.anys-links a:hover { + font-weight: 600; + /* text-decoration: underline; */ +} + +/* --- Form fields --- */ +.form-table th { + width: 200px; + padding: 12px 10px; + vertical-align: top; +} + +.form-table td { + padding: 12px 10px; +} + +.form-table input[type="text"], +.form-table input[type="number"], +.form-table select, +.form-table textarea { + width: 100%; + max-width: 100%; + border: 1px solid #ccc; + border-radius: 3px; + padding: 6px 8px; + font-size: 13px; + background: #fff; + box-sizing: border-box; +} + +.form-table .description { + color: #666; + font-size: 12px; + margin-top: 4px; +} + +input[type="checkbox"], +input[type="radio"] { + vertical-align: middle; +} + +/* --- Notice tweaks --- */ +.notice.updated { + margin-top: 16px; +} + +/* Tabs container. */ +.anys-tabs { + padding-inline: 2rem; + border-block: 1px solid #e5e5e5; + background-color: white; + padding-top: 0; +} + +/* Tab item (override WP defaults). */ +.anys-tabs .anys-tab { + background: transparent; + border: none; + box-shadow: none; + margin-right: 18px; + padding: 10px 0; + color: #7E7E7F; + border-bottom: 3px solid transparent; + transition: border-color 0.2s ease, color 0.2s ease; +} + +/* Hover state. */ +.anys-tabs .anys-tab:hover { + color: #0a0a0a; + border-bottom-color: #0a0a0a; +} + +/* Active state: underline only (no background). */ +.anys-tabs .anys-tab.nav-tab-active { + background: transparent; + border: none; + border-bottom: 3px solid #1d2327; /* active underline. */ + color: #1d2327; + font-weight: 600; +} + +/* Focus (remove blue box-shadow on tabs). */ +.anys-tabs .anys-tab:focus { + outline: none; + box-shadow: none; +} + +/* --- Flex form layout (replaces table layout) --- */ +.anys-field-group { + display: flex; + flex-direction: column; + gap: 16px; + padding: 12px 0; + border-bottom: 1px solid #f1f1f1; +} + +.anys-field-group:last-child { + border-bottom: 0; +} + +.anys-field-label { + + font-style: normal; + font-weight: 500; + font-size: 13px; + line-height: 16px; + /* identical to box height */ + + color: #000000; +} + +.anys-field-label label { + font-weight: 600; +} + +.anys-field-control { + flex: 1 1 auto; + min-width: 0; /* Prevents overflow in narrow screens. */ +} + +.anys-field-control .regular-text, +.anys-field-control input[type="text"], +.anys-field-control input[type="number"], +.anys-field-control select, +.anys-field-control textarea { + width: 100%; + max-width: 640px; /* Optional: keeps inputs from being too wide. */ + box-sizing: border-box; + padding: 2px 8px; +} + +.anys-field-control .description { + color: #666; + font-size: 12px; +} + +/* Responsive tweak: stack on small screens. */ +@media (max-width: 782px) { + .anys-field-group { + flex-direction: column; + gap: 8px; + } + .anys-field-label { + flex-basis: auto; + padding-top: 0; + } + .anys-settings-grid{ + + grid-template-columns: 1fr; + } + + .anys-main { + order: 2; + } + + .anys-sidebar { + order: 1; + } + + .anys-page-header{ + flex-direction: column; + align-items: flex-start; + gap: 12px; + } +} + +#wpcontent{ + padding-left: 0; +} + +/* Grid: 2 columns on desktop, 1 column on mobile */ +.anys-settings-grid { + display: grid; + grid-template-columns: 1fr 300px; + gap: 24px; + align-items: start; +} + +/* Card + links */ +.anys-card { background:#fff; border:1px solid #dcdcde; border-radius:8px; padding:12px; } +.anys-links { list-style:none; margin:0; padding:0; } +.anys-links li { margin:8px 0; } +.anys-links a { text-decoration:none; } + +/* Floating Action Button (mobile) */ +.anys-fab { + position: fixed; + right: 16px; + bottom: 16px; + width: 56px; + height: 56px; + border-radius: 50%; + border: none; + background: #2271b1; /* WP blue */ + color: #fff; + box-shadow: 0 6px 16px rgba(0,0,0,0.2); + cursor: pointer; + display: none; /* hidden on desktop */ + z-index: 1000; +} + +/* Mobile sheet */ +.anys-mobile-sheet[hidden] { display: none; } +.anys-mobile-sheet { + position: fixed; + inset: 0; + z-index: 999; + display: none; /* default closed */ +} +.anys-mobile-sheet.is-open { display: block; } +.anys-mobile-sheet__backdrop { + position: absolute; + inset: 0; + background: rgba(0,0,0,0.35); +} +.anys-mobile-sheet__content { + position: absolute; + right: 12px; + bottom: 76px; /* leave room for FAB */ + min-width: 260px; + max-width: 90vw; + background: #fff; + border-radius: 12px; + box-shadow: 0 10px 24px rgba(0,0,0,0.25); + padding: 12px 14px 14px; +} +.anys-mobile-sheet__content h3 { margin-top: 0; } +.anys-mobile-sheet__close { + position:absolute; top:8px; right:8px; + border:none; background:transparent; font-size:18px; cursor:pointer; +} + +/* Breakpoint: WP admin common mobile width ~782px */ +@media (max-width: 782px) { + .anys-settings-grid { grid-template-columns: 1fr; } + .anys-sidebar { display: none; } /* hide desktop sidebar */ + .anys-fab { display: inline-flex; align-items: center; justify-content: center; } +} diff --git a/includes/settings/settings.php b/includes/settings/settings.php new file mode 100644 index 0000000..35f364c --- /dev/null +++ b/includes/settings/settings.php @@ -0,0 +1,238 @@ + 'General', + 'integrations' => 'Integrations', + 'functions' => 'Functions', + 'views' => 'Views', + ]; + + /** @var self Singleton instance. */ + private static $instance; + + /** + * Returns the singleton instance. + * + * @since NEXT + * + * @return self Instance. + */ + public static function get_instance(): self { + return self::$instance ?? ( self::$instance = new self() ); + } + + /** + * Initializes the class by registering admin hooks. + * + * @since NEXT + */ + private function __construct() { + add_action( 'admin_menu', [ $this, 'register_menu_page' ] ); + add_action( 'admin_init', [ $this, 'handle_save' ] ); + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] ); + } + + /** Prevents cloning. */ + private function __clone() {} + + /** Prevents unserialization. */ + private function __wakeup() {} + + /** + * Registers the settings page under WP Admin → Settings. + * + * @since NEXT + */ + public function register_menu_page() { + add_options_page( + __( 'Anything Shortcodes', 'anys' ), + __( 'Anything Shortcodes', 'anys' ), + 'manage_options', + $this->page_slug, + [ $this, 'render_page' ] + ); + } + + /** + * Handles saving the settings array. + * + * @since NEXT + */ + public function handle_save() { + // Verifies admin context and permissions. + if ( ! is_admin() || ! current_user_can( 'manage_options' ) ) { + return; + } + + // Validates nonce and persists options. + if ( + isset( $_POST['anys'], $_POST['_anys_nonce'] ) && + wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_anys_nonce'] ) ), 'anys_save_settings' ) + ) { + $incoming = is_array( $_POST['anys'] ?? null ) ? wp_unslash( $_POST['anys'] ) : []; + + // Stores options without altering structure; escaping occurs on output. + update_option( $this->option_name, $incoming ); + + // Redirects to avoid form resubmission. + $redirect = add_query_arg( + [ + 'page' => $this->page_slug, + 'tab' => $this->current_tab_slug(), + 'updated' => 'true', + ], + admin_url( 'options-general.php' ) + ); + wp_safe_redirect( $redirect ); + exit; + } + } + + /** + * Renders the settings page and includes the active tab view. + * + * @since NEXT + */ + public function render_page() { + // Verifies capability. + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + $active_tab = $this->current_tab_slug(); + $tabs = $this->tabs; + + echo '
'; + + // Renders page header with title and PRO CTA. + echo '
'; + echo '

' . esc_html__( 'Anything Shortcodes', 'anys' ) . '

'; + echo '' + . esc_html__( 'Unlock Extra Features with Anything Shortcodes PRO', 'anys' ) . + ''; + echo '
'; + + // Displays update notice. + // if ( isset( $_GET['updated'] ) && 'true' === $_GET['updated'] ) { + // echo '

' . + // esc_html__( 'Settings saved.', 'anys' ) . + // '

'; + // } + + // Renders tab navigation. + echo ''; + + // Loads the view file for the active tab. + $view_file = $this->view_path( $active_tab ); + + if ( file_exists( $view_file ) ) { + // Provides $options and nonce to the view. + $options = get_option( $this->option_name, [] ); + $form_nonce = wp_create_nonce( 'anys_save_settings' ); + include $view_file; + } else { + echo '

' . esc_html__( 'The requested settings tab could not be found.', 'anys' ) . '

'; + } + + echo '
'; + } + + /** + * Returns the sanitized current tab slug (without the "anys-" prefix). + * + * @since NEXT + * + * @return string Tab slug. + */ + private function current_tab_slug(): string { + $requested = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : ''; + + if ( $requested && 0 === strpos( $requested, 'anys-' ) ) { + $requested = substr( $requested, 5 ); + } + + if ( ! $requested || ! array_key_exists( $requested, $this->tabs ) ) { + return 'general'; + } + + return $requested; + } + + /** + * Returns the absolute path to a view file for the given tab. + * + * @since NEXT + * + * @param string $tab Tab slug. + * @return string Absolute path. + */ + private function view_path( string $tab ): string { + error_log( 'Loading view for tab: ' . __DIR__ . '/views/' . $tab . '.php' ); + return __DIR__ . '/views/' . $tab . '.php'; + } + + /** + * Enqueues admin CSS for the settings page. + * + * @since NEXT + */ + public function enqueue_admin_assets( $hook ) { + // Ensures style loads only on our settings page. + if ( 'settings_page_' . $this->page_slug !== $hook ) { + return; + } + + $url = ANYS_CSS_URL . 'settings.css'; + + wp_enqueue_style( + 'anys-admin-settings', + $url, + [], + 'NEXT' + ); + } +} + +/** Boots the singleton immediately when this file loads. */ +Anys_Settings_Page::get_instance(); diff --git a/includes/settings/views/functions.php b/includes/settings/views/functions.php new file mode 100644 index 0000000..58a6c93 --- /dev/null +++ b/includes/settings/views/functions.php @@ -0,0 +1,42 @@ + +
+
+ +
+ +
+ + +

+ +

+
+
+ +
+
+ +
+
+ +

+ +

+
+
+ +
+
+ +
+
+ +

+ +

+
+
+ +
+
+ +
+
+ +

+ +

+
+
+ +
+
+ +
+
+ esc_html__( 'OAuth', 'anys' ), + 'token' => esc_html__( 'API Token', 'anys' ), + ]; + foreach ( $auth_opts as $val => $label ) : + $field_id = 'anys_integration_auth_method_' . $val; + ?> + + +

+ +

+
+
+ +
+
+ +
+
+ +

+ +

+
+
+ + +
+
+ +
+ + + + +
+
+ + + +
+ + + + + diff --git a/includes/settings/views/views.php b/includes/settings/views/views.php new file mode 100644 index 0000000..04afe7d --- /dev/null +++ b/includes/settings/views/views.php @@ -0,0 +1,140 @@ + + + +
+
+ +
+
+ +

+ +

+
+
+ + +
+
+ +
+
+ +

+ +

+
+
+ + +
+
+ +
+
+ esc_html__( 'Plain text', 'anys' ), + 'escaped' => esc_html__( 'Escaped HTML', 'anys' ), + 'raw' => esc_html__( 'Raw (no escaping)', 'anys' ), + ]; + foreach ( $title_modes as $val => $label ) : + $field_id = 'anys_views_title_mode_' . $val; + ?> + + +

+ +

+
+
+ + +
+
+ +
+
+ +

+ +

+
+
+ + +
+
+ +
+
+ +

+ +

+
+
+ + Date: Wed, 29 Oct 2025 10:57:57 +0330 Subject: [PATCH 2/6] fix(settings, ui): resolve saving issue, fix Quick Links display, and organize admin styles - Prevented settings overwrite across tabs - Fixed Quick Links sidebar visibility and mobile behavior - Reorganized and cleaned up CSS with structured sections --- assets/css/settings.css | 374 ++++++++++++-------------- assets/js/admin-mobile-sidebar.js | 38 +++ includes/settings/settings.php | 89 +++++- includes/settings/views/functions.php | 27 +- 4 files changed, 308 insertions(+), 220 deletions(-) create mode 100644 assets/js/admin-mobile-sidebar.js diff --git a/assets/css/settings.css b/assets/css/settings.css index 5b3385b..8a8714d 100644 --- a/assets/css/settings.css +++ b/assets/css/settings.css @@ -1,110 +1,153 @@ /** - * Anything Shortcodes Admin Styles - * + * Anything Shortcodes — Admin Styles * Provides layout and visuals for the plugin settings page. * + * Table of Contents: + * 1. Base / Global + * 2. Header + * 3. Tabs + * 4. Sidebar / Card / Links + * 5. Main / Form + * 6. Mobile FAB & Sheet + * 7. Responsive (≤782px) + * * @since NEXT */ -/* --- Global layout --- */ +/* ====================================== + 1. Base / Global + ====================================== */ +#wpcontent { padding-left: 0; } + +.anys-settings-grid { + display: grid; + grid-template-columns: 1fr 300px; + gap: 24px; + align-items: start; + margin-top: 20px; + padding-inline: 2rem; +} + +/* ====================================== + 2. Header + ====================================== */ .anys-page-header { display: flex; justify-content: space-between; align-items: center; - /* margin-bottom: 12px; */ - background-color: white; + background: #fff; padding: 1rem 2rem; } .anys-title { - font-size: 24px; margin: 0; + font-size: 24px; font-weight: 600; } -.anys-settings-grid { - display: grid; - grid-template-columns: 1fr 280px; - gap: 24px; - margin-top: 20px; - padding-inline: 2rem; -} - -.anys-main { - border-radius: 6px; -} - -/* --- CTA (PRO banner) --- */ .anys-pro-cta { background: #DAFFD9; border: 1px solid transparent; color: #1D2327; font-weight: 500; - padding: 16px 16px; + padding: 16px; border-radius: 3px; text-decoration: none; font-size: 13px; - transition: all 0.2s ease; + transition: all .2s ease; +} + +.anys-pro-cta:hover { + background: #d8f3e3d6; + color: #1D2327; + border-color: #b8e0c3; +} + +/* ====================================== + 3. Tabs + ====================================== */ +.anys-tabs { + padding-inline: 2rem; + padding-top: 0; + background: #fff; + border-block: 1px solid #e5e5e5; +} + +.anys-tabs .anys-tab { + background: transparent; + border: none; + box-shadow: none; + margin-right: 18px; + padding: 10px 0; + color: #7E7E7F; + border-bottom: 3px solid transparent; + transition: border-color .2s ease, color .2s ease; +} + +.anys-tabs .anys-tab:hover { + color: #0a0a0a; + border-bottom-color: #0a0a0a; +} + +.anys-tabs .anys-tab.nav-tab-active { + background: transparent; + border: none; + border-bottom: 3px solid #1d2327; + color: #1d2327; + font-weight: 600; +} + +.anys-tabs .anys-tab:focus { + outline: none; + box-shadow: none; } +/* ====================================== + 4. Sidebar / Card / Links + ====================================== */ .anys-sidebar { - background: #ffffff; + background: #fff; border: 1px solid #e0e0e0; border-radius: 6px; align-self: start; } -.anys-pro-cta:hover { - background: #d8f3e3d6; - color: #1D2327; - border: 1px solid #b8e0c3; -} - -/* --- Quick Links card --- */ .anys-card { border-top: 2px solid #f0f0f0; padding: 1rem; } - .anys-sidebar h3 { margin: 0; font-size: 14px; font-weight: 600; - padding: 0.8rem 1rem; -} - -.anys-links { - list-style: none; - margin: 0; - padding: 0; + padding: .8rem 1rem; } -.anys-links li { - margin: 8px 0; -} +.anys-links { list-style: none; margin: 0; padding: 0; } +.anys-links li { margin: 8px 0; } .anys-links a { - color: #000000; + color: #000; text-decoration: none; font-size: 13px; } -.anys-links a:hover { - font-weight: 600; - /* text-decoration: underline; */ -} +.anys-links a:hover { font-weight: 600; } +.anys-links a:focus { outline: none; box-shadow: none; } + +/* ====================================== + 5. Main / Form + ====================================== */ +.anys-main { border-radius: 6px; } -/* --- Form fields --- */ .form-table th { width: 200px; padding: 12px 10px; vertical-align: top; } -.form-table td { - padding: 12px 10px; -} +.form-table td { padding: 12px 10px; } .form-table input[type="text"], .form-table input[type="number"], @@ -127,57 +170,8 @@ } input[type="checkbox"], -input[type="radio"] { - vertical-align: middle; -} - -/* --- Notice tweaks --- */ -.notice.updated { - margin-top: 16px; -} - -/* Tabs container. */ -.anys-tabs { - padding-inline: 2rem; - border-block: 1px solid #e5e5e5; - background-color: white; - padding-top: 0; -} +input[type="radio"] { vertical-align: middle; } -/* Tab item (override WP defaults). */ -.anys-tabs .anys-tab { - background: transparent; - border: none; - box-shadow: none; - margin-right: 18px; - padding: 10px 0; - color: #7E7E7F; - border-bottom: 3px solid transparent; - transition: border-color 0.2s ease, color 0.2s ease; -} - -/* Hover state. */ -.anys-tabs .anys-tab:hover { - color: #0a0a0a; - border-bottom-color: #0a0a0a; -} - -/* Active state: underline only (no background). */ -.anys-tabs .anys-tab.nav-tab-active { - background: transparent; - border: none; - border-bottom: 3px solid #1d2327; /* active underline. */ - color: #1d2327; - font-weight: 600; -} - -/* Focus (remove blue box-shadow on tabs). */ -.anys-tabs .anys-tab:focus { - outline: none; - box-shadow: none; -} - -/* --- Flex form layout (replaces table layout) --- */ .anys-field-group { display: flex; flex-direction: column; @@ -186,28 +180,20 @@ input[type="radio"] { border-bottom: 1px solid #f1f1f1; } -.anys-field-group:last-child { - border-bottom: 0; -} +.anys-field-group:last-child { border-bottom: 0; } .anys-field-label { - - font-style: normal; - font-weight: 500; - font-size: 13px; - line-height: 16px; - /* identical to box height */ - - color: #000000; + font-weight: 500; + font-size: 13px; + line-height: 16px; + color: #000; } -.anys-field-label label { - font-weight: 600; -} +.anys-field-label label { font-weight: 600; } .anys-field-control { flex: 1 1 auto; - min-width: 0; /* Prevents overflow in narrow screens. */ + min-width: 0; } .anys-field-control .regular-text, @@ -216,7 +202,7 @@ input[type="radio"] { .anys-field-control select, .anys-field-control textarea { width: 100%; - max-width: 640px; /* Optional: keeps inputs from being too wide. */ + max-width: 640px; box-sizing: border-box; padding: 2px 8px; } @@ -226,105 +212,95 @@ input[type="radio"] { font-size: 12px; } -/* Responsive tweak: stack on small screens. */ -@media (max-width: 782px) { - .anys-field-group { - flex-direction: column; - gap: 8px; - } - .anys-field-label { - flex-basis: auto; - padding-top: 0; - } - .anys-settings-grid{ - - grid-template-columns: 1fr; - } - - .anys-main { - order: 2; - } - - .anys-sidebar { - order: 1; - } - - .anys-page-header{ - flex-direction: column; - align-items: flex-start; - gap: 12px; - } -} - -#wpcontent{ - padding-left: 0; -} - -/* Grid: 2 columns on desktop, 1 column on mobile */ -.anys-settings-grid { - display: grid; - grid-template-columns: 1fr 300px; - gap: 24px; - align-items: start; +/* ====================================== + 6. Mobile FAB & Sheet + ====================================== */ +.anys-fab { + position: fixed; + right: 16px; + bottom: 16px; + width: 56px; + height: 56px; + border-radius: 50%; + border: none; + background: #2271b1; + color: #fff; + box-shadow: 0 6px 16px rgba(0,0,0,.2); + cursor: pointer; + display: none; + z-index: 1000; } -/* Card + links */ -.anys-card { background:#fff; border:1px solid #dcdcde; border-radius:8px; padding:12px; } -.anys-links { list-style:none; margin:0; padding:0; } -.anys-links li { margin:8px 0; } -.anys-links a { text-decoration:none; } - -/* Floating Action Button (mobile) */ -.anys-fab { - position: fixed; - right: 16px; - bottom: 16px; - width: 56px; - height: 56px; - border-radius: 50%; - border: none; - background: #2271b1; /* WP blue */ - color: #fff; - box-shadow: 0 6px 16px rgba(0,0,0,0.2); - cursor: pointer; - display: none; /* hidden on desktop */ - z-index: 1000; +.anys-fab span { + font-size: 24px; + line-height: 28px; + margin-top: -2px; } -/* Mobile sheet */ .anys-mobile-sheet[hidden] { display: none; } + .anys-mobile-sheet { - position: fixed; - inset: 0; - z-index: 999; - display: none; /* default closed */ + position: fixed; + inset: 0; + z-index: 999; + display: none; } + .anys-mobile-sheet.is-open { display: block; } + .anys-mobile-sheet__backdrop { - position: absolute; - inset: 0; - background: rgba(0,0,0,0.35); + position: absolute; + inset: 0; } + .anys-mobile-sheet__content { - position: absolute; - right: 12px; - bottom: 76px; /* leave room for FAB */ - min-width: 260px; - max-width: 90vw; - background: #fff; - border-radius: 12px; - box-shadow: 0 10px 24px rgba(0,0,0,0.25); - padding: 12px 14px 14px; + position: absolute; + right: 12px; + bottom: 76px; + min-width: 260px; + max-width: 90vw; + background: #fff; + border-radius: 12px; + box-shadow: 0 10px 24px rgba(0,0,0,.25); + padding: 12px 14px 14px; + z-index: 50; } + .anys-mobile-sheet__content h3 { margin-top: 0; } + .anys-mobile-sheet__close { - position:absolute; top:8px; right:8px; - border:none; background:transparent; font-size:18px; cursor:pointer; + position: absolute; + top: 8px; + right: 8px; + border: none; + background: transparent; + font-size: 18px; + cursor: pointer; } -/* Breakpoint: WP admin common mobile width ~782px */ +/* ====================================== + 7. Responsive (≤ 782px) + ====================================== */ @media (max-width: 782px) { - .anys-settings-grid { grid-template-columns: 1fr; } - .anys-sidebar { display: none; } /* hide desktop sidebar */ - .anys-fab { display: inline-flex; align-items: center; justify-content: center; } + .anys-settings-grid { grid-template-columns: 1fr; } + .anys-sidebar { display: none; } + .anys-main { order: 2; } + .anys-sidebar { order: 1; } + + .anys-page-header { + flex-direction: column; + align-items: flex-start; + gap: 12px; + } + + .anys-field-group { gap: 8px; } + .anys-field-label { padding-top: 0; } + + #wpcontent { padding-left: 0 !important; } + + .anys-fab { + display: inline-flex; + align-items: center; + justify-content: center; + } } diff --git a/assets/js/admin-mobile-sidebar.js b/assets/js/admin-mobile-sidebar.js new file mode 100644 index 0000000..7c640b1 --- /dev/null +++ b/assets/js/admin-mobile-sidebar.js @@ -0,0 +1,38 @@ +(function(){ + // Elements + var fab = document.querySelector('.anys-fab'); + var sheet = document.getElementById('anys-mobile-menu'); + var closeBtn = sheet ? sheet.querySelector('.anys-mobile-sheet__close') : null; + var backdrop = sheet ? sheet.querySelector('.anys-mobile-sheet__backdrop') : null; + + if(!fab || !sheet) return; + + // Open/close helpers + function openSheet(){ + sheet.hidden = false; + sheet.classList.add('is-open'); + fab.setAttribute('aria-expanded', 'true'); + // Focus first link for accessibility + var firstLink = sheet.querySelector('a,button,[tabindex]:not([tabindex="-1"])'); + if(firstLink) firstLink.focus(); + } + function closeSheet(){ + sheet.classList.remove('is-open'); + fab.setAttribute('aria-expanded', 'false'); + // Use a microtask so CSS can apply before hiding + setTimeout(function(){ sheet.hidden = true; }, 0); + } + + // Toggle + fab.addEventListener('click', function(){ + var expanded = fab.getAttribute('aria-expanded') === 'true'; + if(expanded) { closeSheet(); } else { openSheet(); } + }); + + // Close handlers + if(closeBtn) closeBtn.addEventListener('click', closeSheet); + if(backdrop) backdrop.addEventListener('click', closeSheet); + document.addEventListener('keydown', function(e){ + if(e.key === 'Escape') closeSheet(); + }); +})(); diff --git a/includes/settings/settings.php b/includes/settings/settings.php index 35f364c..c5919fb 100644 --- a/includes/settings/settings.php +++ b/includes/settings/settings.php @@ -98,8 +98,17 @@ public function handle_save() { ) { $incoming = is_array( $_POST['anys'] ?? null ) ? wp_unslash( $_POST['anys'] ) : []; - // Stores options without altering structure; escaping occurs on output. - update_option( $this->option_name, $incoming ); + // Get existing options. + $existing = get_option( $this->option_name, [] ); + if ( ! is_array( $existing ) ) { + $existing = []; + } + + // Sanitize & merge recursively (new values override old ones). + $merged = self::merge_and_sanitize_settings( $existing, $incoming ); + + // Persist merged options. + update_option( $this->option_name, $merged ); // Redirects to avoid form resubmission. $redirect = add_query_arg( @@ -139,13 +148,6 @@ public function render_page() { ''; echo ''; - // Displays update notice. - // if ( isset( $_GET['updated'] ) && 'true' === $_GET['updated'] ) { - // echo '

' . - // esc_html__( 'Settings saved.', 'anys' ) . - // '

'; - // } - // Renders tab navigation. echo ''; @@ -170,7 +174,9 @@ public function render_page() { // Provides $options and nonce to the view. $options = get_option( $this->option_name, [] ); $form_nonce = wp_create_nonce( 'anys_save_settings' ); + include $view_file; + } else { echo '

' . esc_html__( 'The requested settings tab could not be found.', 'anys' ) . '

'; } @@ -223,15 +229,74 @@ public function enqueue_admin_assets( $hook ) { return; } - $url = ANYS_CSS_URL . 'settings.css'; - wp_enqueue_style( 'anys-admin-settings', - $url, + ANYS_CSS_URL . 'settings.css', [], 'NEXT' ); + + wp_enqueue_script( + 'anys-admin-mobile-sidebar', + ANYS_JS_URL . 'admin-mobile-sidebar.js', + array(), + '1.0.0', + true + ); } + + /** + * Merges and sanitizes plugin settings recursively. + * + * Existing options are preserved unless replaced. + * Strings are sanitized and arrays are merged recursively. + * Special handling is applied for the 'whitelisted_functions' field. + * + * @since NEXT + * + * @param array $existing_options Previously saved options. + * @param array $new_submitted_options Newly submitted options. + * + * @return array Sanitized merged options. + */ + private static function merge_and_sanitize_settings( array $existing_options, array $new_submitted_options ): array { + // Existing and new settings are merged (new replaces old). + $merged_options = array_replace_recursive( $existing_options, $new_submitted_options ); + + // 'whitelisted_functions' field is normalized. + if ( isset( $merged_options['whitelisted_functions'] ) ) { + $raw_whitelisted = $merged_options['whitelisted_functions']; + + if ( is_string( $raw_whitelisted ) ) { + $lines = preg_split( "/\r\n|\r|\n/", $raw_whitelisted ); + $function_list = array_map( 'trim', (array) $lines ); + } elseif ( is_array( $raw_whitelisted ) ) { + $function_list = array_map( 'trim', $raw_whitelisted ); + } else { + $function_list = []; + } + + // Empty and duplicate entries are removed. + $merged_options['whitelisted_functions'] = array_values( + array_unique( array_filter( $function_list, 'strlen' ) ) + ); + } + + // Recursive sanitization is applied to scalar values. + array_walk_recursive( $merged_options, function ( &$value ) { + if ( is_string( $value ) ) { + $value = sanitize_text_field( $value ); + } elseif ( is_bool( $value ) ) { + $value = (bool) $value; + } elseif ( is_numeric( $value ) ) { + // Numeric values are kept as-is. + } + } ); + + return $merged_options; + } + + } /** Boots the singleton immediately when this file loads. */ diff --git a/includes/settings/views/functions.php b/includes/settings/views/functions.php index 58a6c93..7f66fa1 100644 --- a/includes/settings/views/functions.php +++ b/includes/settings/views/functions.php @@ -1,16 +1,26 @@
@@ -28,7 +38,7 @@ cols="50" class="large-text code" placeholder="" - > + >

@@ -38,5 +48,4 @@ class="large-text code" Date: Wed, 29 Oct 2025 15:37:40 +0330 Subject: [PATCH 3/6] refactor(settings): unify docblocks and make tabs translatable Standardized all method comments with @since NEXT and added i18n-safe tab labels initialized in constructor. --- includes/settings/settings.php | 383 +++++++++++++++++++-------------- 1 file changed, 225 insertions(+), 158 deletions(-) diff --git a/includes/settings/settings.php b/includes/settings/settings.php index c5919fb..c07c875 100644 --- a/includes/settings/settings.php +++ b/includes/settings/settings.php @@ -20,21 +20,40 @@ */ final class Anys_Settings_Page { - /** Option name used to store all settings. */ + /** + * Option name used to store all settings. + * + * @var string + * + * @since NEXT + */ private $option_name = 'anys'; - /** Slug used for the settings page. */ + /** + * Slug used for the settings page. + * + * @var string + * + * @since NEXT + */ private $page_slug = 'anys-settings'; - /** Supported tabs and labels. */ - private $tabs = [ - 'general' => 'General', - 'integrations' => 'Integrations', - 'functions' => 'Functions', - 'views' => 'Views', - ]; + /** + * Supported tabs and labels. + * + * @var array + * + * @since NEXT + */ + private $tabs = []; - /** @var self Singleton instance. */ + /** + * Singleton instance. + * + * @var self|null + * + * @since NEXT + */ private static $instance; /** @@ -42,7 +61,7 @@ final class Anys_Settings_Page { * * @since NEXT * - * @return self Instance. + * @return self Instance is returned. */ public static function get_instance(): self { return self::$instance ?? ( self::$instance = new self() ); @@ -52,23 +71,47 @@ public static function get_instance(): self { * Initializes the class by registering admin hooks. * * @since NEXT + * + * @return void Nothing is returned. */ private function __construct() { add_action( 'admin_menu', [ $this, 'register_menu_page' ] ); add_action( 'admin_init', [ $this, 'handle_save' ] ); - add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] ); + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] ); + + // Initialize tab labels. + $this->tabs = [ + 'general' => __( 'General', 'anys' ), + 'integrations' => __( 'Integrations', 'anys' ), + 'functions' => __( 'Functions', 'anys' ), + 'views' => __( 'Views', 'anys' ), + ]; } - /** Prevents cloning. */ + /** + * Prevents cloning. + * + * @since NEXT + * + * @return void Nothing is returned. + */ private function __clone() {} - /** Prevents unserialization. */ + /** + * Prevents unserialization. + * + * @since NEXT + * + * @return void Nothing is returned. + */ private function __wakeup() {} /** * Registers the settings page under WP Admin → Settings. * * @since NEXT + * + * @return void Nothing is returned. */ public function register_menu_page() { add_options_page( @@ -83,7 +126,11 @@ public function register_menu_page() { /** * Handles saving the settings array. * + * Nonce and capability are verified and the merged options are persisted. + * * @since NEXT + * + * @return void Nothing is returned. */ public function handle_save() { // Verifies admin context and permissions. @@ -98,17 +145,17 @@ public function handle_save() { ) { $incoming = is_array( $_POST['anys'] ?? null ) ? wp_unslash( $_POST['anys'] ) : []; - // Get existing options. - $existing = get_option( $this->option_name, [] ); - if ( ! is_array( $existing ) ) { - $existing = []; - } + // Gets existing options. + $existing = get_option( $this->option_name, [] ); + if ( ! is_array( $existing ) ) { + $existing = []; + } - // Sanitize & merge recursively (new values override old ones). - $merged = self::merge_and_sanitize_settings( $existing, $incoming ); + // Sanitizes & merges recursively (new values override old ones). + $merged = self::merge_and_sanitize_settings( $existing, $incoming ); - // Persist merged options. - update_option( $this->option_name, $merged ); + // Persists merged options. + update_option( $this->option_name, $merged ); // Redirects to avoid form resubmission. $redirect = add_query_arg( @@ -127,69 +174,75 @@ public function handle_save() { /** * Renders the settings page and includes the active tab view. * + * Capability is verified and the tab UI plus view file are rendered. + * * @since NEXT + * + * @return void Nothing is returned. */ public function render_page() { - // Verifies capability. - if ( ! current_user_can( 'manage_options' ) ) { - return; - } - - $active_tab = $this->current_tab_slug(); - $tabs = $this->tabs; - - echo '

'; - - // Renders page header with title and PRO CTA. - echo '
'; - echo '

' . esc_html__( 'Anything Shortcodes', 'anys' ) . '

'; - echo '' - . esc_html__( 'Unlock Extra Features with Anything Shortcodes PRO', 'anys' ) . - ''; - echo '
'; - - // Renders tab navigation. - echo ''; - - // Loads the view file for the active tab. - $view_file = $this->view_path( $active_tab ); - - if ( file_exists( $view_file ) ) { - // Provides $options and nonce to the view. - $options = get_option( $this->option_name, [] ); - $form_nonce = wp_create_nonce( 'anys_save_settings' ); - - include $view_file; - - } else { - echo '

' . esc_html__( 'The requested settings tab could not be found.', 'anys' ) . '

'; - } - - echo '
'; - } + // Verifies capability. + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + $active_tab = $this->current_tab_slug(); + $tabs = $this->tabs; + + echo '
'; + + // Renders page header with title and PRO CTA. + echo '
'; + echo '

' . esc_html__( 'Anything Shortcodes', 'anys' ) . '

'; + echo '' + . esc_html__( 'Unlock Extra Features with Anything Shortcodes PRO', 'anys' ) . + ''; + echo '
'; + + // Renders tab navigation. + echo ''; + + // Loads the view file for the active tab. + $view_file = $this->view_path( $active_tab ); + + if ( file_exists( $view_file ) ) { + // Provides $options and nonce to the view. + $options = get_option( $this->option_name, [] ); + $form_nonce = wp_create_nonce( 'anys_save_settings' ); + + include $view_file; + + } else { + echo '

' . esc_html__( 'The requested settings tab could not be found.', 'anys' ) . '

'; + } + + echo '
'; + } /** - * Returns the sanitized current tab slug (without the "anys-" prefix). + * Returns the sanitized current tab slug without the "anys-" prefix. + * + * The value is validated against the supported tabs, and a default is applied. * * @since NEXT * - * @return string Tab slug. + * @return string Tab slug is returned. */ private function current_tab_slug(): string { $requested = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : ''; @@ -208,96 +261,110 @@ private function current_tab_slug(): string { /** * Returns the absolute path to a view file for the given tab. * + * The path is constructed relative to the current directory. + * * @since NEXT * - * @param string $tab Tab slug. - * @return string Absolute path. + * @param string $tab Tab slug is accepted. + * + * @return string Absolute path is returned. */ private function view_path( string $tab ): string { - error_log( 'Loading view for tab: ' . __DIR__ . '/views/' . $tab . '.php' ); return __DIR__ . '/views/' . $tab . '.php'; } - /** - * Enqueues admin CSS for the settings page. - * - * @since NEXT - */ - public function enqueue_admin_assets( $hook ) { - // Ensures style loads only on our settings page. - if ( 'settings_page_' . $this->page_slug !== $hook ) { - return; - } - - wp_enqueue_style( - 'anys-admin-settings', - ANYS_CSS_URL . 'settings.css', - [], - 'NEXT' - ); - - wp_enqueue_script( - 'anys-admin-mobile-sidebar', - ANYS_JS_URL . 'admin-mobile-sidebar.js', - array(), - '1.0.0', - true - ); - } - - /** - * Merges and sanitizes plugin settings recursively. - * - * Existing options are preserved unless replaced. - * Strings are sanitized and arrays are merged recursively. - * Special handling is applied for the 'whitelisted_functions' field. - * - * @since NEXT + /** + * Enqueues admin CSS and JS for the settings page. + * + * Assets are conditionally loaded on the plugin settings screen. + * + * @since NEXT + * + * @param string $hook Current admin page hook is accepted. + * @return void Nothing is returned. + */ + public function enqueue_admin_assets( $hook ) { + // Ensures style loads only on our settings page. + if ( 'settings_page_' . $this->page_slug !== $hook ) { + return; + } + + wp_enqueue_style( + 'anys-admin-settings', + ANYS_CSS_URL . 'settings.css', + [], + 'NEXT' + ); + + wp_enqueue_script( + 'anys-admin-mobile-sidebar', + ANYS_JS_URL . 'admin-mobile-sidebar.js', + [], + '1.0.0', + true + ); + } + + /** + * Merges and sanitizes plugin settings recursively. + * + * Existing options are preserved unless replaced. Strings are sanitized and + * arrays are merged recursively. Special handling is applied for the + * 'whitelisted_functions' field. + * + * @since NEXT + * + * @param array $existing_options Previously saved options are accepted. + * @param array $new_submitted_options Newly submitted options are accepted. * - * @param array $existing_options Previously saved options. - * @param array $new_submitted_options Newly submitted options. - * - * @return array Sanitized merged options. - */ - private static function merge_and_sanitize_settings( array $existing_options, array $new_submitted_options ): array { - // Existing and new settings are merged (new replaces old). - $merged_options = array_replace_recursive( $existing_options, $new_submitted_options ); - - // 'whitelisted_functions' field is normalized. - if ( isset( $merged_options['whitelisted_functions'] ) ) { - $raw_whitelisted = $merged_options['whitelisted_functions']; - - if ( is_string( $raw_whitelisted ) ) { - $lines = preg_split( "/\r\n|\r|\n/", $raw_whitelisted ); - $function_list = array_map( 'trim', (array) $lines ); - } elseif ( is_array( $raw_whitelisted ) ) { - $function_list = array_map( 'trim', $raw_whitelisted ); - } else { - $function_list = []; - } - - // Empty and duplicate entries are removed. - $merged_options['whitelisted_functions'] = array_values( - array_unique( array_filter( $function_list, 'strlen' ) ) - ); - } - - // Recursive sanitization is applied to scalar values. - array_walk_recursive( $merged_options, function ( &$value ) { - if ( is_string( $value ) ) { - $value = sanitize_text_field( $value ); - } elseif ( is_bool( $value ) ) { - $value = (bool) $value; - } elseif ( is_numeric( $value ) ) { - // Numeric values are kept as-is. - } - } ); - - return $merged_options; - } + * @return array Sanitized merged options are returned. + */ + private static function merge_and_sanitize_settings( array $existing_options, array $new_submitted_options ): array { + // Existing and new settings are merged (new replaces old). + $merged_options = array_replace_recursive( $existing_options, $new_submitted_options ); + + // 'whitelisted_functions' field is normalized. + if ( isset( $merged_options['whitelisted_functions'] ) ) { + $raw_whitelisted = $merged_options['whitelisted_functions']; + + if ( is_string( $raw_whitelisted ) ) { + $lines = preg_split( "/\r\n|\r|\n/", $raw_whitelisted ); + $function_list = array_map( 'trim', (array) $lines ); + } elseif ( is_array( $raw_whitelisted ) ) { + $function_list = array_map( 'trim', $raw_whitelisted ); + } else { + $function_list = []; + } + + // Empty and duplicate entries are removed. + $merged_options['whitelisted_functions'] = array_values( + array_unique( array_filter( $function_list, 'strlen' ) ) + ); + } + // Recursive sanitization is applied to scalar values. + array_walk_recursive( + $merged_options, + function ( &$value ) { + if ( is_string( $value ) ) { + $value = sanitize_text_field( $value ); + } elseif ( is_bool( $value ) ) { + $value = (bool) $value; + } elseif ( is_numeric( $value ) ) { + // Numeric values are kept as-is. + } + } + ); + return $merged_options; + } } -/** Boots the singleton immediately when this file loads. */ +/** + * Boots the singleton immediately when this file loads. + * + * @since NEXT + * + * @return void Nothing is returned. + */ Anys_Settings_Page::get_instance(); From 21e986daf69fb72e135b65ec6d848111f0ae21cf Mon Sep 17 00:00:00 2001 From: Amirreza Nazemi Date: Wed, 29 Oct 2025 16:43:50 +0330 Subject: [PATCH 4/6] fix(settings): correct magic method visibility for clone and wakeup --- includes/settings/settings.php | 38 +++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/includes/settings/settings.php b/includes/settings/settings.php index c07c875..1306241 100644 --- a/includes/settings/settings.php +++ b/includes/settings/settings.php @@ -88,23 +88,27 @@ private function __construct() { ]; } - /** - * Prevents cloning. - * - * @since NEXT - * - * @return void Nothing is returned. - */ - private function __clone() {} - - /** - * Prevents unserialization. - * - * @since NEXT - * - * @return void Nothing is returned. - */ - private function __wakeup() {} + /** + * Prevents cloning. + * + * @since NEXT + * + * @return void Nothing is returned. + */ + public function __clone() { + throw new \Exception( 'Cloning of this class is not allowed.' ); + } + + /** + * Prevents unserialization. + * + * @since NEXT + * + * @return void Nothing is returned. + */ + public function __wakeup() { + throw new \Exception( 'Unserialization of this class is not allowed.' ); + } /** * Registers the settings page under WP Admin → Settings. From 138788a428e00287553041e75a0735ccec0c366c Mon Sep 17 00:00:00 2001 From: Amirreza Nazemi Date: Sat, 8 Nov 2025 17:03:54 +0330 Subject: [PATCH 5/6] Sync branch structure with latest updates --- .../modules/settings-page/settings-page.php | 415 +++++++++++++----- .../modules/settings-page/views/functions.php | 51 +++ .../modules/settings-page/views/general.php | 37 ++ .../settings-page/views/integrations.php | 112 +++++ .../modules/settings-page/views/layout.php | 59 +++ .../modules/settings-page/views/views.php | 140 ++++++ includes/settings/settings.php | 374 ---------------- includes/settings/views/functions.php | 51 --- includes/settings/views/general.php | 37 -- includes/settings/views/integrations.php | 112 ----- includes/settings/views/layout.php | 59 --- includes/settings/views/views.php | 140 ------ 12 files changed, 696 insertions(+), 891 deletions(-) create mode 100644 includes/modules/settings-page/views/functions.php create mode 100644 includes/modules/settings-page/views/general.php create mode 100644 includes/modules/settings-page/views/integrations.php create mode 100644 includes/modules/settings-page/views/layout.php create mode 100644 includes/modules/settings-page/views/views.php delete mode 100644 includes/settings/settings.php delete mode 100644 includes/settings/views/functions.php delete mode 100644 includes/settings/views/general.php delete mode 100644 includes/settings/views/integrations.php delete mode 100644 includes/settings/views/layout.php delete mode 100644 includes/settings/views/views.php diff --git a/includes/modules/settings-page/settings-page.php b/includes/modules/settings-page/settings-page.php index 83c0d38..098f461 100644 --- a/includes/modules/settings-page/settings-page.php +++ b/includes/modules/settings-page/settings-page.php @@ -1,186 +1,365 @@ + * + * @since NEXT + */ + private $tabs = []; + + /** + * Singleton instance. + * + * @var self|null + * + * @since NEXT + */ + private static $instance; + + /** + * Returns the singleton instance. + * + * @since NEXT * - * @since 1.1.0 + * @return self Instance is returned. */ - protected function add_hooks() { - add_action( 'admin_menu', [ $this, 'add_settings_page' ] ); - add_action( 'admin_init', [ $this, 'register_settings' ] ); - add_filter( 'plugin_action_links_anything-shortcodes/anything-shortcodes.php', [ $this, 'modify_plugin_action_links' ] ); + public static function get_instance(): self { + return self::$instance ?? ( self::$instance = new self() ); } /** - * Adds settings page under Settings menu. + * Initializes the class by registering admin hooks. * - * @since 1.1.0 + * @since NEXT + * + * @return void Nothing is returned. + */ + private function __construct() { + add_action( 'admin_menu', [ $this, 'register_menu_page' ] ); + add_action( 'admin_init', [ $this, 'handle_save' ] ); + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] ); + + // Initialize tab labels. + $this->tabs = [ + 'general' => __( 'General', 'anys' ), + 'integrations' => __( 'Integrations', 'anys' ), + 'functions' => __( 'Functions', 'anys' ), + 'views' => __( 'Views', 'anys' ), + ]; + } + + /** + * Prevents cloning. + * + * @since NEXT + * + * @return void Nothing is returned. + */ + public function __clone() { + throw new \Exception( 'Cloning of this class is not allowed.' ); + } + + /** + * Prevents unserialization. + * + * @since NEXT + * + * @return void Nothing is returned. */ - public function add_settings_page() { + public function __wakeup() { + throw new \Exception( 'Unserialization of this class is not allowed.' ); + } + + /** + * Registers the settings page under WP Admin → Settings. + * + * @since NEXT + * + * @return void Nothing is returned. + */ + public function register_menu_page() { add_options_page( - esc_html__( 'Anything Shortcodes Settings', 'anys' ), - esc_html__( 'Anything Shortcodes', 'anys' ), + __( 'Anything Shortcodes', 'anys' ), + __( 'Anything Shortcodes', 'anys' ), 'manage_options', - 'anys-settings', - [ $this, 'render_settings_page' ] + $this->page_slug, + [ $this, 'render_page' ] ); } /** - * Registers settings, sections, and fields. + * Handles saving the settings array. + * + * Nonce and capability are verified and the merged options are persisted. + * + * @since NEXT * - * @since 1.1.0 + * @return void Nothing is returned. */ - public function register_settings() { - register_setting( - 'anys_settings_group', - 'anys_settings', - [ $this, 'sanitize_settings' ] - ); + public function handle_save() { + // Verifies admin context and permissions. + if ( ! is_admin() || ! current_user_can( 'manage_options' ) ) { + return; + } - // Functions section. - add_settings_section( - 'anys_functions_section', - esc_html__( 'Functions', 'anys' ), - [ $this, 'functions_section_callback' ], - 'anys-settings' - ); + // Validates nonce and persists options. + if ( + isset( $_POST['anys'], $_POST['_anys_nonce'] ) && + wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_anys_nonce'] ) ), 'anys_save_settings' ) + ) { + $incoming = is_array( $_POST['anys'] ?? null ) ? wp_unslash( $_POST['anys'] ) : []; - // Whitelist Functions field. - add_settings_field( - 'anys_whitelisted_functions', - esc_html__( 'Whitelisted Functions', 'anys' ), - [ $this, 'whitelisted_functions_callback' ], - 'anys-settings', - 'anys_functions_section' - ); + // Gets existing options. + $existing = get_option( $this->option_name, [] ); + if ( ! is_array( $existing ) ) { + $existing = []; + } + + // Sanitizes & merges recursively (new values override old ones). + $merged = self::merge_and_sanitize_settings( $existing, $incoming ); + + // Persists merged options. + update_option( $this->option_name, $merged ); + + // Redirects to avoid form resubmission. + $redirect = add_query_arg( + [ + 'page' => $this->page_slug, + 'tab' => $this->current_tab_slug(), + 'updated' => 'true', + ], + admin_url( 'options-general.php' ) + ); + wp_safe_redirect( $redirect ); + exit; + } } /** - * Sanitizes settings input. + * Renders the settings page and includes the active tab view. * - * @since 1.1.0 + * Capability is verified and the tab UI plus view file are rendered. * - * @param array $input Raw input values. + * @since NEXT * - * @return array Sanitized values. + * @return void Nothing is returned. */ - public function sanitize_settings( $input ) { - $output = []; + public function render_page() { + // Verifies capability. + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + $active_tab = $this->current_tab_slug(); + $tabs = $this->tabs; + + echo '
'; - // Sanitizes whitelist textarea into trimmed array of function names. - if ( isset( $input['anys_whitelisted_functions'] ) ) { - $functions = explode( "\n", sanitize_textarea_field( $input['anys_whitelisted_functions'] ) ); - $functions = array_map( 'trim', $functions ); - $functions = array_filter( $functions ); // Remove empty lines + // Renders page header with title and PRO CTA. + echo '
'; + echo '

' . esc_html__( 'Anything Shortcodes', 'anys' ) . '

'; + echo '' + . esc_html__( 'Unlock Extra Features with Anything Shortcodes PRO', 'anys' ) . + ''; + echo '
'; + + // Renders tab navigation. + echo ''; + + // Loads the view file for the active tab. + $view_file = $this->view_path( $active_tab ); + + if ( file_exists( $view_file ) ) { + // Provides $options and nonce to the view. + $options = get_option( $this->option_name, [] ); + $form_nonce = wp_create_nonce( 'anys_save_settings' ); + + include $view_file; - $output['anys_whitelisted_functions'] = $functions; } else { - $output['anys_whitelisted_functions'] = []; + echo '

' . esc_html__( 'The requested settings tab could not be found.', 'anys' ) . '

'; } - return $output; + echo '
'; } /** - * Functions section description callback. + * Returns the sanitized current tab slug without the "anys-" prefix. + * + * The value is validated against the supported tabs, and a default is applied. + * + * @since NEXT * - * @since 1.1.0 + * @return string Tab slug is returned. */ - public function functions_section_callback() { - printf( '%s', - esc_html__( 'Adjust plugin functions.', 'anys' ) - ); + private function current_tab_slug(): string { + $requested = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : ''; + + if ( $requested && 0 === strpos( $requested, 'anys-' ) ) { + $requested = substr( $requested, 5 ); + } + + if ( ! $requested || ! array_key_exists( $requested, $this->tabs ) ) { + return 'general'; + } + + return $requested; } /** - * Renders the Whitelisted Functions textarea field. + * Returns the absolute path to a view file for the given tab. + * + * The path is constructed relative to the current directory. + * + * @since NEXT * - * @since 1.1.0 + * @param string $tab Tab slug is accepted. + * + * @return string Absolute path is returned. */ - public function whitelisted_functions_callback() { - $options = get_option( 'anys_settings' ); - $whitelisted_functions = isset( $options['anys_whitelisted_functions'] ) && is_array( $options['anys_whitelisted_functions'] ) - ? $options['anys_whitelisted_functions'] - : []; - - // Gets the default whitelisted functions list as an array. - $default_whitelisted_functions = anys_get_default_whitelisted_functions(); - - // Converts array to a comma-separated string for display. - $default_whitelisted_functions_list = implode( ', ', $default_whitelisted_functions ); - ?> - -

- -

- -
-

-
- -
-
- page_slug !== $hook ) { + return; + } + + wp_enqueue_style( + 'anys-admin-settings', + ANYS_CSS_URL . 'settings.css', + [], + 'NEXT' + ); + + wp_enqueue_script( + 'anys-admin-mobile-sidebar', + ANYS_JS_URL . 'admin-mobile-sidebar.js', + [], + '1.0.0', + true + ); } /** - * Modifies plugin activation links to add Settings link. + * Merges and sanitizes plugin settings recursively. * - * @since 1.1.0 + * Existing options are preserved unless replaced. Strings are sanitized and + * arrays are merged recursively. Special handling is applied for the + * 'whitelisted_functions' field. * - * @param array $links Plugin action links. + * @since NEXT * - * @return array Modified links. + * @param array $existing_options Previously saved options are accepted. + * @param array $new_submitted_options Newly submitted options are accepted. + * + * @return array Sanitized merged options are returned. */ - public function modify_plugin_action_links( $links ) { - $links[] = '' . esc_html__( 'Settings', 'anys' ) . ''; + private static function merge_and_sanitize_settings( array $existing_options, array $new_submitted_options ): array { + // Existing and new settings are merged (new replaces old). + $merged_options = array_replace_recursive( $existing_options, $new_submitted_options ); + + // 'whitelisted_functions' field is normalized. + if ( isset( $merged_options['whitelisted_functions'] ) ) { + $raw_whitelisted = $merged_options['whitelisted_functions']; + + if ( is_string( $raw_whitelisted ) ) { + $lines = preg_split( "/\r\n|\r|\n/", $raw_whitelisted ); + $function_list = array_map( 'trim', (array) $lines ); + } elseif ( is_array( $raw_whitelisted ) ) { + $function_list = array_map( 'trim', $raw_whitelisted ); + } else { + $function_list = []; + } - return $links; + // Empty and duplicate entries are removed. + $merged_options['whitelisted_functions'] = array_values( + array_unique( array_filter( $function_list, 'strlen' ) ) + ); + } + + // Recursive sanitization is applied to scalar values. + array_walk_recursive( + $merged_options, + function ( &$value ) { + if ( is_string( $value ) ) { + $value = sanitize_text_field( $value ); + } elseif ( is_bool( $value ) ) { + $value = (bool) $value; + } elseif ( is_numeric( $value ) ) { + // Numeric values are kept as-is. + } + } + ); + + return $merged_options; } } /** - * Initializes the Settings_Page class. + * Boots the singleton immediately when this file loads. + * + * @since NEXT * - * @since 1.1.0 + * @return void Nothing is returned. */ -Settings_Page::get_instance(); +Anys_Settings_Page::get_instance(); diff --git a/includes/modules/settings-page/views/functions.php b/includes/modules/settings-page/views/functions.php new file mode 100644 index 0000000..ee97426 --- /dev/null +++ b/includes/modules/settings-page/views/functions.php @@ -0,0 +1,51 @@ + +
+
+ +
+ +
+ + +

+ +

+
+
+ +
+
+ +
+
+ +

+ +

+
+
+ +
+
+ +
+
+ +

+ +

+
+
+ +
+
+ +
+
+ +

+ +

+
+
+ +
+
+ +
+
+ esc_html__( 'OAuth', 'anys' ), + 'token' => esc_html__( 'API Token', 'anys' ), + ]; + foreach ( $auth_opts as $val => $label ) : + $field_id = 'anys_integration_auth_method_' . $val; + ?> + + +

+ +

+
+
+ +
+
+ +
+
+ +

+ +

+
+
+ + +
+
+ +
+ + + + +
+
+ + + +
+ + + + + diff --git a/includes/modules/settings-page/views/views.php b/includes/modules/settings-page/views/views.php new file mode 100644 index 0000000..479c867 --- /dev/null +++ b/includes/modules/settings-page/views/views.php @@ -0,0 +1,140 @@ + + + +
+
+ +
+
+ +

+ +

+
+
+ + +
+
+ +
+
+ +

+ +

+
+
+ + +
+
+ +
+
+ esc_html__( 'Plain text', 'anys' ), + 'escaped' => esc_html__( 'Escaped HTML', 'anys' ), + 'raw' => esc_html__( 'Raw (no escaping)', 'anys' ), + ]; + foreach ( $title_modes as $val => $label ) : + $field_id = 'anys_views_title_mode_' . $val; + ?> + + +

+ +

+
+
+ + +
+
+ +
+
+ +

+ +

+
+
+ + +
+
+ +
+
+ +

+ +

+
+
+ + - * - * @since NEXT - */ - private $tabs = []; - - /** - * Singleton instance. - * - * @var self|null - * - * @since NEXT - */ - private static $instance; - - /** - * Returns the singleton instance. - * - * @since NEXT - * - * @return self Instance is returned. - */ - public static function get_instance(): self { - return self::$instance ?? ( self::$instance = new self() ); - } - - /** - * Initializes the class by registering admin hooks. - * - * @since NEXT - * - * @return void Nothing is returned. - */ - private function __construct() { - add_action( 'admin_menu', [ $this, 'register_menu_page' ] ); - add_action( 'admin_init', [ $this, 'handle_save' ] ); - add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] ); - - // Initialize tab labels. - $this->tabs = [ - 'general' => __( 'General', 'anys' ), - 'integrations' => __( 'Integrations', 'anys' ), - 'functions' => __( 'Functions', 'anys' ), - 'views' => __( 'Views', 'anys' ), - ]; - } - - /** - * Prevents cloning. - * - * @since NEXT - * - * @return void Nothing is returned. - */ - public function __clone() { - throw new \Exception( 'Cloning of this class is not allowed.' ); - } - - /** - * Prevents unserialization. - * - * @since NEXT - * - * @return void Nothing is returned. - */ - public function __wakeup() { - throw new \Exception( 'Unserialization of this class is not allowed.' ); - } - - /** - * Registers the settings page under WP Admin → Settings. - * - * @since NEXT - * - * @return void Nothing is returned. - */ - public function register_menu_page() { - add_options_page( - __( 'Anything Shortcodes', 'anys' ), - __( 'Anything Shortcodes', 'anys' ), - 'manage_options', - $this->page_slug, - [ $this, 'render_page' ] - ); - } - - /** - * Handles saving the settings array. - * - * Nonce and capability are verified and the merged options are persisted. - * - * @since NEXT - * - * @return void Nothing is returned. - */ - public function handle_save() { - // Verifies admin context and permissions. - if ( ! is_admin() || ! current_user_can( 'manage_options' ) ) { - return; - } - - // Validates nonce and persists options. - if ( - isset( $_POST['anys'], $_POST['_anys_nonce'] ) && - wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_anys_nonce'] ) ), 'anys_save_settings' ) - ) { - $incoming = is_array( $_POST['anys'] ?? null ) ? wp_unslash( $_POST['anys'] ) : []; - - // Gets existing options. - $existing = get_option( $this->option_name, [] ); - if ( ! is_array( $existing ) ) { - $existing = []; - } - - // Sanitizes & merges recursively (new values override old ones). - $merged = self::merge_and_sanitize_settings( $existing, $incoming ); - - // Persists merged options. - update_option( $this->option_name, $merged ); - - // Redirects to avoid form resubmission. - $redirect = add_query_arg( - [ - 'page' => $this->page_slug, - 'tab' => $this->current_tab_slug(), - 'updated' => 'true', - ], - admin_url( 'options-general.php' ) - ); - wp_safe_redirect( $redirect ); - exit; - } - } - - /** - * Renders the settings page and includes the active tab view. - * - * Capability is verified and the tab UI plus view file are rendered. - * - * @since NEXT - * - * @return void Nothing is returned. - */ - public function render_page() { - // Verifies capability. - if ( ! current_user_can( 'manage_options' ) ) { - return; - } - - $active_tab = $this->current_tab_slug(); - $tabs = $this->tabs; - - echo '
'; - - // Renders page header with title and PRO CTA. - echo '
'; - echo '

' . esc_html__( 'Anything Shortcodes', 'anys' ) . '

'; - echo '' - . esc_html__( 'Unlock Extra Features with Anything Shortcodes PRO', 'anys' ) . - ''; - echo '
'; - - // Renders tab navigation. - echo ''; - - // Loads the view file for the active tab. - $view_file = $this->view_path( $active_tab ); - - if ( file_exists( $view_file ) ) { - // Provides $options and nonce to the view. - $options = get_option( $this->option_name, [] ); - $form_nonce = wp_create_nonce( 'anys_save_settings' ); - - include $view_file; - - } else { - echo '

' . esc_html__( 'The requested settings tab could not be found.', 'anys' ) . '

'; - } - - echo '
'; - } - - /** - * Returns the sanitized current tab slug without the "anys-" prefix. - * - * The value is validated against the supported tabs, and a default is applied. - * - * @since NEXT - * - * @return string Tab slug is returned. - */ - private function current_tab_slug(): string { - $requested = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : ''; - - if ( $requested && 0 === strpos( $requested, 'anys-' ) ) { - $requested = substr( $requested, 5 ); - } - - if ( ! $requested || ! array_key_exists( $requested, $this->tabs ) ) { - return 'general'; - } - - return $requested; - } - - /** - * Returns the absolute path to a view file for the given tab. - * - * The path is constructed relative to the current directory. - * - * @since NEXT - * - * @param string $tab Tab slug is accepted. - * - * @return string Absolute path is returned. - */ - private function view_path( string $tab ): string { - return __DIR__ . '/views/' . $tab . '.php'; - } - - /** - * Enqueues admin CSS and JS for the settings page. - * - * Assets are conditionally loaded on the plugin settings screen. - * - * @since NEXT - * - * @param string $hook Current admin page hook is accepted. - * @return void Nothing is returned. - */ - public function enqueue_admin_assets( $hook ) { - // Ensures style loads only on our settings page. - if ( 'settings_page_' . $this->page_slug !== $hook ) { - return; - } - - wp_enqueue_style( - 'anys-admin-settings', - ANYS_CSS_URL . 'settings.css', - [], - 'NEXT' - ); - - wp_enqueue_script( - 'anys-admin-mobile-sidebar', - ANYS_JS_URL . 'admin-mobile-sidebar.js', - [], - '1.0.0', - true - ); - } - - /** - * Merges and sanitizes plugin settings recursively. - * - * Existing options are preserved unless replaced. Strings are sanitized and - * arrays are merged recursively. Special handling is applied for the - * 'whitelisted_functions' field. - * - * @since NEXT - * - * @param array $existing_options Previously saved options are accepted. - * @param array $new_submitted_options Newly submitted options are accepted. - * - * @return array Sanitized merged options are returned. - */ - private static function merge_and_sanitize_settings( array $existing_options, array $new_submitted_options ): array { - // Existing and new settings are merged (new replaces old). - $merged_options = array_replace_recursive( $existing_options, $new_submitted_options ); - - // 'whitelisted_functions' field is normalized. - if ( isset( $merged_options['whitelisted_functions'] ) ) { - $raw_whitelisted = $merged_options['whitelisted_functions']; - - if ( is_string( $raw_whitelisted ) ) { - $lines = preg_split( "/\r\n|\r|\n/", $raw_whitelisted ); - $function_list = array_map( 'trim', (array) $lines ); - } elseif ( is_array( $raw_whitelisted ) ) { - $function_list = array_map( 'trim', $raw_whitelisted ); - } else { - $function_list = []; - } - - // Empty and duplicate entries are removed. - $merged_options['whitelisted_functions'] = array_values( - array_unique( array_filter( $function_list, 'strlen' ) ) - ); - } - - // Recursive sanitization is applied to scalar values. - array_walk_recursive( - $merged_options, - function ( &$value ) { - if ( is_string( $value ) ) { - $value = sanitize_text_field( $value ); - } elseif ( is_bool( $value ) ) { - $value = (bool) $value; - } elseif ( is_numeric( $value ) ) { - // Numeric values are kept as-is. - } - } - ); - - return $merged_options; - } -} - -/** - * Boots the singleton immediately when this file loads. - * - * @since NEXT - * - * @return void Nothing is returned. - */ -Anys_Settings_Page::get_instance(); diff --git a/includes/settings/views/functions.php b/includes/settings/views/functions.php deleted file mode 100644 index 7f66fa1..0000000 --- a/includes/settings/views/functions.php +++ /dev/null @@ -1,51 +0,0 @@ - -
-
- -
- -
- - -

- -

-
-
- -
-
- -
-
- -

- -

-
-
- -
-
- -
-
- -

- -

-
-
- -
-
- -
-
- -

- -

-
-
- -
-
- -
-
- esc_html__( 'OAuth', 'anys' ), - 'token' => esc_html__( 'API Token', 'anys' ), - ]; - foreach ( $auth_opts as $val => $label ) : - $field_id = 'anys_integration_auth_method_' . $val; - ?> - - -

- -

-
-
- -
-
- -
-
- -

- -

-
-
- - -
-
- -
- - - - -
-
- - - -
- - - - - diff --git a/includes/settings/views/views.php b/includes/settings/views/views.php deleted file mode 100644 index 04afe7d..0000000 --- a/includes/settings/views/views.php +++ /dev/null @@ -1,140 +0,0 @@ - - - -
-
- -
-
- -

- -

-
-
- - -
-
- -
-
- -

- -

-
-
- - -
-
- -
-
- esc_html__( 'Plain text', 'anys' ), - 'escaped' => esc_html__( 'Escaped HTML', 'anys' ), - 'raw' => esc_html__( 'Raw (no escaping)', 'anys' ), - ]; - foreach ( $title_modes as $val => $label ) : - $field_id = 'anys_views_title_mode_' . $val; - ?> - - -

- -

-
-
- - -
-
- -
-
- -

- -

-
-
- - -
-
- -
-
- -

- -

-
-
- - Date: Sun, 9 Nov 2025 10:16:43 +0330 Subject: [PATCH 6/6] refactor(settings): rewrite settings page using Singleton trait and unified hooks structure --- .../modules/settings-page/settings-page.php | 573 +++++++----------- 1 file changed, 219 insertions(+), 354 deletions(-) diff --git a/includes/modules/settings-page/settings-page.php b/includes/modules/settings-page/settings-page.php index 098f461..797d9d3 100644 --- a/includes/modules/settings-page/settings-page.php +++ b/includes/modules/settings-page/settings-page.php @@ -1,365 +1,230 @@ - * - * @since NEXT - */ - private $tabs = []; - - /** - * Singleton instance. - * - * @var self|null - * - * @since NEXT - */ - private static $instance; - - /** - * Returns the singleton instance. - * - * @since NEXT - * - * @return self Instance is returned. - */ - public static function get_instance(): self { - return self::$instance ?? ( self::$instance = new self() ); - } - - /** - * Initializes the class by registering admin hooks. - * - * @since NEXT - * - * @return void Nothing is returned. - */ - private function __construct() { - add_action( 'admin_menu', [ $this, 'register_menu_page' ] ); - add_action( 'admin_init', [ $this, 'handle_save' ] ); - add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] ); - - // Initialize tab labels. - $this->tabs = [ - 'general' => __( 'General', 'anys' ), - 'integrations' => __( 'Integrations', 'anys' ), - 'functions' => __( 'Functions', 'anys' ), - 'views' => __( 'Views', 'anys' ), - ]; - } - - /** - * Prevents cloning. - * - * @since NEXT - * - * @return void Nothing is returned. - */ - public function __clone() { - throw new \Exception( 'Cloning of this class is not allowed.' ); - } - - /** - * Prevents unserialization. - * - * @since NEXT - * - * @return void Nothing is returned. - */ - public function __wakeup() { - throw new \Exception( 'Unserialization of this class is not allowed.' ); - } - - /** - * Registers the settings page under WP Admin → Settings. - * - * @since NEXT - * - * @return void Nothing is returned. - */ - public function register_menu_page() { - add_options_page( - __( 'Anything Shortcodes', 'anys' ), - __( 'Anything Shortcodes', 'anys' ), - 'manage_options', - $this->page_slug, - [ $this, 'render_page' ] - ); - } - - /** - * Handles saving the settings array. - * - * Nonce and capability are verified and the merged options are persisted. - * - * @since NEXT - * - * @return void Nothing is returned. - */ - public function handle_save() { - // Verifies admin context and permissions. - if ( ! is_admin() || ! current_user_can( 'manage_options' ) ) { - return; - } - - // Validates nonce and persists options. - if ( - isset( $_POST['anys'], $_POST['_anys_nonce'] ) && - wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_anys_nonce'] ) ), 'anys_save_settings' ) - ) { - $incoming = is_array( $_POST['anys'] ?? null ) ? wp_unslash( $_POST['anys'] ) : []; - - // Gets existing options. - $existing = get_option( $this->option_name, [] ); - if ( ! is_array( $existing ) ) { - $existing = []; - } - - // Sanitizes & merges recursively (new values override old ones). - $merged = self::merge_and_sanitize_settings( $existing, $incoming ); - - // Persists merged options. - update_option( $this->option_name, $merged ); - - // Redirects to avoid form resubmission. - $redirect = add_query_arg( - [ - 'page' => $this->page_slug, - 'tab' => $this->current_tab_slug(), - 'updated' => 'true', - ], - admin_url( 'options-general.php' ) - ); - wp_safe_redirect( $redirect ); - exit; - } - } - - /** - * Renders the settings page and includes the active tab view. - * - * Capability is verified and the tab UI plus view file are rendered. - * - * @since NEXT - * - * @return void Nothing is returned. - */ - public function render_page() { - // Verifies capability. - if ( ! current_user_can( 'manage_options' ) ) { - return; - } - - $active_tab = $this->current_tab_slug(); - $tabs = $this->tabs; - - echo '
'; - - // Renders page header with title and PRO CTA. - echo '
'; - echo '

' . esc_html__( 'Anything Shortcodes', 'anys' ) . '

'; - echo '' - . esc_html__( 'Unlock Extra Features with Anything Shortcodes PRO', 'anys' ) . - ''; - echo '
'; - - // Renders tab navigation. - echo ''; - - // Loads the view file for the active tab. - $view_file = $this->view_path( $active_tab ); - - if ( file_exists( $view_file ) ) { - // Provides $options and nonce to the view. - $options = get_option( $this->option_name, [] ); - $form_nonce = wp_create_nonce( 'anys_save_settings' ); - - include $view_file; - - } else { - echo '

' . esc_html__( 'The requested settings tab could not be found.', 'anys' ) . '

'; - } - - echo '
'; - } - - /** - * Returns the sanitized current tab slug without the "anys-" prefix. - * - * The value is validated against the supported tabs, and a default is applied. - * - * @since NEXT - * - * @return string Tab slug is returned. - */ - private function current_tab_slug(): string { - $requested = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : ''; - - if ( $requested && 0 === strpos( $requested, 'anys-' ) ) { - $requested = substr( $requested, 5 ); - } - - if ( ! $requested || ! array_key_exists( $requested, $this->tabs ) ) { - return 'general'; - } - - return $requested; - } - - /** - * Returns the absolute path to a view file for the given tab. - * - * The path is constructed relative to the current directory. - * - * @since NEXT - * - * @param string $tab Tab slug is accepted. - * - * @return string Absolute path is returned. - */ - private function view_path( string $tab ): string { - return __DIR__ . '/views/' . $tab . '.php'; - } - - /** - * Enqueues admin CSS and JS for the settings page. - * - * Assets are conditionally loaded on the plugin settings screen. - * - * @since NEXT - * - * @param string $hook Current admin page hook is accepted. - * @return void Nothing is returned. - */ - public function enqueue_admin_assets( $hook ) { - // Ensures style loads only on our settings page. - if ( 'settings_page_' . $this->page_slug !== $hook ) { - return; - } - - wp_enqueue_style( - 'anys-admin-settings', - ANYS_CSS_URL . 'settings.css', - [], - 'NEXT' - ); - - wp_enqueue_script( - 'anys-admin-mobile-sidebar', - ANYS_JS_URL . 'admin-mobile-sidebar.js', - [], - '1.0.0', - true - ); - } - - /** - * Merges and sanitizes plugin settings recursively. - * - * Existing options are preserved unless replaced. Strings are sanitized and - * arrays are merged recursively. Special handling is applied for the - * 'whitelisted_functions' field. - * - * @since NEXT - * - * @param array $existing_options Previously saved options are accepted. - * @param array $new_submitted_options Newly submitted options are accepted. - * - * @return array Sanitized merged options are returned. - */ - private static function merge_and_sanitize_settings( array $existing_options, array $new_submitted_options ): array { - // Existing and new settings are merged (new replaces old). - $merged_options = array_replace_recursive( $existing_options, $new_submitted_options ); - - // 'whitelisted_functions' field is normalized. - if ( isset( $merged_options['whitelisted_functions'] ) ) { - $raw_whitelisted = $merged_options['whitelisted_functions']; - - if ( is_string( $raw_whitelisted ) ) { - $lines = preg_split( "/\r\n|\r|\n/", $raw_whitelisted ); - $function_list = array_map( 'trim', (array) $lines ); - } elseif ( is_array( $raw_whitelisted ) ) { - $function_list = array_map( 'trim', $raw_whitelisted ); - } else { - $function_list = []; - } - - // Empty and duplicate entries are removed. - $merged_options['whitelisted_functions'] = array_values( - array_unique( array_filter( $function_list, 'strlen' ) ) - ); - } - - // Recursive sanitization is applied to scalar values. - array_walk_recursive( - $merged_options, - function ( &$value ) { - if ( is_string( $value ) ) { - $value = sanitize_text_field( $value ); - } elseif ( is_bool( $value ) ) { - $value = (bool) $value; - } elseif ( is_numeric( $value ) ) { - // Numeric values are kept as-is. - } - } - ); - - return $merged_options; - } +final class Settings_Page { + use Singleton; + + /** + * Option name for saving settings. + * + * @var string + */ + private $option_name = 'anys'; + + /** + * Slug for the settings page. + * + * @var string + */ + private $page_slug = 'anys-settings'; + + /** + * Supported tabs. + * + * @var array + */ + private $tabs = []; + + /** + * Adds WordPress admin hooks. + * + * @return void + */ + protected function add_hooks() { + add_action( 'admin_menu', [ $this, 'register_menu_page' ] ); + add_action( 'admin_init', [ $this, 'handle_save' ] ); + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] ); + + $this->tabs = [ + 'general' => __( 'General', 'anys' ), + 'integrations' => __( 'Integrations', 'anys' ), + 'functions' => __( 'Functions', 'anys' ), + 'views' => __( 'Views', 'anys' ), + ]; + } + + /** + * Adds the settings page under WP Admin → Settings. + * + * @return void + */ + public function register_menu_page() { + add_options_page( + __( 'Anything Shortcodes', 'anys' ), + __( 'Anything Shortcodes', 'anys' ), + 'manage_options', + $this->page_slug, + [ $this, 'render_page' ] + ); + } + + /** + * Handles saving settings via POST form. + * + * @return void + */ + public function handle_save() { + if ( ! is_admin() || ! current_user_can( 'manage_options' ) ) { + return; + } + + if ( + isset( $_POST['anys'], $_POST['_anys_nonce'] ) && + wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_anys_nonce'] ) ), 'anys_save_settings' ) + ) { + $incoming = is_array( $_POST['anys'] ?? null ) ? wp_unslash( $_POST['anys'] ) : []; + $existing = get_option( $this->option_name, [] ); + + if ( ! is_array( $existing ) ) { + $existing = []; + } + + $merged = self::merge_and_sanitize_settings( $existing, $incoming ); + update_option( $this->option_name, $merged ); + + wp_safe_redirect( + add_query_arg( + [ + 'page' => $this->page_slug, + 'tab' => $this->current_tab_slug(), + 'updated' => 'true', + ], + admin_url( 'options-general.php' ) + ) + ); + exit; + } + } + + /** + * Renders the settings page and active tab. + * + * @return void + */ + public function render_page() { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + $active_tab = $this->current_tab_slug(); + + echo '
'; + + echo '
'; + echo '

' . esc_html__( 'Anything Shortcodes', 'anys' ) . '

'; + echo '' + . esc_html__( 'Unlock Extra Features with Anything Shortcodes PRO', 'anys' ) . + ''; + echo '
'; + + // Tabs + echo ''; + + $view_file = $this->view_path( $active_tab ); + if ( file_exists( $view_file ) ) { + $options = get_option( $this->option_name, [] ); + $form_nonce = wp_create_nonce( 'anys_save_settings' ); + include $view_file; + } else { + echo '

' . esc_html__( 'The requested settings tab could not be found.', 'anys' ) . '

'; + } + + echo '
'; + } + + /** + * Returns current active tab slug. + * + * @return string + */ + private function current_tab_slug(): string { + $requested = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : ''; + + if ( $requested && 0 === strpos( $requested, 'anys-' ) ) { + $requested = substr( $requested, 5 ); + } + + return array_key_exists( $requested, $this->tabs ) ? $requested : 'general'; + } + + /** + * Returns the path to the view file of the tab. + * + * @param string $tab Tab slug. + * @return string + */ + private function view_path( string $tab ): string { + return __DIR__ . '/views/' . $tab . '.php'; + } + + /** + * Enqueues admin CSS and JS for settings page. + * + * @param string $hook Current admin page hook. + * @return void + */ + public function enqueue_admin_assets( $hook ) { + if ( 'settings_page_' . $this->page_slug !== $hook ) { + return; + } + + wp_enqueue_style( 'anys-admin-settings', ANYS_CSS_URL . 'settings.css', [], 'NEXT' ); + wp_enqueue_script( 'anys-admin-mobile-sidebar', ANYS_JS_URL . 'admin-mobile-sidebar.js', [], '1.0.0', true ); + } + + /** + * Merges and sanitizes plugin settings recursively. + * + * @param array $existing Existing options. + * @param array $new New submitted options. + * @return array Sanitized merged options. + */ + private static function merge_and_sanitize_settings( array $existing, array $new ): array { + $merged = array_replace_recursive( $existing, $new ); + + if ( isset( $merged['whitelisted_functions'] ) ) { + $list = $merged['whitelisted_functions']; + if ( is_string( $list ) ) { + $list = preg_split( "/\r\n|\r|\n/", $list ); + } + $merged['whitelisted_functions'] = array_values( + array_unique( + array_filter( array_map( 'trim', (array) $list ), 'strlen' ) + ) + ); + } + + array_walk_recursive( + $merged, + function ( &$v ) { + if ( is_string( $v ) ) { + $v = sanitize_text_field( $v ); + } + } + ); + + return $merged; + } } -/** - * Boots the singleton immediately when this file loads. - * - * @since NEXT - * - * @return void Nothing is returned. - */ -Anys_Settings_Page::get_instance(); +// Boot the settings page. +Settings_Page::get_instance();