* @copyright 2024 Eddy Prosperi * @license GNU General Public License versione 2 o successiva; vedi LICENSE.txt */ define('MODIFIED', 1); define('NOT_MODIFIED', 2); defined('_JEXEC') or die(); use \Joomla\CMS\Factory; use \Joomla\CMS\Language\Text; use \Joomla\CMS\Installer\Installer; use \Joomla\CMS\Installer\InstallerScript; /** * Updates the database structure of the component * * @version Release: 0.2b * @author Component Creator * @since 0.1b */ class com_highlightsInstallerScript extends InstallerScript { /** * The title of the component (printed on installation and uninstallation messages) * * @var string */ protected $extension = 'Highlights'; /** * The minimum Joomla! version required to install this extension * * @var string */ protected $minimumJoomla = '4.0'; /** * Method called before install/update the component. Note: This method won't be called during uninstall process. * * @param string $type Type of process [install | update] * @param mixed $parent Object who called this method * * @return boolean True if the process should continue, false otherwise * @throws Exception */ public function preflight($type, $parent) { $result = parent::preflight($type, $parent); if (!$result) { return $result; } // logic for preflight before install return $result; } /** * Method to install the component * * @param mixed $parent Object who called this method. * * @return void * * @since 0.2b */ public function install($parent) { $this->installDb($parent); $this->installPlugins($parent); $this->installModules($parent); } /** * Method to update the DB of the component * * @param mixed $parent Object who started the upgrading process * * @return void * * @since 0.2b * @throws Exception */ private function installDb($parent) { $installation_folder = $parent->getParent()->getPath('source'); $app = Factory::getApplication(); if (function_exists('simplexml_load_file') && file_exists($installation_folder . '/installer/structure.xml')) { $component_data = simplexml_load_file($installation_folder . '/installer/structure.xml'); // Check if there are tables to import. foreach ($component_data->children() as $table) { $this->processTable($app, $table); } } else { if (!function_exists('simplexml_load_file')) { $app->enqueueMessage(Text::_('This script needs \'simplexml_load_file\' to update the component')); } else { $app->enqueueMessage(Text::_('Structure file was not found.')); } } } /** * Process a table * * @param CMSApplication $app Application object * @param SimpleXMLElement $table Table to process * * @return void * * @since 0.2b */ private function processTable($app, $table) { $db = Factory::getContainer()->get('DatabaseDriver'); $table_added = false; if (isset($table['action'])) { switch ($table['action']) { case 'add': // Check if the table exists before create the statement if (!$this->existsTable($table['table_name'])) { $create_statement = $this->generateCreateTableStatement($table); $db->setQuery($create_statement); try { $db->execute(); $app->enqueueMessage( Text::sprintf( 'Table `%s` has been successfully created', (string) $table['table_name'] ) ); $table_added = true; } catch (Exception $ex) { $app->enqueueMessage( Text::sprintf( 'There was an error creating the table `%s`. Error: %s', (string) $table['table_name'], $ex->getMessage() ), 'error' ); } } break; case 'change': // Check if the table exists first to avoid errors. if ($this->existsTable($table['old_name']) && !$this->existsTable($table['new_name'])) { try { $db->renameTable($table['old_name'], $table['new_name']); $app->enqueueMessage( Text::sprintf( 'Table `%s` was successfully renamed to `%s`', $table['old_name'], $table['new_name'] ) ); } catch (Exception $ex) { $app->enqueueMessage( Text::sprintf( 'There was an error renaming the table `%s`. Error: %s', $table['old_name'], $ex->getMessage() ), 'error' ); } } else { if (!$this->existsTable($table['table_name'])) { // If the table does not exists, let's create it. $create_statement = $this->generateCreateTableStatement($table); $db->setQuery($create_statement); try { $db->execute(); $app->enqueueMessage( Text::sprintf('Table `%s` has been successfully created', $table['table_name']) ); $table_added = true; } catch (Exception $ex) { $app->enqueueMessage( Text::sprintf( 'There was an error creating the table `%s`. Error: %s', $table['table_name'], $ex->getMessage() ), 'error' ); } } } break; case 'remove': try { // We make sure that the table will be removed only if it exists specifying ifExists argument as true. $db->dropTable((string) $table['table_name'], true); $app->enqueueMessage( Text::sprintf('Table `%s` was successfully deleted', $table['table_name']) ); } catch (Exception $ex) { $app->enqueueMessage( Text::sprintf( 'There was an error deleting Table `%s`. Error: %s', $table['table_name'], $ex->getMessage() ), 'error' ); } break; } } // If the table wasn't added before, let's process the fields of the table if (!$table_added) { if ($this->existsTable($table['table_name'])) { $this->executeFieldsUpdating($app, $table); } } } /** * Checks if a certain exists on the current database * * @param string $table_name Name of the table * * @return boolean True if it exists, false if it does not. */ private function existsTable($table_name) { $db = Factory::getContainer()->get('DatabaseDriver'); $table_name = str_replace('#__', $db->getPrefix(), (string) $table_name); return in_array($table_name, $db->getTableList()); } /** * Generates a 'CREATE TABLE' statement for the tables passed by argument. * * @param SimpleXMLElement $table Table of the database * * @return string 'CREATE TABLE' statement */ private function generateCreateTableStatement($table) { $create_table_statement = ''; if (isset($table->field)) { $fields = $table->children(); $fields_definitions = array(); $indexes = array(); $db = Factory::getContainer()->get('DatabaseDriver'); foreach ($fields as $field) { $field_definition = $this->generateColumnDeclaration($field); if ($field_definition !== false) { $fields_definitions[] = $field_definition; } if ($field['index'] == 'index') { $indexes[] = $field['field_name']; } } foreach ($indexes as $index) { $fields_definitions[] = Text::sprintf( 'INDEX %s (%s ASC)', $db->quoteName((string) $index), $index ); } // Avoid duplicate PK definition if (strpos(implode(',', $fields_definitions), 'PRIMARY KEY') === false) { $fields_definitions[] = 'PRIMARY KEY (`id`)'; } $create_table_statement = Text::sprintf( 'CREATE TABLE IF NOT EXISTS %s (%s)', $table['table_name'], implode(',', $fields_definitions) ); if(isset($table['storage_engine']) && !empty($table['storage_engine'])) { $create_table_statement .= " ENGINE=" . $table['storage_engine']; } if(isset($table['collation'])) { $create_table_statement .= " DEFAULT COLLATE=" . $table['collation']; } } return $create_table_statement; } /** * Generate a column declaration * * @param SimpleXMLElement $field Field data * * @return string Column declaration */ private function generateColumnDeclaration($field) { $db = Factory::getContainer()->get('DatabaseDriver'); $col_name = $db->quoteName((string) $field['field_name']); $data_type = $this->getFieldType($field); if ($data_type !== false) { $default_value = (isset($field['default'])) ? 'DEFAULT ' . $field['default'] : ''; $other_data = ''; if (isset($field['is_autoincrement']) && $field['is_autoincrement'] == 1) { $other_data .= ' AUTO_INCREMENT PRIMARY KEY'; } $comment_value = (isset($field['description'])) ? 'COMMENT ' . $db->quote((string) $field['description']) : ''; if(strtolower($field['field_type']) == 'datetime' || strtolower($field['field_type']) == 'text') { return Text::sprintf( '%s %s %s %s %s', $col_name, $data_type, $default_value, $other_data, $comment_value ); } if((isset($field['required']) && $field['required'] == 1) || $field['field_name'] == 'id') { return Text::sprintf( '%s %s NOT NULL %s %s %s', $col_name, $data_type, $default_value, $other_data, $comment_value ); } return Text::sprintf( '%s %s NULL %s %s %s', $col_name, $data_type, $default_value, $other_data, $comment_value ); } return false; } /** * Generates SQL field type of a field. * * @param SimpleXMLElement $field Field information * * @return mixed SQL string data type, false on failure. */ private function getFieldType($field) { $data_type = (string) $field['field_type']; if (isset($field['field_length']) && ($this->allowsLengthField($data_type) || $data_type == 'ENUM')) { $data_type .= '(' . (string) $field['field_length'] . ')'; } return (!empty($data_type)) ? $data_type : false; } /** * Check if a SQL type allows length values. * * @param string $field_type SQL type * * @return boolean True if it allows length values, false if it does not. */ private function allowsLengthField($field_type) { $allow_length = array( 'INT', 'VARCHAR', 'CHAR', 'TINYINT', 'SMALLINT', 'MEDIUMINT', 'INTEGER', 'BIGINT', 'FLOAT', 'DOUBLE', 'DECIMAL', 'NUMERIC' ); return (in_array((string) $field_type, $allow_length)); } /** * Updates all the fields related to a table. * * @param CMSApplication $app Application Object * @param SimpleXMLElement $table Table information. * * @return void */ private function executeFieldsUpdating($app, $table) { if (isset($table->field)) { foreach ($table->children() as $field) { $table_name = (string) $table['table_name']; $this->processField($app, $table_name, $field); } } } /** * Process a certain field. * * @param CMSApplication $app Application object * @param string $table_name The name of the table that contains the field. * @param SimpleXMLElement $field Field Information. * * @return void */ private function processField($app, $table_name, $field) { $db = Factory::getContainer()->get('DatabaseDriver'); if (isset($field['action'])) { switch ($field['action']) { case 'add': $result = $this->addField($table_name, $field); if ($result === MODIFIED) { $app->enqueueMessage( Text::sprintf('Field `%s` has been successfully added', $field['field_name']) ); } else { if ($result !== NOT_MODIFIED) { $app->enqueueMessage( Text::sprintf( 'There was an error adding the field `%s`. Error: %s', $field['field_name'], $result ), 'error' ); } } break; case 'change': if (isset($field['old_name']) && isset($field['new_name'])) { if ($this->existsField($table_name, $field['old_name']) && !$this->existsField($table_name, $field['new_name'])) { $renaming_statement = Text::sprintf( 'ALTER TABLE %s CHANGE %s %s %s', $table_name, $db->quoteName($field['old_name']->__toString()), $db->quoteName($field['new_name']->__toString()), $this->getFieldType($field) ); $db->setQuery($renaming_statement); try { $db->execute(); $app->enqueueMessage( Text::sprintf('Field `%s` has been successfully modified', $field['old_name']) ); } catch (Exception $ex) { $app->enqueueMessage( Text::sprintf( 'There was an error modifying the field `%s`. Error: %s', $field['field_name'], $ex->getMessage() ), 'error' ); } } else { $result = $this->addField($table_name, $field); if ($result === MODIFIED) { $app->enqueueMessage( Text::sprintf('Field `%s` has been successfully modified', $field['field_name']) ); } else { if ($result !== NOT_MODIFIED) { $app->enqueueMessage( Text::sprintf( 'There was an error modifying the field `%s`. Error: %s', $field['field_name'], $result ), 'error' ); } } } } else { $result = $this->addField($table_name, $field); if ($result === MODIFIED) { $app->enqueueMessage( Text::sprintf('Field `%s` has been successfully modified', $field['field_name']) ); } else { if ($result !== NOT_MODIFIED) { $app->enqueueMessage( Text::sprintf( 'There was an error modifying the field `%s`. Error: %s', $field['field_name'], $result ), 'error' ); } } } break; case 'remove': // Check if the field exists first to prevent issue removing the field if ($this->existsField($table_name, $field['field_name'])) { $drop_statement = Text::sprintf( 'ALTER TABLE `%s` DROP COLUMN `%s`', $table_name, $field['field_name'] ); $db->setQuery($drop_statement); try { $db->execute(); $app->enqueueMessage( Text::sprintf('Field `%s` has been successfully deleted', $field['field_name']) ); } catch (Exception $ex) { $app->enqueueMessage( Text::sprintf( 'There was an error deleting the field `%s`. Error: %s', $field['field_name'], $ex->getMessage() ), 'error' ); } } break; } } else { $result = $this->addField($table_name, $field); if ($result === MODIFIED) { $app->enqueueMessage( Text::sprintf('Field `%s` has been successfully added', $field['field_name']) ); } else { if ($result !== NOT_MODIFIED) { $app->enqueueMessage( Text::sprintf( 'There was an error adding the field `%s`. Error: %s', $field['field_name'], $result ), 'error' ); } } } } /** * Add a field if it does not exists or modify it if it does. * * @param string $table_name Table name * @param SimpleXMLElement $field Field Information * * @return mixed Constant on success(self::$MODIFIED | self::$NOT_MODIFIED), error message if an error occurred */ private function addField($table_name, $field) { $db = Factory::getContainer()->get('DatabaseDriver'); $query_generated = false; // Check if the field exists first to prevent issues adding the field if ($this->existsField($table_name, $field['field_name'])) { if ($this->needsToUpdate($table_name, $field)) { $change_statement = $this->generateChangeFieldStatement($table_name, $field); $db->setQuery($change_statement); $query_generated = true; } } else { $add_statement = $this->generateAddFieldStatement($table_name, $field); $db->setQuery($add_statement); $query_generated = true; } if ($query_generated) { try { $db->execute(); return MODIFIED; } catch (Exception $ex) { return $ex->getMessage(); } } return NOT_MODIFIED; } /** * Checks if a field exists on a table * * @param string $table_name Table name * @param string $field_name Field name * * @return boolean True if exists, false if it do */ private function existsField($table_name, $field_name) { $db = Factory::getContainer()->get('DatabaseDriver'); return in_array((string) $field_name, array_keys($db->getTableColumns($table_name))); } /** * Check if a field needs to be updated. * * @param string $table_name Table name * @param SimpleXMLElement $field Field information * * @return boolean True if the field has to be updated, false otherwise */ private function needsToUpdate($table_name, $field) { if(!isset($field['action']) || $field['field_name'] == 'id') { return false; } $db = Factory::getContainer()->get('DatabaseDriver'); $query = Text::sprintf( 'SHOW FULL COLUMNS FROM `%s` WHERE Field LIKE %s', $table_name, $db->quote((string) $field['field_name']) ); $db->setQuery($query); $field_info = $db->loadObject(); if (strcasecmp($field_info->Type, $this->getFieldType($field)) !=0) { return true; } else { return false; } } /** * Generates an change column statement * * @param string $table_name Table name * @param SimpleXMLElement $field Field Information * * @return string Change column statement */ private function generateChangeFieldStatement($table_name, $field) { $column_declaration = $this->generateColumnDeclaration($field); return Text::sprintf('ALTER TABLE %s MODIFY %s', $table_name, $column_declaration); } /** * Generates an add column statement * * @param string $table_name Table name * @param SimpleXMLElement $field Field Information * * @return string Add column statement */ private function generateAddFieldStatement($table_name, $field) { $column_declaration = $this->generateColumnDeclaration($field); return Text::sprintf('ALTER TABLE %s ADD %s', $table_name, $column_declaration); } /** * Installs plugins for this component * * @param mixed $parent Object who called the install/update method * * @return void */ private function installPlugins($parent) { $installation_folder = $parent->getParent()->getPath('source'); $app = Factory::getApplication(); /* @var $plugins SimpleXMLElement */ if (method_exists($parent, 'getManifest')) { $plugins = $parent->getManifest()->plugins; } else { $plugins = $parent->get('manifest')->plugins; } if (count($plugins->children())) { $db = Factory::getContainer()->get('DatabaseDriver'); $query = $db->getQuery(true); foreach ($plugins->children() as $plugin) { $pluginName = (string) $plugin['plugin']; $pluginGroup = (string) $plugin['group']; $path = $installation_folder . '/plugins/' . $pluginGroup . '/' . $pluginName; $installer = new Installer; if (!$this->isAlreadyInstalled('plugin', $pluginName, $pluginGroup)) { $result = $installer->install($path); } else { $result = $installer->update($path); } if ($result) { $app->enqueueMessage('Plugin ' . $pluginName . ' was installed successfully'); } else { $app->enqueueMessage('There was an issue installing the plugin ' . $pluginName, 'error'); } $query ->clear() ->update('#__extensions') ->set('enabled = 1') ->where( array( 'type LIKE ' . $db->quote('plugin'), 'element LIKE ' . $db->quote($pluginName), 'folder LIKE ' . $db->quote($pluginGroup) ) ); $db->setQuery($query); $db->execute(); } } } /** * Check if an extension is already installed in the system * * @param string $type Extension type * @param string $name Extension name * @param mixed $folder Extension folder(for plugins) * * @return boolean */ private function isAlreadyInstalled($type, $name, $folder = null) { $result = false; switch ($type) { case 'plugin': $result = file_exists(JPATH_PLUGINS . '/' . $folder . '/' . $name); break; case 'module': $result = file_exists(JPATH_SITE . '/modules/' . $name); break; } return $result; } /** * Installs plugins for this component * * @param mixed $parent Object who called the install/update method * * @return void */ private function installModules($parent) { $installation_folder = $parent->getParent()->getPath('source'); $app = Factory::getApplication(); if (method_exists($parent, 'getManifest')) { $modules = $parent->getManifest()->modules; } else { $modules = $parent->get('manifest')->modules; } if (!empty($modules)) { if (count($modules->children())) { foreach ($modules->children() as $module) { $moduleName = (string) $module['module']; $path = $installation_folder . '/modules/' . $moduleName; $installer = new Installer; if (!$this->isAlreadyInstalled('module', $moduleName)) { $result = $installer->install($path); } else { $result = $installer->update($path); } if ($result) { $app->enqueueMessage('Module ' . $moduleName . ' was installed successfully'); } else { $app->enqueueMessage('There was an issue installing the module ' . $moduleName, 'error'); } } } } } /** * Method to update the component * * @param mixed $parent Object who called this method. * * @return void */ public function update($parent) { $this->installDb($parent); $this->installPlugins($parent); $this->installModules($parent); } /** * Method to uninstall the component * * @param mixed $parent Object who called this method. * * @return void */ public function uninstall($parent) { $this->uninstallPlugins($parent); $this->uninstallModules($parent); } /** * Uninstalls plugins * * @param mixed $parent Object who called the uninstall method * * @return void */ private function uninstallPlugins($parent) { $app = Factory::getApplication(); if (method_exists($parent, 'getManifest')) { $plugins = $parent->getManifest()->plugins; } else { $plugins = $parent->get('manifest')->plugins; } if (count($plugins->children())) { $db = Factory::getContainer()->get('DatabaseDriver'); $query = $db->getQuery(true); foreach ($plugins->children() as $plugin) { $pluginName = (string) $plugin['plugin']; $pluginGroup = (string) $plugin['group']; $query ->clear() ->select('extension_id') ->from('#__extensions') ->where( array( 'type LIKE ' . $db->quote('plugin'), 'element LIKE ' . $db->quote($pluginName), 'folder LIKE ' . $db->quote($pluginGroup) ) ); $db->setQuery($query); $extension = $db->loadResult(); if (!empty($extension)) { $installer = new Installer; $result = $installer->uninstall('plugin', $extension); if ($result) { $app->enqueueMessage('Plugin ' . $pluginName . ' was uninstalled successfully'); } else { $app->enqueueMessage('There was an issue uninstalling the plugin ' . $pluginName, 'error'); } } } } } /** * Uninstalls plugins * * @param mixed $parent Object who called the uninstall method * * @return void */ private function uninstallModules($parent) { $app = Factory::getApplication(); if (method_exists($parent, 'getManifest')) { $modules = $parent->getManifest()->modules; } else { $modules = $parent->get('manifest')->modules; } if (!empty($modules)) { if (count($modules->children())) { $db = Factory::getContainer()->get('DatabaseDriver'); $query = $db->getQuery(true); foreach ($modules->children() as $plugin) { $moduleName = (string) $plugin['module']; $query ->clear() ->select('extension_id') ->from('#__extensions') ->where( array( 'type LIKE ' . $db->quote('module'), 'element LIKE ' . $db->quote($moduleName) ) ); $db->setQuery($query); $extension = $db->loadResult(); if (!empty($extension)) { $installer = new Installer; $result = $installer->uninstall('module', $extension); if ($result) { $app->enqueueMessage('Module ' . $moduleName . ' was uninstalled successfully'); } else { $app->enqueueMessage('There was an issue uninstalling the module ' . $moduleName, 'error'); } } } } } } /** * @param string $type type * @param string $parent parent * * @return boolean * @since Kunena */ public function postflight($type, $parent) { return true; } }