self::arg( array( 'type' => 'boolean', 'default' => false, ) ), ); register_rest_route( self::REST_NAMESPACE, '/reports/batch', array( 'methods' => \WP_REST_Server::CREATABLE, 'callback' => array( $this, 'batch' ), 'permission_callback' => array( $this, 'can_view' ), 'args' => array_merge( array( 'ids' => self::arg( array( 'type' => 'array', 'required' => true, 'minItems' => 1, 'maxItems' => self::MAX_BATCH, 'items' => array( 'type' => 'string', 'pattern' => '^[a-z0-9_]+$', 'maxLength' => 64, ), ) ), ), self::range_args(), $refresh ), ) ); register_rest_route( self::REST_NAMESPACE, '/reports/(?P[a-z0-9_]+)', array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $this, 'report' ), 'permission_callback' => array( $this, 'can_view' ), 'args' => array_merge( array( 'id' => self::arg( array( 'type' => 'string', 'pattern' => '^[a-z0-9_]+$', 'maxLength' => 64, ) ), ), self::range_args(), $refresh ), ) ); } /** * GET /reports/{id} → {data, meta}. * * @param \WP_REST_Request $request Request. * @return \WP_REST_Response|\WP_Error */ public function report( \WP_REST_Request $request ) { $id = (string) $request->get_param( 'id' ); $def = $this->plugin->registry()->get( $id ); if ( null === $def ) { return new \WP_Error( 'tagsmith_unknown_report', __( 'Unknown report.', 'tagsmith' ), array( 'status' => 404 ) ); } // Realtime reports cover the last 30 minutes whatever range is asked for. $range = 'realtime' === $def['api'] ? DateRange::from_request( 'today' ) : self::range_from( $request ); try { return new \WP_REST_Response( $this->plugin->reports()->run( $id, $range, array(), $this->force( $request ) ) ); } catch ( ApiError $e ) { return self::error( $e ); } } /** * POST /reports/batch → {data: {id: result|{error}}, meta: {range}}. * * Realtime ids run individually with the same 'today' range override as * GET /reports/{id}; the rest go through ReportRunner::run_batch(). The * input order is kept. * * @param \WP_REST_Request $request Request. */ public function batch( \WP_REST_Request $request ): \WP_REST_Response { $ids = array_values( array_unique( array_map( 'strval', (array) $request->get_param( 'ids' ) ) ) ); $range = self::range_from( $request ); $force = $this->force( $request ); $reports = $this->plugin->reports(); $registry = $this->plugin->registry(); $realtime = array_values( array_filter( $ids, static fn( string $id ): bool => 'realtime' === ( $registry->get( $id )['api'] ?? null ) ) ); $others = array_values( array_diff( $ids, $realtime ) ); $results = array() === $others ? array() : $reports->run_batch( $others, $range, $force ); foreach ( $realtime as $id ) { try { $results[ $id ] = $reports->run( $id, DateRange::from_request( 'today' ), array(), $force ); } catch ( ApiError $e ) { $results[ $id ] = array( 'error' => array( 'slug' => $e->slug(), 'message' => $e->user_message(), ), ); } } $ordered = array(); foreach ( $ids as $id ) { if ( array_key_exists( $id, $results ) ) { $ordered[ $id ] = $results[ $id ]; } } return new \WP_REST_Response( array( 'data' => $ordered, 'meta' => array( 'range' => $range->to_array() ), ) ); } /** * Whether to bypass the cache: only when asked, only for administrators, * and at most once a minute site-wide (otherwise the request is served * normally rather than rejected). * * @param \WP_REST_Request $request Request. */ private function force( \WP_REST_Request $request ): bool { if ( true !== $request->get_param( 'refresh' ) || ! current_user_can( 'manage_options' ) ) { return false; } if ( false !== get_transient( self::FORCE_LOCK ) ) { return false; } set_transient( self::FORCE_LOCK, 1, MINUTE_IN_SECONDS ); return true; } }