Make WordPress Core

Opened 2 years ago

Last modified 6 months ago

#34358 new defect (bug)

plugin_dir_url( __FILE__ ) returns plugins directory when plugin symlinked to mu-plugins

Reported by: scamartist26 Owned by:
Milestone: Future Release Priority: normal
Severity: normal Version: 4.3.1
Component: Plugins Keywords: has-patch needs-unit-tests needs-refresh
Focuses: Cc:


With a plugin symlinked to the mu-plugins folder, paths to find assets via the URL are broken if using plugin_dir_url(). This does not happen when using the WPMU_PLUGIN_URL constant. However, if using the constant then the plugin is not portable to the normal plugins directory without additional checks.


* Plugin is symlinked to mu-plugins directory

// http://example.com/wp-content/plugins/my-plugin-dir/
define( 'BAD_PLUGIN_URL', plugin_dir_url( __FILE__ ) );

// http://example.com/wp-content/mu-plugins/my-plugin-dir/
define( 'GOOD_PLUGIN_URL', WPMU_PLUGIN_URL . '/my-plugin-dir/' );

Attachments (1)

urls_for_symlinked_mu_plugins.34358.diff (2.2 KB) - added by mattmckenny 2 years ago.
Proposed fix for 34358

Download all attachments as: .zip

Change History (19)

#1 @jdgrimes
2 years ago

Related: #16953, especially [27999]

Basically, symlinked MU plugins (or any "single file" plugins) are not supported. (Not my fault.)

I'm not 100% sure that this is what is causing your problem, but if so, it might be possible to work around this by calling wp_register_plugin_realpath( __FILE__ ) yourself.

But since the decision was made before not to support symlinked MU plugins, I think that the core committers will need to understand your use-case better before considering changing their minds, if indeed that is the cause of this bug.

#2 @Ipstenu
2 years ago

The usecase interests me.

If you're trying to have a plugin with a lot of files in an MU plugin, you would do this:


Put all the files in the folder myplugin and you can call them from myplugin.php :) Works fine. Even works with plugins_url()

#3 @scamartist26
2 years ago

Here is the current setup.


all_plugins_loader.php has the includes for all main /myplugin/myplugin.php files as noted here: 16953#comment:120. The myplugin_symlinked works fine to run the application other than any calls using plugins_url() or plugin_dir_url(). Those functions do not use the same catch that is in the plugin_basename() for the $realdir variable here: https://core.trac.wordpress.org/browser/tags/4.3.1/src/wp-includes/plugin.php#L656. Therefore it defaults to the plugins directory.

2 years ago

Proposed fix for 34358

#4 @mattmckenny
2 years ago

I have confirmed the issue as well. plugins_ur() doesn't resolve custom paths registered via the wp_register_plugin_realpath() before comparing the file path to the mu-plugins path as found here https://core.trac.wordpress.org/browser/tags/4.3.1/src/wp-includes/link-template.php#L2943.

My suggested fix extracts the functionality found starting here, through line 660, https://core.trac.wordpress.org/browser/tags/4.3.1/src/wp-includes/plugin.php#L654, and move it into a new function wp_get_plugin_realname() . This function is then called in both the plugin_basename() and plugins_url() and allows this file path / mu-pluins path comparison to return as expected. Hopefully this https://core.trac.wordpress.org/attachment/ticket/34358/urls_for_symlinked_mu_plugins.34358.diff illustrates the change better than my description.

#5 @Ipstenu
2 years ago


The usecase means we'd like to know why is that symlinked? :)

Why do you need the folder symlinked to start with?

#6 @mattmckenny
2 years ago

The use case I have is that I would like to maintain a few of our own, in-house, plugins each with their own separate Git repo. Due to hosting setup, we have to commit everything to our WordPress site's Git repo so subtrees, submodules and composer dependency management don't work for us (at least not from what I've been able to tell.)

These mu-plugins are in a common shared directory on my development box outside of any particular WordPress install. I would like to symlink to these plugins so I can develop new features within the context of a specific install but outside of the Git history for that install. This way I can manage new features, pull requests, etc for that specific plugin. After a new release has been made for the plugin, we replace the symlink with the actual plugin files and make a single commit with the new plugin version. This gets committed to the WordPress install's repo and we deploy.

The plugins need to be symlinked because, as @scamartist26 mentioned, any assets served via the URL (JS, CSS, images) are broken because they resolve to the plugins and not mu-plugins directory.

Hopefully that sheds some light on what we're up against and our need to have this "fixed".

I look forward to any feedback or suggestions.


#7 @jdgrimes
2 years ago

  • Component changed from Filesystem API to Plugins
  • Keywords has-patch needs-unit-tests needs-refresh added

Looking at this last night, I also came to the conclusion that we'd need to do something like urls_for_symlinked_mu_plugins.34358.diff. It could use some unit tests though.

The patch will also need a few tweaks before (if) it gets committed:

  • There is an extra newline on line 685.
  • There is an extra space before the summary on line 728
  • The long line in the docblock description should probably be wrapped, and maybe use {@see ...} notation for the reference to wp_register_plugin_realpath().
  • The @param and @return descriptions should end with a period.
  • There is an extra newline on line 752.

Also, as far as usecase goes, I think it might be helpful if you also explain why this needs to be a mu-plugin instead of a regular plugin.

#8 @mattmckenny
2 years ago

@jdgrimes Thanks for taking a look at this and helping me with this patch. I'm ashamed to say it's my first time offering a patch so your guidance is greatly appreciated.

I'm happy to clean up the patch and work on unit tests. I just wanted to make sure the issue had some traction and that others thought this may be useful.

We're using this as a mu-plugin so they get loaded earlier than standard plugins and in a very specific order as listed in our mu-loader.php file.

#9 @scamartist26
2 years ago

Commenting here once again because I believe this is still a big issue for advanced development. Where does the community stand here? My opinion is that MU plugins should be able to access assets via symlinks the same way normal plugins do. It puts development on a much faster track in every situation with WordPress.

#10 @scamartist26
2 years ago


New PLUGIN for my NETWORK... I have ten something DB's to to test locally, with several different plugin configurations. I need to add a new plugin to ALL of them, and want to do so via the mu-plugins folder because it is there for all things. None of the assets are available during testing.

/use case>;

Also, hosting deployment of separate repos.....

Last edited 2 years ago by scamartist26 (previous) (diff)

#11 @andy
2 years ago

This bug just bit me and I independently wrote essentially the same patch to fix it. When I came to create a new ticket, Trac found this one. A ticket saved is a ticket earned, right?

Our use case is a WordPress host where customers are allowed to use mu-plugins but we also require a suite of mu-plugins. This suite needs to be symlinked because copying it into every customer's directory would waste space and time and inevitably introduce bugs during times of inconsistency. With a symlink we can add, change and remove plugins in this suite without touching customer directories and without them going out of sync with each other.

#12 @johnjamesjacoby
2 years ago

This use-case is spooky, and neat, so naturally I'm intrigued.

  • My initial reaction: why not?
  • Then: maintaining some mu-plugins externally while allowing others to be local, sounds messy
  • My final reaction: why not?

There's no foreseeable reason to purposely disallow this functionality, should someone be savvy and informed enough to try it and have an installation type that benefits from it.

I'm a +1 on this patch, FWIW.

Re: unit tests, what are we thinking? Compare globals to function calls to confirm the paths match?

#13 @rmccue
2 years ago

FWIW: the reason we didn't support symlinks for mu-plugins is because they're only files, whereas normal plugins are files or directories. It doesn't make much sense for just files to have the realpath registered.

Presumably if you're using subdirectories, you have a loader file as well; this loader file should be calling wp_register_plugin_realpath() on the files it loads to mirror how WP loads these files. It should Just Work if you do this; is that not the case?

#14 @mattmckenny
2 years ago

Thanks for the suggestion @rmccue

We are in fact using a loader file in the mu-plugins directory to load plugin files which reside in subdirectories of mu-plugins. I just tried your recommendation to properly load a symlinked plugin by registering through wp_register_plugin_path() first. My loader file has a few lines thas look something like this:

$symlinked_plugin = dirname(__FILE__) . '/symlinked-plugin/symlinked-plugin.php';

wp_register_plugin_realpath( $symlinked_plugin );

include_once( $symlinked_plugin );

and then printing out the $wp_plugin_paths global gives me this:

    [/Users/matt/Sites/example-site/wp-content/mu-plugins/symlinked-plugin] => /Users/matt/Sites/_plugins/symlinked-plugin

The plugin works for the most part, with the exception of our "assets". The symlinked plugin is enqueueing a CSS file, let's call is symlinked.css. When I view the source of the rendered page, I see that the file has been loaded but it's loading it through the 'plugins' directory, not the 'mu-plugins' directory. The loaded CSS file looks like this:

<link rel='stylesheet' id='symlinked-css'  href='https://www.example.com/wp-content/plugins/symlinked-plugin/symlinked.css?ver=4.4.2' type='text/css' media='all' />

As you can imagine, this doesn't work since the CSS is actually in the 'mu-plugins' subdirectory.

Again, thank you for your help. Please let me know if I've missing something in my setup and approach.

#15 @rmccue
2 years ago

  • Milestone changed from Awaiting Review to 4.6

Gotcha; sounds like we do need explicit support for mu-plugins then.

#16 @ocean90
21 months ago

  • Milestone changed from 4.6 to Future Release

No traction in two months, punting.

#17 @will_c
11 months ago

I also came across this issue recently while trying use MU-Plugins for a multi-tenant setup. I had a slightly different structure, but encountered a similar issue where static assets had the full path included in their url. Happy to share a longer write-up if useful, but not sure how widely relevant our use-case is.

We have a symlinked wp root directory as well as a symlinked content directory that points to a directory outside the wp root. We have all of our plugins inside of mu-plugins and are using a loader.php to reference the main file for each plugin.

Using multiple symlinks in our setup was causing issues when plugins were calling plugins_url() or plugin_dir_url() and passing in a reference to the main plugin file. plugins_url() calls plugin_basename() which uses preg_replace (https://github.com/WordPress/WordPress/blob/master/wp-includes/plugin.php#L657) to try and determine the relative path of the current plugin.

In our case this was not matching the WPMU_PLUGIN_DIR because it was using the full path (including the symlinks) while the $file passed into the plugin_basename() was using the shorter direct link to the mu-plugins (without the symlinked folders). This caused plugin_basename() to return the full path to the plugin rather than the relative path.

The solution was to define WP_CONTENT_DIR using realpath():

define('WP_CONTENT_DIR', realpath(dirname(__FILE__) . '/content'));

rather than:

define('WP_CONTENT_DIR', dirname(__FILE__) . /content');.

If you're only defining WPMU_PLUGIN_DIR as a location outside the wp root, the same solution would apply, just use realpath() the same way as above rather than referencing a symlinked location directly.

Everything works as expected now that we're using realpath().

This ticket was mentioned in Slack in #core by mizner. View the logs.

6 months ago

Note: See TracTickets for help on using tickets.