<?php
/*!
* Jetpack CRM
* https://jetpackcrm.com
*
* Logic concerned with retrieving packages from our CDN and installing locally
*
*/
namespace Automattic\JetpackCRM;
// block direct access
defined( 'ZEROBSCRM_PATH' ) || exit( 0 );
/*
* Class encapsulating logic concerned with retrieving packages from our CDN and installing locally
*/
class Package_Installer {
/*
* Stores packages stack
*/
private $packages = array();
/**
* Package install directory
*
* @var string
*/
private $package_dir;
/*
* Init
*/
public function __construct() { // phpcs:ignore Squiz.Commenting.FunctionComment.WrongStyle
$root_storage_info = jpcrm_storage_dir_info();
if ( ! $root_storage_info ) {
throw new Exception( 'Jetpack CRM data folder could not be created!' );
}
// set $package_dir
$this->package_dir = $root_storage_info['path'] . '/packages/';
// define packages
$this->packages = array(
'oauth_dependencies' => array(
'title' => __( 'OAuth Connection dependencies', 'zero-bs-crm' ),
'version' => 1.0,
'target_dir' => $this->package_dir,
'install_method' => 'unzip',
'post_install_call' => '',
),
);
// does the working directory exist? If not create
if ( ! file_exists( $this->package_dir ) ) {
wp_mkdir_p( $this->package_dir );
// double check
if ( ! file_exists( $this->package_dir ) ) {
// we're going to struggle to install packages
// Add admin notification
zeroBSCRM_notifyme_insert_notification( get_current_user_id(), -999, -1, 'package.installer.dir_create_error', __( 'Main Working Directory', 'zero-bs-crm' ) );
} else {
jpcrm_create_and_secure_dir_from_external_access( $this->package_dir, true );
}
}
}
/*
* Returns a list of packages available via our CDN
*/
public function list_all_available_packages( $with_install_status = false ){
// list packages available
// later we could retrieve this via CDN's package_versions.json
$packages = $this->packages;
// if with install status, cycle through and check those
if ( $with_install_status ){
$return = array();
foreach ( $packages as $package_key => $package_info ){
$return[ $package_key ] = $package_info;
$return[ $package_key ]['installed'] = $this->get_package_install_info( $package_key );
// check for failed attempts
$return[ $package_key ]['failed_installs'] = $this->get_fail_counter( $package_key );
}
$packages = $return;
}
return $packages;
}
/*
* Returns info array on package
*
* @param string $package_key - a slug for a particular package
*
*/
public function get_package_info( $package_key = '' ){
if ( isset( $this->packages[ $package_key ] ) ){
$package = $this->packages[ $package_key ];
// supplement with local install info
$package['installed'] = $this->get_package_install_info( $package_key );
// check for failed attempts
$package['failed_installs'] = $this->get_fail_counter( $package_key );
return $package;
}
return false;
}
/*
* Returns installed package info from package's version_info.json file
*
* @param string $package_key - a slug for a particular package
*
*/
public function get_package_install_info( $package_key = '' ){
// retrieve version_info.json file and load info
$package_version_info_file = $this->package_dir . $package_key . '/version_info.json';
if ( file_exists( $package_version_info_file ) ){
/* Example version_info.json file:
{
"key": "my_package_key",
"version": "1.0"
}
*/
$data = file_get_contents( $package_version_info_file );
return json_decode( $data, true );
}
return false;
}
/*
* Checks to see if a package is available on CDN
*
* @param string $package_key - a slug for a particular package
*
* @returns bool(ish)
*/
public function package_is_available( $package_key = '' ){
return ( $this->get_package_info( $package_key ) ? true : false ); // package doesn't exist on CDN or no connection
}
/*
* Checks to see if a package is available locally
*
* @param string $package_key - a slug for a particular package
* @param float $min_version - if > 0, installed version is compared and returns true only if min version met
*
*/
public function package_is_installed( $package_key = '', $min_version = 0 ){
// retrieve installed version data if available
$installed_info = $this->get_package_install_info( $package_key );
if ( !is_array( $installed_info ) ){
return false;
} else {
// check min version
if ( $min_version > 0 ){
$local_version = $installed_info['version'];
if ( version_compare( $local_version, $min_version, '>=' ) ) {
// meets minimum version
return true;
}
} else {
// no min version check, but does seem to be installed
return true;
}
}
// +- check extraction endpoint (e.g. are files actually there?)
return false; // package doesn't exist or isn't installed
}
/*
* Checks to see if a package is available locally, if it isn't, installs it where possible
*
* @param string $package_key - a slug for a particular package
* @param float $min_version - if > 0, installed version is compared and returns true only if min version met
*
*/
public function ensure_package_is_installed( $package_key = '', $min_version = 0 ){
if ( !$this->package_is_installed( $package_key, $min_version ) ){
// not installed, try to install/update(reinstall) it
return $this->retrieve_and_install_package( $package_key, true );
}
// include composer autoload if it exists
$potential_composer_autoload_path = $this->package_dir . '/' . $package_key . '/vendor/autoload.php';
if ( file_exists( $potential_composer_autoload_path ) ) {
require_once $potential_composer_autoload_path;
}
return true;
}
/*
* Retrieves a package zip from our CDN and installs it locally
*/
public function retrieve_and_install_package( $package_key = '', $force_reinstall = false){
global $zbs;
// package exists?
if ( !$this->package_is_available( $package_key) ){
return false;
}
// package already installed?
if ( $this->package_is_installed( $package_key ) && !$force_reinstall ){
return true;
}
// failed already 3 times?
if ( $this->get_fail_counter( $package_key ) >= 3 ){
// Add error msg
zeroBSCRM_notifyme_insert_notification( get_current_user_id(), -999, -1, 'package.installer.fail_count_over', $package_key );
return false;
}
// here we set a failsafe which sets an option value such that if this install does not complete
// ... we'll know the package install failed (e.g. page timeout shorter than download/unzip time)
// ... that way we can avoid retrying 50000 times.
$this->increment_fail_counter( $package_key );
// Retrieve & install the package
$package_info = $this->get_package_info( $package_key );
$installed = false;
// Directories
$working_dir = $this->package_dir;
if ( !file_exists( $working_dir ) ){
wp_mkdir_p( $working_dir );
jpcrm_create_and_secure_dir_from_external_access( $working_dir, true );
}
$target_dir = $package_info['target_dir'];
if ( !file_exists( $target_dir ) ) {
wp_mkdir_p( $target_dir );
jpcrm_create_and_secure_dir_from_external_access( $target_dir, true );
}
// did that work?
if ( !file_exists( $target_dir ) || !file_exists( $working_dir ) ) {
// error creating directories
// Add admin notification
zeroBSCRM_notifyme_insert_notification( get_current_user_id(), -999, -1, 'package.installer.dir_create_error', $package_info['title'] . ' (' . $package_key . ')' );
// return error
return false;
}
// Filenames
$source_file_name = $package_key . '.zip';
// Attempt to download and install
if ( file_exists( $target_dir ) && file_exists( $working_dir ) ){
// if force reinstall, clear out previous directory contents
if ( $this->package_is_installed( $package_key ) && $force_reinstall ){
// empty it out!
jpcrm_delete_files_from_directory( $target_dir );
}
// Retrieve package
$libs = zeroBSCRM_retrieveFile( $zbs->urls['extdlpackages'] . $source_file_name, $working_dir . $source_file_name, array( 'timeout' => 30 ) );
// Process package
if ( file_exists( $working_dir . $source_file_name ) ){
switch ( $package_info['install_method'] ){
// expand a zipped package
case 'unzip':
// Expand
$expanded = zeroBSCRM_expandArchive( $working_dir . $source_file_name, $target_dir );
if ( $expanded ){
// Check success?
if ( !zeroBSCRM_is_dir_empty( $target_dir ) ){
// appears to have worked, tidy up:
if ( file_exists( $working_dir . $source_file_name ) ){
unlink( $working_dir . $source_file_name );
}
$installed = true;
} else {
// Add admin notification
zeroBSCRM_notifyme_insert_notification( get_current_user_id(), -999, -1, 'package.installer.unzip_error', $package_info['title'] . ' (' . $package_key . ')' );
return false;
}
} else {
// failed to open the .zip, remove it
if ( file_exists( $working_dir . $source_file_name ) ){
unlink( $working_dir . $source_file_name );
}
// Add admin notification
zeroBSCRM_notifyme_insert_notification( get_current_user_id(), -999, -1, 'package.installer.unzip_error', $package_info['title'] . ' (' . $package_key . ')' );
return false;
}
break;
// 1 file package installs, copy to target location
default:
// TBD, add when we need this.
break;
}
// if successfully installed, do any follow-on tasks
if ( $installed ){
// Success. Reset fail counter
$this->clear_fail_counter( $package_key );
// if the $package_info has ['post_install_call'] set, call that
if ( isset( $package_info['post_install_call'] ) && function_exists( $package_info['post_install_call'] ) ){
call_user_func( $package_info['post_install_call'], $package_info );
}
return true;
}
} else {
// Add error msg
zeroBSCRM_notifyme_insert_notification( get_current_user_id(), -999, -1, 'package.installer.dl_error', $package_info['title'] . ' (' . $package_key . ')' );
}
} else {
// Add error msg
zeroBSCRM_notifyme_insert_notification( get_current_user_id(), -999, -1, 'package.installer.dir_create_error', $package_info['title'] . ' (' . $package_key . ')' );
}
return false;
}
/*
* Gets a package fail counter option value
*/
private function get_fail_counter( $package_key = ''){
return (int)get_option( 'jpcrm_package_fail_' . $package_key, 0 );
}
/*
* Adds a tick to a package fail counter option ( to track failed installs )
*/
private function increment_fail_counter( $package_key = ''){
// here we set a failsafe which sets an option value such that if this install does not complete
// ... we'll know the package install failed (e.g. page timeout shorter than download/unzip time)
// ... that way we can avoid retrying 50000 times.
$existing_fails = $this->get_fail_counter( $package_key );
update_option( 'jpcrm_package_fail_' . $package_key, $existing_fails + 1, false );
}
/*
* Resets fail counter for a package
*/
private function clear_fail_counter( $package_key = ''){
// simple.
delete_option( 'jpcrm_package_fail_' . $package_key );
}
}