array( 'name' => __( 'AcquisitionSuite Files', 'acquisitionsuite' ), 'singular_name' => __( 'AcquisitionSuite File', 'acquisitionsuite' ), 'add_new' => __( 'Upload New File', 'acquisitionsuite' ), 'add_new_item' => __( 'Upload New File', 'acquisitionsuite' ), 'edit_item' => __( 'Edit File', 'acquisitionsuite' ), 'view_item' => __( 'View File', 'acquisitionsuite' ), 'search_items' => __( 'Search Files', 'acquisitionsuite' ), 'not_found' => __( 'No files found.', 'acquisitionsuite' ), 'not_found_in_trash' => __( 'No files found in trash.', 'acquisitionsuite' ), ), 'public' => false, 'show_ui' => true, 'show_in_menu' => false, // We use our own menu 'capability_type' => 'post', 'capabilities' => array( 'edit_post' => 'manage_options', 'read_post' => 'manage_options', 'delete_post' => 'manage_options', 'edit_posts' => 'manage_options', 'edit_others_posts' => 'manage_options', 'delete_posts' => 'manage_options', 'publish_posts' => 'manage_options', 'read_private_posts' => 'manage_options', ), 'supports' => array( 'title' ), 'rewrite' => false, ) ); } /** * Return the allowed MIME types. */ public static function allowed_mime_types() { return array( 'pdf' => 'application/pdf', 'zip' => 'application/zip', 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'mp4' => 'video/mp4', 'mp3' => 'audio/mpeg', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', ); } /** * Handle file upload and create the post. * * @param array $file $_FILES element * @param string $title Human-readable title * @return int|WP_Error Post ID or error */ public static function handle_upload( $file, $title ) { if ( ! function_exists( 'wp_handle_upload' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } // Validate extension $ext = strtolower( pathinfo( $file['name'], PATHINFO_EXTENSION ) ); $allowed = get_option( 'acquis_allowed_types', array_keys( self::allowed_mime_types() ) ); if ( ! in_array( $ext, (array) $allowed, true ) ) { return new WP_Error( 'acquis_invalid_type', __( 'File type not allowed.', 'acquisitionsuite' ) ); } // Move to protected directory $upload_dir = wp_upload_dir(); // Normalize to forward slashes before this touches the DB: WP core's // update_post_meta()/add_metadata() always calls wp_unslash() on // string values (it assumes callers pass raw $_POST-style data, // which WP pre-slashes) any literal backslash here would be // silently stripped (not converted), corrupting the stored path. // basedir is usually already forward-slashed, but isn't guaranteed // on every host (e.g. some Windows/IIS setups), so normalize // explicitly rather than relying on that. $protect_dir = wp_normalize_path( $upload_dir['basedir'] . '/acquis-protected' ); $unique_name = wp_unique_filename( $protect_dir, sanitize_file_name( $file['name'] ) ); $destination = $protect_dir . '/' . $unique_name; // Ensure the protected directory itself still exists and has its // access-denial files in place recreate them if anything has been // deleted since activation (migrations, security scans, etc.). if ( ! is_dir( $protect_dir ) ) { wp_mkdir_p( $protect_dir ); } if ( ! file_exists( $protect_dir . '/.htaccess' ) ) { // WP Filesystem API per WP.org Guideline 7 global $wp_filesystem; if ( ! function_exists( 'WP_Filesystem' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } WP_Filesystem(); $wp_filesystem->put_contents( $protect_dir . '/.htaccess', "Options -Indexes Deny from all ", FS_CHMOD_FILE ); } if ( ! file_exists( $protect_dir . '/index.php' ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents global $wp_filesystem; if ( empty( $wp_filesystem ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; WP_Filesystem(); } $wp_filesystem->put_contents( $protect_dir . '/index.php', ' false, 'unique_filename_callback' => null ); $movefile = wp_handle_upload( $file, $overrides ); if ( isset( $movefile['error'] ) ) { return new WP_Error( 'acquis_upload_error', $movefile['error'] ); } // Re-move from default uploads to protected dir $tmp_path = $movefile['file']; global $wp_filesystem; if ( empty( $wp_filesystem ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; WP_Filesystem(); } if ( ! $wp_filesystem->move( $tmp_path, $destination, true ) ) { wp_delete_file( $tmp_path ); return new WP_Error( 'acquis_move_error', __( 'Could not move file to protected directory.', 'acquisitionsuite' ) ); } // Create post $post_id = wp_insert_post( array( 'post_title' => sanitize_text_field( $title ), 'post_type' => self::POST_TYPE, 'post_status' => 'publish', ) ); if ( is_wp_error( $post_id ) ) { wp_delete_file( $destination ); return $post_id; } // Store metadata update_post_meta( $post_id, '_acquis_file_path', $destination ); update_post_meta( $post_id, '_acquis_file_name', $unique_name ); update_post_meta( $post_id, '_acquis_file_ext', $ext ); update_post_meta( $post_id, '_acquis_file_size', filesize( $destination ) ); update_post_meta( $post_id, '_acquis_mime_type', $movefile['type'] ); return $post_id; } /** * Create a gated "file" entry backed by an external URL (Google Drive, * Dropbox, etc.) instead of an uploaded file no protected-directory * handling applies since there's no local file to guard. * * @param string $url * @param string $title * @return int|WP_Error Post ID or error */ public static function handle_url_file( $url, $title ) { $url = esc_url_raw( trim( $url ) ); if ( '' === $url || ! wp_http_validate_url( $url ) ) { return new WP_Error( 'acquis_invalid_url', __( 'Please enter a valid http(s) URL.', 'acquisitionsuite' ) ); } $post_id = wp_insert_post( array( 'post_title' => sanitize_text_field( $title ), 'post_type' => self::POST_TYPE, 'post_status' => 'publish', ) ); if ( is_wp_error( $post_id ) ) { return $post_id; } update_post_meta( $post_id, '_acquis_file_source_type', 'url' ); update_post_meta( $post_id, '_acquis_file_url', $url ); return $post_id; } /** * Get file metadata by post ID. * * @param int $file_id * @return array|false */ /** * All uploaded gated files, most recent first used to power file * picker dropdowns (e.g. the form builder's shortcode helper) so * admins never have to manually look up or type a numeric file ID. * * @return array */ public static function get_all_files() { $posts = get_posts( array( 'post_type' => self::POST_TYPE, 'posts_per_page' => -1, 'orderby' => 'date', 'order' => 'DESC', 'post_status' => 'publish', ) ); return array_map( function( $post ) { $source_type = get_post_meta( $post->ID, '_acquis_file_source_type', true ); return array( 'id' => $post->ID, 'title' => $post->post_title, 'ext' => strtoupper( ( get_post_meta( $post->ID, '_acquis_file_ext', true ) ?: get_post_meta( $post->ID, '_sll_file_ext', true ) ) ), 'source_type' => in_array( $source_type, array( 'upload', 'url' ), true ) ? $source_type : 'upload', ); }, $posts ); } public static function get_file_data( $file_id ) { $post = get_post( absint( $file_id ) ); if ( ! $post || $post->post_type !== self::POST_TYPE ) { return false; } $source_type = get_post_meta( $post->ID, '_acquis_file_source_type', true ); return array( 'id' => $post->ID, 'title' => $post->post_title, 'source_type' => in_array( $source_type, array( 'upload', 'url' ), true ) ? $source_type : 'upload', 'url' => get_post_meta( $post->ID, '_acquis_file_url', true ), 'path' => ( get_post_meta( $post->ID, '_acquis_file_path', true ) ?: get_post_meta( $post->ID, '_sll_file_path', true ) ), 'name' => ( get_post_meta( $post->ID, '_acquis_file_name', true ) ?: get_post_meta( $post->ID, '_sll_file_name', true ) ), 'ext' => ( get_post_meta( $post->ID, '_acquis_file_ext', true ) ?: get_post_meta( $post->ID, '_sll_file_ext', true ) ), 'size' => ( get_post_meta( $post->ID, '_acquis_file_size', true ) ?: get_post_meta( $post->ID, '_sll_file_size', true ) ), 'mime_type' => ( get_post_meta( $post->ID, '_acquis_mime_type', true ) ?: get_post_meta( $post->ID, '_sll_mime_type', true ) ), ); } /** * Format bytes to human-readable size. */ public static function format_bytes( $bytes ) { if ( $bytes >= 1073741824 ) return number_format( $bytes / 1073741824, 2 ) . ' GB'; if ( $bytes >= 1048576 ) return number_format( $bytes / 1048576, 2 ) . ' MB'; if ( $bytes >= 1024 ) return number_format( $bytes / 1024, 2 ) . ' KB'; return $bytes . ' B'; } }