(RFC 9745) * Sunset: (RFC 8594) * Link: ; rel="successor-version" * * Each use fires `kdocs_deprecated_route` so sites can log who still calls them. */ class Legacy { /** * When the 1.x routes were deprecated (2.0). */ const DEPRECATED = '2026-11-20'; /** * When they go away (3.0, at the earliest). */ const SUNSET = '2027-12-31'; /** * Hooks. */ public function register() { add_action( 'kdocs_rest_routes', array( $this, 'routes' ), 99 ); add_filter( 'rest_post_dispatch', array( __CLASS__, 'headers' ), 10, 3 ); } /** * Deprecated route => replacement. * * @return array */ public static function aliases() { /** * Filters the deprecated routes and their replacements. * * @param array $aliases Old route => new route (both with the namespace). */ return (array) apply_filters( 'kdocs_rest_aliases', array( '/' . Routes::NS . '/analytics' => '/' . Routes::NS . '/analytics/overview', ) ); } /** * Registers the 1.x routes whose replacements exist (Pro analytics). * * @param string $ns Namespace. */ public function routes( $ns ) { if ( ! class_exists( '\KarmaDocs\Pro\Analytics' ) || ! \KarmaDocs\Features::is_pro() ) { return; } register_rest_route( $ns, '/analytics', array( 'methods' => 'GET', 'callback' => array( $this, 'analytics' ), 'permission_callback' => array( Routes::class, 'can_analytics' ), 'args' => array( 'days' => array( 'type' => 'integer', 'default' => 30, 'minimum' => 1, 'maximum' => 365, ), 'kb' => array( 'type' => 'integer', 'default' => 0, ), ), ) ); } /** * Internal GET on a current route. * * @param string $route Route below the namespace. * @param array $query Query. * @return array */ private static function fetch( $route, array $query ) { $request = new \WP_REST_Request( 'GET', '/' . Routes::NS . $route ); $request->set_query_params( $query ); $response = rest_do_request( $request ); return $response->is_error() ? array() : (array) $response->get_data(); } /** * GET /analytics (1.x): totals, daily views, top articles and searches. * * @param \WP_REST_Request $request Request. * @return \WP_REST_Response */ public function analytics( $request ) { $query = array( 'days' => (int) $request['days'], 'kb' => (int) $request['kb'], ); $overview = self::fetch( '/analytics/overview', $query ); $search = self::fetch( '/analytics/search', $query ); $kpis = array_column( (array) ( $overview['kpis'] ?? array() ), null, 'id' ); $series = (array) ( $overview['series'] ?? array() ); $yes = (int) array_sum( array_column( $series, 'helpful_yes' ) ); $no = (int) array_sum( array_column( $series, 'helpful_no' ) ); $totals = (array) ( $search['totals'] ?? array() ); $response = rest_ensure_response( array( 'range' => array( 'from' => $overview['range']['from'] ?? '', 'days' => (int) $request['days'], ), 'totals' => array( 'views' => (int) ( $kpis['views']['value'] ?? 0 ), 'viewsChange' => $kpis['views']['change'] ?? null, 'searches' => (int) ( $totals['searches'] ?? 0 ), 'failed' => (int) ( $totals['zero_results'] ?? 0 ), 'yes' => $yes, 'no' => $no, 'score' => ( $yes + $no ) ? (int) round( $yes / ( $yes + $no ) * 100 ) : null, ), 'series' => array_map( static fn( $day ) => array( 'date' => $day['date'], 'views' => (int) $day['views'], ), $series ), 'topArticles' => array_map( static fn( $row ) => array( 'id' => (int) $row['id'], 'title' => $row['title'], 'views' => (int) $row['views'], 'score' => $row['helpful'] ?? null, 'link' => $row['link'], 'editLink' => $row['editLink'], ), (array) ( $overview['top'] ?? array() ) ), 'topSearches' => array_map( static fn( $row ) => array( 'term' => $row['term'], 'count' => (int) $row['searches'], // 1.x stored the result count of each search; 2.0 keeps zero / non-zero. 'results' => (int) $row['zero_results'] >= (int) $row['searches'] ? 0 : null, ), (array) ( $search['terms'] ?? array() ) ), 'failed' => array_map( static fn( $row ) => array( 'term' => $row['term'], 'count' => (int) ( $row['searches'] ?? $row['zero_results'] ?? 0 ), ), (array) ( $search['zero'] ?? array() ) ), ) ); return self::headers( $response, rest_get_server(), $request ); } /** * Deprecation headers on 1.x routes. * * @param \WP_HTTP_Response $response Response. * @param \WP_REST_Server $server Server. * @param \WP_REST_Request $request Request. * @return \WP_HTTP_Response */ public static function headers( $response, $server, $request ) { $aliases = self::aliases(); $route = (string) $request->get_route(); if ( ! isset( $aliases[ $route ] ) || ! $response instanceof \WP_HTTP_Response ) { return $response; } if ( $response->get_headers()['Deprecation'] ?? false ) { return $response; // Already marked by the route itself. } $response->header( 'Deprecation', '@' . strtotime( self::DEPRECATED . ' 00:00:00 UTC' ) ); $response->header( 'Sunset', gmdate( 'D, d M Y H:i:s', strtotime( self::SUNSET . ' 00:00:00 UTC' ) ) . ' GMT' ); $response->header( 'Link', '<' . esc_url_raw( rest_url( ltrim( $aliases[ $route ], '/' ) ) ) . '>; rel="successor-version"' ); /** * Fires when a deprecated 1.x route is used. * * @param string $route Deprecated route. * @param string $replacement Replacement route. */ do_action( 'kdocs_deprecated_route', $route, $aliases[ $route ] ); return $response; } }