WP Manifestindependent plugin directory
manifest / media / custom-document-folder

Custom Document Folder WP Registry grade D+. High-severity findings · 2 findingsWP RegistryHigh-severity findings · 2 findingsOpen the reportD+

Organize document uploads by automatically directing specific file types to custom folders based on their extensions.

★ 3stars
59release downloads
954composer installs
1.1.1latest · 11mo ago

Install

Download custom-document-folder.zip From the 1.1.1 release on GitHub, Nov 25, 2025

Upload it under Plugins → Add New Plugin → Upload Plugin, then activate.

Install straight from GitHub with WP-CLI:

wp plugin install https://github.com/soderlind/custom-document-folder/releases/download/1.1.1/custom-document-folder.zip

On Packagist as soderlind/custom-document-folder:

composer require soderlind/custom-document-folder
Updates itself. It ships its own updater (Plugin Update Checker), so new versions appear under Dashboard → Updates.

Organize document uploads by automatically directing specific file types to custom folders based on their extensions.

Description

Custom Document Folder automatically organizes your WordPress media uploads by directing specific document types to dedicated folders. Instead of having all files mixed together in date-based folders, you can configure which file extensions should be uploaded to their own organized folders.

Features

  • ✨ Modern Settings Interface - Beautiful grid-based UI with search functionality
  • 📁 Category Organization - Extensions grouped by type (Documents, Images, Video, Audio, Archives, etc.)
  • 🔍 Live Search - Filter extensions and categories in real-time
  • ⚡ Bulk Actions - Select/deselect all visible extensions at once
  • 🎯 Collapsible Categories - Media categories (Audio, Images, Video, Other) hidden by default
  • 🔢 Live Counter - See how many extensions are selected
  • 📂 Custom Folder Routing - Each extension gets its own folder (e.g., /pdf/, /docx/, /xlsx/)
  • 🔒 Security Focused - Input sanitization with sanitize_text_field()
  • ✅ MIME Type Validation - Only allows file types that WordPress already permits
  • 🔄 Backward Compatible - Existing files remain in their original locations
  • 📄 PDF Default - Comes pre-configured with PDF support
  • 🗑️ Non-unique Filename Handling - Automatically removes old attachments when re-uploading

Installation

  • Quick Install

    • Download custom-document-folder.zip
    • Upload via Plugins > Add New > Upload Plugin
    • Activate the plugin.
    • Go to Settings > Document Folder to configure
  • Composer Install

     composer require soderlind/custom-document-folder
  • Updates

Usage

  1. Navigate to Settings > Document Folder
  2. Check the extensions you want to organize (PDF is pre-selected)
  3. Use the search box to quickly find specific extensions
  4. Click "Show More Categories" to see audio, image, and video extensions
  5. Click "Select All" or "Deselect All" for bulk actions
  6. Save your settings
  7. New uploads of selected types will automatically go to their designated folders

Example

If you select pdf and docx extensions:

  • PDF files upload to → /wp-content/uploads/pdf/
  • DOCX files upload to → /wp-content/uploads/docx/
  • Other files continue uploading to standard date-based folders

Requirements

  • WordPress: 6.7 or higher
  • PHP: 8.3 or higher
  • PHP Extensions: None required beyond standard WordPress requirements

Frequently Asked Questions

What happens to files uploaded before activating the plugin?

They remain in their original locations. This plugin only affects new uploads after you configure and save your settings.

Can I select multiple file extensions?

Yes! Select as many extensions as you need. The counter shows how many are currently selected.

Will this break my existing media library?

No. Existing files stay where they are. Only new uploads of selected extensions are redirected to custom folders.

Does this work with multisite?

Yes. Each site in a multisite installation can configure its own extensions independently.

Can I organize images and videos too?

Yes! Click "Show More Categories" to reveal and select image, video, and audio extensions. However, some themes and plugins expect images in date-based folders.

Development

Architecture

custom-document-folder/
├── custom-document-folder.php  # Main plugin file
├── tests/
│   ├── TestCase.php           # Base test class
│   ├── SettingsTest.php       # Settings tests
│   ├── UploadTest.php         # Upload tests
│   ├── cdf-filter-test.php    # WP-CLI filter test
│   └── TEST-RESULTS.md        # Test documentation
├── composer.json              # Composer configuration
├── phpunit.xml.dist          # PHPUnit configuration
├── CHANGELOG.md              # Version history
└── README.md                 # This file

WordPress Hooks

The plugin uses these WordPress hooks:

  • upload_dir (priority 20) - Modifies upload directory path
  • wp_handle_upload_prefilter - Captures file extension early
  • wp_check_filetype_and_ext - Validates file types
  • wp_handle_upload_overrides - Handles non-unique filenames
  • sanitize_file_name - Removes old attachments

Code Standards

  • PHP 8.3 with strict types and namespaces
  • WordPress Coding Standards compliant
  • PHPUnit test coverage
  • Brain Monkey for WordPress function mocking
  • Namespace: Soderlind\DocumentFolder

Testing

Run PHPUnit Tests

# Install dependencies
composer install

# Run tests
composer test

# Run with coverage
composer test:coverage

Run WP-CLI Filter Tests

wp eval-file wp-content/plugins/custom-document-folder/tests/cdf-filter-test.php

Test Results

PHPUnit 9.6.29
OK (7 tests, 21 assertions)

All core functionality verified:

  • ✅ Settings sanitization
  • ✅ Invalid input handling
  • ✅ XSS protection
  • ✅ PDF upload path modification
  • ✅ Non-selected files use default path
  • ✅ Multiple extension handling
  • ✅ File type validation

Contributing

Contributions are welcome! Please follow these guidelines:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Development Setup

# Clone repository
git clone https://github.com/soderlind/custom-document-folder.git
cd custom-document-folder

# Install dependencies
composer install

# Run tests
composer test

Changelog

See CHANGELOG.md for version history.

Support

License

This plugin is licensed under the GPL v2 or later.

Copyright (C) 2025 Per Soderlind

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

Author

Per Soderlind

Credits

Built with:


Made with ❤️ for the WordPress community

Releases

4 releases. Each count is every asset in that release; expand a row for the breakdown.

Tag
Published
Assets
Downloads
1.1.1 latest
Nov 25, 2025 11mo ago
custom-document-folder.zip
56
Nov 25, 2025 11mo ago
custom-document-folder.zip
3
Nov 24, 2025 11mo ago
tag only
—
Nov 24, 2025 11mo ago
tag only
—
D+ grade

Security

Custom Document Folder 1.1.1 · audited by WP Registry

High-severity findings.

2 high
Audited release
1.1.1
Findings
2
Worst severity
high
Content hash
fc5ae0a864127fd1bc400d6a…

Findings

  • high sanitize_file_name force-deletes other users' attachments via LIKE match

    missing_capability

    filename_filter is hooked to sanitize_file_name with no capability, ownership, or selected-extension check. Every core upload path that reaches wp_unique_filename() (Media Library, REST /wp/v2/media, XML-RPC wp.uploadFile) therefore runs get_posts() for post_type=attachment with meta_query _wp_attached_file LIKE $name. WordPress LIKE-wraps the value as %name%, so uploading a.jpg deletes every attachment whose stored path contains that substring (including other authors' and administrators' files such as 2024/01/camera.jpg). Each hit is passed to wp_delete_attachment($id, true), which has no current_user_can() gate and the true flag skips trash. Authors normally cannot delete_others_posts; this bypasses that. numberposts=-1 makes the blast site-wide. The hook is not limited to selected document types, so any allowed upload (jpg, pdf, …) is a deletion primitive. Subscriber/customer cannot hit this on a default role map (no upload_files); Author+ can.

    custom-document-folder.php:605-624

    Recommendation

    Do not delete attachments from sanitize_file_name. If same-name replacement is required, match _wp_attached_file exactly (not LIKE), restrict to the current upload destination, and only delete attachments the current user can delete (current_user_can('delete_post', $id)). Never pass true to wp_delete_attachment unless the user can force-delete. Limit the hook to selected extensions and to an actual upload of that file, not every sanitize_file_name call.

  • high unique_filename_callback disables uniqueness for all uploads, overwriting others' files

    file_access

    non_unique_filename() is hooked to wp_handle_upload_overrides and sets unique_filename_callback for every wp_handle_upload (not sideloads). Core wp_unique_filename() then skips its collision loop and uses the callback's return value. The callback compares strtolower($extension) to selected_extensions (e.g. 'pdf'), but WordPress passes $ext with a leading dot ('.pdf'). The in_array is therefore always false, remove_old_attach() never runs, and the original basename is always returned — including for non-selected types such as jpg in /uploads/YYYY/MM/. PHP move_uploaded_file overwrites an existing file at that path. Selected types share a flat /uploads/{ext}/ folder, so invoice.pdf uploaded by an author replaces the site-wide invoice.pdf. The same filter also forces test_form=false globally, dropping core's $_POST['action'] check (endpoints still have their own nonces). Subscriber/customer cannot upload by default; Author+ can. Observable: existing public media URLs serve the attacker's bytes while leftover attachment rows may still point at the overwritten path.

    custom-document-folder.php:552-575

    Recommendation

    Do not install a global unique_filename_callback. Let core wp_unique_filename() assign -1, -2 suffixes. If replacement is required, strip the leading dot from $extension, apply the callback only for selected extensions, and only after confirming the current user owns (or can delete) the target attachment. Do not set test_form=false for all uploads.

WP Registry hashes the installable build and reports on that exact bytes-for-bytes copy. Embargoed findings are withheld until they are disclosed, so a clean verdict means nothing public is outstanding. WP Manifest does not audit code itself.