The snippet described below is not currently implemented in Permalink Manager plugin, but I have decided to share the snippet, so it can be easily reused and adjusted by anyone who wants to change the strutcture of WordPress media files permalinks.
What does it change?
The code featured in this post allows to shorten and adjust the URLs of media files stored in WordPress Media Library. By default all attachments are stored in
wp-content/uploads
folder, where all the uploaded files are organized into month- and year-based subfolders, e.g.
http://website.com/wp-content/uploads/2017/09/sample-media-library-image.png
The hollowing snippet allows to change/rewrite the uploads URLs (without moving them) to match the new permastructure, where all the attachments mimic a different directory tree, e.g.
http://website.com/media/sample-media-library-image.png
It might be especially helpful if you would like to mask, rewrite or hide the original permalinks of Media Library items (e.g. for security reasons) or make the attachment files URLs more SEO-friendly.
Will it break my website?
Theoretically, it should not, but it may work incorrectly if you are using the same filename for multiple uploads in Media Library (single URL will point to multiple attachments then). Please also note that in some specific cases, implementing this code to your website may slightly decrease its performance and break the behavior of some WordPress plugins using PHP file-handling functions (e.g. it will not be compatible with Aqua-Resizer class).
How to implement it?
You can paste the snippet directly to your theme’s
functions.php
file or create a mini plugin using the code and following the instructions. If you need any help with integrating this code with your website and you are not a WordPress developer, please contact me via email for a commercial support.
Step 1. Rewrite uploads directory and check if request URL matches the new permastructure
Firstly, we will need to define the new directory (e.g.
media
) that will be used to rewrite/mask the uploads URLs. detect if parsed URL could be treated as a media file attachment. Then, we need to define three functions:
- first one
bis_findfile()
will be used to search for the requested file inwp-content/uploads
directories - the second one
bis_get_allowed_extensions()
will contain the list of all allowed attachment extensions - the files with different extensions will be ignored - the third one
bis_detect_image()
will check if the correct URL was requested and display the attachment’s contents in the end.
The simplest and most intuitive solution for examining the requested URLs would be to use plain REGEX rule (regular expressions) and probe the requested URL to check if it ends with specific extension (one of MIME types specified in
$mime_types
variable).
Please also do not forget to call for two globals
$wp
and
$wpdb
inside
bis_findfile()
function.
// You can change the directory name, but please always keep slash in the end.
DEFINE('NEW_MEDIA_DIR', 'media/');
// If you want to make the uploads URLs look like:
// http://website.com/sample-media-library-image.png
// use this instead:
// DEFINE('NEW_MEDIA_DIR', '');
function bis_findfile($pattern, $flags = 0) {
	$files = glob($pattern, $flags);
	foreach (glob(dirname($pattern).'/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) {
		$files = array_merge($files, bis_findfile($dir.'/'.basename($pattern), $flags));
	}
	return $files;
}
function bis_get_allowed_extensions() {
	return array('jpg', 'jpeg', 'jpe', 'gif', 'png', 'bmp', 'tif', 'tiff', 'ico', 'pdf');
}
function bis_detect_image($request) {
	global $wp, $wpdb;
	// Allowed MIME types
	$mime_types_array = bis_get_allowed_extensions();
	$mime_types = implode("|", $mime_types_array);
	// Prepare the new directory name for REGEX rule
	$new_media_dir = preg_quote(NEW_MEDIA_DIR, '/');
	// Check if requested file is an attachment
	preg_match("/{$new_media_dir}(.+)\.({$mime_types})/", $wp->request, $is_file);
	if(!empty($is_file)) {
	// ... Step 2 & 3 ...
	}
Step 2. Check if the requested file exists
To improved the code performance, in the begining we have to make sure that the requested file is stored in WordPress uploads directories and if the filename is used by any attachment uploaded with WordPress Media Library.
Theoretically we should always use
glob()
(native PHP function) which would be the most clever way to detect the files’ URLs, but it may not recognize the filenames containing non-ASCII characters (e.g. letters with accents). Therefore, we will check if the filename contains them and use SQL formula for non-standard filenames. It is not a perfect solution, but it is the most generic way to find the files on servers that use various filename encoding. The side effect is that it may affect the pageload time, especially if your database is pretty large, but it applies only to HTTP requests for attachments with non-ASCII characters.
As you may notice, we defined a variable named
$upload_dir
. It will be used as a part of LIKE statement to narrow the SQL formula and also in
glob()
PHP function used in
bis_findfile()
function.
For all non-ASCII filename, where we need to use SQL formula, we need to check if the requested URL refers to thumbnail’s file. If it does, we need to separate the thumbnails suffix (e.g.
-200x400
) from original filenames, before we search for the real path of attachment in the MySQL database.
All thumbnails filenames ends with the dimensions suffix – for instance, the thumbnail for
dummy_image.jpg
(original filename) resized to 200x400px will be stored in the server as
dummy_image-200x400.jpg
. The thumbnail’s dimensions suffix is removed from the filename because WordPress stores in
wp_posts
table the URLs only for the original files and not for the thumbnails. When the original file (no thumbnail) is found, the suffix is appended back to the original path of found attachment file.
To sum up, for standard filenames (e.g.
abecadlo.jpg
) we will use
bis_findfile()
function based on PHP’s native
glob()
function. Sometimes, the filenames contain e.g. letters with accents, e.g.
ą, ę, ś, ż
(e.g.
ąbęćądłó.jpg
) and then we would need to use SQL formula to find the attachment paths in
wp_posts
table (guid column, to be more specific).
// Get the uploads dir used by WordPress to host the media files
$upload_dir = wp_upload_dir();
// Decode the URI-encoded characters
$filename = basename(urldecode($wp->request));
// Check if filename contains non-ASCII characters. If does, use SQL to find the file on the server
if(preg_match('/[^\x20-\x7f]/', $filename)) {
	// Check if the file is a thumbnail
	preg_match("/(.*)(-[\d]+x[\d]+)([\S]{3,4})/", $filename, $is_thumb);
	// Prepare the pattern
	$pattern = "{$upload_dir['baseurl']}/%/{$filename}";
	// Use the full size URL in SQL query (remove the thumb appendix)
	$pattern = (!empty($is_thumb[2])) ? preg_replace("/(-[\d]*x[\d]*)/", "", "{$upload_dir['baseurl']}/%/{$filename}") : $pattern;
	$file_url = $wpdb->get_var($wpdb->prepare("SELECT guid FROM $wpdb->posts WHERE guid LIKE %s", $pattern));
	if(!empty($file_url)) {
		// Replace the URL with DIR
		$file_dir = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $file_url);
		// Get the original path
		$file_dir = (!empty($is_thumb[2])) ? str_replace($is_thumb[1], "{$is_thumb[1]}{$is_thumb[2]}", $file_dir) : $file_dir;
	}
} else {
	// Prepare the pattern
	$pattern = "{$upload_dir['basedir']}/*/{$filename}";
	$found_files = bis_findfile($pattern);
	// Get the original path if file is found
	$file_dir = (!empty($found_files[0])) ? $found_files[0] : false;
}
Step 3. Load the found attachment file when the “artificial” URL is requested
Now, when the full path of the requested attachment is found, we should double check if
$file_dir
variable is set and it is not empty. Afterwards, we need to get the MIME type of that file using (
mime_content_type()
function), so we can set-up proper HTTP headers (“Content-type:”) and finally output the contents of the file with
readfile()
function.
// Double check if the file exists
if(!empty($file_dir) && file_exists($file_dir)) {
	$file_mime = mime_content_type($file_dir);
	// Set headers
	header('Content-type: ' . $file_mime);
	readfile($file_dir);
	die();
}
Step 4. Filter/rewrite the WordPress uploads’ files URLs
By now, the “artificial” URLs should work as aliases for original URLs and they both can be used at the same time.
http://website.com/media/sample-media-library-image.png
works now as an alias for
http://website.com/wp-content/uploads/2017/09/sample-media-library-image.png
.
To dynamically replace the original URLs in WordPress front-end content, we need to filter two hooks:
the_content
and
wp_get_attachment_url
and replace with REGEX the old tree structure (e.g. wp-content/uploads/2017/09) with our new “artificial” directory (media).
function bis_shorten_media_url($attachment_url) {
	$mime_types_array = bis_get_allowed_extensions();
	$extension = pathinfo($attachment_url, PATHINFO_EXTENSION);
	// Only the selected file extension should be rewritten
	if(in_array($extension, $mime_types_array)) {
		$home_url = preg_quote(rtrim(get_home_url(), "/"), "/");
		$attachment_url = preg_replace("/(?!{$home_url})(wp-content\/uploads\/[\d]{4}\/[\d]{2}\/)/ui", NEW_MEDIA_DIR, $attachment_url);
	}
	return $attachment_url;
}
add_filter('wp_get_attachment_url', 'bis_shorten_media_url');
add_filter('the_content', 'bis_shorten_media_url');
Full snippet
If you would like to support me in developing Permalink Manager, please consider buying Permalink Manager Pro
Buy the plugin here.
Hi, this is working nice but mess up videos. The media file is not found anymore. So how can we change your code to prevent from rewriting url for .mp4 / .webm files? Thanks a lot!
Hi,
I updated the snippet code and now only the attachments with file extensions selected in bis_get_allowed_extensions() function will be filtered.
Does this store any information in the database? I've been running this for over a year now but have recently chosen to remove the plugin. However, upon removing the code from functions.php, refreshing permalinks, and removing cache files etc, some of the media paths appear to be permanently changed?
Hi. Thank you for posting this snippet! We found this snippet doesn't work when running it from a staging site subfolder. Do you have a fix for that?
Unfortunately, I do not have any ready solution for WP installations stored in a subdirectory :/