Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
77.78% covered (warning)
77.78%
56 / 72
50.00% covered (danger)
50.00%
5 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Plugin_Storage
77.78% covered (warning)
77.78%
56 / 72
50.00% covered (danger)
50.00%
5 / 10
39.88
0.00% covered (danger)
0.00%
0 / 1
 upsert
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get_one
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 get_all
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 delete
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 ensure_configured
57.14% covered (warning)
57.14%
4 / 7
0.00% covered (danger)
0.00%
0 / 1
6.97
 configure
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 set_flag_to_refresh_active_connected_plugins
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 maybe_update_active_connected_plugins
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
7.01
 update_active_plugins_option
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
4.37
 update_active_plugins_wpcom_no_sync_fallback
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Storage for plugin connection information.
4 *
5 * @package automattic/jetpack-connection
6 */
7
8namespace Automattic\Jetpack\Connection;
9
10use Jetpack_Options;
11use WP_Error;
12
13/**
14 * The class serves a single purpose - to store the data which plugins use the connection, along with some auxiliary information.
15 */
16class Plugin_Storage {
17
18    const ACTIVE_PLUGINS_OPTION_NAME = 'jetpack_connection_active_plugins';
19
20    /**
21     * Transient name used as flag to indicate that the active connected plugins list needs refreshing.
22     */
23    const ACTIVE_PLUGINS_REFRESH_FLAG = 'jetpack_connection_active_plugins_refresh';
24
25    /**
26     * Whether this class was configured for the first time or not.
27     *
28     * @var boolean
29     */
30    private static $configured = false;
31
32    /**
33     * Connected plugins.
34     *
35     * @var array
36     */
37    private static $plugins = array();
38
39    /**
40     * The blog ID the storage is setup for.
41     * The data will be refreshed if the blog ID changes.
42     * Used for the multisite networks.
43     *
44     * @var int
45     */
46    private static $current_blog_id = null;
47
48    /**
49     * Add or update the plugin information in the storage.
50     *
51     * @param string $slug Plugin slug.
52     * @param array  $args Plugin arguments, optional.
53     *
54     * @return bool
55     */
56    public static function upsert( $slug, array $args = array() ) {
57        self::$plugins[ $slug ] = $args;
58
59        return true;
60    }
61
62    /**
63     * Retrieve the plugin information by slug.
64     * WARNING: the method cannot be called until Plugin_Storage::configure is called, which happens on plugins_loaded
65     * Even if you don't use Jetpack Config, it may be introduced later by other plugins,
66     * so please make sure not to run the method too early in the code.
67     *
68     * @param string $slug The plugin slug.
69     *
70     * @return array|null|WP_Error
71     */
72    public static function get_one( $slug ) {
73        $plugins = self::get_all();
74
75        if ( $plugins instanceof WP_Error ) {
76            return $plugins;
77        }
78
79        return empty( $plugins[ $slug ] ) ? null : $plugins[ $slug ];
80    }
81
82    /**
83     * Retrieve info for all plugins that use the connection.
84     * WARNING: the method cannot be called until Plugin_Storage::configure is called, which happens on plugins_loaded
85     * Even if you don't use Jetpack Config, it may be introduced later by other plugins,
86     * so please make sure not to run the method too early in the code.
87     *
88     * @return array|WP_Error
89     */
90    public static function get_all() {
91        $maybe_error = self::ensure_configured();
92
93        if ( $maybe_error instanceof WP_Error ) {
94            return $maybe_error;
95        }
96
97        return self::$plugins;
98    }
99
100    /**
101     * Remove the plugin connection info from Jetpack.
102     * WARNING: the method cannot be called until Plugin_Storage::configure is called, which happens on plugins_loaded
103     * Even if you don't use Jetpack Config, it may be introduced later by other plugins,
104     * so please make sure not to run the method too early in the code.
105     *
106     * @param string $slug The plugin slug.
107     *
108     * @return bool|WP_Error
109     */
110    public static function delete( $slug ) {
111        $maybe_error = self::ensure_configured();
112
113        if ( $maybe_error instanceof WP_Error ) {
114            return $maybe_error;
115        }
116
117        if ( array_key_exists( $slug, self::$plugins ) ) {
118            unset( self::$plugins[ $slug ] );
119        }
120
121        return true;
122    }
123
124    /**
125     * The method makes sure that `Jetpack\Config` has finished, and it's now safe to retrieve the list of plugins.
126     *
127     * @return bool|WP_Error
128     */
129    private static function ensure_configured() {
130        if ( ! self::$configured ) {
131            return new WP_Error( 'too_early', __( 'You cannot call this method until Jetpack Config is configured', 'jetpack-connection' ) );
132        }
133
134        if ( is_multisite() && get_current_blog_id() !== self::$current_blog_id ) {
135            if ( self::$current_blog_id ) {
136                // If blog ID got changed, pull the list of active plugins for that blog from the database.
137                self::$plugins = (array) get_option( self::ACTIVE_PLUGINS_OPTION_NAME, array() );
138            }
139            self::$current_blog_id = get_current_blog_id();
140        }
141
142        return true;
143    }
144
145    /**
146     * Called once to configure this class after plugins_loaded.
147     *
148     * @return void
149     */
150    public static function configure() {
151        if ( self::$configured ) {
152            return;
153        }
154
155        self::$configured = true;
156
157        add_action( 'update_option_active_plugins', array( __CLASS__, 'set_flag_to_refresh_active_connected_plugins' ) );
158
159        self::maybe_update_active_connected_plugins();
160    }
161
162    /**
163     * Set a flag to indicate that the active connected plugins list needs to be updated.
164     * This will happen when the `active_plugins` option is updated.
165     *
166     * @see configure
167     */
168    public static function set_flag_to_refresh_active_connected_plugins() {
169        set_transient( self::ACTIVE_PLUGINS_REFRESH_FLAG, time() );
170    }
171
172    /**
173     * Determine if we need to update the active connected plugins list.
174     */
175    public static function maybe_update_active_connected_plugins() {
176        $maybe_error = self::ensure_configured();
177
178        if ( $maybe_error instanceof WP_Error ) {
179            return;
180        }
181        // Only attempt to update the option if the corresponding flag is set.
182        if ( ! get_transient( self::ACTIVE_PLUGINS_REFRESH_FLAG ) ) {
183            return;
184        }
185        // Only attempt to update the option on POST requests.
186        // This will prevent the option from being updated multiple times due to concurrent requests.
187        if ( ! ( isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] ) ) {
188            return;
189        }
190
191        delete_transient( self::ACTIVE_PLUGINS_REFRESH_FLAG );
192
193        if ( is_multisite() ) {
194            self::$current_blog_id = get_current_blog_id();
195        }
196
197        // If a plugin was activated or deactivated.
198        // self::$plugins is populated in Config::ensure_options_connection().
199        $configured_plugin_keys = array_keys( self::$plugins );
200        $stored_plugin_keys     = array_keys( (array) get_option( self::ACTIVE_PLUGINS_OPTION_NAME, array() ) );
201        sort( $configured_plugin_keys );
202        sort( $stored_plugin_keys );
203
204        if ( $configured_plugin_keys !== $stored_plugin_keys ) {
205            self::update_active_plugins_option();
206        }
207    }
208
209    /**
210     * Updates the active plugins option with current list of active plugins.
211     *
212     * @return void
213     */
214    public static function update_active_plugins_option() {
215        // Note: Since this option is synced to wpcom, if you change its structure, you have to update the sanitizer at wpcom side.
216        update_option( self::ACTIVE_PLUGINS_OPTION_NAME, self::$plugins );
217        if ( ! class_exists( 'Automattic\Jetpack\Sync\Settings' ) || ! \Automattic\Jetpack\Sync\Settings::is_sync_enabled() ) {
218            self::update_active_plugins_wpcom_no_sync_fallback();
219            // Remove the checksum for active plugins, so it gets recalculated when sync gets activated.
220            $jetpack_callables_sync_checksum = Jetpack_Options::get_raw_option( 'jetpack_callables_sync_checksum' );
221            if ( isset( $jetpack_callables_sync_checksum['jetpack_connection_active_plugins'] ) ) {
222                unset( $jetpack_callables_sync_checksum['jetpack_connection_active_plugins'] );
223                Jetpack_Options::update_raw_option( 'jetpack_callables_sync_checksum', $jetpack_callables_sync_checksum );
224            }
225        }
226    }
227
228    /**
229     * Update active plugins option with current list of active plugins on WPCOM.
230     * This is a fallback to ensure this option is always up to date on WPCOM in case
231     * Sync is not present or disabled.
232     *
233     * @since 1.34.0
234     */
235    private static function update_active_plugins_wpcom_no_sync_fallback() {
236        $connection = new Manager();
237        if ( ! $connection->is_connected() ) {
238            return;
239        }
240
241        $site_id = \Jetpack_Options::get_option( 'id' );
242
243        $body = wp_json_encode(
244            array(
245                'active_connected_plugins' => self::$plugins,
246            ),
247            JSON_UNESCAPED_SLASHES
248        );
249
250        Client::wpcom_json_api_request_as_blog(
251            sprintf( '/sites/%d/jetpack-active-connected-plugins', $site_id ),
252            '2',
253            array(
254                'headers' => array( 'content-type' => 'application/json' ),
255                'method'  => 'POST',
256            ),
257            $body,
258            'wpcom'
259        );
260    }
261}