importer = $importer ?? new StoryImporter( new StoryContentBuilder( new FormatRegistry( new ApiClient() ) ) ); } /** * Registering the story routes. */ protected function routes(): array { $routes = array(); foreach ( array( 'publication', 'element' ) as $name ) { $routes[ '/' . $name ] = array( 'methods' => 'POST', 'callback' => 'element' === $name ? array( $this, 'create_from_global_export' ) : array( $this, 'create' ), 'permission_callback' => array( $this, 'require_token' ), ); $routes[ '/' . $name . '/(?P\d+)' ] = array( array( 'methods' => 'PUT', 'callback' => 'element' === $name ? array( $this, 'update_from_global_export' ) : array( $this, 'update' ), 'permission_callback' => array( $this, 'require_token' ), 'args' => $this->post_id_arg(), ), array( 'methods' => 'DELETE', 'callback' => array( $this, 'delete' ), 'permission_callback' => array( $this, 'require_token' ), 'args' => $this->post_id_arg(), ), ); } return $routes; } /** * Importing a story as a new post. * * @param WP_REST_Request>|WP_REST_Request $request Incoming request. * * @return WP_REST_Response What became of the story. */ public function create( $request ): WP_REST_Response { return $this->import( $request, false, false ); } /** * Importing a story as a new post, from a global export. * * @param WP_REST_Request>|WP_REST_Request $request Incoming request. * * @return WP_REST_Response What became of the story. */ public function create_from_global_export( $request ): WP_REST_Response { return $this->import( $request, false, true ); } /** * Importing a story onto an existing post. * * @param WP_REST_Request>|WP_REST_Request $request Incoming request. * * @return WP_REST_Response What became of the story. */ public function update( $request ): WP_REST_Response { return $this->import( $request, true, false ); } /** * Importing a story onto an existing post, from a global export. * * @param WP_REST_Request>|WP_REST_Request $request Incoming request. * * @return WP_REST_Response What became of the story. */ public function update_from_global_export( $request ): WP_REST_Response { return $this->import( $request, true, true ); } /** * Retiring the post of a story deleted in Kordiam. * * @param WP_REST_Request>|WP_REST_Request $request Incoming request. * * @return WP_REST_Response The result. */ public function delete( $request ): WP_REST_Response { $post_id = (int) $request->get_param( 'post_id' ); $rejected = $this->reject_unusable_post( $post_id, false ); if ( null !== $rejected ) { return $rejected; } $this->importer->retire( $post_id ); return Response::accepted_no_action(); } /** * Running an import and turning its outcome into a response. * * @param WP_REST_Request>|WP_REST_Request $request Incoming request. * @param bool $updating Whether an existing post is the target. * @param bool $global_export Whether this is a global export route. * * @return WP_REST_Response What became of the story. */ private function import( $request, bool $updating, bool $global_export ): WP_REST_Response { $payload = StoryPayload::from( $request->get_json_params() ); if ( $payload->is_empty() ) { return Response::text( 'Error: Empty JSON request data', 400 ); } if ( ! $updating ) { return $this->respond( $this->importer->create( $payload, $global_export ), $global_export ); } $post_id = (int) $request->get_param( 'post_id' ); $rejected = $this->reject_unusable_post( $post_id, $global_export ); if ( null !== $rejected ) { return $rejected; } $payload = $payload->with_post_id( $post_id ); // A public post keeps its slug so inbound links do not break. if ( $this->is_public( $post_id ) ) { $payload = $payload->without_slug(); } return $this->respond( $this->importer->update( $payload, $global_export ), $global_export ); } /** * Turning an outcome into the response Kordiam expects. * * @param ImportOutcome $outcome What happened. * @param bool $global_export Whether this is a global export route. * * @return WP_REST_Response The response. */ private function respond( ImportOutcome $outcome, bool $global_export ): WP_REST_Response { switch ( $outcome->result ) { case ImportResult::SKIPPED: return Response::accepted_no_action(); case ImportResult::REJECTED: return Response::text( $outcome->message, 428 ); case ImportResult::FAILED: return Response::text( $outcome->message, 500 ); case ImportResult::REMOVED: case ImportResult::SUCCEEDED: return $this->created_payload( $outcome->post_id, $global_export ); } } /** * Building the payload Kordiam records against a story. * * @param int $post_id Post ID. * @param bool $global_export Whether this is a global export route. * * @return WP_REST_Response The response. */ private function created_payload( int $post_id, bool $global_export ): WP_REST_Response { if ( $global_export ) { return Response::created( array( 'id' => $post_id ) ); } $edit_link = get_edit_post_link( $post_id, 'raw' ); if ( ! is_string( $edit_link ) || '' === $edit_link ) { $edit_link = admin_url( 'post.php?post=' . $post_id . '&action=edit' ); } $permalink = get_permalink( $post_id ); return Response::created( array( 'id' => $post_id, 'cmsEditLink' => $edit_link, 'cmsOpenLink' => is_string( $permalink ) ? $permalink : '', ) ); } /** * Refusing a request whose target post cannot be used. * * A story already removed in Kordiam deliberately answers 200, not an error. * * @param int $post_id Post ID from the route. * @param bool $global_export Whether this is a global export route. * * @return WP_REST_Response|null A refusal, or null to carry on. */ private function reject_unusable_post( int $post_id, bool $global_export ): ?WP_REST_Response { if ( $post_id <= 0 ) { return Response::text( '', 406 ); } if ( null === get_post( $post_id ) ) { return Response::text( '', 404 ); } if ( ! $global_export && StoryMetaWriter::is_removed( $post_id ) ) { return Response::text( 'Post was removed by Kordiam in previous user actions on the Kordiam side', 200 ); } return null; } /** * Checking whether a post is already visible to readers. * * @param int $post_id Post ID. * * @return bool Whether the post is published or scheduled. */ private function is_public( int $post_id ): bool { $status = get_post_status( $post_id ); return 'publish' === $status || 'future' === $status; } /** * Describing the post_id route parameter. * * @return array> Argument definition. */ private function post_id_arg(): array { return array( 'post_id' => array( 'type' => 'integer', 'required' => true, 'sanitize_callback' => 'absint', ), ); } }