1 | <?php |
---|
2 | namespace Mesoconcepts\WP\Plugin\MUPluginLoader; |
---|
3 | |
---|
4 | /* |
---|
5 | * Plugin Name: MU Plugin Loader |
---|
6 | * Plugin URI: http://www.mesoconcepts.com |
---|
7 | * Description: Automatically loads, and checks for updates of, MU plugins in subfolders |
---|
8 | * Author: Denis de Bernardy |
---|
9 | * Author URI: http://www.mesoconcepts.com |
---|
10 | * Version: 0.1-alpha |
---|
11 | */ |
---|
12 | |
---|
13 | /** |
---|
14 | * WP mu-plugin loader |
---|
15 | * |
---|
16 | * References: |
---|
17 | * |
---|
18 | * - https://core.trac.wordpress.org/ticket/14359 |
---|
19 | * |
---|
20 | * @author Denis de Bernardy |
---|
21 | **/ |
---|
22 | class MUPluginLoaderPlugin |
---|
23 | { |
---|
24 | /** |
---|
25 | * The plugin files |
---|
26 | * |
---|
27 | * @var array $plugin_files |
---|
28 | **/ |
---|
29 | protected $plugin_files; |
---|
30 | |
---|
31 | |
---|
32 | /** |
---|
33 | * The full plugin data as returned by WP |
---|
34 | **/ |
---|
35 | protected $plugin_data; |
---|
36 | |
---|
37 | |
---|
38 | /** |
---|
39 | * Constructor |
---|
40 | **/ |
---|
41 | public function __construct() |
---|
42 | { |
---|
43 | $this->findPlugins(); |
---|
44 | |
---|
45 | add_filter('http_request_args', array($this, 'injectMUPluginHttpArgs'), 10, 2); |
---|
46 | add_filter('pre_set_site_transient_update_plugins', array($this, 'stripMUPluginPackageUrls')); |
---|
47 | |
---|
48 | add_filter('pre_set_transient_plugin_slugs', array($this, 'injectMUPluginSlugs')); |
---|
49 | |
---|
50 | if ($this->inPluginAdmin()) { |
---|
51 | add_filter('plugin_action_links', array($this, 'injectMUPluginActionLinks'), 10, 4); |
---|
52 | add_filter('network_admin_plugin_action_links', array($this, 'injectMUPluginActionLinks'), 10, 4); |
---|
53 | } |
---|
54 | } |
---|
55 | |
---|
56 | |
---|
57 | /** |
---|
58 | * Injects MU plugin data during update checks |
---|
59 | * |
---|
60 | * @param array $args |
---|
61 | * @param string $url |
---|
62 | * @return array $args |
---|
63 | **/ |
---|
64 | public function injectMUPluginHttpArgs($args, $url) |
---|
65 | { |
---|
66 | # We only care about plugin update checks |
---|
67 | if ($url != 'https://api.wordpress.org/plugins/update-check/1.1/') { |
---|
68 | return $args; |
---|
69 | } |
---|
70 | |
---|
71 | $this->findPlugins(true); |
---|
72 | $mu_plugins = $this->plugin_data; |
---|
73 | $to_send = json_decode($args['body']['plugins']); |
---|
74 | $plugins = (object) array_merge((array) $to_send->plugins, (array) $this->plugin_data); |
---|
75 | $to_send->plugins = $plugins; |
---|
76 | $args['body']['plugins'] = json_encode($to_send); |
---|
77 | |
---|
78 | return $args; |
---|
79 | } |
---|
80 | |
---|
81 | |
---|
82 | /** |
---|
83 | * Strips package URIs from MU Plugin update responses, to disable auto-updates |
---|
84 | * |
---|
85 | * @param object $value The old update_plugins transient value |
---|
86 | * @return object $value The new update_plugins transient value |
---|
87 | **/ |
---|
88 | public function stripMUPluginPackageUrls($value) |
---|
89 | { |
---|
90 | if (!isset($value->response)) { |
---|
91 | return $value; |
---|
92 | } |
---|
93 | |
---|
94 | $this->findPlugins(true); |
---|
95 | $mu_plugins = array_keys($this->plugin_data); |
---|
96 | $responses = $value->response; |
---|
97 | |
---|
98 | foreach ($value->response as $plugin => $response) { |
---|
99 | if (in_array($plugin, $mu_plugins)) { |
---|
100 | unset($response->package); |
---|
101 | } |
---|
102 | } |
---|
103 | |
---|
104 | return $value; |
---|
105 | } |
---|
106 | |
---|
107 | |
---|
108 | /** |
---|
109 | * Injects our extra plugins in the $plugins global (!) when in the plugins screen |
---|
110 | * |
---|
111 | * This shenanigan is fragile and subject to break if WP ever changes its |
---|
112 | * API, but we've no adequate filter at our disposal... |
---|
113 | * |
---|
114 | * @param array $plugin_slugs |
---|
115 | * @return array $plugin_slugs |
---|
116 | **/ |
---|
117 | public function injectMUPluginSlugs($plugin_slugs) |
---|
118 | { |
---|
119 | if (!$this->inPluginAdmin()) { |
---|
120 | return $plugin_slugs; |
---|
121 | } |
---|
122 | |
---|
123 | global $plugins; |
---|
124 | |
---|
125 | # Bail early if WP ever changes its API |
---|
126 | if (!isset($plugins) || !isset($plugins['mustuse'])) { |
---|
127 | return $plugin_slugs; |
---|
128 | } |
---|
129 | |
---|
130 | # Merge our plugins |
---|
131 | $mu_plugins = array_merge($plugins['mustuse'], $this->plugin_data); |
---|
132 | uasort($mu_plugins, function($a, $b) { |
---|
133 | return strnatcasecmp($a['Name'], $b['Name']); |
---|
134 | }); |
---|
135 | $plugins['mustuse'] = $mu_plugins; |
---|
136 | |
---|
137 | if (current_user_can('update_plugins')) { |
---|
138 | $current = get_site_transient('update_plugins'); |
---|
139 | foreach ($plugins['mustuse'] as $plugin_file => $plugin_data ) { |
---|
140 | if (isset($current->response[$plugin_file])) { |
---|
141 | $plugins['mustuse'][$plugin_file]['update'] = true; |
---|
142 | $plugins['upgrade'][$plugin_file] = $plugins['mustuse'][$plugin_file]; |
---|
143 | } |
---|
144 | } |
---|
145 | } |
---|
146 | |
---|
147 | return $plugin_slugs; |
---|
148 | } |
---|
149 | |
---|
150 | |
---|
151 | /** |
---|
152 | * Disables actions and the checkbox in the plugin upgrade table |
---|
153 | * |
---|
154 | * @param array $actions |
---|
155 | * @param string $plugin_file |
---|
156 | * @param array $plugin_data |
---|
157 | * @param string $context |
---|
158 | * @return array $actions |
---|
159 | **/ |
---|
160 | public function injectMUPluginActionLinks($actions, $plugin_file, $plugin_data, $context) |
---|
161 | { |
---|
162 | # Bail immediately if we're not interested |
---|
163 | if ($context != 'upgrade' || !isset($this->plugin_data[$plugin_file])) { |
---|
164 | return $actions; |
---|
165 | } |
---|
166 | else { |
---|
167 | # Let WP localize this as needed |
---|
168 | $label = _n('Must-Use <span class="count">(%s)</span>', 'Must-Use <span class="count">(%s)</span>', 1); |
---|
169 | $label = trim(preg_replace("#<.+>#", '', $label)); |
---|
170 | # This id is subject to change with WP versions |
---|
171 | $cb = "checkbox_" . md5($plugin_data['Name']); |
---|
172 | $script = <<<EOS |
---|
173 | |
---|
174 | <script> |
---|
175 | var $cb = jQuery("#$cb"); |
---|
176 | $cb.parents('tr.inactive:first').removeClass('inactive').addClass('active'); |
---|
177 | $cb.replaceWith(''); |
---|
178 | </script> |
---|
179 | EOS; |
---|
180 | return array( |
---|
181 | 'mustuse-upgrade' => $label . $script, |
---|
182 | ); |
---|
183 | } |
---|
184 | |
---|
185 | return $actions; |
---|
186 | } |
---|
187 | |
---|
188 | |
---|
189 | /** |
---|
190 | * Whether we're in a plugin admin area or not |
---|
191 | * |
---|
192 | * @return boolean $in_plugin_admin |
---|
193 | **/ |
---|
194 | protected function inPluginAdmin() |
---|
195 | { |
---|
196 | foreach (array( |
---|
197 | '/wp-admin/plugins.php', |
---|
198 | '/wp-admin/network/plugins.php', |
---|
199 | '/wp-admin/update-core.php', |
---|
200 | '/wp-admin/network/update-core.php', |
---|
201 | ) as $url_pattern) { |
---|
202 | if (strpos($_SERVER['REQUEST_URI'], $url_pattern) !== false) return true; |
---|
203 | } |
---|
204 | return false; |
---|
205 | } |
---|
206 | |
---|
207 | |
---|
208 | /** |
---|
209 | * Returns mu plugins in subfolders |
---|
210 | * |
---|
211 | * Plugins must be loaded in the global scope |
---|
212 | * |
---|
213 | * References: |
---|
214 | * |
---|
215 | * - https://core.trac.wordpress.org/ticket/22802 |
---|
216 | * |
---|
217 | * @return array $plugin_files |
---|
218 | **/ |
---|
219 | public function getPluginFiles() |
---|
220 | { |
---|
221 | return $this->plugin_files; |
---|
222 | } |
---|
223 | |
---|
224 | /** |
---|
225 | * Finds mu plugins in subfolders |
---|
226 | * |
---|
227 | * @param boolean @fetch_data |
---|
228 | **/ |
---|
229 | protected function findPlugins($fetch_data = false) |
---|
230 | { |
---|
231 | # Don't do this more than once |
---|
232 | if ($this->plugin_files && ($this->plugin_data || !$fetch_data)) { |
---|
233 | return; |
---|
234 | } |
---|
235 | |
---|
236 | $plugins = false; |
---|
237 | |
---|
238 | # Fetch cached plugins unless we're in the plugins area |
---|
239 | if (!$this->inPluginAdmin() && ($this->plugin_data || !$fetch_data)) { |
---|
240 | $plugins = get_site_transient('mu_plugins_loader'); |
---|
241 | } |
---|
242 | |
---|
243 | # Invalidate cache if anything is going to break |
---|
244 | if ($plugins !== false) { |
---|
245 | foreach ($plugins as $plugin) { |
---|
246 | if (!is_readable(__DIR__ . DIRECTORY_SEPARATOR . $plugin)) { |
---|
247 | $plugins = false; |
---|
248 | break; |
---|
249 | } |
---|
250 | } |
---|
251 | } |
---|
252 | |
---|
253 | # Build cache if needed |
---|
254 | if ($plugins === false) { |
---|
255 | if (!function_exists('get_plugins')) { |
---|
256 | require ABSPATH . 'wp-admin/includes/plugin.php'; |
---|
257 | } |
---|
258 | $plugin_data = get_plugins('/../mu-plugins'); |
---|
259 | |
---|
260 | # Keep a copy of the raw data if it's needed or requested |
---|
261 | if ($this->inPluginAdmin() || $fetch_data) { |
---|
262 | $this->plugin_data = $plugin_data; |
---|
263 | } |
---|
264 | |
---|
265 | $plugins = array_keys($plugin_data); |
---|
266 | $plugins = array_filter($plugins, function($plugin) { |
---|
267 | return dirname($plugin) != '.'; |
---|
268 | }); |
---|
269 | |
---|
270 | # Cache for future use |
---|
271 | set_site_transient('mu_plugins_loader', $plugins); |
---|
272 | } |
---|
273 | |
---|
274 | # Cache the results |
---|
275 | $this->plugin_files = $plugins; |
---|
276 | } |
---|
277 | } # END class |
---|
278 | |
---|
279 | # Bootstrap plugin |
---|
280 | global $mu_plugin_loader_plugin; |
---|
281 | $mu_plugin_loader_plugin = new MUPluginLoaderPlugin; |
---|
282 | |
---|
283 | # Load MU plugins |
---|
284 | foreach ($mu_plugin_loader_plugin->getPluginFiles() as $extra_mu_plugin) { |
---|
285 | include_once __DIR__ . DIRECTORY_SEPARATOR . $extra_mu_plugin; |
---|
286 | } |
---|
287 | unset($extra_mu_plugin); |
---|