feat(media-folder-pro): add virtual folder system for WordPress media library
Custom WordPress plugin that replaces the default flat media library with a structured folder view. Features: hierarchical folders via custom taxonomy, sidebar folder tree, drag & drop, modal integration with Elementor/builders, bulk assign, upload auto-assign, toast notifications. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class MFP_Ajax_Handler {
|
||||
|
||||
private MFP_Taxonomy $taxonomy;
|
||||
|
||||
public function __construct( MFP_Taxonomy $taxonomy ) {
|
||||
$this->taxonomy = $taxonomy;
|
||||
}
|
||||
|
||||
public function register_hooks(): void {
|
||||
$actions = [
|
||||
'mfp_create_folder',
|
||||
'mfp_rename_folder',
|
||||
'mfp_delete_folder',
|
||||
'mfp_move_folder',
|
||||
'mfp_get_folders',
|
||||
'mfp_assign_media',
|
||||
'mfp_upload_to_folder',
|
||||
];
|
||||
|
||||
foreach ( $actions as $action ) {
|
||||
add_action( "wp_ajax_{$action}", [ $this, $action ] );
|
||||
}
|
||||
}
|
||||
|
||||
private function verify_request(): void {
|
||||
check_ajax_referer( 'mfp_nonce', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'upload_files' ) ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Brak uprawnień.', 'media-folder-pro' ) ], 403 );
|
||||
}
|
||||
}
|
||||
|
||||
public function mfp_create_folder(): void {
|
||||
$this->verify_request();
|
||||
|
||||
$name = sanitize_text_field( wp_unslash( $_POST['name'] ?? '' ) );
|
||||
$parent_id = (int) ( $_POST['parent_id'] ?? 0 );
|
||||
|
||||
if ( empty( $name ) ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Nazwa folderu jest wymagana.', 'media-folder-pro' ) ] );
|
||||
}
|
||||
|
||||
$result = wp_insert_term( $name, MFP_Taxonomy::TAXONOMY, [
|
||||
'parent' => $parent_id,
|
||||
] );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
wp_send_json_error( [ 'message' => $result->get_error_message() ] );
|
||||
}
|
||||
|
||||
$term = get_term( $result['term_id'], MFP_Taxonomy::TAXONOMY );
|
||||
|
||||
wp_send_json_success( [
|
||||
'folder' => [
|
||||
'id' => $term->term_id,
|
||||
'name' => $term->name,
|
||||
'slug' => $term->slug,
|
||||
'parent' => $term->parent,
|
||||
'count' => 0,
|
||||
'children' => [],
|
||||
],
|
||||
] );
|
||||
}
|
||||
|
||||
public function mfp_rename_folder(): void {
|
||||
$this->verify_request();
|
||||
|
||||
$folder_id = (int) ( $_POST['folder_id'] ?? 0 );
|
||||
$name = sanitize_text_field( wp_unslash( $_POST['name'] ?? '' ) );
|
||||
|
||||
if ( ! $folder_id || empty( $name ) ) {
|
||||
wp_send_json_error( [ 'message' => __( 'ID folderu i nowa nazwa są wymagane.', 'media-folder-pro' ) ] );
|
||||
}
|
||||
|
||||
$result = wp_update_term( $folder_id, MFP_Taxonomy::TAXONOMY, [
|
||||
'name' => $name,
|
||||
] );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
wp_send_json_error( [ 'message' => $result->get_error_message() ] );
|
||||
}
|
||||
|
||||
$term = get_term( $result['term_id'], MFP_Taxonomy::TAXONOMY );
|
||||
|
||||
wp_send_json_success( [
|
||||
'folder' => [
|
||||
'id' => $term->term_id,
|
||||
'name' => $term->name,
|
||||
'slug' => $term->slug,
|
||||
],
|
||||
] );
|
||||
}
|
||||
|
||||
public function mfp_delete_folder(): void {
|
||||
$this->verify_request();
|
||||
|
||||
$folder_id = (int) ( $_POST['folder_id'] ?? 0 );
|
||||
|
||||
if ( ! $folder_id ) {
|
||||
wp_send_json_error( [ 'message' => __( 'ID folderu jest wymagane.', 'media-folder-pro' ) ] );
|
||||
}
|
||||
|
||||
if ( $this->taxonomy->folder_has_children( $folder_id ) ) {
|
||||
wp_send_json_error( [
|
||||
'message' => __( 'Folder nie jest pusty. Usuń najpierw zawartość i podfoldery.', 'media-folder-pro' ),
|
||||
'code' => 'not_empty',
|
||||
] );
|
||||
}
|
||||
|
||||
$result = wp_delete_term( $folder_id, MFP_Taxonomy::TAXONOMY );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
wp_send_json_error( [ 'message' => $result->get_error_message() ] );
|
||||
}
|
||||
|
||||
wp_send_json_success();
|
||||
}
|
||||
|
||||
public function mfp_move_folder(): void {
|
||||
$this->verify_request();
|
||||
|
||||
$folder_id = (int) ( $_POST['folder_id'] ?? 0 );
|
||||
$new_parent_id = (int) ( $_POST['new_parent_id'] ?? 0 );
|
||||
|
||||
if ( ! $folder_id ) {
|
||||
wp_send_json_error( [ 'message' => __( 'ID folderu jest wymagane.', 'media-folder-pro' ) ] );
|
||||
}
|
||||
|
||||
if ( $this->taxonomy->would_create_cycle( $folder_id, $new_parent_id ) ) {
|
||||
wp_send_json_error( [
|
||||
'message' => __( 'Nie można przenieść folderu do samego siebie lub swojego podfolderu.', 'media-folder-pro' ),
|
||||
] );
|
||||
}
|
||||
|
||||
$result = wp_update_term( $folder_id, MFP_Taxonomy::TAXONOMY, [
|
||||
'parent' => $new_parent_id,
|
||||
] );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
wp_send_json_error( [ 'message' => $result->get_error_message() ] );
|
||||
}
|
||||
|
||||
$term = get_term( $result['term_id'], MFP_Taxonomy::TAXONOMY );
|
||||
|
||||
wp_send_json_success( [
|
||||
'folder' => [
|
||||
'id' => $term->term_id,
|
||||
'name' => $term->name,
|
||||
'parent' => $term->parent,
|
||||
],
|
||||
] );
|
||||
}
|
||||
|
||||
public function mfp_assign_media(): void {
|
||||
$this->verify_request();
|
||||
|
||||
$attachment_ids = array_map( 'intval', (array) ( $_POST['attachment_ids'] ?? [] ) );
|
||||
$folder_id = (int) ( $_POST['folder_id'] ?? 0 );
|
||||
|
||||
if ( empty( $attachment_ids ) ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Brak wybranych mediów.', 'media-folder-pro' ) ] );
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
foreach ( $attachment_ids as $id ) {
|
||||
if ( $id <= 0 ) {
|
||||
continue;
|
||||
}
|
||||
if ( get_post_type( $id ) !== 'attachment' ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$terms = $folder_id > 0 ? [ $folder_id ] : [];
|
||||
$result = wp_set_object_terms( $id, $terms, MFP_Taxonomy::TAXONOMY );
|
||||
|
||||
if ( ! is_wp_error( $result ) ) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Return updated folder counts.
|
||||
$all_terms = get_terms( [
|
||||
'taxonomy' => MFP_Taxonomy::TAXONOMY,
|
||||
'hide_empty' => false,
|
||||
'fields' => 'all',
|
||||
] );
|
||||
|
||||
$folder_counts = [];
|
||||
if ( ! is_wp_error( $all_terms ) ) {
|
||||
foreach ( $all_terms as $term ) {
|
||||
$folder_counts[ $term->term_id ] = (int) $term->count;
|
||||
}
|
||||
}
|
||||
|
||||
wp_send_json_success( [
|
||||
'count' => $count,
|
||||
'folder_counts' => $folder_counts,
|
||||
] );
|
||||
}
|
||||
|
||||
public function mfp_upload_to_folder(): void {
|
||||
$this->verify_request();
|
||||
|
||||
$attachment_id = (int) ( $_POST['attachment_id'] ?? 0 );
|
||||
$folder_id = (int) ( $_POST['folder_id'] ?? 0 );
|
||||
|
||||
if ( ! $attachment_id || ! $folder_id ) {
|
||||
wp_send_json_error( [ 'message' => __( 'ID załącznika i folderu są wymagane.', 'media-folder-pro' ) ] );
|
||||
}
|
||||
|
||||
if ( get_post_type( $attachment_id ) !== 'attachment' ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Nieprawidłowy załącznik.', 'media-folder-pro' ) ] );
|
||||
}
|
||||
|
||||
$result = wp_set_object_terms( $attachment_id, [ $folder_id ], MFP_Taxonomy::TAXONOMY );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
wp_send_json_error( [ 'message' => $result->get_error_message() ] );
|
||||
}
|
||||
|
||||
wp_send_json_success();
|
||||
}
|
||||
|
||||
public function mfp_get_folders(): void {
|
||||
$this->verify_request();
|
||||
|
||||
wp_send_json_success( [
|
||||
'folders' => $this->taxonomy->get_folder_tree(),
|
||||
] );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class MFP_Media_Query {
|
||||
|
||||
public function register(): void {
|
||||
add_filter( 'ajax_query_attachments_args', [ $this, 'filter_by_folder' ] );
|
||||
add_action( 'add_attachment', [ $this, 'assign_folder_on_upload' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign folder to attachment during upload if mfp_folder_id is in POST data.
|
||||
* This runs server-side during async-upload.php, so no race condition.
|
||||
*/
|
||||
public function assign_folder_on_upload( int $attachment_id ): void {
|
||||
if ( empty( $_POST['mfp_folder_id'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$folder_id = (int) $_POST['mfp_folder_id'];
|
||||
if ( $folder_id <= 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_set_object_terms( $attachment_id, [ $folder_id ], MFP_Taxonomy::TAXONOMY );
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject tax_query into media grid AJAX queries when media_folder param is present.
|
||||
*
|
||||
* @param array $query WP_Query args for attachment query.
|
||||
* @return array Modified query args.
|
||||
*/
|
||||
public function filter_by_folder( array $query ): array {
|
||||
if ( empty( $_REQUEST['query']['media_folder'] ) ) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
$folder_id = (int) $_REQUEST['query']['media_folder'];
|
||||
|
||||
if ( ! isset( $query['tax_query'] ) ) {
|
||||
$query['tax_query'] = [];
|
||||
}
|
||||
|
||||
if ( $folder_id === -1 ) {
|
||||
// Uncategorized: media without any folder.
|
||||
$query['tax_query'][] = [
|
||||
'taxonomy' => MFP_Taxonomy::TAXONOMY,
|
||||
'operator' => 'NOT EXISTS',
|
||||
];
|
||||
} elseif ( $folder_id > 0 ) {
|
||||
$query['tax_query'][] = [
|
||||
'taxonomy' => MFP_Taxonomy::TAXONOMY,
|
||||
'terms' => [ $folder_id ],
|
||||
'field' => 'term_id',
|
||||
];
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
122
wp-content/plugins/media-folder-pro/includes/class-taxonomy.php
Normal file
122
wp-content/plugins/media-folder-pro/includes/class-taxonomy.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class MFP_Taxonomy {
|
||||
|
||||
public const TAXONOMY = 'media_folder';
|
||||
|
||||
public function register(): void {
|
||||
register_taxonomy( self::TAXONOMY, 'attachment', [
|
||||
'labels' => [
|
||||
'name' => __( 'Foldery mediów', 'media-folder-pro' ),
|
||||
'singular_name' => __( 'Folder mediów', 'media-folder-pro' ),
|
||||
'add_new_item' => __( 'Dodaj nowy folder', 'media-folder-pro' ),
|
||||
'edit_item' => __( 'Edytuj folder', 'media-folder-pro' ),
|
||||
],
|
||||
'hierarchical' => true,
|
||||
'public' => false,
|
||||
'show_ui' => false,
|
||||
'show_in_rest' => true,
|
||||
'show_admin_column' => false,
|
||||
'query_var' => false,
|
||||
'rewrite' => false,
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{id: int, name: string, slug: string, parent: int, count: int, children: array}>
|
||||
*/
|
||||
public function get_folder_tree(): array {
|
||||
$terms = get_terms( [
|
||||
'taxonomy' => self::TAXONOMY,
|
||||
'hide_empty' => false,
|
||||
'orderby' => 'name',
|
||||
'order' => 'ASC',
|
||||
] );
|
||||
|
||||
if ( is_wp_error( $terms ) || empty( $terms ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$flat = [];
|
||||
foreach ( $terms as $term ) {
|
||||
$flat[ $term->term_id ] = [
|
||||
'id' => $term->term_id,
|
||||
'name' => $term->name,
|
||||
'slug' => $term->slug,
|
||||
'parent' => $term->parent,
|
||||
'count' => (int) $term->count,
|
||||
'children' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$tree = [];
|
||||
foreach ( $flat as $id => &$node ) {
|
||||
if ( $node['parent'] === 0 ) {
|
||||
$tree[] = &$flat[ $id ];
|
||||
} elseif ( isset( $flat[ $node['parent'] ] ) ) {
|
||||
$flat[ $node['parent'] ]['children'][] = &$flat[ $id ];
|
||||
} else {
|
||||
$tree[] = &$flat[ $id ];
|
||||
}
|
||||
}
|
||||
unset( $node );
|
||||
|
||||
return $tree;
|
||||
}
|
||||
|
||||
public function folder_has_children( int $folder_id ): bool {
|
||||
$children = get_terms( [
|
||||
'taxonomy' => self::TAXONOMY,
|
||||
'parent' => $folder_id,
|
||||
'hide_empty' => false,
|
||||
'number' => 1,
|
||||
'fields' => 'ids',
|
||||
] );
|
||||
|
||||
if ( ! empty( $children ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$attachments = get_posts( [
|
||||
'post_type' => 'attachment',
|
||||
'post_status' => 'inherit',
|
||||
'tax_query' => [
|
||||
[
|
||||
'taxonomy' => self::TAXONOMY,
|
||||
'terms' => $folder_id,
|
||||
],
|
||||
],
|
||||
'posts_per_page' => 1,
|
||||
'fields' => 'ids',
|
||||
] );
|
||||
|
||||
return ! empty( $attachments );
|
||||
}
|
||||
|
||||
public function would_create_cycle( int $folder_id, int $new_parent_id ): bool {
|
||||
if ( $new_parent_id === 0 ) {
|
||||
return false;
|
||||
}
|
||||
if ( $folder_id === $new_parent_id ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$current = $new_parent_id;
|
||||
while ( $current !== 0 ) {
|
||||
$term = get_term( $current, self::TAXONOMY );
|
||||
if ( is_wp_error( $term ) || ! $term ) {
|
||||
break;
|
||||
}
|
||||
if ( $term->parent === $folder_id ) {
|
||||
return true;
|
||||
}
|
||||
$current = $term->parent;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user