'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 '
';
// 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 '';
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 '';
}
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 '' . esc_html__( 'Create a custom role', 'frontglut' ) . '
';
}
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' ) );
}
}