Files
thetempusproject/app/classes/installer.php
Joey Kimsey d7e8b586d7 various updates
remove dependence on jQuery
add image delete
Admin ui fix for mobile
image updates to new style
update comments
2025-02-05 06:36:29 -05:00

646 lines
24 KiB
PHP

<?php
/**
* app/classes/installer.php
*
* This class is used for the installation, regulation, tracking, and updating of
* the application. It handles installing the application, installing and updating
* models as well as the database, and generating and checking the htaccess file.
*
* @version 5.0.1
* @author Joey Kimsey <Joey@thetempusproject.com>
* @link https://TheTempusProject.com
* @license https://opensource.org/licenses/MIT [MIT LICENSE]
*/
namespace TheTempusProject\Classes;
use TheTempusProject\Bedrock\Functions\Check;
use TheTempusProject\Bedrock\Functions\Code;
use TheTempusProject\Bedrock\Functions\Cookie;
use TheTempusProject\Canary\Bin\Canary as Debug;
use TheTempusProject\Hermes\Functions\Redirect;
use TheTempusProject\Hermes\Functions\Route as Routes;
use TheTempusProject\Bedrock\Functions\Session;
use TheTempusProject\Classes\DatabaseModel;
use TheTempusProject\Classes\Plugin;
class Installer {
const MATRIX_MAP = [
'installPreferences' => 'preferenceMatrix',
'installPermissions' => 'permissionMatrix',
'installConfigs' => 'configMatrix',
'installTable' => 'databaseMatrix',
'installResources' => 'resourceMatrix',
];
private $override = false;
private $status = null;
private static $installJson = null;
private static $errors = [];
public function __construct() {
Debug::log( 'Installer initialized.' );
if ( self::$installJson === null ) {
self::$installJson = self::getJson();
}
}
public function getComposerJson() {
if ( !file_exists( COMPOSER_JSON_LOCATION ) ) {
Debug::error( 'No install json found.' );
return false;
}
return json_decode( file_get_contents( COMPOSER_JSON_LOCATION ), true );
}
public function getComposerLock() {
if ( !file_exists( COMPOSER_LOCK_LOCATION ) ) {
Debug::error( 'No install json found.' );
return false;
}
return json_decode( file_get_contents( COMPOSER_LOCK_LOCATION ), true );
}
public static function emptyModule( $type, $folder, $filename = '' ) {
switch ( $type ) {
case 'model':
if ( empty( $filename ) ) {
$class = '';
$class_name = '';
break;
}
$class = convertFileNameToModelClass( $filename );
$class_name = convertFileNameToClassName( $filename );
break;
case 'plugin':
if ( empty( $folder ) ) {
$class = '';
$class_name = '';
break;
}
$class = convertFileNameToPluginClass( $folder );
$class_name = convertFolderToClassName( $folder );
break;
}
if ( empty( $folder ) ) {
$folder = '';
}
$object = (object) [
'name' => $class_name,
'class' => $class,
'version' => '0.0',
'installedVersion' => '',
'folder' => $folder,
'type' => $type,
'installDate' => '',
'lastUpdate' => '',
'installStatus' => INSTALL_STATUS_NOT_INSTALLED,
'enabled' => false,
'enabled_txt' => 'no',
];
return $object;
}
/**
* This function automatically attempts to install all models in the
* specified directory.
* NOTE: The 'Models/ folder is used by default.
*
* @param string $directory - The directory you wish to install all
* models from.
* @return boolean
*/
public function getErrors( $array = true ) {
if ( $array ) {
$out = [];
foreach (self::$errors as $error) {
if ( ! is_array($error) ) {
exit(var_export($error,true));
}
$out[] = $error['errorInfo'];
}
} else {
$out = self::$errors;
}
return $out;
}
private static function getJson() {
if ( file_exists( INSTALL_JSON_LOCATION ) ) {
$content = file_get_contents( INSTALL_JSON_LOCATION );
$json = json_decode( $content, true );
} else {
touch( INSTALL_JSON_LOCATION );
$json = [];
}
return $json;
}
public static function saveJson() {
$encodedJson = json_encode( self::$installJson );
if ( !file_exists( INSTALL_JSON_LOCATION ) ) {
$content = file_get_contents( $location );
$json = json_decode( $content, true );
$fh = fopen( INSTALL_JSON_LOCATION, 'w' );
}
$writeSuccess = file_put_contents( INSTALL_JSON_LOCATION, $encodedJson );
if ( $writeSuccess ) {
return true;
}
return false;
}
public function getModule( $name ) {
$name = ucfirst( $name );
if ( isset( self::$installJson['modules'][$name] ) ) {
if ( isset( self::$installJson['modules'][$name]['enabled'] ) ) {
if ( self::$installJson['modules'][$name]['enabled'] == true ) {
self::$installJson['modules'][$name]['enabled_txt'] = '<span class="text-success">Yes</span>';
} else {
self::$installJson['modules'][$name]['enabled_txt'] = '<span class="text-danger">No</span>';
}
// in this case only, we save an array to remove the objects later, so an array stored is a success.
if ( ! empty( self::$installJson['modules'][$name]['resources_installed'] ) && is_array( self::$installJson['modules'][$name]['resources_installed'] ) ) {
self::$installJson['modules'][$name]['resources_installed'] = INSTALL_STATUS_SUCCESS;
}
}
return self::$installJson['modules'][$name];
}
Debug::info( "install module not found: $name" );
return false;
}
public function getModules( $includeObjects ) {
if ( isset( self::$installJson['modules'] ) ) {
if ( empty( $includeObjects ) ) {
return self::$installJson['modules'];
} else {
$data = self::$installJson['modules'];
foreach ( $data as $name => $module) {
$class_object = new $module['class'];
$data[$name]['class_object'] = $class_object;
}
return $data;
}
}
Debug::error( "install modules not found" );
return false;
}
public function getNode( $name) {
if ( isset( self::$installJson[$name] ) ) {
return self::$installJson[$name];
}
}
public function setNode( $name, $value, $save = false ) {
self::$installJson[$name] = $value;
if ( $save !== false ) {
return self::saveJson();
}
return true;
}
public function setModule( $name, $value, $save = false ) {
if ( !isset( self::$installJson['modules'] ) ) {
self::$installJson['modules'] = [];
}
self::$installJson['modules'][$name] = $value;
if ( $save !== false ) {
return self::saveJson();
}
return true;
}
private function removeModule( $name, $save = false ) {
if ( !isset( self::$installJson['modules'] ) ) {
Debug::error( 'No modules installed' );
return false;
}
if ( !isset( self::$installJson['modules'][$name] ) ) {
Debug::error( 'Module not installed' );
return false;
}
self::$installJson['modules'][$name] = null;
if ( $save !== false ) {
return self::saveJson();
}
return true;
}
public function findModelFlags( $classObject ) {
$install_flags = [];
foreach ( self::MATRIX_MAP as $install_flag => $matrix_name ) {
if ( !empty( $classObject->$matrix_name ) ) {
$install_flags[$install_flag] = true;
} else {
$install_flags[$install_flag] = false;
}
}
return $install_flags;
}
public function getPluginInfo( $folder) {
$object = self::emptyModule( 'plugin', $folder );
$location = $folder . 'plugin.php';
if ( file_exists( $location ) ) {
include_once $location;
} else {
self::$errors[] = ['errorInfo' => "Could not find the requested plugin file: $location"];
return $object;
}
if ( ! class_exists( $object->class ) ) {
Debug::warn( 'Cannot get plugin version from class: ' . $object->class . ', class does NOT exist.');
return $object;
}
$class_object = new $object->class;
$object->version = $class_object->pluginVersion;
$module = $class_object->module;
if ( false !== $module ) {
$objectArray = (array) $object;
$object = (object) array_replace( $objectArray, $module );
}
$object->class_object = $class_object;
return $object;
}
public function getModelInfo( $filename, $folder = '' ) {
Debug::debug( 'getModelInfo filename: ' . $filename . ', folder: ' . $folder);
$object = self::emptyModule( 'model', $folder, $filename );
if ( ! class_exists( $object->class ) ) {
Debug::warn( 'Cannot get model version from class: ' . $object->class . ', class does NOT exist.');
return $object;
}
$class_object = new $object->class;
$object->version = $class_object->modelVersion;
$module = $this->getModule( $object->name );
if ( false !== $module ) {
$objectArray = (array) $object;
$object = (object) array_replace( $objectArray, $module );
}
$object->class_object = $class_object;
return $object;
}
public function getAvailablePlugins() {
$plugins = Plugin::getPluginDirectories();
$list = [];
foreach ( $plugins as $pluginName => $locations ) {
foreach ( $locations as $location ) {
foreach ( $location as $currentFolder => $file ) {
if ( 'plugin.php' == $file ) {
$list[ $pluginName ] = $this->getPluginInfo( str_ireplace( 'plugin.php', '', $currentFolder ) );
}
}
}
}
return $list;
}
public function getModelList( $folder ) {
$files = scandir( $folder );
$models = [];
array_shift( $files );
array_shift( $files );
foreach ( $files as $index => $filename ) {
if ( stripos( $filename, '.php' ) ) {
$list[] = $this->getModelInfo( $filename, $folder );
}
}
return $list;
}
public function installPlugin( $module_data, $flags = [], $defaultFlagValue = true ) {
Debug::log( 'Installing Plugin: ' . $module_data->name );
$errors = [];
if ( INSTALL_STATUS_INSTALLED === $module_data->installStatus ) {
Debug::warn( "$name has already been successfully installed" );
return true;
}
if ( empty( $module_data->class_object ) ) {
self::$errors[] = [ 'errorInfo' => 'installPlugin: class_object not found: ' . $module_data->class ];
return false;
}
// normalize install flags
foreach ( PLUGIN_INSTALL_FLAGS as $flag_type ) {
// add any missing flags
if ( ! isset( $flags[ $flag_type ] ) ) {
$flags[ $flag_type ] = $defaultFlagValue;
}
// exclude any flags that have already been successfully installed
if ( !empty( $module_data->$flag_type ) && $module_data->$flag_type == INSTALL_STATUS_SUCCESS ) {
Debug::warn( "$flag_type has already been successfully executed" );
$flags[ $flag_type ] = false;
}
}
list( $install_data, $errors ) = $module_data->class_object->install( $flags );
$objectArray = (array) $module_data;
$module_data = array_replace( $objectArray, $install_data );
$module_data['installedVersion'] = $module_data['version'];
$module_data['lastUpdate'] = time();
if ( empty( $module_data['installDate'] ) ) {
$module_data['installDate'] = time();
}
unset($module_data['class_object']); // we don't want the class object though
$this->setModule( $module_data['name'], $module_data, true );
$this->updateInstallStatus( $module_data['name'] );
if ( !empty( $errors ) ) {
self::$errors = array_merge( self::$errors, $errors );
return false;
}
$errors[] = [ 'errorInfo' => $module_data['name'] . " Plugin has been installed." ];
self::$errors = array_merge( self::$errors, $errors );
return true;
}
public function uninstallPlugin( $module_data, $flags = [], $defaultFlagValue = true ) {
Debug::log( 'Uninstalling Plugin: ' . $module_data->name );
$errors = [];
if ( empty( $module_data->class_object ) ) {
self::$errors[] = [ 'errorInfo' => 'uninstallPlugin: class_object not found: ' . $module_data->class ];
return false;
}
// normalize install flags
foreach ( PLUGIN_INSTALL_FLAGS as $flag_type ) {
// add any missing flags
if ( ! isset( $flags[ $flag_type ] ) ) {
$flags[ $flag_type ] = $defaultFlagValue;
}
}
$errors = $module_data->class_object->uninstall( $flags );
if ( !empty( $errors ) ) {
self::$errors = array_merge( self::$errors, $errors );
return false;
}
$this->removeModule( $module_data->name, true );
$errors[] = [ 'errorInfo' => $module_data->name . " Plugin has been uninstalled." ];
self::$errors = array_merge( self::$errors, $errors );
return true;
}
public function installModel( $module_data, $flags = [], $defaultFlagValue = true, $updateModule = true ) {
Debug::log( 'Installing Model: ' . $module_data->name );
$errors = [];
if ( INSTALL_STATUS_INSTALLED === $module_data->installStatus ) {
Debug::warn( "$module_data->name has already been successfully installed" );
return true;
}
if ( empty( $module_data->class_object ) ) {
self::$errors[] = [ 'errorInfo' => 'installModel class_object not found: ' . $module_data->class ];
return false;
}
// normalize install flags
$model_flags = $this->findModelFlags( $module_data->class_object );
foreach ( MODEL_INSTALL_FLAGS as $flag_type ) {
// add any missing flags
if ( ! isset( $flags[ $flag_type ] ) ) {
$flags[ $flag_type ] = $defaultFlagValue;
}
// exclude any flags that have already been successfully installed
if ( ! empty( $module_data->$flag_type ) && $module_data->$flag_type == INSTALL_STATUS_SUCCESS ) {
Debug::warn( "$flag_type has already been successfully installed" );
$flags[ $flag_type ] = false;
}
if ( $flags[ $flag_type ] === true ) {
// exclude any flags we can't do anything with
if ( ! isset( $model_flags[ $flag_type ] ) ) {
Debug::warn( "$flag_type cannot be installed due to installFlags on the model." );
$flags[ $flag_type ] = false;
}
// check to make sure we have the proper mapping
$matrix = self::MATRIX_MAP[ $flag_type ];
// exclude any flags we don't have a matric map for
if ( empty( $module_data->class_object->$matrix ) ) {
Debug::warn( "$flag_type does not have a proper matrix map and cannot be installed." );
$module_data->$flag_type = INSTALL_STATUS_NOT_FOUND;
}
}
}
list( $install_data, $errors ) = $module_data->class_object->install( $flags );
$objectArray = (array) $module_data;
$module_data = array_replace( $objectArray, $install_data );
$module_data['installedVersion'] = $module_data['version'];
$module_data['lastUpdate'] = time();
if ( empty( $module_data['installDate'] ) ) {
$module_data['installDate'] = time();
}
if ($updateModule) {
unset($module_data['class_object']); // we don't want the class object though
$this->setModule( $module_data['name'], $module_data, true );
$this->updateInstallStatus( $module_data['name'] );
}
if ( !empty( $errors ) ) {
self::$errors = array_merge( self::$errors, $errors );
return false;
}
$errors[] = [ 'errorInfo' => $module_data['name'] . " model has been installed." ];
self::$errors = array_merge( self::$errors, $errors );
return true;
}
public function uninstallModel( $module_data, $flags = [], $defaultFlagValue = true, $updateModule = true ) {
Debug::log( 'Uninstalling Model: ' . $module_data->name );
$errors = [];
if ( empty( $module_data->class_object ) ) {
self::$errors[] = [ 'errorInfo' => 'uninstallModel: class_object not found: ' . $module_data->class ];
return false;
}
// normalize install flags
$model_flags = $this->findModelFlags( $module_data->class_object );
foreach ( MODEL_INSTALL_FLAGS as $flag_type ) {
// add any missing flags
if ( ! isset( $flags[ $flag_type ] ) ) {
$flags[ $flag_type ] = $defaultFlagValue;
}
if ( $flags[ $flag_type ] === true ) {
// exclude any flags we can't do anything with
if ( ! isset( $model_flags[ $flag_type ] ) ) {
Debug::warn( "$flag_type cannot be installed due to installFlags on the model." );
$flags[ $flag_type ] = false;
}
// check to make sure we have the proper mapping
$matrix = self::MATRIX_MAP[ $flag_type ];
// exclude any flags we don't have a matric map for
if ( empty( $module_data->class_object->$matrix ) ) {
Debug::warn( "$flag_type does not have a proper matrix map and cannot be uninstalled." );
$module_data->$flag_type = INSTALL_STATUS_NOT_FOUND;
}
}
}
list( $install_data, $errors ) = $module_data->class_object->uninstall( $flags );
if ( !empty( $errors ) ) {
self::$errors = array_merge( self::$errors, $errors );
return false;
}
$errors[] = [ 'errorInfo' => $module_data->name . " model has been uninstalled." ];
self::$errors = array_merge( self::$errors, $errors );
return true;
}
private function updateInstallStatus( $name ) {
$modelInfo = $this->getModule( $name );
if ( $modelInfo === false ) {
return;
}
if ( $modelInfo['type'] == 'plugin' ) {
$flags = PLUGIN_INSTALL_FLAGS;
} else {
$flags = MODEL_INSTALL_FLAGS;
}
foreach ( $flags as $flag_type ) {
if ( empty( $modelInfo[ $flag_type ] ) || ! in_array( $modelInfo[ $flag_type ], [ INSTALL_STATUS_SUCCESS, INSTALL_STATUS_NOT_REQUIRED ] ) ) {
$modelInfo['installStatus'] = INSTALL_STATUS_PARTIALLY_INSTALLED;
break;
}
$modelInfo['installStatus'] = INSTALL_STATUS_INSTALLED;
}
$this->setModule( $name, $modelInfo, true );
}
/**
* Checks the root directory for a .htaccess file and compares it with
* the .htaccess file the application generates by default.
*
* NOTE: The $override flag will cause this function to automatically generate a
* new htaccess file if the .htaccess found in the root directory does not match
* the default generated version.
*
* @param boolean $create - Optional flag to generate and save a new htaccess
* if none is found.
*
* @return boolean - Returns true if the htaccess file was found or
* created, false otherwise.
*/
public function saveHtaccess( $text = false ) {
if ( false === $text ) {
$text = $this->generateHtaccess();
}
if ( file_exists( HTACCESS_LOCATION ) ) {
Debug::error( "Can't overwrite existing htaccess file" );
return false;
}
return file_put_contents( HTACCESS_LOCATION, $text );
}
public function checkHtaccess( $create = false ) {
$check = 0;
$findRewrite1 = "RewriteEngine On\n";
$findRewrite2 = 'RewriteBase ' . Routes::getRoot() . "\n\n";
if ( file_exists( HTACCESS_LOCATION ) ) {
$htaccess = file_get_contents( HTACCESS_LOCATION );
if ( $htaccess === $this->generateHtaccess() ) {
return true;
}
if ( stripos( $htaccess, $findRewrite1 ) ) {
$check++;
}
if ( stripos( $htaccess, $findRewrite2 ) ) {
$check++;
}
if ( $check === 2 ) {
return true;
}
}
return false;
}
public function baseHtaccess() {
$out = "# Intercepts for images not found
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^images/(.*)$ index.php?error=image404&url=$1 [L,NC,QSA]
# Intercepts for uploads not found
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^uploads/(.*)$ index.php?error=upload404&url=$1 [L,NC,QSA]
# Intercepts other errors
RewriteRule ^errors/(.*)$ index.php?error=$1 [L,NC,QSA]
# Intercept all traffic not originating locally and not going to images or uploads
RewriteCond %{REMOTE_ADDR} !^127\.0\.0\.1
RewriteCond %{REMOTE_ADDR} !^\:\:1
RewriteCond %{REQUEST_URI} !^(.*)/images/(.*)$ [NC]
RewriteCond %{REQUEST_URI} !^(.*)/uploads/(.*)$ [NC]
RewriteCond %{REQUEST_URI} !^(.*).js$ [NC]
RewriteCond %{REQUEST_URI} !^(.*).css$ [NC]
RewriteCond %{REQUEST_URI} !^(.*).ico$ [NC]
RewriteRule ^(.+)$ index.php?url=$1 [QSA,L]
# Catchall for any non existent files or folders
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.+)$ index.php?url=$1 [QSA,L]";
return str_ireplace( ' ', '', $out );
}
/**
* Generates the default htaccess file for the application. This will funnel
* all traffic that comes into the application directory to index.php where we
* use that data to construct the desired page using the controller.
*
* @param string $docroot - A custom document root to use instead of the default.
*
* @return string - The generated contents of the htaccess file.
*/
protected function generateHtaccess( $docroot = null, $rewrite = true ) {
$out = '';
if ( empty( $docroot ) ) {
$docroot = Routes::getRoot();
}
if ( $rewrite === true ) {
$out .= "RewriteEngine On\n\n";
}
$out .= "RewriteBase $docroot\n\n";
$out .= $this->baseHtaccess();
return $out;
}
}