* @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'] = 'Yes'; } else { self::$installJson['modules'][$name]['enabled_txt'] = 'No'; } } 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 uninstalled." ); $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 installed." ); $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 ( ! 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; } }