(string) $filename, 'errors' => array(), 'warnings' => array(), 'notes' => array(), 'title' => '', 'slug' => '', 'status' => 'draft', 'excerpt' => '', 'categories' => array(), 'tags' => array(), 'seo_title' => '', 'meta_description' => '', 'focus_keyword' => '', 'featured_image' => '', 'image_alt' => '', 'content' => '', 'hash' => sha1( $raw ), 'duplicate' => 0, ); $valid_utf8 = function_exists( 'wp_is_valid_utf8' ) ? wp_is_valid_utf8( $raw ) : seems_utf8( $raw ); // phpcs:ignore WordPress.WP.DeprecatedFunctions.seems_utf8Found -- only used on WordPress older than 6.9. if ( ! $valid_utf8 ) { $item['errors'][] = __( 'The file is not valid UTF-8.', 'draftdrop' ); return $item; } $parsed = Draftdrop_Frontmatter::parse( $raw ); $meta = $parsed['meta']; if ( in_array( 'no_front_matter', $parsed['errors'], true ) ) { $item['errors'][] = __( 'No front matter found. The file must start with a line of three dashes.', 'draftdrop' ); return $item; } if ( in_array( 'bad_line', $parsed['errors'], true ) ) { $item['warnings'][] = __( 'Some front matter lines could not be read and were ignored.', 'draftdrop' ); } $item['title'] = self::text( isset( $meta['title'] ) ? $meta['title'] : '' ); if ( '' === $item['title'] ) { $item['errors'][] = __( 'The title is missing.', 'draftdrop' ); } $slug_source = isset( $meta['slug'] ) ? self::text( $meta['slug'] ) : ''; $item['slug'] = sanitize_title( '' !== $slug_source ? $slug_source : $item['title'] ); if ( '' === $item['slug'] && '' !== $item['title'] ) { $item['errors'][] = __( 'A valid slug could not be created.', 'draftdrop' ); } $status = isset( $meta['status'] ) ? strtolower( self::text( $meta['status'] ) ) : 'draft'; if ( '' === $status ) { $status = 'draft'; } if ( ! in_array( $status, array( 'draft', 'pending' ), true ) ) { /* translators: %s: status given in the file. */ $item['warnings'][] = sprintf( __( 'Status "%s" is not allowed in this version. Saved as draft.', 'draftdrop' ), $status ); $status = 'draft'; } $item['status'] = $status; $item['excerpt'] = isset( $meta['excerpt'] ) ? sanitize_textarea_field( is_array( $meta['excerpt'] ) ? implode( ' ', $meta['excerpt'] ) : (string) $meta['excerpt'] ) : ''; $cats = isset( $meta['categories'] ) ? $meta['categories'] : ( isset( $meta['category'] ) ? $meta['category'] : array() ); $item['categories'] = self::terms( $cats, false ); $item['tags'] = self::terms( isset( $meta['tags'] ) ? $meta['tags'] : array(), true ); $item['seo_title'] = self::text( isset( $meta['seo_title'] ) ? $meta['seo_title'] : '' ); $item['meta_description'] = self::text( isset( $meta['meta_description'] ) ? $meta['meta_description'] : '' ); $item['focus_keyword'] = self::text( isset( $meta['focus_keyword'] ) ? $meta['focus_keyword'] : '' ); $item['image_alt'] = self::text( isset( $meta['image_alt'] ) ? $meta['image_alt'] : '' ); if ( '' === $item['meta_description'] ) { $item['warnings'][] = __( 'No meta description.', 'draftdrop' ); } elseif ( self::length( $item['meta_description'] ) > 160 ) { /* translators: %d: number of characters. */ $item['warnings'][] = sprintf( __( 'Meta description is %d characters (about 160 recommended).', 'draftdrop' ), self::length( $item['meta_description'] ) ); } if ( '' !== $item['seo_title'] && self::length( $item['seo_title'] ) > 60 ) { /* translators: %d: number of characters. */ $item['warnings'][] = sprintf( __( 'SEO title is %d characters (about 60 recommended).', 'draftdrop' ), self::length( $item['seo_title'] ) ); } if ( '' === $item['focus_keyword'] ) { $item['warnings'][] = __( 'No focus keyphrase.', 'draftdrop' ); } $image = isset( $meta['featured_image'] ) ? esc_url_raw( self::text( $meta['featured_image'] ) ) : ''; if ( '' !== $image ) { if ( wp_http_validate_url( $image ) ) { $item['featured_image'] = $image; } else { $item['warnings'][] = __( 'The featured image URL is not valid and will be skipped.', 'draftdrop' ); } } // Body: Markdown to HTML, sanitize, then to blocks. $markdown = ltrim( $parsed['body'], "\n" ); $parsedown = new Draftdrop_Parsedown(); $html = $parsedown->text( $markdown ); $cleaned = Draftdrop_Sanitizer::clean( $html ); if ( ! empty( $cleaned['removed'] ) ) { /* translators: %s: list of HTML tags, for example "script, iframe". */ $item['warnings'][] = sprintf( __( 'Unsafe HTML was removed: %s.', 'draftdrop' ), implode( ', ', $cleaned['removed'] ) ); } $html = wp_kses_post( $cleaned['html'] ); if ( preg_match( '#^\s*]*>.*?\s*#is', $html ) ) { $html = preg_replace( '#^\s*]*>.*?\s*#is', '', $html, 1 ); $item['notes'][] = __( 'Leading H1 removed (the title comes from front matter).', 'draftdrop' ); } if ( '' === trim( wp_strip_all_tags( $html ) ) && false === stripos( $html, ' 0, 'notes' => array(), 'error' => '', ); if ( ! current_user_can( 'edit_posts' ) ) { $result['error'] = __( 'You are not allowed to create posts.', 'draftdrop' ); return $result; } $meta = array( self::HASH_META => $item['hash'] ); // Written at insert time so the SEO plugin indexes the values immediately. if ( '' !== $item['seo_title'] ) { $meta['_yoast_wpseo_title'] = $item['seo_title']; } if ( '' !== $item['meta_description'] ) { $meta['_yoast_wpseo_metadesc'] = $item['meta_description']; } if ( '' !== $item['focus_keyword'] ) { $meta['_yoast_wpseo_focuskw'] = $item['focus_keyword']; } $postarr = array( 'post_type' => 'post', 'post_status' => $item['status'], 'post_title' => $item['title'], 'post_name' => $item['slug'], 'post_excerpt' => $item['excerpt'], 'post_content' => $item['content'], 'post_author' => get_current_user_id(), 'meta_input' => $meta, ); $cat_ids = self::resolve_categories( $item['categories'], $result['notes'] ); if ( ! empty( $cat_ids ) ) { $postarr['post_category'] = $cat_ids; } if ( ! empty( $item['tags'] ) ) { $postarr['tags_input'] = $item['tags']; } $post_id = wp_insert_post( wp_slash( $postarr ), true ); if ( is_wp_error( $post_id ) ) { $result['error'] = $post_id->get_error_message(); return $result; } $result['post_id'] = (int) $post_id; if ( '' !== $item['featured_image'] ) { $image = self::sideload_featured_image( $post_id, $item['featured_image'], $item['image_alt'], $item['title'] ); if ( is_wp_error( $image ) ) { /* translators: %s: error message. */ $result['notes'][] = sprintf( __( 'Featured image skipped: %s', 'draftdrop' ), $image->get_error_message() ); } } return $result; } /** * Look up an existing post with the same content hash or slug. * * @param string $slug Post slug. * @param string $hash Content hash. * @return int Post ID or 0. */ private static function find_duplicate( $slug, $hash ) { $by_hash = get_posts( array( 'post_type' => 'post', 'post_status' => 'any', 'meta_key' => self::HASH_META, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_value' => $hash, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value 'posts_per_page' => 1, 'fields' => 'ids', 'no_found_rows' => true, ) ); if ( ! empty( $by_hash ) ) { return (int) $by_hash[0]; } $by_slug = get_posts( array( 'post_type' => 'post', 'post_status' => 'any', 'name' => $slug, 'posts_per_page' => 1, 'fields' => 'ids', 'no_found_rows' => true, ) ); return empty( $by_slug ) ? 0 : (int) $by_slug[0]; } /** * Resolve category names to IDs, creating missing ones when allowed. * * @param string[] $names Category names or slugs. * @param string[] $notes Notes (by reference). * @return int[] */ private static function resolve_categories( array $names, array &$notes ) { $ids = array(); foreach ( $names as $name ) { $term = get_term_by( 'name', $name, 'category' ); if ( ! $term ) { $term = get_term_by( 'slug', sanitize_title( $name ), 'category' ); } if ( $term ) { $ids[] = (int) $term->term_id; continue; } if ( current_user_can( 'manage_categories' ) ) { $created = wp_insert_term( $name, 'category' ); if ( ! is_wp_error( $created ) ) { $ids[] = (int) $created['term_id']; /* translators: %s: category name. */ $notes[] = sprintf( __( 'Category "%s" was created.', 'draftdrop' ), $name ); continue; } } /* translators: %s: category name. */ $notes[] = sprintf( __( 'Category "%s" does not exist and was not assigned.', 'draftdrop' ), $name ); } return array_values( array_unique( $ids ) ); } /** * Download an image and set it as the featured image. * * @param int $post_id Post ID. * @param string $url Image URL (http/https, validated). * @param string $alt Alt text. * @param string $title Attachment title. * @return true|WP_Error */ private static function sideload_featured_image( $post_id, $url, $alt, $title ) { if ( ! current_user_can( 'upload_files' ) ) { return new WP_Error( 'draftdrop_cap', __( 'You are not allowed to upload files.', 'draftdrop' ) ); } require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; $tmp = download_url( $url, 30 ); // Uses wp_safe_remote_get(): private and loopback hosts are refused. if ( is_wp_error( $tmp ) ) { return $tmp; } $size = filesize( $tmp ); if ( false === $size || $size > self::MAX_IMAGE_BYTES ) { wp_delete_file( $tmp ); return new WP_Error( 'draftdrop_size', __( 'The image is larger than 8 MB.', 'draftdrop' ) ); } $info = wp_getimagesize( $tmp ); $extensions = array( 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp', ); if ( ! is_array( $info ) || empty( $info['mime'] ) || ! isset( $extensions[ $info['mime'] ] ) ) { wp_delete_file( $tmp ); return new WP_Error( 'draftdrop_type', __( 'Only JPEG, PNG, GIF and WebP images are accepted.', 'draftdrop' ) ); } $base = sanitize_file_name( pathinfo( (string) wp_parse_url( $url, PHP_URL_PATH ), PATHINFO_FILENAME ) ); if ( '' === $base ) { $base = 'draftdrop-image'; } $file_array = array( 'name' => $base . '.' . $extensions[ $info['mime'] ], 'tmp_name' => $tmp, ); $attachment_id = media_handle_sideload( $file_array, $post_id, $title ); if ( is_wp_error( $attachment_id ) ) { wp_delete_file( $tmp ); return $attachment_id; } if ( '' !== $alt ) { update_post_meta( $attachment_id, '_wp_attachment_image_alt', $alt ); } set_post_thumbnail( $post_id, $attachment_id ); return true; } /** * Sanitize a scalar (or join a list) into plain text. * * @param mixed $value Value. * @return string */ private static function text( $value ) { if ( is_array( $value ) ) { $value = implode( ', ', $value ); } return sanitize_text_field( (string) $value ); } /** * Normalise a list of terms. * * @param mixed $value String or list. * @param bool $split_commas Split a plain string on commas. * @return string[] */ private static function terms( $value, $split_commas ) { if ( is_string( $value ) ) { $value = $split_commas ? explode( ',', $value ) : array( $value ); } if ( ! is_array( $value ) ) { return array(); } $out = array(); foreach ( $value as $term ) { $term = sanitize_text_field( (string) $term ); if ( '' !== $term ) { $out[] = $term; } } return array_values( array_unique( $out ) ); } /** * Character length that is safe for multibyte text. * * @param string $s String. * @return int */ private static function length( $s ) { return function_exists( 'mb_strlen' ) ? mb_strlen( $s, 'UTF-8' ) : strlen( $s ); } }