add_menu( array( 'id' => 'stats', 'href' => admin_url( 'admin.php?page=stats' ), 'title' => "
$alt
", ) ); } /** * Add a Stats link to the site-name menu, next to Dashboard. * * @return void */ public static function add_site_menu_link() { global $wp_admin_bar; // WordPress.com adds its own Stats link to this menu. if ( ! is_object( $wp_admin_bar ) || ! $wp_admin_bar->get_node( 'dashboard' ) || ! current_user_can( 'view_stats' ) || ( new Host() )->is_wpcom_platform() ) { return; } $wp_admin_bar->add_node( array( 'parent' => 'site-name', 'id' => 'jetpack-stats', 'title' => __( 'Stats', 'jetpack-stats-admin' ), 'href' => admin_url( 'admin.php?page=stats' ), ) ); } /** * Get the local URL that serves a chart image. * * @param string $chart One of the CHARTS. * @return string */ public static function get_chart_src( $chart ) { return add_query_arg( array( 'page' => 'stats', 'chart' => $chart, ), admin_url( 'admin.php' ) ); } /** * Serve a chart image and stop, when the request asks for one. * * The browser cannot fetch the chart from WordPress.com directly, because only the blog token can read it. * * @return void */ public static function maybe_serve_chart() { // URLs with `proxy` come from the Jetpack plugin's deprecated stats_get_image_chart_src(), and its own handler forwards their extra parameters. if ( isset( $_GET['proxy'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- An image request carries no nonce, and it changes nothing. return; } $chart = self::get_requested_chart(); if ( null === $chart || ! Stats_Options::get_option( 'admin_bar' ) || ! current_user_can( 'view_stats' ) ) { return; } $image = self::fetch_chart( $chart ); if ( null === $image ) { status_header( 502 ); exit( 0 ); } header( 'Content-Type: ' . $image['type'] ); header( 'Content-Length: ' . strlen( $image['body'] ) ); echo $image['body']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Binary image data. exit( 0 ); } /** * Fetch a chart image from WordPress.com with the blog token. * * @param string $chart One of the CHARTS. * @return array{type: string, body: string}|null The image, or null when WordPress.com did not return one. */ public static function fetch_chart( $chart ) { $server = Constants::get_constant( 'STATS_DASHBOARD_SERVER' ) ?? 'dashboard.wordpress.com'; // WordPress.com accepts the blog token on this file only when `page`, `proxy` and `blog` are all present. $url = add_query_arg( array( 'page' => 'stats', 'proxy' => '', 'blog' => Stats_Options::get_option( 'blog_id' ), ), "https://{$server}/wp-includes/charts/{$chart}.php" ); $response = Client::remote_request( array( 'url' => $url, 'method' => 'GET', 'timeout' => 90, 'user_id' => 0, ) ); // A failed request has the code '', which PHP 8 cannot divide. $code = (int) wp_remote_retrieve_response_code( $response ); $type = wp_remote_retrieve_header( $response, 'content-type' ); $body = wp_remote_retrieve_body( $response ); if ( 2 !== (int) ( $code / 100 ) || ! is_string( $type ) || ! str_starts_with( $type, 'image/' ) || '' === $body ) { return null; } return array( 'type' => $type, 'body' => $body, ); } /** * Keep chart requests from redirecting to upgrade.php while a database upgrade is pending. * * @see wp-admin/admin.php, which compares the stored `db_version` with `$wp_db_version`. * * @param mixed $version The stored database version. * @return mixed */ public static function ignore_db_version( $version ) { if ( is_admin() && null !== self::get_requested_chart() ) { global $wp_db_version; return $wp_db_version; } return $version; } /** * Get the chart the current request asks for. * * @return string|null One of the CHARTS, or null when the request is not for a chart. */ private static function get_requested_chart() { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- An image request carries no nonce, and it changes nothing. if ( ! isset( $_GET['page'] ) || ! isset( $_GET['chart'] ) || 'stats' !== $_GET['page'] ) { return null; } $chart = sanitize_key( wp_unslash( $_GET['chart'] ) ); // phpcs:enable WordPress.Security.NonceVerification.Recommended return in_array( $chart, self::CHARTS, true ) ? $chart : null; } }