schedules = $schedules; $this->sanitizer = $sanitizer; } /** * Registers hooks. * * @return void */ public function register(): void { add_action( 'rest_api_init', array( $this, 'register_routes' ) ); } /** * Registers REST routes. * * @return void */ public function register_routes(): void { register_rest_route( SettingsController::ROUTE_NAMESPACE, '/scheduled-elements', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'can_manage' ), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'can_manage' ), ), ) ); register_rest_route( SettingsController::ROUTE_NAMESPACE, '/scheduled-elements/(?P[a-z0-9]{12})', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'can_manage' ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'can_manage' ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'can_manage' ), ), ) ); } /** * Only administrators manage the schedule list. * * @return bool */ public function can_manage(): bool { return current_user_can( self::CAPABILITY ); } /** * Every schedule, and the site's timezone for the form. * * @return WP_REST_Response */ public function get_items(): WP_REST_Response { return new WP_REST_Response( array( 'schedules' => array_map( array( $this, 'prepare' ), $this->schedules->all() ), 'timezone' => ScheduledElements::timezone_label(), ) ); } /** * One schedule. * * @param WP_REST_Request $request Request. * @phpstan-param WP_REST_Request> $request * @return WP_REST_Response|WP_Error */ public function get_item( WP_REST_Request $request ) { $schedule = $this->schedules->get( (string) $request->get_param( 'id' ) ); return null === $schedule ? $this->not_found() : new WP_REST_Response( $this->prepare( $schedule ) ); } /** * Adds a schedule. * * @param WP_REST_Request $request Request. * @phpstan-param WP_REST_Request> $request * @return WP_REST_Response|WP_Error */ public function create_item( WP_REST_Request $request ) { $schedule = $this->sanitizer->from_input( $this->input( $request ) ); if ( $schedule instanceof WP_Error ) { return $schedule; } $this->schedules->save( $schedule ); return new WP_REST_Response( $this->prepare( $schedule ), 201 ); } /** * Changes a schedule. * * @param WP_REST_Request $request Request. * @phpstan-param WP_REST_Request> $request * @return WP_REST_Response|WP_Error */ public function update_item( WP_REST_Request $request ) { $existing = $this->schedules->get( (string) $request->get_param( 'id' ) ); if ( null === $existing ) { return $this->not_found(); } $input = $this->input( $request ); if ( 'block' === $existing['source'] ) { // Targets and times belong to the block: only the name and switch change here. $schedule = $existing; $schedule['name'] = sanitize_text_field( (string) ( $input['name'] ?? $existing['name'] ) ); $schedule['enabled'] = ! empty( $input['enabled'] ); } else { $schedule = $this->sanitizer->from_input( $input, $existing ); if ( $schedule instanceof WP_Error ) { return $schedule; } } $this->schedules->save( $schedule ); return new WP_REST_Response( $this->prepare( $schedule ) ); } /** * Deletes a schedule. * * @param WP_REST_Request $request Request. * @phpstan-param WP_REST_Request> $request * @return WP_REST_Response|WP_Error */ public function delete_item( WP_REST_Request $request ) { $id = (string) $request->get_param( 'id' ); return $this->schedules->delete( $id ) ? new WP_REST_Response( array( 'deleted' => true, 'id' => $id, ) ) : $this->not_found(); } /** * A schedule for the screen: times in the site's timezone, both for the * form and to read, the chosen posts' titles, and its status now. * * @param array $schedule Stored schedule. * @return array */ public function prepare( array $schedule ): array { $posts = array(); foreach ( $schedule['posts'] as $post_id ) { $title = get_the_title( $post_id ); $posts[] = array( 'id' => $post_id, 'title' => '' !== $title ? html_entity_decode( wp_strip_all_tags( $title ), ENT_QUOTES, 'UTF-8' ) : sprintf( /* translators: %d: post ID. */ __( '#%d (no title)', 'yangra-conditional' ), $post_id ), ); } return array( 'id' => $schedule['id'], 'name' => $schedule['name'], 'targets' => $schedule['targets'], 'showAt' => $this->local( $schedule['show_at'], ScheduleSanitizer::LOCAL_FORMAT ), 'hideAt' => $this->local( $schedule['hide_at'], ScheduleSanitizer::LOCAL_FORMAT ), 'showLabel' => $this->local( $schedule['show_at'], $this->display_format() ), 'hideLabel' => $this->local( $schedule['hide_at'], $this->display_format() ), 'scope' => $schedule['scope'], 'posts' => $posts, 'enabled' => $schedule['enabled'], 'source' => $schedule['source'], 'status' => ScheduledElements::status( $schedule ), 'location' => 'block' === $schedule['source'] ? $this->location( (string) ( $schedule['location'] ?? '' ) ) : null, 'notFound' => ! empty( $schedule['not_found'] ), ); } /** * Where a block schedule's block is, as people read it, with a link to * edit it there. * * @param string $location "post:{id}" or "widget:{id}". * @return array{label: string, editUrl: string} */ private function location( string $location ): array { if ( 1 === preg_match( '/^post:(\d+)$/', $location, $match ) ) { $post = get_post( (int) $match[1] ); if ( null !== $post ) { $type = get_post_type_object( $post->post_type ); $title = '' !== $post->post_title ? html_entity_decode( wp_strip_all_tags( $post->post_title ), ENT_QUOTES, 'UTF-8' ) : __( '(no title)', 'yangra-conditional' ); return array( /* translators: 1: post title, 2: post type, e.g. "Page". */ 'label' => sprintf( __( '%1$s (%2$s)', 'yangra-conditional' ), $title, null !== $type ? $type->labels->singular_name : $post->post_type ), 'editUrl' => (string) get_edit_post_link( $post->ID, 'raw' ), ); } } if ( 0 === strpos( $location, 'widget:' ) ) { return array( 'label' => __( 'A block widget', 'yangra-conditional' ), 'editUrl' => admin_url( 'widgets.php' ), ); } return array( 'label' => __( 'Unknown', 'yangra-conditional' ), 'editUrl' => '', ); } /** * The submitted fields. * * @param WP_REST_Request $request Request. * @phpstan-param WP_REST_Request> $request * @return array */ private function input( WP_REST_Request $request ): array { $input = array(); foreach ( array( 'name', 'targets', 'showAt', 'hideAt', 'scope', 'posts', 'enabled' ) as $key ) { if ( null !== $request->get_param( $key ) ) { $input[ $key ] = $request->get_param( $key ); } } return $input; } /** * A timestamp in the site's timezone, or '' for none. * * @param int|null $timestamp UTC timestamp. * @param string $format Date format. * @return string */ private function local( ?int $timestamp, string $format ): string { return null === $timestamp ? '' : (string) wp_date( $format, $timestamp ); } /** * The site's date and time format, for reading. * * @return string */ private function display_format(): string { return get_option( 'date_format' ) . ' ' . get_option( 'time_format' ); } /** * A 404 for an unknown schedule. * * @return WP_Error */ private function not_found(): WP_Error { return new WP_Error( 'yangra_conditional_schedule_not_found', __( 'This schedule does not exist.', 'yangra-conditional' ), array( 'status' => 404 ) ); } }