Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
23.40% covered (danger)
23.40%
11 / 47
20.00% covered (danger)
20.00%
1 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
WPCOM_REST_API_V2_Endpoint_Admin_Bar
25.00% covered (danger)
25.00%
11 / 44
20.00% covered (danger)
20.00%
1 / 5
84.30
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 register_routes
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 get_item_permissions_check
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 get_item
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 filter_nodes
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2/**
3 * REST API endpoint for admin bar.
4 *
5 * @package automattic/jetpack
6 */
7
8if ( ! defined( 'ABSPATH' ) ) {
9    exit( 0 );
10}
11
12/**
13 * Class WPCOM_REST_API_V2_Endpoint_Admin_Bar
14 */
15class WPCOM_REST_API_V2_Endpoint_Admin_Bar extends WP_REST_Controller {
16
17    /**
18     * Namespace prefix.
19     *
20     * @var string
21     */
22    public $namespace = 'wpcom/v2';
23
24    /**
25     * Endpoint base route.
26     *
27     * @var string
28     */
29    public $rest_base = 'admin-bar';
30
31    /**
32     * Top-level admin bar node IDs that are considered safe to show.
33     *
34     * @var string[]
35     */
36    const ALLOWED_TOP_LEVEL_NODES = array( 'wp-logo', 'site-name', 'updates', 'comments', 'new-content', 'my-account' );
37
38    /**
39     * WPCOM_REST_API_V2_Endpoint_Admin_Bar constructor.
40     */
41    public function __construct() {
42        add_action( 'rest_api_init', array( $this, 'register_routes' ) );
43    }
44
45    /**
46     * Register routes.
47     */
48    public function register_routes() {
49        register_rest_route(
50            $this->namespace,
51            $this->rest_base . '/',
52            array(
53                array(
54                    'methods'             => WP_REST_Server::READABLE,
55                    'callback'            => array( $this, 'get_item' ),
56                    'permission_callback' => array( $this, 'get_item_permissions_check' ),
57                ),
58            )
59        );
60    }
61
62    /**
63     * Checks if a given request has access to the admin bar.
64     *
65     * @param WP_REST_Request $request Full details about the request.
66     * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
67     */
68    public function get_item_permissions_check( $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
69        if ( ! current_user_can( 'manage_options' ) ) {
70            return new WP_Error(
71                'rest_forbidden',
72                __( 'Sorry, you are not allowed to view the admin bar on this site.', 'jetpack' ),
73                array( 'status' => rest_authorization_required_code() )
74            );
75        }
76
77        return true;
78    }
79
80    /**
81     * Retrieves the admin bar registered for the current site, filtered to
82     * the allowed top-level nodes and their descendants.
83     *
84     * @param WP_REST_Request $request Full details about the request.
85     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
86     */
87    public function get_item( $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
88        global $wp_admin_bar;
89
90        if ( ! class_exists( 'WP_Screen' ) ) {
91            require_once ABSPATH . 'wp-admin/includes/class-wp-screen.php';
92        }
93
94        if ( ! function_exists( 'set_current_screen' ) ) {
95            require_once ABSPATH . 'wp-admin/includes/screen.php';
96        }
97
98        // Simulate a wp-admin context.
99        set_current_screen( 'dashboard' );
100
101        add_filter( 'show_admin_bar', '__return_true', 999 );
102        _wp_admin_bar_init();
103
104        ob_start();
105        do_action_ref_array( 'admin_bar_menu', array( &$wp_admin_bar ) );
106        ob_clean();
107
108        $nodes          = $wp_admin_bar->get_nodes() ?? array();
109        $filtered_nodes = $this->filter_nodes( $nodes, self::ALLOWED_TOP_LEVEL_NODES );
110
111        return rest_ensure_response( array( 'nodes' => array_values( $filtered_nodes ) ) );
112    }
113
114    /**
115     * Filters admin bar nodes to only include allowed top-level items and
116     * their descendants.
117     *
118     * @param array $nodes       All admin bar nodes keyed by ID.
119     * @param array $allowed_ids Top-level node IDs to keep.
120     * @return array Filtered nodes.
121     */
122    private function filter_nodes( array $nodes, array $allowed_ids ) {
123        $allowed = array();
124
125        foreach ( $nodes as $id => $node ) {
126            if ( in_array( $id, $allowed_ids, true ) ) {
127                $allowed[ $id ] = $node;
128                continue;
129            }
130
131            $current = $node;
132            while ( ! empty( $current->parent ) && isset( $nodes[ $current->parent ] ) ) {
133                if ( in_array( $current->parent, $allowed_ids, true ) ) {
134                    $allowed[ $id ] = $node;
135                    break;
136                }
137                $current = $nodes[ $current->parent ];
138            }
139        }
140
141        return $allowed;
142    }
143}
144
145wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Admin_Bar' );