Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 136
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Autoupdate
0.00% covered (danger)
0.00%
0 / 133
0.00% covered (danger)
0.00%
0 / 13
3782
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 autoupdate_translation
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
90
 autoupdate_theme
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 autoupdate_core
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 expect
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 automatic_updates_complete
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 get_log
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 log_items
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 bump_stats
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
42
 get_successful_updates
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 get_possible_failures
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
132
 get_plugin_slug
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
72
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2/**
3 * Handles items that have been selected for automatic updates.
4 * Hooks into WP_Automatic_Updater
5 *
6 * @package automattic/jetpack
7 */
8
9if ( ! defined( 'ABSPATH' ) ) {
10    exit( 0 );
11}
12
13/**
14 * Handles items that have been selected for automatic updates.
15 * Hooks into WP_Automatic_Updater
16 */
17class Jetpack_Autoupdate {
18
19    /**
20     * Results.
21     *
22     * @var array
23     */
24    private $results = array();
25
26    /**
27     * Expected updates.
28     *
29     * @var array
30     */
31    private $expected = array();
32
33    /**
34     * Successful updates.
35     *
36     * @var array
37     */
38    private $success = array(
39        'plugin' => array(),
40        'theme'  => array(),
41    );
42
43    /**
44     * Failed updates.
45     *
46     * @var array
47     */
48    private $failed = array(
49        'plugin' => array(),
50        'theme'  => array(),
51    );
52
53    /**
54     * Static instance.
55     *
56     * @var self
57     */
58    private static $instance = null;
59
60    /**
61     * Initialize and fetch the static instance.
62     *
63     * @return self
64     */
65    public static function init() {
66        if ( self::$instance === null ) {
67            self::$instance = new Jetpack_Autoupdate();
68        }
69
70        return self::$instance;
71    }
72
73    /** Constructor. */
74    private function __construct() {
75        if (
76            /** This filter is documented in class.jetpack-json-api-endpoint.php */
77            apply_filters( 'jetpack_json_manage_api_enabled', true )
78        ) {
79            add_filter( 'auto_update_theme', array( $this, 'autoupdate_theme' ), 10, 2 );
80            add_filter( 'auto_update_core', array( $this, 'autoupdate_core' ), 10, 2 );
81            add_filter( 'auto_update_translation', array( $this, 'autoupdate_translation' ), 10, 2 );
82            add_action( 'automatic_updates_complete', array( $this, 'automatic_updates_complete' ), 999, 1 );
83        }
84    }
85
86    /**
87     * Filter function for `auto_update_translation`.
88     *
89     * @param bool|null $update Whether to update.
90     * @param object    $item  The update offer.
91     * @return bool|null Whether to update.
92     */
93    public function autoupdate_translation( $update, $item ) {
94        // Autoupdate all translations.
95        if ( Jetpack_Options::get_option( 'autoupdate_translations', false ) ) {
96            return true;
97        }
98
99        if ( ! isset( $item->slug ) || ! isset( $item->type ) ) {
100            return $update;
101        }
102
103        // Themes.
104        $autoupdate_themes_translations = Jetpack_Options::get_option( 'autoupdate_themes_translations', array() );
105        $autoupdate_theme_list          = Jetpack_Options::get_option( 'autoupdate_themes', array() );
106
107        if ( ( in_array( $item->slug, $autoupdate_themes_translations, true ) || in_array( $item->slug, $autoupdate_theme_list, true ) )
108            && 'theme' === $item->type
109        ) {
110            $this->expect( $item->type . ':' . $item->slug, 'translation' );
111
112            return true;
113        }
114
115        // Plugins.
116        $autoupdate_plugin_translations = Jetpack_Options::get_option( 'autoupdate_plugins_translations', array() );
117        $autoupdate_plugin_list         = (array) get_site_option( 'auto_update_plugins', array() );
118        $plugin_files                   = array_unique( array_merge( $autoupdate_plugin_list, $autoupdate_plugin_translations ) );
119        $plugin_slugs                   = array_map( array( __CLASS__, 'get_plugin_slug' ), $plugin_files );
120
121        if ( in_array( $item->slug, $plugin_slugs, true )
122            && 'plugin' === $item->type
123        ) {
124            $this->expect( $item->type . ':' . $item->slug, 'translation' );
125            return true;
126        }
127
128        return $update;
129    }
130
131    /**
132     * Filter function for `auto_update_theme`.
133     *
134     * @param bool|null $update Whether to update.
135     * @param object    $item  The update offer.
136     * @return bool|null Whether to update.
137     */
138    public function autoupdate_theme( $update, $item ) {
139        if ( ! isset( $item->theme ) ) {
140            return $update;
141        }
142        $autoupdate_theme_list = Jetpack_Options::get_option( 'autoupdate_themes', array() );
143        if ( in_array( $item->theme, $autoupdate_theme_list, true ) ) {
144            $this->expect( $item->theme, 'theme' );
145            return true;
146        }
147
148        return $update;
149    }
150
151    /**
152     * Filter function for `auto_update_core`.
153     *
154     * @param bool|null $update Whether to update.
155     * @return bool|null Whether to update.
156     */
157    public function autoupdate_core( $update ) {
158        $autoupdate_core = Jetpack_Options::get_option( 'autoupdate_core', false );
159        if ( $autoupdate_core ) {
160            return $autoupdate_core;
161        }
162
163        return $update;
164    }
165
166    /**
167     * Stores the an item identifier to the expected array.
168     *
169     * @param string $item Example: 'jetpack/jetpack.php' for type 'plugin' or 'twentyfifteen' for type 'theme'.
170     * @param string $type 'plugin' or 'theme'.
171     */
172    private function expect( $item, $type ) {
173        if ( ! isset( $this->expected[ $type ] ) ) {
174            $this->expected[ $type ] = array();
175        }
176        $this->expected[ $type ][] = $item;
177    }
178
179    /**
180     * On completion of an automatic update, let's store the results.
181     *
182     * @param mixed $results - Sent by WP_Automatic_Updater after it completes an autoupdate action. Results may be empty.
183     */
184    public function automatic_updates_complete( $results ) {
185        if ( empty( $this->expected ) ) {
186            return;
187        }
188        $this->results = empty( $results ) ? self::get_possible_failures() : $results;
189
190        add_action( 'shutdown', array( $this, 'bump_stats' ) );
191
192        Jetpack::init();
193
194        $items_to_log = array( 'plugin', 'theme', 'translation' );
195        foreach ( $items_to_log as $items ) {
196            $this->log_items( $items );
197        }
198
199        Jetpack::log( 'autoupdates', $this->get_log() );
200    }
201
202    /**
203     * Get log data.
204     *
205     * @return array Data.
206     */
207    public function get_log() {
208        return array(
209            'results' => $this->results,
210            'failed'  => $this->failed,
211            'success' => $this->success,
212        );
213    }
214
215    /**
216     * Iterates through expected items ( plugins or themes ) and compares them to actual results.
217     *
218     * @param string $items 'plugin' or 'theme'.
219     */
220    private function log_items( $items ) {
221        if ( ! isset( $this->expected[ $items ] ) ) {
222            return;
223        }
224
225        $item_results = $this->get_successful_updates( $items );
226
227        if ( is_array( $this->expected[ $items ] ) ) {
228            foreach ( $this->expected[ $items ] as $item ) {
229                if ( in_array( $item, $item_results, true ) ) {
230                    $this->success[ $items ][] = $item;
231                } else {
232                    $this->failed[ $items ][] = $item;
233                }
234            }
235        }
236    }
237
238    /**
239     * Bump stats.
240     */
241    public function bump_stats() {
242        $instance = Jetpack::init();
243        $log      = array();
244        // Bump numbers.
245
246        if ( ! empty( $this->success['theme'] ) ) {
247            $instance->stat( 'autoupdates/theme-success', is_countable( $this->success['theme'] ) ? count( $this->success['theme'] ) : 0 );
248            $log['themes_success'] = $this->success['theme'];
249        }
250
251        if ( ! empty( $this->failed['theme'] ) ) {
252            $instance->stat( 'autoupdates/theme-fail', is_countable( $this->failed['theme'] ) ? count( $this->failed['theme'] ) : 0 );
253            $log['themes_failed'] = $this->failed['theme'];
254        }
255
256        $instance->do_stats( 'server_side' );
257
258        // Send a more detailed log to logstash.
259        if ( ! empty( $log ) ) {
260            $xml            = new Jetpack_IXR_Client(
261                array(
262                    'user_id' => get_current_user_id(),
263                )
264            );
265            $log['blog_id'] = Jetpack_Options::get_option( 'id' );
266            $xml->query( 'jetpack.debug_autoupdate', $log );
267        }
268    }
269
270    /**
271     * Parses the autoupdate results generated by WP_Automatic_Updater and returns a simple array of successful items.
272     *
273     * @param string $type 'plugin' or 'theme'.
274     * @return array
275     */
276    private function get_successful_updates( $type ) {
277        $successful_updates = array();
278
279        if ( ! isset( $this->results[ $type ] ) ) {
280            return $successful_updates;
281        }
282
283        foreach ( $this->results[ $type ] as $result ) {
284            if ( $result->result ) {
285                switch ( $type ) {
286                    case 'theme':
287                        $successful_updates[] = $result->item->theme;
288                        break;
289                    case 'translation':
290                        $successful_updates[] = $result->item->type . ':' . $result->item->slug;
291                        break;
292                }
293            }
294        }
295
296        return $successful_updates;
297    }
298
299    /**
300     * Get possible failure codes.
301     *
302     * @return string[] Failure codes.
303     */
304    public static function get_possible_failures() {
305        $result = array();
306        // Lets check some reasons why it might not be working as expected.
307        include_once ABSPATH . '/wp-admin/includes/admin.php';
308        include_once ABSPATH . '/wp-admin/includes/class-wp-upgrader.php';
309        $upgrader = new WP_Automatic_Updater();
310
311        if ( $upgrader->is_disabled() ) {
312            $result[] = 'autoupdates-disabled';
313        }
314        if ( ! is_main_site() ) {
315            $result[] = 'is-not-main-site';
316        }
317        if ( ! is_main_network() ) {
318            $result[] = 'is-not-main-network';
319        }
320        if ( $upgrader->is_vcs_checkout( ABSPATH ) ) {
321            $result[] = 'site-on-vcs';
322        }
323        if ( $upgrader->is_vcs_checkout( WP_PLUGIN_DIR ) ) {
324            $result[] = 'plugin-directory-on-vcs';
325        }
326        if ( $upgrader->is_vcs_checkout( WP_CONTENT_DIR ) ) {
327            $result[] = 'content-directory-on-vcs';
328        }
329        $lock = get_option( 'auto_updater.lock' );
330        if ( $lock > ( time() - HOUR_IN_SECONDS ) ) {
331            $result[] = 'lock-is-set';
332        }
333        $skin = new Automatic_Upgrader_Skin();
334        include_once ABSPATH . 'wp-admin/includes/file.php';
335        include_once ABSPATH . 'wp-admin/includes/template.php';
336        if ( ! $skin->request_filesystem_credentials( false, ABSPATH, false ) ) {
337            $result[] = 'no-system-write-access';
338        }
339        if ( ! $skin->request_filesystem_credentials( false, WP_PLUGIN_DIR, false ) ) {
340            $result[] = 'no-plugin-directory-write-access';
341        }
342        if ( ! $skin->request_filesystem_credentials( false, WP_CONTENT_DIR, false ) ) {
343            $result[] = 'no-wp-content-directory-write-access';
344        }
345
346        return $result;
347    }
348
349    /**
350     * Get the plugin slug.
351     *
352     * @param string $plugin_file Plugin file.
353     * @return string Slug.
354     */
355    public static function get_plugin_slug( $plugin_file ) {
356        $update_plugins = get_site_transient( 'update_plugins' );
357        if ( isset( $update_plugins->no_update ) ) {
358            if ( isset( $update_plugins->no_update[ $plugin_file ]->slug ) ) {
359                $slug = $update_plugins->no_update[ $plugin_file ]->slug;
360            }
361        }
362        if ( empty( $slug ) && isset( $update_plugins->response ) ) {
363            if ( isset( $update_plugins->response[ $plugin_file ]->slug ) ) {
364                $slug = $update_plugins->response[ $plugin_file ]->slug;
365            }
366        }
367
368        // Try to infer from the plugin file if not cached.
369        if ( empty( $slug ) ) {
370            $slug = dirname( $plugin_file );
371            if ( '.' === $slug ) {
372                $slug = preg_replace( '/(.+)\.php$/', '$1', $plugin_file );
373            }
374        }
375        return $slug;
376    }
377}
378
379Jetpack_Autoupdate::init();