'POST', 'callback' => array( __CLASS__, 'check' ), 'permission_callback' => self::can( 'dropdms_upload' ), 'args' => array( 'hash' => array( 'type' => 'string', 'required' => true, 'pattern' => '^[A-Fa-f0-9]{64}$', ), ), ) ); register_rest_route( self::NS, '/upload', array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'upload' ), 'permission_callback' => self::can( 'dropdms_upload' ), ) ); register_rest_route( self::NS, '/documents/(?P\d+)/reocr', array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'reocr' ), 'permission_callback' => self::can( 'dropdms_edit' ), ) ); register_rest_route( self::NS, '/documents/(?P\d+)/file', array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'file' ), 'permission_callback' => self::can( 'dropdms_view' ), ) ); register_rest_route( self::NS, '/documents/(?P\d+)/thumb', array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'thumb' ), 'permission_callback' => self::can( 'dropdms_view' ), ) ); } /** * Permission callback for a capability. * * @param string $cap Capability. * @return callable */ private static function can( $cap ) { return static function () use ( $cap ) { return current_user_can( $cap ); }; } /** * Checks by SHA-256 whether a document already exists (before the OCR runs in the browser). * * @param WP_REST_Request $request Request. */ public static function check( WP_REST_Request $request ) { $existing = DropDMS_Documents::find_by_hash( strtolower( (string) $request['hash'] ) ); return array( 'success' => true, 'exists' => null !== $existing, 'id' => $existing ? (int) $existing->id : 0, 'title' => $existing ? $existing->title : '', ); } /** * Receives a file together with the text recognized in the browser. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error */ public static function upload( WP_REST_Request $request ) { $files = $request->get_file_params(); $file = $files['file'] ?? null; if ( ! is_array( $file ) || UPLOAD_ERR_OK !== ( $file['error'] ?? UPLOAD_ERR_NO_FILE ) ) { $error = is_array( $file ) ? (int) ( $file['error'] ?? 0 ) : 0; // Request larger than post_max_size: PHP drops all form data. $length = isset( $_SERVER['CONTENT_LENGTH'] ) ? absint( $_SERVER['CONTENT_LENGTH'] ) : 0; if ( in_array( $error, array( UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE ), true ) || ( ! $file && $length > wp_convert_hr_to_bytes( (string) ini_get( 'post_max_size' ) ) ) ) { return self::error( 'dropdms_size', __( 'The file is too large.', 'dropdms' ), 413 ); } return self::error( 'dropdms_upload', __( 'The upload failed.', 'dropdms' ), 400 ); } $tmp = (string) $file['tmp_name']; if ( ! is_uploaded_file( $tmp ) ) { return self::error( 'dropdms_upload', __( 'The upload failed.', 'dropdms' ), 400 ); } $size = (int) filesize( $tmp ); if ( $size < 1 || $size > (int) DropDMS_Settings::get( 'max_upload_mb' ) * MB_IN_BYTES ) { return self::error( 'dropdms_size', __( 'The file is too large.', 'dropdms' ), 413 ); } $db_limit = DropDMS_Storage::max_file_size(); if ( $size > $db_limit ) { return self::error( 'dropdms_packet', /* translators: %s: file size, e.g. "16 MB" */ sprintf( __( 'The database accepts files up to %s (max_allowed_packet). Ask your host to raise this value.', 'dropdms' ), size_format( $db_limit ) ), 413 ); } $mime = self::mime( $tmp ); if ( ! isset( DropDMS_Documents::ALLOWED[ $mime ] ) ) { return self::error( 'dropdms_type', __( 'This file type is not allowed (PDF, JPG, PNG, WebP, GIF).', 'dropdms' ), 415 ); } // Duplicate check by content, not by file name. $hash = hash_file( 'sha256', $tmp ); $existing = DropDMS_Documents::find_by_hash( $hash ); if ( $existing ) { return rest_ensure_response( self::duplicate( $existing ) ); } $category = absint( $request->get_param( 'category' ) ); if ( ! DropDMS_Categories::exists( $category ) ) { $category = 0; } $ext = DropDMS_Documents::ALLOWED[ $mime ]; $name = DropDMS_Documents::clean_name( (string) ( $file['name'] ?? '' ), $ext ); $title = trim( pathinfo( $name, PATHINFO_FILENAME ) ); $text = DropDMS_Documents::clean_text( (string) $request->get_param( 'ocr_text' ) ); try { $id = DropDMS_Documents::create( array( 'title' => '' !== $title ? $title : $name, 'category_id' => $category, 'filename' => $name, 'mime' => $mime, 'filesize' => $size, 'sha256' => $hash, 'ocr_text' => $text, ), $tmp, self::read_thumb( $files ) ); } catch ( Throwable $e ) { // Concurrent upload of the same file: the unique index caught it. $existing = DropDMS_Documents::find_by_hash( $hash ); if ( $existing ) { return rest_ensure_response( self::duplicate( $existing ) ); } return self::error( 'dropdms_save', __( 'The document could not be saved.', 'dropdms' ) . ' (' . $e->getMessage() . ')', 500 ); } return rest_ensure_response( array( 'success' => true, 'id' => $id, 'title' => '' !== $title ? $title : $name, 'chars' => mb_strlen( $text ), ) ); } /** * Replaces the recognized text (and thumbnail) of an existing document. * * @param WP_REST_Request $request Request. * @return array|WP_Error */ public static function reocr( WP_REST_Request $request ) { $id = absint( $request['id'] ); $doc = DropDMS_Documents::get( $id ); if ( ! $doc ) { return self::error( 'dropdms_not_found', __( 'Document not found.', 'dropdms' ), 404 ); } $text = DropDMS_Documents::clean_text( (string) $request->get_param( 'ocr_text' ) ); $thumb = self::read_thumb( $request->get_file_params() ); if ( null !== $thumb ) { DropDMS_Storage::store_thumb( $id, $thumb ); } DropDMS_Documents::update( $id, array( 'ocr_text' => $text ) ); return array( 'success' => true, 'id' => $id, 'chars' => mb_strlen( $text ), 'text' => $text, ); } /** * Sends the stored file (inline=1 shows it in the browser). * * @param WP_REST_Request $request Request. * @return WP_Error|void */ public static function file( WP_REST_Request $request ) { $doc = DropDMS_Documents::get( absint( $request['id'] ) ); if ( ! $doc ) { return self::error( 'dropdms_not_found', __( 'Document not found.', 'dropdms' ), 404 ); } DropDMS_Files::stream( $doc, (bool) $request->get_param( 'inline' ) ); } /** * Sends the thumbnail or a placeholder. * * @param WP_REST_Request $request Request. * @return WP_Error|void */ public static function thumb( WP_REST_Request $request ) { $doc = DropDMS_Documents::get( absint( $request['id'] ) ); if ( ! $doc ) { return self::error( 'dropdms_not_found', __( 'Document not found.', 'dropdms' ), 404 ); } DropDMS_Files::thumb( (int) $doc->id, $doc->mime ); } /** * Reads the optional JPEG thumbnail created in the browser (max. 400 KB). * * @param array $files File parameters of the request. * @return string|null */ private static function read_thumb( array $files ) { $file = $files['thumb'] ?? null; if ( ! is_array( $file ) || UPLOAD_ERR_OK !== ( $file['error'] ?? UPLOAD_ERR_NO_FILE ) || ! is_uploaded_file( (string) $file['tmp_name'] ) ) { return null; } $tmp = (string) $file['tmp_name']; $size = (int) filesize( $tmp ); if ( $size < 1 || $size > self::MAX_THUMB || 'image/jpeg' !== self::mime( $tmp ) || false === wp_getimagesize( $tmp ) ) { return null; } $data = file_get_contents( $tmp ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local temp file. return false === $data ? null : $data; } /** * MIME type by content (finfo), not by the name or the browser. * * @param string $path File path. */ private static function mime( $path ) { $finfo = finfo_open( FILEINFO_MIME_TYPE ); $mime = $finfo ? (string) finfo_file( $finfo, $path ) : ''; if ( $finfo ) { finfo_close( $finfo ); } return $mime; } /** * Response for a file that is already in the archive. * * @param object $existing Existing document (id, title). */ private static function duplicate( $existing ) { return array( 'success' => false, 'duplicate' => true, 'id' => (int) $existing->id, 'title' => $existing->title, 'message' => __( 'Already in the archive', 'dropdms' ), ); } /** * Error with HTTP status. * * @param string $code Error code. * @param string $message Message. * @param int $status HTTP status. */ private static function error( $code, $message, $status ) { return new WP_Error( $code, $message, array( 'status' => $status ) ); } }