WP_REST_Server::READABLE, 'callback' => [ $this, 'index' ], 'permission_callback' => $auth, ], [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'create' ], 'permission_callback' => $auth, ], ] ); register_rest_route( $ns, '/redirects/(?P\d+)', [ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'show' ], 'permission_callback' => $auth, ], [ 'methods' => WP_REST_Server::EDITABLE, 'callback' => [ $this, 'update' ], 'permission_callback' => $auth, ], [ 'methods' => WP_REST_Server::DELETABLE, 'callback' => [ $this, 'destroy' ], 'permission_callback' => $auth, ], ] ); register_rest_route( $ns, '/redirects/check', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'check' ], 'permission_callback' => $auth, ] ); register_rest_route( $ns, '/redirects/bulk', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'bulk' ], 'permission_callback' => $auth, ] ); register_rest_route( $ns, '/redirects/for-post/(?P\d+)', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'for_post' ], 'permission_callback' => static fn( WP_REST_Request $request ): bool => RestModule::can_manage() && current_user_can( 'edit_post', (int) $request['id'] ), ] ); register_rest_route( $ns, '/redirects/restore', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'restore' ], 'permission_callback' => $auth, ] ); } public function index( WP_REST_Request $request ): WP_REST_Response { $result = $this->repository->find( [ 'search' => (string) $request->get_param( 'search' ), 'view' => (string) ( $request->get_param( 'view' ) ?? 'all' ), 'group' => (string) $request->get_param( 'group' ), 'code' => (int) $request->get_param( 'code' ), 'orderby' => (string) ( $request->get_param( 'orderby' ) ?? 'created' ), 'order' => (string) ( $request->get_param( 'order' ) ?? 'desc' ), 'page' => (int) ( $request->get_param( 'page' ) ?? 1 ), 'per_page' => (int) ( $request->get_param( 'per_page' ) ?? 25 ), ] ); return new WP_REST_Response( [ 'items' => array_map( [ self::class, 'present' ], $result['items'] ), 'total' => $result['total'], 'counts' => $this->repository->counts( self::filter( $request ) ), 'groups' => $this->repository->groups(), // The whole site, whatever is filtered: the figures at the top of the screen. 'totals' => $this->repository->counts(), 'not_found' => ( new NotFoundLog() )->counts(), ] ); } public function show( WP_REST_Request $request ): WP_REST_Response|WP_Error { $rule = $this->repository->get( (int) $request['id'] ); if ( null === $rule ) { return new WP_Error( 'codebold_not_found', __( 'That redirect no longer exists.', 'codebold-redirects' ), [ 'status' => 404 ] ); } return new WP_REST_Response( self::present( $rule ) ); } public function create( WP_REST_Request $request ): WP_REST_Response|WP_Error { $result = $this->manager->create( $this->input( $request ) ); if ( is_wp_error( $result ) ) { return self::error( $result ); } return new WP_REST_Response( $this->saved( $result ), 201 ); } public function update( WP_REST_Request $request ): WP_REST_Response|WP_Error { $result = $this->manager->update( (int) $request['id'], $this->input( $request ) ); if ( is_wp_error( $result ) ) { return self::error( $result ); } return new WP_REST_Response( $this->saved( $result ) ); } /** * What the screen needs after a save: the rule, anything worth a second look, and * how many broken addresses the save just fixed. * * @param array{rule: Rule, warnings: list>} $result * @return array */ private function saved( array $result ): array { return [ 'item' => self::present( $result['rule'] ), 'warnings' => $result['warnings'], 'resolved' => Plugin::instance()->service( Recorder::class )->last_resolved(), // Handed back so an undo can restore them; see Recorder::last_resolved_rows(). 'resolved_rows' => Plugin::instance()->service( Recorder::class )->last_resolved_rows(), 'counts' => $this->repository->counts(), ]; } public function destroy( WP_REST_Request $request ): WP_REST_Response { $deleted = $this->manager->delete( [ (int) $request['id'] ] ); return new WP_REST_Response( [ 'deleted' => array_map( [ self::class, 'raw' ], $deleted ), 'counts' => $this->repository->counts(), ] ); } /** * Validate without saving, for the warnings shown while somebody types. */ public function check( WP_REST_Request $request ): WP_REST_Response { $result = $this->manager->check( $this->input( $request ), (int) $request->get_param( 'id' ) ); if ( is_wp_error( $result ) ) { return new WP_REST_Response( [ 'ok' => false, 'error' => [ 'code' => $result->get_error_code(), 'message' => $result->get_error_message(), 'data' => $result->get_error_data() ], 'warnings' => [], ] ); } return new WP_REST_Response( [ 'ok' => true, 'warnings' => $result['warnings'], 'preview' => [ 'source' => $result['rule']->source, 'match_type' => $result['rule']->match_type, 'target' => $result['rule']->target, 'linked' => 'url' !== $result['rule']->target_type ? $result['rule']->target_label() : '', ], ] ); } public function bulk( WP_REST_Request $request ): WP_REST_Response|WP_Error { $ids = RestModule::ids( $request->get_param( 'ids' ) ); $action = (string) $request->get_param( 'action' ); if ( [] === $ids ) { return new WP_Error( 'codebold_nothing', __( 'Select at least one redirect first.', 'codebold-redirects' ), [ 'status' => 400 ] ); } $out = [ 'action' => $action, 'count' => 0 ]; switch ( $action ) { case 'enable': case 'disable': $out['count'] = $this->repository->set_enabled( $ids, 'enable' === $action ); break; case 'delete': $deleted = $this->manager->delete( $ids ); $out['count'] = count( $deleted ); $out['deleted'] = array_map( [ self::class, 'raw' ], $deleted ); break; case 'reset': $out['count'] = $this->repository->reset_hits( $ids ); break; case 'code': $out['count'] = $this->repository->set_code( $ids, (int) $request->get_param( 'code' ) ); break; case 'group': $out['count'] = $this->repository->set_group( $ids, (string) $request->get_param( 'group' ) ); break; default: return new WP_Error( 'codebold_bad_action', __( 'Unknown bulk action.', 'codebold-redirects' ), [ 'status' => 400 ] ); } $out['counts'] = $this->repository->counts( self::filter( $request ) ); $out['groups'] = $this->repository->groups(); $out['totals'] = $this->repository->counts(); return new WP_REST_Response( $out ); } /** * The old addresses that lead to one post, for the editor box to redraw after a save. */ public function for_post( WP_REST_Request $request ): WP_REST_Response { $leads = array_values( array_filter( $this->repository->pointing_at( 'post', (int) $request['id'] ), static fn( Rule $r ): bool => 'post' !== $r->origin ) ); return new WP_REST_Response( [ 'leads' => array_map( static fn( Rule $r ): array => [ 'source' => $r->source, 'hits' => $r->hits ], array_slice( $leads, 0, 8 ) ), 'more' => max( 0, count( $leads ) - 8 ), ] ); } public function restore( WP_REST_Request $request ): WP_REST_Response { $rows = $request->get_param( 'rules' ); $count = $this->manager->restore( is_array( $rows ) ? array_values( $rows ) : [] ); return new WP_REST_Response( [ 'restored' => $count, 'counts' => $this->repository->counts() ] ); } /** * The list's current search and group, which counts in a response should reflect. * * @return array{search: string, group: string} */ private static function filter( WP_REST_Request $request ): array { // The list sends its group filter as "group"; a bulk request uses "group" for the // group to move things to, so it sends its filter as "filter_group" instead. $group = $request->get_param( 'filter_group' ); if ( null === $group && 'GET' === $request->get_method() ) { $group = $request->get_param( 'group' ); } return [ 'search' => (string) $request->get_param( 'search' ), 'group' => (string) $group, ]; } /** @return array */ private function input( WP_REST_Request $request ): array { $params = (array) $request->get_json_params() + (array) $request->get_body_params(); return array_intersect_key( $params, array_flip( self::FIELDS ) ); } /** * A validation failure is the person's to fix, not a server fault: 400 unless the * error says otherwise, with its data kept so the screen can link to a duplicate. */ private static function error( WP_Error $error ): WP_Error { $data = (array) $error->get_error_data(); if ( ! isset( $data['status'] ) ) { $data['status'] = 400; } return new WP_Error( $error->get_error_code(), $error->get_error_message(), $data ); } /** * A rule as the screen draws it. * * @return array */ public static function present( Rule $rule ): array { $out = $rule->to_array(); $link = 'term' === $rule->target_type ? get_term_link( $rule->target_id ) : null; $current = match ( $rule->target_type ) { 'post' => Engine::post_url( $rule->target_id ), 'term' => is_string( $link ) ? $link : null, default => '' !== $rule->target ? $rule->target : null, }; $out['target_url'] = $current ?? $rule->target; $out['target_path'] = null !== $current && Address::parse( $current )->is_local() ? Address::parse( $current )->relative() : (string) $current; $out['broken'] = 'post' === $rule->target_type && null === Engine::post_url( $rule->target_id ); $out['moves'] = $rule->moves(); $out['source_url'] = Rule::EXACT === $rule->match_type ? Engine::absolute( $rule->source ) : ''; // The form works in the site's time zone; storage is UTC. $out['starts_at_local'] = null !== $rule->starts_at ? get_date_from_gmt( $rule->starts_at, 'Y-m-d\TH:i' ) : ''; $out['ends_at_local'] = null !== $rule->ends_at ? get_date_from_gmt( $rule->ends_at, 'Y-m-d\TH:i' ) : ''; return $out; } /** * A rule exactly as stored, for undo. * * @return array */ public static function raw( Rule $rule ): array { return get_object_vars( $rule ); } }