Make WordPress Core

Ticket #27124: mc-muplugin-loader.php

File mc-muplugin-loader.php, 8.0 KB (added by Denis-de-Bernardy, 11 years ago)
Line 
1<?php
2namespace 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 **/
22class 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>
175var $cb = jQuery("#$cb");
176$cb.parents('tr.inactive:first').removeClass('inactive').addClass('active');
177$cb.replaceWith('');
178</script>
179EOS;
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
280global $mu_plugin_loader_plugin;
281$mu_plugin_loader_plugin = new MUPluginLoaderPlugin;
282
283# Load MU plugins
284foreach ($mu_plugin_loader_plugin->getPluginFiles() as $extra_mu_plugin) {
285    include_once __DIR__ . DIRECTORY_SEPARATOR . $extra_mu_plugin;
286}
287unset($extra_mu_plugin);