Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 111
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
Package_Installer
0.00% covered (danger)
0.00%
0 / 110
0.00% covered (danger)
0.00%
0 / 11
2162
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
20
 list_all_available_packages
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 get_package_info
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 get_package_install_info
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 package_is_available
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 package_is_installed
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 ensure_package_is_installed
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 retrieve_and_install_package
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 1
552
 get_fail_counter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 increment_fail_counter
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 clear_fail_counter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/*
3 * Jetpack CRM
4 * https://jetpackcrm.com
5 *
6 * Logic concerned with retrieving packages from our CDN and installing locally
7 *
8 */
9namespace Automattic\JetpackCRM;
10
11// block direct access
12defined( 'ZEROBSCRM_PATH' ) || exit( 0 );
13
14/*
15* Class encapsulating logic concerned with retrieving packages from our CDN and installing locally
16*/
17class Package_Installer {
18
19    /*
20    * Stores packages stack
21    */
22    private $packages = array();
23
24    /**
25     * Package install directory
26     *
27     * @var string
28     */
29    private $package_dir;
30
31    /*
32    * Init
33    */
34    public function __construct() { // phpcs:ignore Squiz.Commenting.FunctionComment.WrongStyle
35
36        $root_storage_info = jpcrm_storage_dir_info();
37
38        if ( ! $root_storage_info ) {
39            throw new Exception( 'Jetpack CRM data folder could not be created!' );
40        }
41
42        // set $package_dir
43        $this->package_dir = $root_storage_info['path'] . '/packages/';
44
45        // define packages
46        $this->packages = array(
47
48            'oauth_dependencies' => array(
49
50                'title'             => __( 'OAuth Connection dependencies', 'zero-bs-crm' ),
51                'version'           => 1.0,
52                'target_dir'        => $this->package_dir,
53                'install_method'    => 'unzip',
54                'post_install_call' => '',
55
56            ),
57
58        );
59
60        // does the working directory exist? If not create
61        if ( ! file_exists( $this->package_dir ) ) {
62
63            wp_mkdir_p( $this->package_dir );
64
65            // double check
66            if ( ! file_exists( $this->package_dir ) ) {
67
68                // we're going to struggle to install packages
69                // Add admin notification
70                zeroBSCRM_notifyme_insert_notification( get_current_user_id(), -999, -1, 'package.installer.dir_create_error', __( 'Main Working Directory', 'zero-bs-crm' ) );
71
72            } else {
73                jpcrm_create_and_secure_dir_from_external_access( $this->package_dir, true );
74            }
75        }
76    }
77
78    /*
79    * Returns a list of packages available via our CDN
80    */
81    public function list_all_available_packages( $with_install_status = false ) {
82
83        // list packages available
84        // later we could retrieve this via CDN's package_versions.json
85        $packages = $this->packages;
86
87        // if with install status, cycle through and check those
88        if ( $with_install_status ) {
89
90            $return = array();
91            foreach ( $packages as $package_key => $package_info ) {
92
93                $return[ $package_key ]              = $package_info;
94                $return[ $package_key ]['installed'] = $this->get_package_install_info( $package_key );
95
96                // check for failed attempts
97                $return[ $package_key ]['failed_installs'] = $this->get_fail_counter( $package_key );
98
99            }
100
101            $packages = $return;
102
103        }
104
105        return $packages;
106    }
107
108    /*
109    * Returns info array on package
110    *
111    * @param string $package_key - a slug for a particular package
112    *
113    */
114    public function get_package_info( $package_key = '' ) {
115
116        if ( isset( $this->packages[ $package_key ] ) ) {
117
118            $package = $this->packages[ $package_key ];
119
120            // supplement with local install info
121            $package['installed'] = $this->get_package_install_info( $package_key );
122
123            // check for failed attempts
124            $package['failed_installs'] = $this->get_fail_counter( $package_key );
125
126            return $package;
127
128        }
129
130        return false;
131    }
132
133    /*
134    * Returns installed package info from package's version_info.json file
135    *
136    * @param string $package_key - a slug for a particular package
137    *
138    */
139    public function get_package_install_info( $package_key = '' ) {
140
141        // retrieve version_info.json file and load info
142        $package_version_info_file = $this->package_dir . $package_key . '/version_info.json';
143
144        if ( file_exists( $package_version_info_file ) ) {
145
146            /*
147            Example version_info.json file:
148
149            {
150                "key": "my_package_key",
151                "version": "1.0"
152            }
153
154            */
155            $data = file_get_contents( $package_version_info_file );
156            return json_decode( $data, true );
157
158        }
159
160        return false;
161    }
162
163    /*
164    * Checks to see if a package is available on CDN
165    *
166    * @param string $package_key - a slug for a particular package
167    *
168    * @returns bool(ish)
169    */
170    public function package_is_available( $package_key = '' ) {
171
172        return ( $this->get_package_info( $package_key ) ? true : false ); // package doesn't exist on CDN or no connection
173    }
174
175    /*
176    * Checks to see if a package is available locally
177    *
178    * @param string $package_key - a slug for a particular package
179    * @param float $min_version - if > 0, installed version is compared and returns true only if min version met
180    *
181    */
182    public function package_is_installed( $package_key = '', $min_version = 0 ) {
183
184        // retrieve installed version data if available
185        $installed_info = $this->get_package_install_info( $package_key );
186        if ( ! is_array( $installed_info ) ) {
187
188            return false;
189
190        } else {
191
192            // check min version
193            if ( $min_version > 0 ) {
194
195                $local_version = $installed_info['version'];
196                if ( version_compare( $local_version, $min_version, '>=' ) ) {
197
198                    // meets minimum version
199                    return true;
200
201                }
202            } else {
203
204                // no min version check, but does seem to be installed
205                return true;
206
207            }
208        }
209
210        // +- check extraction endpoint (e.g. are files actually there?)
211
212        return false; // package doesn't exist or isn't installed
213    }
214
215    /*
216    * Checks to see if a package is available locally, if it isn't, installs it where possible
217    *
218    * @param string $package_key - a slug for a particular package
219    * @param float $min_version - if > 0, installed version is compared and returns true only if min version met
220    *
221    */
222    public function ensure_package_is_installed( $package_key = '', $min_version = 0 ) {
223
224        if ( ! $this->package_is_installed( $package_key, $min_version ) ) {
225
226            // not installed, try to install/update(reinstall) it
227            return $this->retrieve_and_install_package( $package_key, true );
228
229        }
230
231        // include composer autoload if it exists
232        $potential_composer_autoload_path = $this->package_dir . '/' . $package_key . '/vendor/autoload.php';
233        if ( file_exists( $potential_composer_autoload_path ) ) {
234            require_once $potential_composer_autoload_path;
235        }
236
237        return true;
238    }
239
240    /*
241    * Retrieves a package zip from our CDN and installs it locally
242    */
243    public function retrieve_and_install_package( $package_key = '', $force_reinstall = false ) {
244
245        global $zbs;
246
247        // package exists?
248        if ( ! $this->package_is_available( $package_key ) ) {
249
250            return false;
251
252        }
253
254        // package already installed?
255        if ( $this->package_is_installed( $package_key ) && ! $force_reinstall ) {
256
257            return true;
258
259        }
260
261        // failed already 3 times?
262        if ( $this->get_fail_counter( $package_key ) >= 3 ) {
263
264            // Add error msg
265            zeroBSCRM_notifyme_insert_notification( get_current_user_id(), -999, -1, 'package.installer.fail_count_over', $package_key );
266
267            return false;
268        }
269
270        // here we set a failsafe which sets an option value such that if this install does not complete
271        // ... we'll know the package install failed (e.g. page timeout shorter than download/unzip time)
272        // ... that way we can avoid retrying 50000 times.
273        $this->increment_fail_counter( $package_key );
274
275        // Retrieve & install the package
276        $package_info = $this->get_package_info( $package_key );
277        $installed    = false;
278
279        // Directories
280        $working_dir = $this->package_dir;
281        if ( ! file_exists( $working_dir ) ) {
282            wp_mkdir_p( $working_dir );
283            jpcrm_create_and_secure_dir_from_external_access( $working_dir, true );
284        }
285        $target_dir = $package_info['target_dir'];
286        if ( ! file_exists( $target_dir ) ) {
287            wp_mkdir_p( $target_dir );
288            jpcrm_create_and_secure_dir_from_external_access( $target_dir, true );
289        }
290
291        // did that work?
292        if ( ! file_exists( $target_dir ) || ! file_exists( $working_dir ) ) {
293
294            // error creating directories
295            // Add admin notification
296            zeroBSCRM_notifyme_insert_notification( get_current_user_id(), -999, -1, 'package.installer.dir_create_error', $package_info['title'] . ' (' . $package_key . ')' );
297
298            // return error
299            return false;
300
301        }
302
303        // Filenames
304        $source_file_name = $package_key . '.zip';
305
306        // Attempt to download and install
307        if ( file_exists( $target_dir ) && file_exists( $working_dir ) ) {
308
309            // if force reinstall, clear out previous directory contents
310            if ( $this->package_is_installed( $package_key ) && $force_reinstall ) {
311
312                // empty it out!
313                jpcrm_delete_files_from_directory( $target_dir );
314
315            }
316
317            // Retrieve package
318            $libs = zeroBSCRM_retrieveFile( $zbs->urls['extdlpackages'] . $source_file_name, $working_dir . $source_file_name, array( 'timeout' => 30 ) );
319
320            // Process package
321            if ( file_exists( $working_dir . $source_file_name ) ) {
322
323                switch ( $package_info['install_method'] ) {
324
325                    // expand a zipped package
326                    case 'unzip':
327                        // Expand
328                        $expanded = zeroBSCRM_expandArchive( $working_dir . $source_file_name, $target_dir );
329
330                        if ( $expanded ) {
331
332                            // Check success?
333                            if ( ! zeroBSCRM_is_dir_empty( $target_dir ) ) {
334
335                                // appears to have worked, tidy up:
336                                if ( file_exists( $working_dir . $source_file_name ) ) {
337                                    unlink( $working_dir . $source_file_name );
338                                }
339
340                                $installed = true;
341
342                            } else {
343
344                                // Add admin notification
345                                zeroBSCRM_notifyme_insert_notification( get_current_user_id(), -999, -1, 'package.installer.unzip_error', $package_info['title'] . ' (' . $package_key . ')' );
346
347                                return false;
348
349                            }
350                        } else {
351
352                            // failed to open the .zip, remove it
353                            if ( file_exists( $working_dir . $source_file_name ) ) {
354                                unlink( $working_dir . $source_file_name );
355                            }
356
357                            // Add admin notification
358                            zeroBSCRM_notifyme_insert_notification( get_current_user_id(), -999, -1, 'package.installer.unzip_error', $package_info['title'] . ' (' . $package_key . ')' );
359
360                            return false;
361
362                        }
363
364                        break;
365
366                    // 1 file package installs, copy to target location
367                    default:
368                        // TBD, add when we need this.
369
370                        break;
371
372                }
373
374                // if successfully installed, do any follow-on tasks
375                if ( $installed ) {
376
377                    // Success. Reset fail counter
378                    $this->clear_fail_counter( $package_key );
379
380                    // if the $package_info has ['post_install_call'] set, call that
381                    if ( isset( $package_info['post_install_call'] ) && function_exists( $package_info['post_install_call'] ) ) {
382
383                        call_user_func( $package_info['post_install_call'], $package_info );
384
385                    }
386
387                    return true;
388
389                }
390            } else {
391
392                // Add error msg
393                zeroBSCRM_notifyme_insert_notification( get_current_user_id(), -999, -1, 'package.installer.dl_error', $package_info['title'] . ' (' . $package_key . ')' );
394
395            }
396        } else {
397
398            // Add error msg
399            zeroBSCRM_notifyme_insert_notification( get_current_user_id(), -999, -1, 'package.installer.dir_create_error', $package_info['title'] . ' (' . $package_key . ')' );
400
401        }
402
403        return false;
404    }
405
406    /*
407    * Gets a package fail counter option value
408    */
409    private function get_fail_counter( $package_key = '' ) {
410
411        return (int) get_option( 'jpcrm_package_fail_' . $package_key, 0 );
412    }
413
414    /*
415    * Adds a tick to a package fail counter option ( to track failed installs )
416    */
417    private function increment_fail_counter( $package_key = '' ) {
418
419        // here we set a failsafe which sets an option value such that if this install does not complete
420        // ... we'll know the package install failed (e.g. page timeout shorter than download/unzip time)
421        // ... that way we can avoid retrying 50000 times.
422        $existing_fails = $this->get_fail_counter( $package_key );
423        update_option( 'jpcrm_package_fail_' . $package_key, $existing_fails + 1, false );
424    }
425
426    /*
427    * Resets fail counter for a package
428    */
429    private function clear_fail_counter( $package_key = '' ) {
430
431        // simple.
432        delete_option( 'jpcrm_package_fail_' . $package_key );
433    }
434}