diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php index 3dcd945e91..00f5e10c72 100644 --- a/modules/images/image-loading-optimization/detection.php +++ b/modules/images/image-loading-optimization/detection.php @@ -40,12 +40,17 @@ function ilo_print_detection_script() { */ $detection_time_window = apply_filters( 'perflab_image_loading_detection_time_window', 5000 ); + $query_vars = ilo_get_normalized_query_vars(); + $slug = ilo_get_page_metrics_slug( $query_vars ); + $detect_args = array( 'serveTime' => $serve_time, 'detectionTimeWindow' => $detection_time_window, 'isDebug' => WP_DEBUG, 'restApiEndpoint' => rest_url( ILO_REST_API_NAMESPACE . ILO_PAGE_METRICS_ROUTE ), 'restApiNonce' => wp_create_nonce( 'wp_rest' ), + 'pageMetricsSlug' => $slug, + 'pageMetricsHmac' => ilo_get_slug_hmac( $slug ), // TODO: Or would a nonce make more sense with the $slug being the action? ); wp_print_inline_script_tag( sprintf( diff --git a/modules/images/image-loading-optimization/detection/detect.js b/modules/images/image-loading-optimization/detection/detect.js index 7498e3d81d..dec3879ba3 100644 --- a/modules/images/image-loading-optimization/detection/detect.js +++ b/modules/images/image-loading-optimization/detection/detect.js @@ -104,6 +104,8 @@ function getBreadcrumbs( leafElement ) { * @param {boolean} args.isDebug Whether to show debug messages. * @param {string} args.restApiEndpoint URL for where to send the detection data. * @param {string} args.restApiNonce Nonce for writing to the REST API. + * @param {string} args.pageMetricsSlug Slug for page metrics. + * @param {string} args.pageMetricsHmac HMAC for the page metric slug. */ export default async function detect( { serveTime, @@ -111,6 +113,8 @@ export default async function detect( { isDebug, restApiEndpoint, restApiNonce, + pageMetricsSlug, + pageMetricsHmac, } ) { const runTime = new Date().valueOf(); @@ -259,7 +263,9 @@ export default async function detect( { /** @type {PageMetrics} */ const pageMetrics = { - url: win.location.href, // TODO: Consider sending canonical URL instead. + url: win.location.href, + slug: pageMetricsSlug, + hmac: pageMetricsHmac, viewport: { width: win.innerWidth, height: win.innerHeight, diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php index c2ac6b38e4..63e5ccc3b7 100644 --- a/modules/images/image-loading-optimization/storage/data.php +++ b/modules/images/image-loading-optimization/storage/data.php @@ -96,6 +96,30 @@ function ilo_get_normalized_query_vars() { return $normalized_query_vars; } +/** + * Gets slug for page metrics. + * + * @see ilo_get_normalized_query_vars() + * + * @param array $query_vars Normalized query vars. + * @return string Slug. + */ +function ilo_get_page_metrics_slug( $query_vars ) { + return md5( wp_json_encode( $query_vars ) ); +} + +/** + * Compute HMAC for page metrics slug. + * + * This is used in the REST API to authenticate the storage of new page metrics from a given URL. + * + * @param string $slug Page metrics slug. + * @return false HMAC. + */ +function ilo_get_slug_hmac( $slug ) { + return hash_hmac( 'sha1', $slug, wp_salt() ); +} + /** * Unshift a new page metric onto an array of page metrics. * diff --git a/modules/images/image-loading-optimization/storage/post-type.php b/modules/images/image-loading-optimization/storage/post-type.php index 578dcc2db2..d3bfefdc32 100644 --- a/modules/images/image-loading-optimization/storage/post-type.php +++ b/modules/images/image-loading-optimization/storage/post-type.php @@ -37,28 +37,18 @@ function ilo_register_page_metrics_post_type() { } add_action( 'init', 'ilo_register_page_metrics_post_type' ); -/** - * Gets slug for page metrics post. - * - * @param string $url URL. - * @return string Slug for URL. - */ -function ilo_get_page_metrics_slug( $url ) { - return md5( $url ); -} - /** * Get page metrics post. * - * @param string $url URL. + * @param string $slug Page metrics slug. * @return WP_Post|null Post object if exists. */ -function ilo_get_page_metrics_post( $url ) { +function ilo_get_page_metrics_post( $slug ) { $post_query = new WP_Query( array( 'post_type' => ILO_PAGE_METRICS_POST_TYPE, 'post_status' => 'publish', - 'name' => ilo_get_page_metrics_slug( $url ), + 'name' => $slug, 'posts_per_page' => 1, 'no_found_rows' => true, 'cache_results' => true, @@ -114,7 +104,6 @@ function ilo_parse_stored_page_metrics( WP_Post $post ) { * The $validated_page_metric parameter has the following array shape: * * { - * 'url': string, * 'viewport': array{ * 'width': int, * 'height': int @@ -122,21 +111,20 @@ function ilo_parse_stored_page_metrics( WP_Post $post ) { * 'elements': array * } * - * @param array $validated_page_metric Page metric, already validated by REST API. - * + * @param string $url URL for the page metrics. This is used purely as metadata. + * @param string $slug Page metrics slug (computed from query vars). + * @param array $validated_page_metric Page metric, already validated by REST API. * @return int|WP_Error Post ID or WP_Error otherwise. */ -function ilo_store_page_metric( array $validated_page_metric ) { - $url = $validated_page_metric['url']; - unset( $validated_page_metric['url'] ); // Not stored in post_content but rather in post_title/post_name. +function ilo_store_page_metric( $url, $slug, array $validated_page_metric ) { $validated_page_metric['timestamp'] = time(); // TODO: What about storing a version identifier? $post_data = array( - 'post_title' => $url, + 'post_title' => $url, // TODO: Should we keep this? It can help with debugging. ); - $post = ilo_get_page_metrics_post( $url ); + $post = ilo_get_page_metrics_post( $slug ); if ( $post instanceof WP_Post ) { $post_data['ID'] = $post->ID; @@ -150,7 +138,7 @@ function ilo_store_page_metric( array $validated_page_metric ) { $page_metrics = array(); } } else { - $post_data['post_name'] = ilo_get_page_metrics_slug( $url ); + $post_data['post_name'] = $slug; $page_metrics = array(); } diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php index 4844392d3a..fb2c2c9868 100644 --- a/modules/images/image-loading-optimization/storage/rest-api.php +++ b/modules/images/image-loading-optimization/storage/rest-api.php @@ -65,6 +65,22 @@ function ilo_register_endpoint() { return true; }, ), + 'slug' => array( + 'type' => 'string', + 'required' => true, + 'pattern' => '^[0-9a-f]{32}$', + ), + 'hmac' => array( + 'type' => 'string', + 'required' => true, + 'pattern' => '^[0-9a-f]+$', + 'validate_callback' => static function ( $hmac, WP_REST_Request $request ) { + if ( ! hash_equals( $hmac, ilo_get_slug_hmac( $request->get_param( 'slug' ) ) ) ) { + return new WP_Error( 'invalid_hmac', __( 'HMAC comparison failure.', 'performance-lab' ) ); + } + return true; + }, + ), 'viewport' => array( 'description' => __( 'Viewport dimensions', 'performance-lab' ), 'type' => 'object', @@ -142,7 +158,7 @@ function ilo_handle_rest_request( WP_REST_Request $request ) { ilo_set_page_metric_storage_lock(); $page_metric = $request->get_json_params(); - $result = ilo_store_page_metric( $page_metric ); + $result = ilo_store_page_metric( $page_metric['url'], $page_metric['slug'], $request->get_json_params() ); if ( $result instanceof WP_Error ) { return $result; @@ -152,7 +168,7 @@ function ilo_handle_rest_request( WP_REST_Request $request ) { array( 'success' => true, 'post_id' => $result, - 'data' => ilo_parse_stored_page_metrics( ilo_get_page_metrics_post( $page_metric['url'] ) ), // TODO: Remove this debug data. + 'data' => ilo_parse_stored_page_metrics( ilo_get_page_metrics_post( $page_metric['slug'] ) ), // TODO: Remove this debug data. ) ); }