WordPress.org

Make WordPress Core

Opened 2 years ago

Last modified 12 days ago

#16953 assigned enhancement

Allow symlinked plugins

Reported by: scribu Owned by:
Priority: normal Milestone: Future Release
Component: Plugins Version:
Severity: normal Keywords: has-patch dev-feedback
Cc: yincrash, paulschreiber@…, boonebgorges@…, djpaul@…, coenjacobs@…, lkraav, simon@…, junk@…, juzzin, mitcho@…, kenn@…, hertzog@…, kwisatz, info@…, kawauso, synapticism, markus.magnuson@…, cklosowski@…, mike@…, imtiedup@…, johnbeales@…, beaver6813, jason@…, david@…, cor@…, JeromeC, francesco.laffi@…, rodrigosprimo@…, andyadamscp@…, andkrup, tollmanz@…

Description (last modified by scribu)

There are many scenarios where one would like to have a plugin's folder symlinked to another location.

A couple of these scenarios are described in #13550.

However, when using symlinks, code such as this fails:

plugins_url( 'script.js', __FILE__ );

This happens because __FILE__ resolves to the real path, which confuses plugin_basename().

The most simple and most flexible solution is to add a filter to plugin_basename(), leaving individual devs to handle symlinked paths, depending on their environment.

Attachments (9)

16953.diff (552 bytes) - added by scribu 2 years ago.
'pre_plugin_basename' filter
test.zip (2.1 KB) - added by scribu 2 years ago.
WP_fix_for_plugin_basename_to_allow_symlinks_2011-07-28.patch (711 bytes) - added by augustash 23 months ago.
Fix for correctly extracting the local directory and filename regardless if the file comes from a symlink or not
WP_fix_for_plugin_basename_to_allow_symlinks_2011-07-28.2.patch (1.8 KB) - added by augustash 23 months ago.
16953.alternative.diff (2.7 KB) - added by mitchoyoshitaka 11 months ago.
Alternative approach, which caches symlink targets and replaces them
symlinked_plugins.diff (2.6 KB) - added by MikeSchinkel 4 months ago.
Patch to enable plugin_url() to support symlinked plugins.
symlinked_plugins-2.diff (2.3 KB) - added by MikeSchinkel 4 months ago.
2nd attempt at patch to enable plugin_url() to support symlinked plugins.
pre-plugin-basename.diff (673 bytes) - added by MikeSchinkel 8 weeks ago.
pre-plugin-basename.diff
16953.2.diff (891 bytes) - added by nacin 3 weeks ago.

Download all attachments as: .zip

Change History (76)

scribu2 years ago

'pre_plugin_basename' filter

comment:1 scribu2 years ago

  • Keywords has-patch added
  • Owner set to scribu
  • Status changed from new to accepted

Test files coming up.

scribu2 years ago

comment:2 scribu2 years ago

  • Description modified (diff)
  • Keywords commit added
  • Milestone changed from Awaiting Review to 3.2

To test:

  1. Extract test.zip into a wp root directory
  2. Activate Test Plugin

The archive already contains a symlink, so it probably won't work on Windows out of the box.

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

comment:3 scribu2 years ago

  • Description modified (diff)

comment:4 scribu2 years ago

  • Description modified (diff)

comment:5 scribu2 years ago

  • Description modified (diff)

comment:6 westi2 years ago

  • Milestone changed from 3.2 to Future Release

Not a 3.2 feature moving to Future Release.

comment:7 scribu2 years ago

  • Milestone Future Release deleted
  • Resolution set to worksforme
  • Status changed from accepted to closed

Well what'ya know... there's already a 'plugins_url' filter which can be used just as well.

comment:8 scribu2 years ago

  • Milestone set to Future Release
  • Resolution worksforme deleted
  • Status changed from closed to reopened

Found a case that's not covered by the 'plugins_url' filter: register_*_hook().

comment:9 ciantic2 years ago

Yes, very annoying, it boils down to lack of feature in PHP, see PHP bug 46260 (I actually believe best way to fix PHP bug is to create new constant like __FILELINK__ to keep backwards compatibility). But getting that fixed anytime soon is not reality.

@scribu, can you elaborate what kind of problems you encountered using plugins_url filter hack for symlinkked plugins? The problem on those register_*_hook() seems to be in plugin_basename().

Alternative for plugins_url filter that converts plugin realpath to WP plugin path is naturally to make all symlinked plugins to create their URL differently, e.g. using plugin WPMU_PLUGIN_URL and WP_PLUGIN_URL.

comment:10 scribu2 years ago

Well, anything that uses plugin_basename(__FILE__) won't work properly and can't be fixed by hooking into 'plugins_url'.

Changing each plugin to pass to plugin_basename() what it would expect is not a good solution.

Hence the proposed filter, which would allow to handle this from a single point.

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

comment:11 scribu2 years ago

  • Keywords commit removed

An alternative solution would be to allow plugins from multiple directories, like we do for themes.

augustash23 months ago

Fix for correctly extracting the local directory and filename regardless if the file comes from a symlink or not

comment:12 augustash23 months ago

  • Resolution set to worksforme
  • Status changed from reopened to closed

the above patch has been tested in our local and production environments. The patch is really simple as all that's needed is to use the basename() and dirname() functions to extract the local directory and filename from the passed in file.

dirname doc = http://www.php.net/manual/en/function.dirname.php

basename doc = http://www.php.net/manual/en/function.basename.php

or use pathinfo to get all the information = http://www.php.net/manual/en/function.pathinfo.php

Last edited 23 months ago by augustash (previous) (diff)

comment:13 scribu23 months ago

  • Resolution worksforme deleted
  • Status changed from closed to reopened

The ticket will be closed automatically when a fix is commited.

What's up with WP_fix_for_plugin_basename_to_allow_symlinks_2011-07-28.2.patch?

comment:14 follow-up: yincrash22 months ago

this patch is incorrect. it only works two levels deep. my issue is that that a lot of the jetpack modules are acting screwy when symlinked, and their JS and CSS files are 3/4 levels deep in the plugins folder.

comment:15 yincrash22 months ago

  • Cc yincrash added

comment:16 in reply to: ↑ 14 augustash21 months ago

To clarify, the WP_fix_for_plugin_basename_to_allow_symlinks_2011-07-28.2.patch is a patch to the WP_fix_for_plugin_basename_to_allow_symlinks_2011-07-28.patch so apply the WP_fix_for_plugin_basename_to_allow_symlinks_2011-07-28.patch first then the WP_fix_for_plugin_basename_to_allow_symlinks_2011-07-28.2.patch.

Replying to yincrash:

this patch is incorrect. it only works two levels deep. my issue is that that a lot of the jetpack modules are acting screwy when symlinked, and their JS and CSS files are 3/4 levels deep in the plugins folder.

This patch is not incorrect. You're welcome to contribute to it if you have found scenarios that have nested paths, but I'll bet those scenarios are easily solved by using relative paths to the directory determined by this plugin basename patch.

comment:17 paulschreiber18 months ago

  • Cc paulschreiber@… added

I keep getting bit by this and end up patching individual plugins (Raw HTML Pro, Advanced Custom Fields, More Fields, Post Page Association, etc.) They end up generating bad paths to CSS and JS — the <link> and <script> tags have the filesystem path embedded in them.

It would be nice if WordPress' plugins_url() did the right thing out of the box.

There are other complaints about symlinked plugins:

I urged you to seriously consider fixing this for 3.3.x or 3.4.

Last edited 18 months ago by paulschreiber (previous) (diff)

comment:18 boonebgorges15 months ago

  • Cc boonebgorges@… added

comment:19 DJPaul15 months ago

  • Cc djpaul@… added

comment:20 CoenJacobs14 months ago

  • Cc coenjacobs@… added

comment:21 lkraav14 months ago

  • Cc lkraav added

comment:22 gandhiano14 months ago

Had the same issue - symlinks are defintely important if one is managing a complex WP farm.

Applying the patches did solve the issue, so I propose that these are included upstream.

comment:23 scribu12 months ago

  • Owner scribu deleted
  • Status changed from reopened to assigned

comment:24 simonwheatley11 months ago

  • Cc simon@… added

comment:25 leewillis7711 months ago

  • Cc junk@… added

comment:26 juzzin11 months ago

  • Cc juzzin added

mitchoyoshitaka11 months ago

Alternative approach, which caches symlink targets and replaces them

comment:27 mitchoyoshitaka11 months ago

  • Cc mitcho@… added

Patching up plugin_basename() (or allowing for that patching) isn't the only issue here; symlinked files will show up in the plugins listing in wp-admin, but symlinked directories are not. I just attached a patch which also touches get_plugins() to support this.

My fix to plugin_basename() is also a little more robust, but perhaps more overhead than wanted. I hope this patch is helpful for others.

In the mean time, if this functionality should come as a plugin, scribu's pre_plugin_basename isn't the only hook needed; a filter in get_plugins() will also be necessary. If these filters are added, I'd be happy to release a "Symlink Plugins Support" plugin.

comment:28 kchrist10 months ago

  • Cc kenn@… added

comment:29 rhertzog9 months ago

  • Cc hertzog@… added

FTR, this feature is also needed for the setup picked by the official Debian package where we wanted to have packaged plugins in and manually installed plugins kept in two different directories (which is not really possible, so we put symlink to packaged plugins in the directory which is controlled by the local administrator). It would thus be nice to see this issue fixed.

See http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=686228 for a report of this bug on the Debian side.

comment:30 kwisatz8 months ago

  • Cc kwisatz added

Weirdly enough, while augustash's patch works for me (debian wp 3.4.2 package), mitchoyoshitaka's (included in there) does not.

comment:31 kchrist8 months ago

This is also an issue for WordPress sites deployed via Capistrano.

Capistrano keeps previous deployments of the site so you can easily roll back changes. So the actual filesystem path to the site might be:

/home/username/web/example.com/releases/20121018101521/public

But it's symlinked as:

/home/username/web/example.com/current/public

The latter is the path in my web server config. The current directory is a symlink to the latest deployment, which is actually the datestamped releases/20121018101521 directory.

As a result, the entire site is in a symlinked directory. This isn't an issue for most plugins but it's come up more than once in my experience running a handful of sites this way.

comment:32 follow-up: scribu6 months ago

#22802 was marked as a duplicate.

comment:33 toscho6 months ago

  • Cc info@… added

comment:34 in reply to: ↑ 32 MikeSchinkel6 months ago

Replying to scribu:

#22802 was marked as a duplicate.

#22802 is related, but it's not a duplicate.

comment:35 scribu6 months ago

Since #22802 was reopened anyway, let's keep this ticket focused on a solution that would enable symlinked plugins without plugin authors having to do anything special.

comment:36 kawauso5 months ago

  • Cc kawauso added

comment:37 synapticism5 months ago

  • Cc synapticism added

comment:38 markus.magnuson5 months ago

  • Cc markus.magnuson@… added

comment:39 follow-up: markus.magnuson5 months ago

I resolved all my plugin problems caused by symlinks in the wp-content path by using the realpath() function in my wp-config.php. I simply replace this:

define('WP_CONTENT_DIR',$_SERVER['DOCUMENT_ROOT'] . '/wp-content');

With this:

define('WP_CONTENT_DIR', realpath($_SERVER['DOCUMENT_ROOT'] . '/wp-content'));

Thought that might help anyone ending up in this ticket when troubleshooting.

comment:40 cklosows5 months ago

  • Cc cklosowski@… added

comment:41 MikeSchinkel4 months ago

  • Cc mike@… added
  • Keywords dev-feedback added
  • Version set to trunk

I've uploaded a path with another approach. The patch hooks 'pre_update_option_active_plugins' so anytime 'active_plugins' is saved it finds which active plugins have been symlinked and saves an array to a 'symlinked_plugins' key of wp_options. The array has an element for each of the symlinked plugins with the array key being realpath( $virtual_plugin_file ) and the value being virtual_plugin_file, i.e.

update_options( 'symlinked_plugins', array(
   '/Users/user/Plugins/my-plugin-1/my-plugin-1.php' =>  
   '/Users/user/Sites/test/wp-content/plugins/my-plugin-1/my-plugin-1.php',
 
   '/Users/user/Plugins/my-plugin-2/my-plugin-2.php' => 
   '/Users/user/Sites/test/wp-content/plugins/my-plugin-2/my-plugin-2.php',
));

Then plugins_url() simply checks to see if the value of the $plugin parameter -- which is typically passed in as __FILE__ -- is a key in the array returned by get_option( 'symlinked_plugins' ) and if yes it assigns $plugin with that array element's value.

I think that's all it takes. I've been circling this problem for a while so maybe this is all we need to enable plugins_url() to work correctly with symlinked plugins. But I might have missed a requirement and if so will need others to point out what I've missed.

Also please consider this a starting point for discussion; I don't expect that it's completely ready for production, but crossing fingers it might be.

MikeSchinkel4 months ago

Patch to enable plugin_url() to support symlinked plugins.

comment:42 follow-up: scribu4 months ago

Re symlinked_plugins.diff: the check you added to plugin_url() should be moved to plugin_basename(), since that's the more general function (and also the one that doesn't have a filter).

comment:43 in reply to: ↑ 42 MikeSchinkel4 months ago

Replying to scribu:

Re symlinked_plugins.diff: the check you added to plugin_url() should be moved to plugin_basename(), since that's the more general function (and also the one that doesn't have a filter).

Originally I didn't put in plugin_basename() because it looked like that would not have worked but I just tested and you are correct, it works. I also tested it with a symlinked plugin where the virtual directory was different than the real directory and it still worked correctly, i.e. my-plugin-dev/my-plugin.php vs. my-plugin/my-plugin.php:

Virtual Path => /Users/my-user/Site/my-site/wp-content/plugins/my-plugin-dev/my-plugin.php 
Real Path    => /Users/my-user/Plugins/my-plugin/my-plugin.php 

Attached is a new patch with the check moved to plugins_url().

Version 0, edited 4 months ago by MikeSchinkel (next)

MikeSchinkel4 months ago

2nd attempt at patch to enable plugin_url() to support symlinked plugins.

comment:44 anointed4 months ago

  • Cc imtiedup@… added

comment:45 johnnyb3 months ago

  • Cc johnbeales@… added

comment:46 beaver68133 months ago

  • Cc beaver6813 added

comment:47 lingfish3 months ago

  • Cc jason@… added

comment:48 SergeyBiryukov3 months ago

  • Version trunk deleted

comment:49 ddean2 months ago

  • Cc david@… added

comment:50 anointed2 months ago

This has been working pretty well over the past few months.

Is there any chance of this making it into 3.6 so that I can update WordPress without fear of loosing all these code changes?

It has made a HUGE difference for me on the organization of my server files and makes maintaining dozens of sites on a single server so much easier. I no longer have to worry about any clients getting lazy and not updating their plugins and potentially leading to a security breach.

Has anyone at all come up with a problem that would cause this not to work properly in time for 3.6?

*If for some reason it can't be done in time, is it possible to turn this into an 'mu' plugin in the meantime?

comment:51 luv4wp8 weeks ago

Looks like MikeSchinkels code works for me.

Agree with everyone else here, please add this to the core. Putting it into 3.6 would be great so we don't have to wait 6 months.

MikeSchinkel8 weeks ago

pre-plugin-basename.diff

comment:52 follow-up: MikeSchinkel8 weeks ago

If adding my patch to the core is too much for 3.6, can you please consider adding a 'pre_plugin_basename' hook that would allow us to create an mu-plugin to do the rest? I've attached a patch for that, above.

comment:53 in reply to: ↑ 52 anointed8 weeks ago

Replying to MikeSchinkel:

If adding my patch to the core is too much for 3.6, can you please consider adding a 'pre_plugin_basename' hook that would allow us to create an mu-plugin to do the rest? I've attached a patch for that, above.

+1 if we can't have a core patch, this would work just fine for now!

comment:54 follow-up: SergeyBiryukov8 weeks ago

16953.diff could go in before the beta, but we don't typically add new hooks after feature freeze.

comment:55 in reply to: ↑ 54 MikeSchinkel8 weeks ago

Replying to SergeyBiryukov:

16953.diff could go in before the beta, but we don't typically add new hooks after feature freeze.

Given how this ticket has been languishing for 2 years with lots of interest and having come full circle, it sure would be nice. :)

comment:56 corvannoorloos8 weeks ago

  • Cc cor@… added

comment:57 in reply to: ↑ 39 ; follow-up: aubreypwd6 weeks ago

I can confirm that the below solution worked for me in using:

define('WP_PLUGIN_DIR', realpath('/home/152858/domains/example.com/html/all/wp-content/plugins') );
define('WP_PLUGIN_URL', 'http://example.com/all/wp-content/plugins');
define('WP_THEMES_DIR', realpath('/home/152858/domains/example.com/html/all/wp-content/themes') );
define('WP_THEMES_URL', 'http://example.com/all/wp-content/themes');
define('WP_CONTENT_DIR', realpath('/home/152858/domains/example.com/html/all/wp-content') );
define('WP_CONTENT_URL', 'http://example.com/all/wp-content');

Replying to markus.magnuson:

I resolved all my plugin problems caused by symlinks in the wp-content path by using the realpath() function in my wp-config.php. I simply replace this:

define('WP_CONTENT_DIR',$_SERVER['DOCUMENT_ROOT'] . '/wp-content');

With this:

define('WP_CONTENT_DIR', realpath($_SERVER['DOCUMENT_ROOT'] . '/wp-content'));

Thought that might help anyone ending up in this ticket when troubleshooting.

comment:58 JeromeC6 weeks ago

  • Cc JeromeC added

comment:59 francescolaffi5 weeks ago

  • Cc francesco.laffi@… added

comment:60 rodrigosprimo5 weeks ago

  • Cc rodrigosprimo@… added

comment:61 andyadams4 weeks ago

  • Cc andyadamscp@… added

comment:62 andkrup3 weeks ago

  • Cc andkrup added

comment:63 tollmanz3 weeks ago

  • Cc tollmanz@… added

comment:64 in reply to: ↑ 57 mikezielonka3 weeks ago

Worked for me as well.

Replying to aubreypwd:

I can confirm that the below solution worked for me in using:

define('WP_PLUGIN_DIR', realpath('/home/152858/domains/example.com/html/all/wp-content/plugins') );
define('WP_PLUGIN_URL', 'http://example.com/all/wp-content/plugins');
define('WP_THEMES_DIR', realpath('/home/152858/domains/example.com/html/all/wp-content/themes') );
define('WP_THEMES_URL', 'http://example.com/all/wp-content/themes');
define('WP_CONTENT_DIR', realpath('/home/152858/domains/example.com/html/all/wp-content') );
define('WP_CONTENT_URL', 'http://example.com/all/wp-content');

Replying to markus.magnuson:

I resolved all my plugin problems caused by symlinks in the wp-content path by using the realpath() function in my wp-config.php. I simply replace this:

define('WP_CONTENT_DIR',$_SERVER['DOCUMENT_ROOT'] . '/wp-content');

With this:

define('WP_CONTENT_DIR', realpath($_SERVER['DOCUMENT_ROOT'] . '/wp-content'));

Thought that might help anyone ending up in this ticket when troubleshooting.

nacin3 weeks ago

comment:65 follow-up: nacin3 weeks ago

I didn't see until now there was a consensus on a simple filter. My question: What's more useful? pre_plugin_basename, or one just before the return? I'd think the final filter would be more useful, since it smooths out the processing?

comment:66 in reply to: ↑ 65 ; follow-up: MikeSchinkel12 days ago

Replying to nacin:

My question: What's more useful? pre_plugin_basename, or one just before the return? I'd think the final filter would be more useful, since it smooths out the processing?

Yes, given that you are passing in both a post-processed value and a pre-processed value a 'plugin_basename' would definitely be more generally useful and also still meet the need stated on comment 52 of this thread.

Last edited 12 days ago by MikeSchinkel (previous) (diff)

comment:67 in reply to: ↑ 66 MikeSchinkel12 days ago

Replying to MikeSchinkel:

Replying to nacin:

My question: What's more useful? pre_plugin_basename, or one just before the return? I'd think the final filter would be more useful, since it smooths out the processing?

Yes, given that you are passing in both a post-processed value and a pre-processed value a 'plugin_basename' would definitely be more generally useful and also still meet the need stated on comment 52 of this thread.

UPDATE: Except, the hook would have to duplicate the logic in lines 567-574 in 16953.2.diff​ when changing the path, so when the goal is to meet the need stated on comment 52 a 'pre_plugin_basename' would be a better choice because it would limit the potential for error on the part of the plugin author.

Note: See TracTickets for help on using tickets.