Draftdrop). * * @package Draftdrop */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Admin UI and request handlers. */ class Draftdrop_Admin { const SLUG = 'draftdrop'; const MAX_FILES = 20; const MAX_FILE_BYTES = 1048576; // 1 MB per file. const MAX_TOTAL_BYTES = 4194304; // 4 MB per batch. /** * Register hooks. * * @return void */ public static function init() { add_action( 'admin_menu', array( __CLASS__, 'menu' ) ); add_action( 'admin_post_draftdrop_preview', array( __CLASS__, 'handle_preview' ) ); add_action( 'admin_post_draftdrop_import', array( __CLASS__, 'handle_import' ) ); } /** * Add Tools > Draftdrop. * * @return void */ public static function menu() { add_management_page( __( 'Draftdrop', 'draftdrop' ), __( 'Draftdrop', 'draftdrop' ), 'edit_posts', self::SLUG, array( __CLASS__, 'render' ) ); } /** * Whether the current user may use the importer. * * @return bool */ private static function allowed() { return current_user_can( 'edit_posts' ) && current_user_can( 'upload_files' ); } /** * Redirect back to the screen. * * @param array $args Query args. * @return void */ private static function redirect( array $args ) { $args['page'] = self::SLUG; wp_safe_redirect( add_query_arg( $args, admin_url( 'tools.php' ) ) ); exit; } /** * Clean a token from the request. * * @param mixed $value Raw value. * @return string */ private static function token( $value ) { return preg_replace( '/[^A-Za-z0-9]/', '', (string) $value ); } /** * Step 1: read uploaded files and store them for the preview. * * @return void */ public static function handle_preview() { if ( ! self::allowed() ) { wp_die( esc_html__( 'You are not allowed to do this.', 'draftdrop' ), '', array( 'response' => 403 ) ); } check_admin_referer( 'draftdrop_preview' ); $files = self::read_uploads(); if ( is_wp_error( $files ) ) { self::redirect( array( 'draftdrop_error' => $files->get_error_code() ) ); } $token = wp_generate_password( 16, false ); set_transient( 'draftdrop_p_' . $token, array( 'user' => get_current_user_id(), 'files' => $files, ), HOUR_IN_SECONDS ); self::redirect( array( 'draftdrop_preview' => $token ) ); } /** * Read and validate uploaded files (they are never stored on disk by us). * * @return array|WP_Error List of array( name, raw ). */ private static function read_uploads() { // The nonce is verified in handle_preview(), the only caller. // phpcs:disable WordPress.Security.NonceVerification.Missing if ( ! isset( $_FILES['draftdrop_files']['name'], $_FILES['draftdrop_files']['tmp_name'], $_FILES['draftdrop_files']['error'] ) ) { return new WP_Error( 'no_files' ); } if ( ! is_array( $_FILES['draftdrop_files']['name'] ) || ! is_array( $_FILES['draftdrop_files']['tmp_name'] ) || ! is_array( $_FILES['draftdrop_files']['error'] ) ) { return new WP_Error( 'no_files' ); } // Names are sanitized as file names. Temp paths are generated by PHP and are only ever used after is_uploaded_file(). $names = array_map( 'sanitize_file_name', $_FILES['draftdrop_files']['name'] ); $tmps = array_map( 'sanitize_text_field', $_FILES['draftdrop_files']['tmp_name'] ); $errors = array_map( 'absint', $_FILES['draftdrop_files']['error'] ); // phpcs:enable WordPress.Security.NonceVerification.Missing if ( count( $names ) > self::MAX_FILES ) { return new WP_Error( 'too_many' ); } $files = array(); $total = 0; foreach ( $names as $i => $name ) { $error = isset( $errors[ $i ] ) ? (int) $errors[ $i ] : UPLOAD_ERR_NO_FILE; if ( UPLOAD_ERR_NO_FILE === $error ) { continue; } if ( UPLOAD_ERR_OK !== $error ) { return new WP_Error( 'upload_failed' ); } $name = (string) $name; $ext = strtolower( pathinfo( $name, PATHINFO_EXTENSION ) ); if ( ! in_array( $ext, array( 'md', 'markdown' ), true ) ) { return new WP_Error( 'bad_type' ); } $tmp = isset( $tmps[ $i ] ) ? (string) $tmps[ $i ] : ''; if ( '' === $tmp || ! is_uploaded_file( $tmp ) ) { return new WP_Error( 'upload_failed' ); } $size = filesize( $tmp ); if ( false === $size || $size > self::MAX_FILE_BYTES ) { return new WP_Error( 'too_big' ); } $total += $size; if ( $total > self::MAX_TOTAL_BYTES ) { return new WP_Error( 'too_big' ); } $raw = file_get_contents( $tmp ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- reading a local upload temp file. if ( false === $raw ) { return new WP_Error( 'upload_failed' ); } $files[] = array( 'name' => $name, 'raw' => $raw, ); } if ( empty( $files ) ) { return new WP_Error( 'no_files' ); } return $files; } /** * Step 2: create posts for the selected files. * * @return void */ public static function handle_import() { if ( ! self::allowed() ) { wp_die( esc_html__( 'You are not allowed to do this.', 'draftdrop' ), '', array( 'response' => 403 ) ); } check_admin_referer( 'draftdrop_import' ); $token = isset( $_POST['token'] ) ? self::token( sanitize_text_field( wp_unslash( $_POST['token'] ) ) ) : ''; $data = get_transient( 'draftdrop_p_' . $token ); if ( ! is_array( $data ) || get_current_user_id() !== (int) $data['user'] ) { self::redirect( array( 'draftdrop_error' => 'expired' ) ); } $selected = isset( $_POST['selected'] ) ? array_map( 'absint', (array) wp_unslash( $_POST['selected'] ) ) : array(); if ( function_exists( 'wp_raise_memory_limit' ) ) { wp_raise_memory_limit( 'admin' ); } $results = array(); foreach ( $selected as $index ) { if ( ! isset( $data['files'][ $index ] ) ) { continue; } $file = $data['files'][ $index ]; $item = Draftdrop_Importer::prepare( $file['raw'], $file['name'] ); if ( ! empty( $item['errors'] ) ) { $results[] = array( 'title' => '' !== $item['title'] ? $item['title'] : $file['name'], 'post_id' => 0, 'status' => '', 'notes' => $item['errors'], 'error' => true, ); continue; } $created = Draftdrop_Importer::create( $item ); $results[] = array( 'title' => $item['title'], 'post_id' => $created['post_id'], 'status' => $item['status'], 'notes' => array_merge( $item['notes'], $created['notes'], '' !== $created['error'] ? array( $created['error'] ) : array() ), 'error' => '' !== $created['error'], ); } delete_transient( 'draftdrop_p_' . $token ); set_transient( 'draftdrop_r_' . $token, array( 'user' => get_current_user_id(), 'results' => $results, ), HOUR_IN_SECONDS ); self::redirect( array( 'draftdrop_done' => $token ) ); } /** * Screen router. * * @return void */ public static function render() { if ( ! self::allowed() ) { wp_die( esc_html__( 'You are not allowed to access this page.', 'draftdrop' ), '', array( 'response' => 403 ) ); } // phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only screen routing; every write is nonce-protected. $preview = isset( $_GET['draftdrop_preview'] ) ? self::token( sanitize_text_field( wp_unslash( $_GET['draftdrop_preview'] ) ) ) : ''; $done = isset( $_GET['draftdrop_done'] ) ? self::token( sanitize_text_field( wp_unslash( $_GET['draftdrop_done'] ) ) ) : ''; $error = isset( $_GET['draftdrop_error'] ) ? sanitize_key( wp_unslash( $_GET['draftdrop_error'] ) ) : ''; // phpcs:enable echo '
' . esc_html( self::error_message( $error ) ) . '
' . esc_html( self::error_message( 'expired' ) ) . '
'; self::render_upload(); return; } if ( ! defined( 'WPSEO_VERSION' ) ) { echo '' . esc_html__( 'Yoast SEO was not detected. SEO fields will still be saved and will be used once it is active.', 'draftdrop' ) . '
' . esc_html( self::error_message( 'expired' ) ) . '
'; self::render_upload(); return; } ?>