importer = $importer ?? new AttachmentImporter(); $this->resolver = $resolver ?? new AttachmentResolver(); } /** * Registering the file routes. */ protected function routes(): array { $args = array( 'type' => 'string', 'required' => false, 'validate_callback' => static fn ( $value ): bool => is_string( $value ), ); return array( '/files' => array( 'methods' => 'POST', 'callback' => array( $this, 'store' ), 'permission_callback' => array( $this, 'require_token' ), 'args' => array( 'file' => $args, 'filename' => $args, ), ), '/files/(?P\d+)' => array( 'methods' => 'PUT', 'callback' => array( $this, 'replace' ), 'permission_callback' => array( $this, 'require_token' ), 'args' => array( 'cms_id' => array( 'type' => 'integer', 'required' => true, 'sanitize_callback' => 'absint', ), 'file' => $args, 'filename' => $args, ), ), ); } /** * Storing a file Kordiam has sent. * * @param WP_REST_Request>|WP_REST_Request $request Incoming request. * * @return WP_REST_Response The stored attachment, or the declined marker. */ public function store( $request ): WP_REST_Response { $filename = $this->filename_from( $request ); $bytes = $this->bytes_from( $request ); if ( '' === $filename || '' === $bytes ) { return Response::ok( $this->declined() ); } if ( $this->is_too_large( $bytes ) || ! $this->is_type_allowed( $filename ) ) { return Response::ok( $this->declined() ); } $attachment_id = $this->importer->import( $bytes, $filename ); if ( 0 === $attachment_id ) { return Response::message( __( 'Could not store the file', 'kordiam' ), 500 ); } return Response::created( $this->resolver->link_payload( $attachment_id ) ); } /** * Answering a request to replace a file already recorded against a story. * * Reports the existing attachment if it still exists under the same name; otherwise stores afresh. * * @param WP_REST_Request>|WP_REST_Request $request Incoming request. * * @return WP_REST_Response The attachment, or the declined marker. */ public function replace( $request ): WP_REST_Response { $cms_id = (int) $request->get_param( 'cms_id' ); $filename = $this->filename_from( $request ); if ( $cms_id > 0 && Config::REJECTED_FILE_ID !== $cms_id ) { $title = $this->resolver->title( $cms_id ); if ( '' !== $title && Str::without_extension( $filename ) === $title ) { return Response::created( $this->resolver->link_payload( $cms_id ) ); } } return $this->store( $request ); } /** * Reading the filename off a request. * * @param WP_REST_Request>|WP_REST_Request $request Incoming request. * * @return string The filename, or an empty string when absent. */ private function filename_from( $request ): string { $filename = $request->get_param( 'filename' ); return is_string( $filename ) ? trim( $filename ) : ''; } /** * Reading the file contents off a request. * * @param WP_REST_Request>|WP_REST_Request $request Incoming request. * * @return string The contents, or an empty string when absent. */ private function bytes_from( $request ): string { $bytes = $request->get_param( 'file' ); return is_string( $bytes ) ? $bytes : ''; } /** * Checking a payload against the site's upload limit. * * Measures the payload itself, not the client-supplied Content-Length. * * @param string $bytes File contents. * * @return bool Whether the file exceeds the limit. */ private function is_too_large( string $bytes ): bool { $limit = (int) wp_max_upload_size(); return 0 < $limit && strlen( $bytes ) > $limit; } /** * Checking whether WordPress will accept a filename's type. * * Uses the site's allowed MIME types, which is also what blocks a .php upload. * * @param string $filename Filename as Kordiam sent it. * * @return bool Whether the type is allowed. */ private function is_type_allowed( string $filename ): bool { $file_type = wp_check_filetype( Str::safe_file_name( $filename ) ); return is_array( $file_type ) && ! empty( $file_type['ext'] ) && ! empty( $file_type['type'] ); } /** * Building the payload that tells Kordiam the file was not stored. * * @return array{id: int} The payload. */ private function declined(): array { return array( 'id' => Config::REJECTED_FILE_ID ); } }