%s', esc_url( admin_url( 'profile.php#amf_show_admin_menu' ) ), esc_html__( 'Settings', 'admin-menu-in-frontend' ) ) ); return $actions; } /** * Whether the user turned the menu on in their profile. * * @param int $user_id User ID. * @return bool */ private function is_enabled( $user_id ) { return (bool) get_user_meta( $user_id, self::META_SHOW, true ); } /** * The captured menu of the current user on this site. * * @return array{hash?: string, html?: string, css?: string, rtl?: bool} */ private function get_menu() { $menu = get_user_option( self::OPTION_MENU, get_current_user_id() ); return is_array( $menu ) ? $menu : array(); } /** * Folded and pinned state of the current user. * * @return array{folded: bool, pinned: bool} */ private function get_state() { $user_id = get_current_user_id(); $pinned = get_user_meta( $user_id, self::META_PINNED ); return array( 'folded' => (bool) get_user_meta( $user_id, self::META_FOLDED, true ), 'pinned' => isset( $pinned[0] ) ? (bool) $pinned[0] : true, ); } /** * Stores the admin menu exactly as core renders it for this user on this site. * * Runs inside the menu markup on every admin screen. Core's own renderer is called a * second time into a buffer, the per-screen "current" marks are removed, and the result * is written only when it differs from the stored copy. */ public function capture_menu() { global $menu, $submenu; $user_id = get_current_user_id(); if ( ! $user_id || ! $this->is_enabled( $user_id ) || ! function_exists( '_wp_menu_output' ) || ! is_array( $menu ) ) { return; } // The network and user admins have menus of their own, not this site's. if ( is_network_admin() || is_user_admin() ) { return; } if ( metadata_exists( 'user', $user_id, self::META_LEGACY ) ) { delete_user_meta( $user_id, self::META_LEGACY ); } ob_start(); _wp_menu_output( $menu, is_array( $submenu ) ? $submenu : array() ); $html = $this->normalize_menu_html( (string) ob_get_clean() ); $scheme = get_user_option( 'admin_color', $user_id ); $scheme = $scheme ? $scheme : 'fresh'; $rtl = is_rtl(); $hash = md5( implode( '|', array( $html, $scheme, (int) $rtl, get_bloginfo( 'version' ), self::VERSION ) ) ); $stored = $this->get_menu(); if ( isset( $stored['hash'] ) && $stored['hash'] === $hash ) { return; } update_user_option( $user_id, self::OPTION_MENU, array( 'hash' => $hash, 'html' => $html, 'css' => $this->scheme_menu_css( $scheme, $rtl ), 'rtl' => $rtl, ) ); } /** * Prints the menu as core prints it on a screen outside every section, with absolute links. * * The result must not depend on the admin screen it was captured on, or every page * view would rewrite the stored copy. * * @param string $html Menu items as printed by _wp_menu_output(). * @return string */ private function normalize_menu_html( $html ) { $tags = new WP_HTML_Tag_Processor( $html ); while ( $tags->next_tag() ) { $class = $tags->get_attribute( 'class' ); if ( is_string( $class ) ) { $classes = array(); $names = preg_split( '/\s+/', trim( $class ) ); $is_top = in_array( 'menu-top', $names, true ); foreach ( $names as $name ) { // A top item that is not current says so, in the same place; a submenu item says nothing. if ( 'wp-has-current-submenu' === $name || ( 'current' === $name && $is_top ) ) { $name = 'wp-not-current-submenu'; } if ( ! in_array( $name, array( '', 'current', 'wp-menu-open', 'opensub' ), true ) ) { $classes[] = $name; } } if ( $classes ) { $tags->set_attribute( 'class', implode( ' ', array_unique( $classes ) ) ); } else { $tags->remove_attribute( 'class' ); } } $tags->remove_attribute( 'aria-current' ); // A marker for core's admin script, set on every fly-out except the current one. $tags->remove_attribute( 'data-ariahaspopup' ); if ( 'A' !== $tags->get_tag() ) { continue; } $href = $tags->get_attribute( 'href' ); // Core prints links relative to wp-admin/, e.g. "edit.php?post_type=page". if ( is_string( $href ) && '' !== $href && ! preg_match( '#^([a-z][a-z0-9+.-]*:|/|\#)#i', $href ) ) { $tags->set_attribute( 'href', admin_url( $href ) ); } } // Removed attributes leave extra spaces in the tag, and only on the current item. return (string) preg_replace_callback( '/<[^<>]+>/', static function ( $tag ) { return preg_replace( array( '/\s+/', '/\s>$/' ), array( ' ', '>' ), $tag[0] ); }, $tags->get_updated_html() ); } /** * The menu rules of the user's admin colour scheme. * * A scheme's stylesheet also styles body, links and form fields of wp-admin, so only the * rules aimed at the menu are taken. * * @param string $scheme Admin colour scheme slug. * @param bool $rtl Whether the admin was right-to-left. * @return string */ private function scheme_menu_css( $scheme, $rtl ) { global $_wp_admin_css_colors; if ( empty( $_wp_admin_css_colors[ $scheme ]->url ) ) { return ''; // "fresh": its colours are in core's admin-menu.css. } $path = $this->local_css_path( $_wp_admin_css_colors[ $scheme ]->url ); if ( $path && $rtl ) { $rtl_path = preg_replace( '/(\.min)?\.css$/', '-rtl$1.css', $path ); $path = is_readable( $rtl_path ) ? $rtl_path : $path; } if ( ! $path ) { return ''; } $css = file_get_contents( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- A local stylesheet of this install. if ( ! is_string( $css ) ) { return ''; } $css = preg_replace( '#/\*.*?\*/#s', '', $css ); $rules = ''; if ( preg_match_all( '/([^{}]+)\{([^{}]*)\}/', $css, $matches, PREG_SET_ORDER ) ) { foreach ( $matches as $rule ) { $selectors = array(); foreach ( explode( ',', $rule[1] ) as $selector ) { $selector = trim( $selector ); if ( false !== strpos( $selector, '#adminmenu' ) || false !== strpos( $selector, '#collapse-' ) ) { $selectors[] = $selector; } // The pin button wears the colours of the collapse button next to it. if ( false !== strpos( $selector, '#collapse-button' ) ) { $selectors[] = str_replace( '#collapse-button', '#amf-pin-button', $selector ); } } if ( $selectors ) { $rules .= implode( ',', $selectors ) . '{' . trim( $rule[2] ) . "}\n"; } } } return str_replace( '', '', $rules ); } /** * Maps a stylesheet URL of this install to its file, or returns '' for anything else. * * @param string $url Stylesheet URL. * @return string */ private function local_css_path( $url ) { $url_path = (string) wp_parse_url( $url, PHP_URL_PATH ); $bases = array( (string) wp_parse_url( content_url( '/' ), PHP_URL_PATH ) => WP_CONTENT_DIR . '/', (string) wp_parse_url( site_url( '/' ), PHP_URL_PATH ) => ABSPATH, ); foreach ( $bases as $base_path => $dir ) { if ( '' === $base_path || 0 !== strpos( $url_path, $base_path ) ) { continue; } $file = realpath( $dir . substr( $url_path, strlen( $base_path ) ) ); $root = realpath( $dir ); if ( $file && $root && 0 === strpos( $file, $root ) && '.css' === substr( $file, -4 ) && is_readable( $file ) ) { return $file; } } return ''; } /** * Whether the menu goes on this public page. * * @return bool */ private function should_show() { if ( null !== $this->show ) { return $this->show; } $show = false; $user_id = get_current_user_id(); if ( $user_id && ! is_admin() && $this->is_enabled( $user_id ) && ! $this->is_builder_or_preview() ) { $menu = $this->get_menu(); $show = ! empty( $menu['html'] ); } /** * Filters whether the admin menu is shown on the current public page. * * @since 1.2.0 * * @param bool $show Whether to show the menu. * @param int $user_id Current user ID. */ $this->show = (bool) apply_filters( 'admin_menu_in_frontend_show', $show, $user_id ); return $this->show; } /** * Previews, embeds and page-builder edit modes, where the menu would be in the way. * * @return bool */ private function is_builder_or_preview() { if ( is_customize_preview() || is_embed() || is_feed() || wp_doing_ajax() || wp_is_json_request() ) { return true; } $builder_vars = array( 'elementor-preview', 'fl_builder', 'et_fb', 'ct_builder', 'bricks', 'brizy-edit', 'brizy-edit-iframe', 'vc_editable', 'tve', 'breakdance', 'breakdance_iframe' ); foreach ( $builder_vars as $var ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reads the page context only. if ( isset( $_GET[ $var ] ) ) { return true; } } return false; } /** * Loads core's own admin-menu stylesheet, the panel styles and the script. */ public function enqueue_assets() { if ( ! $this->should_show() ) { return; } $menu = $this->get_menu(); $rtl = empty( $menu['rtl'] ) ? '' : '-rtl'; $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; wp_enqueue_style( 'amf-core-admin-menu', admin_url( "css/admin-menu{$rtl}{$suffix}.css" ), array( 'dashicons' ), get_bloginfo( 'version' ) ); wp_enqueue_style( 'admin-menu-in-frontend', plugins_url( 'assets/css/admin-menu-in-frontend.css', __FILE__ ), array( 'amf-core-admin-menu' ), self::VERSION ); if ( ! empty( $menu['css'] ) ) { wp_add_inline_style( 'admin-menu-in-frontend', $menu['css'] ); } wp_enqueue_script( 'admin-menu-in-frontend', plugins_url( 'assets/js/admin-menu-in-frontend.js', __FILE__ ), array(), self::VERSION, array( 'in_footer' => true, 'strategy' => 'defer', ) ); wp_localize_script( 'admin-menu-in-frontend', 'amfMenu', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'amf-save-state' ), 'cookie' => self::COOKIE_RECENT, ) ); } /** * Body classes that make room for a pinned menu before any script runs. * * @param string[] $classes Body classes. * @return string[] */ public function body_class( $classes ) { if ( $this->should_show() ) { $state = $this->get_state(); $classes[] = 'amf-has-menu'; if ( $state['pinned'] ) { $classes[] = 'amf-pinned'; } if ( $state['folded'] ) { $classes[] = 'amf-folded'; } } return $classes; } /** * Prints the captured menu. */ public function print_menu() { if ( ! $this->should_show() ) { return; } $menu = $this->get_menu(); $state = $this->get_state(); $classes = array( 'amf-menu' ); if ( $state['folded'] ) { $classes[] = 'folded'; } if ( $state['pinned'] ) { $classes[] = 'amf-pinned'; } printf( '
'; } /** * Saves the folded or pinned state from the public page. */ public function ajax_save_state() { check_ajax_referer( 'amf-save-state', 'nonce' ); $keys = array( 'folded' => self::META_FOLDED, 'pinned' => self::META_PINNED, ); $key = isset( $_POST['key'] ) ? sanitize_key( wp_unslash( $_POST['key'] ) ) : ''; if ( ! isset( $keys[ $key ] ) ) { wp_send_json_error( null, 400 ); } update_user_meta( get_current_user_id(), $keys[ $key ], empty( $_POST['value'] ) ? '' : '1' ); wp_send_json_success(); } /** * Adds the "close the admin panel" button to the toolbar on admin screens. * * @param WP_Admin_Bar $wp_admin_bar Toolbar instance. */ public function add_close_node( $wp_admin_bar ) { $user_id = get_current_user_id(); if ( ! is_admin() || ! $user_id || ! $this->is_enabled( $user_id ) ) { return; } $url = ''; if ( ! empty( $_COOKIE[ self::COOKIE_RECENT ] ) ) { $recent = esc_url_raw( wp_unslash( $_COOKIE[ self::COOKIE_RECENT ] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitised by esc_url_raw() and checked by wp_validate_redirect(). $url = wp_validate_redirect( $recent ); // '' for another site or a scheme esc_url_raw() dropped. } if ( '' === $url ) { $url = home_url( '/' ); } $wp_admin_bar->add_node( array( 'id' => 'amf-close-admin-panel', 'parent' => 'top-secondary', 'title' => '' . esc_html__( 'Close the admin panel', 'admin-menu-in-frontend' ) . '', 'href' => $url, 'meta' => array( 'title' => __( 'Close the admin panel and return to the page you viewed last', 'admin-menu-in-frontend' ), ), ) ); } /** * Icon of the close button. */ public function admin_bar_style() { if ( is_admin_bar_showing() ) { wp_add_inline_style( 'admin-bar', '#wpadminbar #wp-admin-bar-amf-close-admin-panel .ab-icon:before{content:"\f158";top:2px}' ); } } /** * Prints the option under "Toolbar" on the profile screen. * * @param WP_User $user The user being edited. */ public function profile_field( $user ) { ?>