{ (folder) => { (module) }* }* 'modules' => [ 'admin' => [], 'site' => [], ], // plugins => { (folder) => { (element) }* }* 'plugins' => [ 'system' => [], ], ]; /** * Obsolete files and folders to remove from the free version only. This is used when you move a feature from the * free version of your extension to its paid version. If you don't have such a distinction you can ignore this. * * @var array */ protected $removeFilesFree = [ 'files' => [ // Use pathnames relative to your site's root, e.g. // 'administrator/components/com_foobar/helpers/whatever.php' ], 'folders' => [ // Use pathnames relative to your site's root, e.g. // 'administrator/components/com_foobar/baz' ], ]; /** * Obsolete files and folders to remove from both paid and free releases. This is used when you refactor code and * some files inevitably become obsolete and need to be removed. * * @var array */ protected $removeFilesAllVersions = [ 'files' => [ // Use pathnames relative to your site's root, e.g. // 'administrator/components/com_foobar/helpers/whatever.php' ], 'folders' => [ // Use pathnames relative to your site's root, e.g. // 'administrator/components/com_foobar/baz' ], ]; /** * A list of scripts to be copied to the "cli" directory of the site * * @var array */ protected $cliScriptFiles = [ // Use just the filename, e.g. // 'my-cron-script.php' ]; /** * The path inside your package where cli scripts are stored * * @var string */ protected $cliSourcePath = 'cli'; /** * Is the schemaXmlPath class variable a relative path? If set to true the schemaXmlPath variable contains a path * relative to the component's back-end directory. If set to false the schemaXmlPath variable contains an absolute * filesystem path. * * @var boolean */ protected $schemaXmlPathRelative = true; /** * The path where the schema XML files are stored. Its contents depend on the schemaXmlPathRelative variable above * true => schemaXmlPath contains a path relative to the component's back-end directory * false => schemaXmlPath contains an absolute filesystem path * * @var string */ protected $schemaXmlPath = 'sql/xml'; /** * Is this the paid version of the extension? This only determines which files / extensions will be removed. * * @var boolean */ protected $isPaid = false; /** * Module installer script constructor. */ public function __construct() { // Get the plugin name and folder from the class name (it's always plgFolderPluginInstallerScript) if necessary. if (empty($this->componentName)) { $class = get_class($this); $words = preg_replace('/(\s)+/', '_', $class); $words = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $words)); $classParts = explode('_', $words); $this->componentName = 'com_' . $classParts[2]; } } /** * Joomla! pre-flight event. This runs before Joomla! installs or updates the component. This is our last chance to * tell Joomla! if it should abort the installation. * * @param string $type Installation type (install, update, * discover_install) * @param ComponentAdapter $parent Parent object * * @return boolean True to let the installation proceed, false to halt the installation */ public function preflight($type, $parent) { // Check the minimum PHP version if (!$this->checkPHPVersion()) { return false; } // Check the minimum Joomla! version if (!$this->checkJoomlaVersion()) { return false; } // Clear op-code caches to prevent any cached code issues $this->clearOpcodeCaches(); // Workarounds for JInstaller issues. if (in_array($type, ['install', 'discover_install'])) { // Bug fix for "Database function returned no error" $this->bugfixDBFunctionReturnedNoError(); } else { // Bug fix for "Can not build admin menus" $this->bugfixCantBuildAdminMenus(); } return true; } /** * Runs after install, update or discover_update. In other words, it executes after Joomla! has finished installing * or updating your component. This is the last chance you've got to perform any additional installations, clean-up, * database updates and similar housekeeping functions. * * @param string $type install, update or discover_update * @param ComponentAdapter $parent Parent object * * @return void * @throws Exception * */ public function postflight($type, $parent) { // Add ourselves to the list of extensions depending on FOF30 $this->addDependency('fof30', $this->componentName); // Install or update database $dbInstaller = new DatabaseInstaller(Factory::getDbo(), ($this->schemaXmlPathRelative ? JPATH_ADMINISTRATOR . '/components/' . $this->componentName : '') . '/' . $this->schemaXmlPath ); $dbInstaller->updateSchema(); // These workarounds are only needed, and only work, on Joomla! 3.x if (strpos(JVERSION, '3.') === 0) { // Make sure menu items are installed $this->_createAdminMenus($parent); // Make sure menu items are published $this->_reallyPublishAdminMenuItems($parent); } // Which files should I remove? if ($this->isPaid) { // This is the paid version, only remove the removeFilesAllVersions files $removeFiles = $this->removeFilesAllVersions; } else { // This is the free version, remove the removeFilesAllVersions and removeFilesFree files $removeFiles = ['files' => [], 'folders' => []]; if (isset($this->removeFilesAllVersions['files'])) { if (isset($this->removeFilesFree['files'])) { $removeFiles['files'] = array_merge($this->removeFilesAllVersions['files'], $this->removeFilesFree['files']); } else { $removeFiles['files'] = $this->removeFilesAllVersions['files']; } } elseif (isset($this->removeFilesFree['files'])) { $removeFiles['files'] = $this->removeFilesFree['files']; } if (isset($this->removeFilesAllVersions['folders'])) { if (isset($this->removeFilesFree['folders'])) { $removeFiles['folders'] = array_merge($this->removeFilesAllVersions['folders'], $this->removeFilesFree['folders']); } else { $removeFiles['folders'] = $this->removeFilesAllVersions['folders']; } } elseif (isset($this->removeFilesFree['folders'])) { $removeFiles['folders'] = $this->removeFilesFree['folders']; } } // Remove obsolete files and folders $this->removeFilesAndFolders($removeFiles); // Make sure everything is copied properly $this->bugfixFilesNotCopiedOnUpdate($parent); // Copy the CLI files (if any) $this->copyCliFiles($parent); // Show the post-installation page $this->renderPostInstallation($parent); // Uninstall obsolete sub-extensions $this->uninstallObsoleteSubextensions($parent); // Clear the FOF cache $false = false; $cache = Factory::getCache('fof', ''); $cache->store($false, 'cache', 'fof'); // Make sure the Joomla! menu structure is correct $this->_rebuildMenu(); // Add post-installation messages on Joomla! 3.2 and later $this->_applyPostInstallationMessages(); // Clear the opcode caches again - in case someone accessed the extension while the files were being upgraded. $this->clearOpcodeCaches(); } /** * Runs on uninstallation * * @param ComponentAdapter $parent The parent object */ public function uninstall($parent) { // Uninstall database $dbInstaller = new DatabaseInstaller(Factory::getDbo(), ($this->schemaXmlPathRelative ? JPATH_ADMINISTRATOR . '/components/' . $this->componentName : '') . '/' . $this->schemaXmlPath ); $dbInstaller->removeSchema(); // Uninstall post-installation messages on Joomla! 3.2 and later $this->uninstallPostInstallationMessages(); // Remove ourselves from the list of extensions depending on FOF30 $this->removeDependency('fof30', $this->componentName); // Show the post-uninstallation page $this->renderPostUninstallation($parent); } /** * Copies the CLI scripts into Joomla!'s cli directory * * @param ComponentAdapter $parent */ protected function copyCliFiles($parent) { $src = $parent->getParent()->getPath('source'); foreach ($this->cliScriptFiles as $script) { if (is_file(JPATH_ROOT . '/cli/' . $script)) { File::delete(JPATH_ROOT . '/cli/' . $script); } if (is_file($src . '/' . $this->cliSourcePath . '/' . $script)) { File::copy($src . '/' . $this->cliSourcePath . '/' . $script, JPATH_ROOT . '/cli/' . $script); } } } /** * Fix for Joomla bug: sometimes files are not copied on update. * * We have observed that ever since Joomla! 1.5.5, when Joomla! is performing an extension update some files / * folders are not copied properly. This seems to be a bit random and seems to be more likely to happen the more * added / modified files and folders you have. We are trying to work around it by retrying the copy operation * ourselves WITHOUT going through the manifest, based entirely on the conventions we follow for Akeeba Ltd's * extensions. * * @param ComponentAdapter $parent */ protected function bugfixFilesNotCopiedOnUpdate($parent) { Log::add("Joomla! extension update workaround for component $this->componentName", Log::INFO, 'fof3_extension_installation'); $temporarySource = $parent->getParent()->getPath('source'); $copyMap = [ // Backend component files 'backend' => JPATH_ADMINISTRATOR . '/components/' . $this->componentName, 'admin' => JPATH_ADMINISTRATOR . '/components/' . $this->componentName, // Frontend component files 'frontend' => JPATH_SITE . '/components/' . $this->componentName, 'site' => JPATH_SITE . '/components/' . $this->componentName, // Backend language 'language/backend' => JPATH_ADMINISTRATOR . '/language', 'language/admin' => JPATH_ADMINISTRATOR . '/language', // Frontend language 'language/frontend' => JPATH_SITE . '/language', 'language/site' => JPATH_SITE . '/language', // Media files 'media' => JPATH_ROOT . '/media/' . $this->componentName, ]; foreach ($copyMap as $partialSource => $target) { $source = $temporarySource . '/' . $partialSource; Log::add(__CLASS__ . ":: Conditional copy $source to $target", Log::DEBUG, 'fof3_extension_installation'); $this->recursiveConditionalCopy($source, $target); } } /** * Override this method to display a custom component installation message if you so wish * * @param ComponentAdapter $parent Parent class calling us */ protected function renderPostInstallation($parent) { echo "