How to rewrite and shorten WordPress uploads files URLs?

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).

Therefore, before you decide to use this snippet, I would recommend to test the performance and ensure that it does not harm your WordPress installation.
You can revert at anytime the changes, by disabling or removing the snippet. No MySQL data will be altered with this code functional.

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 two functions: first one bis_findfile() will be used to search for the requested file in wp-content/uploads directories and the second 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_detect_image($request) {
 	global $wp, $wpdb;

 	// Allowed MIME types
 	$mime_types = 'jpg|jpeg|jpe|gif|png|bmp|tif|tiff|ico|pdf';

	// 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. Mask the uploads’ files URLs inside WordPress

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($input) {
	$home_url = preg_quote(rtrim(get_home_url(null, null, null), "/"), "/");

	return preg_replace("/(?!{$home_url})(wp-content\/uploads\/[\d]{4}\/[\d]{2}\/)/ui", NEW_MEDIA_DIR, $input);
}
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.

You can also download the snippet from Gist here.

/**
 * Copyright 2017, Maciej Bis - https://permalinkmanager.pro
 */

// 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_detect_image($request) {
	global $wp, $wpdb;

  // Allowed MIME types
  $mime_types = 'jpg|jpeg|jpe|gif|png|bmp|tif|tiff|ico|pdf';

 	// 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)) {
		// 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;
		}
	}

	// 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();
	}

	return $request;
}
add_filter('request', 'bis_detect_image', 999);

function bis_shorten_media_url($input) {
	$home_url = preg_quote(rtrim(get_home_url(null, null, null), "/"), "/");

	return preg_replace("/(?!{$home_url})(wp-content\/uploads\/[\d]{4}\/[\d]{2}\/)/ui", NEW_MEDIA_DIR, $input);
}
add_filter('wp_get_attachment_url', 'bis_shorten_media_url');
add_filter('the_content', 'bis_shorten_media_url');

Leave a Reply

Your email address will not be published. Required fields are marked *

*

*

*