'frontglut_manage_settings' ) ); // Admin-only field types. add_filter( 'frontglut_field_render_page', array( __CLASS__, 'r_page' ), 10, 5 ); add_filter( 'frontglut_field_render_roles', array( __CLASS__, 'r_roles' ), 10, 5 ); add_filter( 'frontglut_field_render_media_url', array( __CLASS__, 'r_media' ), 10, 5 ); add_filter( 'frontglut_field_render_info', array( __CLASS__, 'r_info' ), 10, 5 ); add_filter( 'frontglut_field_sanitize_page', function ( $p, $f, $raw ) { return absint( $raw ); }, 10, 3 ); add_filter( 'frontglut_field_sanitize_roles', function ( $p, $f, $raw ) { return array_values( array_intersect( array_map( 'sanitize_key', (array) $raw ), array_keys( wp_roles()->roles ) ) ); }, 10, 3 ); add_filter( 'frontglut_field_sanitize_media_url', function ( $p, $f, $raw ) { return esc_url_raw( (string) $raw ); }, 10, 3 ); add_filter( 'frontglut_field_sanitize_info', function () { return ''; }, 10, 3 ); } /* ---------- field types ---------- */ public static function r_page( $o, $f, $v, $name, $id ) { return wp_dropdown_pages( array( 'name' => $name, 'id' => $id, 'selected' => (int) $v, 'show_option_none' => __( '— Select page —', 'frontglut' ), 'option_none_value' => 0, 'echo' => 0, 'class' => 'frontglut-input' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_dropdown_pages() escapes its own output. } public static function r_roles( $o, $f, $v, $name, $id ) { $cur = (array) $v; $out = '
'; foreach ( wp_roles()->roles as $slug => $r ) { $out .= ''; } return $out . '
'; } public static function r_media( $o, $f, $v, $name, $id ) { return '
'; } public static function r_info( $o, $f, $v, $name, $id ) { return '
' . wp_kses_post( isset( $f['html'] ) ? $f['html'] : '' ) . '
'; } /* ---------- schema ---------- */ public static function schema() { $s = array(); $s['general'] = array( 'label' => __( 'General', 'frontglut' ), 'order' => 10, 'fields' => array( 'general.dashboard_page' => array( 'type' => 'page', 'label' => __( 'Dashboard page', 'frontglut' ), 'help' => __( 'Page containing [frontglut_dashboard].', 'frontglut' ) ), 'general.login_page' => array( 'type' => 'page', 'label' => __( 'Login page', 'frontglut' ) ), 'general.register_page' => array( 'type' => 'page', 'label' => __( 'Registration page', 'frontglut' ) ), 'general.lost_page' => array( 'type' => 'page', 'label' => __( 'Lost password page', 'frontglut' ) ), 'general.account_page' => array( 'type' => 'page', 'label' => __( 'Account / profile page', 'frontglut' ) ), 'general.layout' => array( 'type' => 'select', 'label' => __( 'Dashboard layout', 'frontglut' ), 'options' => array( 'canvas' => __( 'Canvas (full screen app, no theme header/footer)', 'frontglut' ), 'full' => __( 'Full width inside theme', 'frontglut' ), 'embedded' => __( 'Contained inside theme content', 'frontglut' ) ) ), 'general.dashboard_title' => array( 'type' => 'text', 'label' => __( 'Dashboard title', 'frontglut' ) ), 'general.audit_retention' => array( 'type' => 'number', 'label' => __( 'Keep audit log (days)', 'frontglut' ), 'min' => 1 ), 'general.audit_full_ip' => array( 'type' => 'checkbox', 'label' => __( 'Store full IP addresses in the activity log', 'frontglut' ), 'help' => __( 'Off by default: IP addresses are anonymised (last part removed) before they are stored. Turn on only if your privacy policy covers it.', 'frontglut' ) ), 'general.show_credit' => array( 'type' => 'checkbox', 'label' => __( 'Show "Powered by FrontGlut" credit', 'frontglut' ) ), ) ); $s['theme'] = array( 'label' => __( 'Appearance', 'frontglut' ), 'order' => 20, 'fields' => array( 'theme.primary' => array( 'type' => 'color', 'label' => __( 'Primary colour', 'frontglut' ) ), 'theme.radius' => array( 'type' => 'number', 'label' => __( 'Corner radius (px)', 'frontglut' ), 'min' => 0, 'max' => 28 ), 'theme.font' => array( 'type' => 'select', 'label' => __( 'Font', 'frontglut' ), 'options' => array( 'system' => 'System', 'inherit' => __( 'Inherit from theme', 'frontglut' ), 'serif' => 'Serif', 'mono' => 'Monospace' ) ), 'theme.mode' => array( 'type' => 'select', 'label' => __( 'Colour mode', 'frontglut' ), 'options' => array( 'auto' => __( 'Follow device (light/dark)', 'frontglut' ), 'light' => __( 'Always light', 'frontglut' ), 'dark' => __( 'Always dark', 'frontglut' ) ) ), 'theme.logo' => array( 'type' => 'media_url', 'label' => __( 'Dashboard logo', 'frontglut' ) ), ) ); $em = array( 'emails.async' => array( 'type' => 'checkbox', 'label' => __( 'Send emails in the background (WP-Cron queue)', 'frontglut' ) ), 'emails.from_name' => array( 'type' => 'text', 'label' => __( 'From name', 'frontglut' ) ), 'emails.from_email' => array( 'type' => 'email', 'label' => __( 'From email', 'frontglut' ) ), 'emails.footer' => array( 'type' => 'textarea', 'label' => __( 'Footer text', 'frontglut' ) ), ); foreach ( Mailer::templates() as $key => $t ) { /* translators: %s: value. */ $em[ 'emails.' . $key . '.enabled' ] = array( 'type' => 'checkbox', 'label' => sprintf( __( 'Enable: %s', 'frontglut' ), $t['label'] ) ); $em[ 'emails.' . $key . '.subject' ] = array( 'type' => 'text', 'label' => __( 'Subject', 'frontglut' ) ); /* translators: %s: value. */ $em[ 'emails.' . $key . '.body' ] = array( 'type' => 'textarea', 'label' => __( 'Body', 'frontglut' ), 'help' => isset( $t['vars'] ) ? sprintf( __( 'Placeholders: {site_name}, {site_url}, %s', 'frontglut' ), '{' . str_replace( ', ', '}, {', $t['vars'] ) . '}' ) : '' ); } $s['emails'] = array( 'label' => __( 'Emails', 'frontglut' ), 'order' => 30, 'fields' => $em, 'after' => array( __CLASS__, 'email_test_box' ) ); $s['captcha'] = array( 'label' => __( 'Spam protection', 'frontglut' ), 'order' => 40, 'fields' => array( 'captcha.provider' => array( 'type' => 'select', 'label' => __( 'Provider', 'frontglut' ), 'options' => Captcha::providers() ), 'captcha.site_key' => array( 'type' => 'text', 'label' => __( 'Site key', 'frontglut' ) ), 'captcha.secret_key' => array( 'type' => 'text', 'label' => __( 'Secret key', 'frontglut' ) ), 'captcha.min_score' => array( 'type' => 'number', 'label' => __( 'reCAPTCHA v3 minimum score', 'frontglut' ), 'min' => 0, 'max' => 1 ), 'captcha.where' => array( 'type' => 'checkboxes', 'label' => __( 'Protect', 'frontglut' ), 'options' => array( 'login' => __( 'Login', 'frontglut' ), 'register' => __( 'Registration', 'frontglut' ), 'lost' => __( 'Lost password', 'frontglut' ), 'post' => __( 'Post submission forms', 'frontglut' ), 'enquiry' => __( 'Product enquiries', 'frontglut' ) ) ), ) ); $s['uploads'] = array( 'label' => __( 'Uploads', 'frontglut' ), 'order' => 50, 'fields' => array( 'uploads.allowed' => array( 'type' => 'text', 'label' => __( 'Allowed extensions', 'frontglut' ), 'help' => __( 'Comma separated. Executable types are always blocked.', 'frontglut' ) ), 'uploads.max_mb' => array( 'type' => 'number', 'label' => __( 'Max file size (MB)', 'frontglut' ), 'min' => 0.1 ), 'uploads.per_hour' => array( 'type' => 'number', 'label' => __( 'Uploads per user per hour', 'frontglut' ), 'min' => 1 ), 'uploads.guest_ok' => array( 'type' => 'checkbox', 'label' => __( 'Allow guests to use the standalone uploader', 'frontglut' ) ), ) ); return apply_filters( 'frontglut_settings_schema', $s ); } public static function tabs() { $tabs = array(); foreach ( self::schema() as $slug => $t ) { $tabs[ $slug ] = array( 'label' => $t['label'], 'order' => isset( $t['order'] ) ? $t['order'] : 60, 'schema' => $t ); } $tabs['modules'] = array( 'label' => __( 'Modules', 'frontglut' ), 'order' => 5, 'cb' => array( __CLASS__, 'tab_modules' ) ); $tabs['roles'] = ! current_user_can( 'manage_options' ) ? null : array( 'label' => __( 'Roles & Access', 'frontglut' ), 'order' => 6, 'cb' => array( __CLASS__, 'tab_roles' ) ); $tabs['guide'] = array( 'label' => __( 'How it works', 'frontglut' ), 'order' => 2, 'cb' => array( 'FrontGlut\\Core\\Guide', 'render' ) ); $tabs['addons'] = array( 'label' => __( 'Add-ons', 'frontglut' ), 'order' => 99, 'cb' => array( __CLASS__, 'tab_addons' ) ); $tabs = array_filter( apply_filters( 'frontglut_admin_tabs', $tabs ) ); uasort( $tabs, function ( $a, $b ) { return ( isset( $a['order'] ) ? $a['order'] : 60 ) <=> ( isset( $b['order'] ) ? $b['order'] : 60 ); } ); return $tabs; } public static function menu() { add_menu_page( Plugin::brand(), Plugin::brand(), 'frontglut_manage_settings', 'frontglut', array( __CLASS__, 'page' ), 'dashicons-layout', 58 ); } /** * The Form entries / Knowledge base post types attach to this menu before it exists, * so WP never adds the parent's own entry and the settings page became unreachable. * Add the main shortcuts to the top of the submenu. */ public static function submenu() { global $submenu; $cap = current_user_can( 'frontglut_manage_settings' ) ? 'frontglut_manage_settings' : 'manage_options'; if ( ! current_user_can( $cap ) ) { return; } $links = array( array( __( 'How it works', 'frontglut' ), 'guide' ), array( __( 'Setup', 'frontglut' ), 'setup' ), array( __( 'Settings', 'frontglut' ), 'general' ), array( __( 'Modules', 'frontglut' ), 'modules' ), array( __( 'Forms', 'frontglut' ), 'forms' ), array( __( 'Roles & Access', 'frontglut' ), 'roles' ), ); $tabs = self::tabs(); $new = array(); foreach ( $links as $l ) { if ( isset( $tabs[ $l[1] ] ) ) { $new[] = array( $l[0], $cap, self::url( $l[1] ) ); } } $existing = isset( $submenu['frontglut'] ) ? $submenu['frontglut'] : array(); $submenu['frontglut'] = array_merge( $new, $existing ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride } public static function url( $tab = '', $args = array() ) { return add_query_arg( array_merge( array( 'page' => 'frontglut', 'tab' => $tab ), $args ), admin_url( 'admin.php' ) ); } public static function page() { if ( ! current_user_can( 'frontglut_manage_settings' ) && ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to access this page.', 'frontglut' ) ); } $tabs = self::tabs(); $cur = Request::route( 'tab', 'key', 'general' ); if ( ! isset( $tabs[ $cur ] ) ) { $cur = 'general'; } $groups = array( __( 'Get started', 'frontglut' ) => array( 'guide', 'setup', 'general', 'theme', 'modules' ), __( 'Users', 'frontglut' ) => array( 'auth', 'roles', 'captcha' ), __( 'Content', 'frontglut' ) => array( 'forms', 'posttypes', 'posts', 'dashboard' ), __( 'Store', 'frontglut' ) => array( 'products', 'vendors' ), __( 'Files & email', 'frontglut' ) => array( 'emails', 'files', 'uploads', 'uploads_list' ), __( 'System', 'frontglut' ) => array( 'status', 'audit', 'tools', 'addons' ), ); $placed = array(); foreach ( $groups as $g => $slugs ) { $groups[ $g ] = array_values( array_filter( $slugs, function ( $x ) use ( $tabs ) { return isset( $tabs[ $x ] ); } ) ); $placed = array_merge( $placed, $groups[ $g ] ); } $rest = array_diff( array_keys( $tabs ), $placed ); if ( $rest ) { $groups[ __( 'More', 'frontglut' ) ] = array_values( $rest ); } $dash = (int) Settings::get( 'general.dashboard_page' ); $top = array( 'guide', 'setup', 'general', 'modules', 'forms' ); $toplab = array( 'guide' => __( 'How it works', 'frontglut' ), 'general' => __( 'Settings', 'frontglut' ) ); $icons = array( 'guide' => 'editor-help', 'setup' => 'flag', 'general' => 'admin-settings', 'theme' => 'art', 'modules' => 'screenoptions', 'auth' => 'admin-users', 'roles' => 'shield', 'captcha' => 'shield-alt', 'forms' => 'feedback', 'posttypes' => 'admin-post', 'posts' => 'edit', 'dashboard' => 'menu', 'products' => 'cart', 'vendors' => 'store', 'emails' => 'email-alt', 'files' => 'media-document', 'uploads' => 'upload', 'uploads_list' => 'images-alt2', 'status' => 'heart', 'audit' => 'backup', 'tools' => 'admin-tools', 'addons' => 'plugins-checked', ); $logo = ''; echo '

' . esc_html( Plugin::brand() ) . '

'; echo '
'; echo '
'; if ( $dash && 'publish' === get_post_status( $dash ) ) { echo '' . esc_html__( 'Open dashboard', 'frontglut' ) . ''; } echo '' . esc_html__( 'WP Admin', 'frontglut' ) . '
'; // Side menu (opened by the three-bar button). echo ''; echo '
'; $flash = get_transient( 'frontglut_flash_' . get_current_user_id() ); if ( is_string( $flash ) && '' !== $flash ) { delete_transient( 'frontglut_flash_' . get_current_user_id() ); echo '
' . esc_html( $flash ) . '
'; } echo '

' . esc_html( $tabs[ $cur ]['label'] ) . '

'; $t = $tabs[ $cur ]; if ( isset( $t['schema'] ) ) { self::render_schema( $cur, $t['schema'] ); } elseif ( isset( $t['cb'] ) ) { call_user_func( $t['cb'] ); } echo '
'; } /** * Store a one-time notice for the current user, shown on the next settings screen. * Returns no query args: the message is kept server-side, never passed in the URL. */ public static function flash( $message ) { set_transient( 'frontglut_flash_' . get_current_user_id(), (string) $message, MINUTE_IN_SECONDS ); return array(); } /** Full-screen app: flag our settings screen so admin.css can hide the WordPress menu, admin bar and notices there only. */ public static function body_class( $classes ) { if ( 'frontglut' === Request::route( 'page', 'text' ) ) { $classes .= ' frontglut-fullscreen'; } return $classes; } private static function render_schema( $slug, $schema ) { echo '
'; wp_nonce_field( 'frontglut_save_settings_' . $slug ); echo ''; foreach ( $schema['fields'] as $path => $f ) { $f['key'] = $path; $name = 'frontglut_s[' . str_replace( '.', '][', $path ) . ']'; echo wp_kses( Fields::render( $f, Settings::get( $path, isset( $f['default'] ) ? $f['default'] : '' ), $name, array( 'admin' => true ) ), \FrontGlut\Support\Ui::allowed_html() ); } submit_button(); echo '
'; if ( isset( $schema['after'] ) && is_callable( $schema['after'] ) ) { call_user_func( $schema['after'] ); } } public static function save_settings() { $slug = isset( $_POST['tab'] ) ? sanitize_key( $_POST['tab'] ) : ''; check_admin_referer( 'frontglut_save_settings_' . $slug ); if ( ! current_user_can( 'frontglut_manage_settings' ) && ! current_user_can( 'manage_options' ) ) { wp_die( 'Forbidden', 403 ); } $schema = self::schema(); if ( ! isset( $schema[ $slug ] ) ) { wp_die( 'Unknown tab' ); } $post = \FrontGlut\Support\Sanitize::post_array( 'frontglut_s', 'html' ); // Each value is sanitised again by its field type below. foreach ( $schema[ $slug ]['fields'] as $path => $f ) { $raw = $post; foreach ( explode( '.', $path ) as $part ) { $raw = is_array( $raw ) && array_key_exists( $part, $raw ) ? $raw[ $part ] : ( 'checkbox' === $f['type'] ? '' : null ); if ( null === $raw ) { break; } } if ( null === $raw ) { $raw = in_array( $f['type'], array( 'checkboxes', 'roles', 'multiselect' ), true ) ? array() : ''; } $f['key'] = $path; Settings::set( $path, Fields::sanitize( $f, $raw ) ); } do_action( 'frontglut_settings_saved', $slug ); Audit::log( 'settings_saved', $slug ); wp_safe_redirect( self::url( $slug, self::flash( __( 'Settings saved.', 'frontglut' ) ) ) ); exit; } /* ---------- modules ---------- */ public static function tab_modules() { $m = Plugin::instance()->modules(); echo '
'; wp_nonce_field( 'frontglut_save_modules' ); echo '

' . esc_html__( 'Disabled modules load no code, styles or scripts.', 'frontglut' ) . '

'; foreach ( $m->definitions() as $slug => $d ) { $avail = $m->available( $slug ); echo ''; } echo '
'; submit_button(); echo '
'; } public static function save_modules() { check_admin_referer( 'frontglut_save_modules' ); if ( ! current_user_can( 'frontglut_manage_settings' ) && ! current_user_can( 'manage_options' ) ) { wp_die( 'Forbidden', 403 ); } $on = isset( $_POST['frontglut_mod'] ) ? array_map( 'absint', (array) wp_unslash( $_POST['frontglut_mod'] ) ) : array(); Plugin::instance()->modules()->save( $on ); update_option( 'frontglut_flush_rewrite', 1 ); wp_safe_redirect( self::url( 'modules', self::flash( __( 'Modules saved.', 'frontglut' ) ) ) ); exit; } /* ---------- roles ---------- */ public static function tab_roles() { $caps = Caps::all(); $custom = (array) get_option( 'frontglut_custom_roles', array() ); echo '
'; wp_nonce_field( 'frontglut_save_roles' ); echo '
'; foreach ( wp_roles()->roles as $slug => $r ) { echo ''; } echo ''; foreach ( $caps as $cap => $label ) { echo ''; foreach ( wp_roles()->role_objects as $slug => $role ) { $is_admin = 'administrator' === $slug; echo ''; } echo ''; } echo '
' . esc_html__( 'Capability', 'frontglut' ) . '' . esc_html( translate_user_role( $r['name'] ) ) . ( in_array( $slug, $custom, true ) ? ' ×' : '' ) . '
' . esc_html( $label ) . '
' . esc_html( $cap ) . '
has_cap( $cap ), true, false ) . disabled( $is_admin, true, false ) . '>
'; submit_button( __( 'Save capabilities', 'frontglut' ) ); echo '

' . esc_html__( 'Create a custom role', 'frontglut' ) . '

'; wp_nonce_field( 'frontglut_add_role' ); echo ' '; submit_button( __( 'Create role', 'frontglut' ), 'secondary', 'submit', false ); echo '
'; } public static function save_roles() { check_admin_referer( 'frontglut_save_roles' ); if ( ! current_user_can( 'manage_options' ) ) { // Changing roles/capabilities is for administrators only. wp_die( 'Forbidden', 403 ); } $posted = \FrontGlut\Support\Sanitize::post_array( 'frontglut_caps', 'key' ); // Only used as a lookup against the known capability list. foreach ( wp_roles()->role_objects as $slug => $role ) { if ( 'administrator' === $slug ) { continue; } foreach ( array_keys( Caps::all() ) as $cap ) { if ( ! empty( $posted[ $slug ][ $cap ] ) ) { $role->add_cap( $cap ); } else { $role->remove_cap( $cap ); } } } Audit::log( 'roles_saved' ); wp_safe_redirect( self::url( 'roles', self::flash( __( 'Capabilities saved.', 'frontglut' ) ) ) ); exit; } public static function add_role() { check_admin_referer( 'frontglut_add_role' ); if ( ! current_user_can( 'manage_options' ) ) { // Changing roles/capabilities is for administrators only. wp_die( 'Forbidden', 403 ); } $name = isset( $_POST['role_name'] ) ? sanitize_text_field( wp_unslash( $_POST['role_name'] ) ) : ''; $clone = isset( $_POST['clone'] ) ? sanitize_key( $_POST['clone'] ) : 'subscriber'; $slug = sanitize_key( str_replace( ' ', '_', strtolower( $name ) ) ); if ( $slug && ! get_role( $slug ) && get_role( $clone ) && 'administrator' !== $clone ) { add_role( $slug, $name, get_role( $clone )->capabilities ); $c = (array) get_option( 'frontglut_custom_roles', array() ); $c[] = $slug; update_option( 'frontglut_custom_roles', array_unique( $c ) ); Audit::log( 'role_created', $slug ); } wp_safe_redirect( self::url( 'roles', self::flash( __( 'Role created.', 'frontglut' ) ) ) ); exit; } public static function delete_role() { $slug = isset( $_GET['role'] ) ? sanitize_key( $_GET['role'] ) : ''; check_admin_referer( 'frontglut_delete_role_' . $slug ); $c = (array) get_option( 'frontglut_custom_roles', array() ); if ( current_user_can( 'manage_options' ) && in_array( $slug, $c, true ) ) { remove_role( $slug ); update_option( 'frontglut_custom_roles', array_values( array_diff( $c, array( $slug ) ) ) ); Audit::log( 'role_deleted', $slug ); } wp_safe_redirect( self::url( 'roles', self::flash( __( 'Role deleted.', 'frontglut' ) ) ) ); exit; } /* ---------- add-ons hub ---------- */ public static function tab_addons() { echo '

' . esc_html__( 'FrontGlut is modular. The free core includes everything below; optional paid add-ons extend it.', 'frontglut' ) . '

'; foreach ( Plugin::instance()->modules()->definitions() as $slug => $d ) { echo '
' . esc_html( $d['label'] ) . '

' . esc_html( $d['desc'] ) . '

' . esc_html( Plugin::instance()->modules()->is_enabled( $slug ) ? __( 'Active', 'frontglut' ) : __( 'Off', 'frontglut' ) ) . '
'; } echo '

' . esc_html__( 'Pro add-ons (coming)', 'frontglut' ) . '

'; foreach ( apply_filters( 'frontglut_pro_addons', array( 'Advanced forms & workflow', 'Any admin page on frontend', 'Advanced product tools (bulk, variations)', 'Bookings, auctions, subscriptions', 'Analytics & PDF invoices', 'Vendor packs, verification, staff', 'Payments & split payouts', 'AI suite', 'Live chat & mobile API' ) ) as $n ) { echo '
' . esc_html( $n ) . '
Pro
'; } echo '
'; } /* ---------- email test ---------- */ public static function email_test_box() { echo '

' . esc_html__( 'Send a test email', 'frontglut' ) . '

'; } public static function test_email( $req ) { $to = isset( $req['to'] ) ? sanitize_email( $req['to'] ) : ''; if ( ! is_email( $to ) ) { Ajax::fail( __( 'Enter a valid email.', 'frontglut' ) ); } $ok = Mailer::deliver( array( $to ), __( 'FrontGlut test email', 'frontglut' ), __( 'If you can read this, outgoing email works.', 'frontglut' ) ); if ( ! $ok ) { Ajax::fail( __( 'wp_mail() returned false. Check your mail setup.', 'frontglut' ) ); } return array( 'message' => __( 'Sent.', 'frontglut' ) ); } }