name : bfExtensions.php
<?php
/*
 * @package   bfNetwork
 * @copyright Copyright (C) 2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022 Blue Flame Digital Solutions Ltd. All rights reserved.
 * @license   GNU General Public License version 3 or later
 *
 * @see       https://mySites.guru/
 * @see       https://www.phil-taylor.com/
 *
 * @author    Phil Taylor / Blue Flame Digital Solutions Limited.
 *
 * bfNetwork is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * bfNetwork is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this package.  If not, see http://www.gnu.org/licenses/
 *
 * If you have any questions regarding this code, please contact [email protected]
 */

use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Uri\Uri;

/*
 * If we have not already included bfEncrypt then this must be a direct call, and
 * so we need to decrypt the incoming request.
 */
if (! class_exists('bfEncrypt')) {
    require 'bfEncrypt.php';
}

/*
 * Ok so this is stupid, but we are dealing with XML Parsing on crappy servers on sites with 100+ extensions installed - gulp!
 * 5 Mins! GULP - Most well configured servers will probably not honour this, but in our live tests on crappy servers this seems to work
 */
@set_time_limit(60 * 5);

/**
 * If we have got here then we have already passed through decrypting the encrypted header and so we are sure we are now
 * secure and no one else cannot run the code below.
 */
final class bfExtensions
{
    /**
     * @var JDatabase|JDatabaseDriver|object
     */
    private $db;

    /**
     * Incoming decrypted vars from the request.
     *
     * @var stdClass
     */
    private $_dataObj;

    /**
     * We pass the command to run as a simple integer in our encrypted request this is mainly to speed up the decryption
     * process, plus its a single digit(or 2) rather than a huge string to remember :-).
     */
    private $_methods = array(
        1 => 'getExtensions',
        2 => 'installExtensionFromUrl',
    );

    /**
     * @param stdClass $dataObj
     */
    public function __construct($dataObj = null)
    {
        // init Joomla
        require 'bfInitJoomla.php';

        // Get some Joomla version
        $VERSION = new JVersion();

        if ('1.5' != $VERSION->RELEASE) {
            // ffs falang!
            JFactory::$database = null;
        }

        // Set the request vars
        $this->_dataObj = $dataObj;
    }

    /**
     * I'm the controller - I run methods based on the request integer.
     */
    public function run()
    {
        if (property_exists($this->_dataObj, 'c')) {
            $c = (int) $this->_dataObj->c;
            if (array_key_exists($c, $this->_methods)) {
                // call the right method
                $this->{$this->_methods[$c]} ();
            } else {
                // Die if an unknown function
                bfEncrypt::reply('error', 'No Such method #err1 - ' . $c);
            }
        } else {
            // Die if an unknown function
            bfEncrypt::reply('error', 'No Such method #err2');
        }
    }

    /**
     * Install an extension from a provided URL.
     *
     * Parts of this code are from Joomla CMS, Modified under license.
     *
     * @license   GNU General Public License version 2 or later; see https://github.com/joomla/joomla-cms/blob/staging/LICENSE.txt
     * @copyright Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
     */
    public function installExtensionFromUrl()
    {
        // Manipulate the base Uri in the Joomla Stack to provide compatibility with some 3pd extensions like ACL Manager & ACYMailing!
        try {
            $uri = Uri::getInstance();

            $reflection   = new \ReflectionClass($uri);
            $baseProperty = $reflection->getProperty('base');
            $baseProperty->setAccessible(true);
            $base           = $baseProperty->getValue();
            $base['prefix'] = $uri->toString(array('scheme', 'host'));
            $base['path']   = '/';
            $baseProperty->setValue($base);
        } catch (ReflectionException $e) {
        }

        // Get an installer instance.
        $installer = JInstaller::getInstance();

        /*
         * Unfortunately, we need to load the installer and system plugins so that extensions like 4SEO and others with
         * libraries can access their own code. This builds a bigger stack which we did not really want to do.
         */
        JPluginHelper::importPlugin('installer');
        JPluginHelper::importPlugin('system');
        JFactory::getApplication()->triggerEvent('onAfterInitialise');

        // Get the URL of the package to install.
        $url = $this->_dataObj->url;

        // Download the package at the URL given.
        $p_file = JInstallerHelper::downloadPackage($url);

        // Was the package downloaded?
        if ($p_file) {
            // Unpack the downloaded package file.
            $package = JInstallerHelper::unpack(JFactory::getConfig()->get('tmp_path') . '/' . $p_file, true);

            // Install the package.
            if (! $installer->install($package['dir'])) {
                // There was an error installing the package.
                $msg     = 'There was an error installing the package';
                $result  = false;
                $msgType = 'error';
            } else {
                // Package installed successfully.
                $msg     = 'Package installed successfully';
                $result  = true;
                $msgType = 'message';
            }

            // Cleanup the install files.
            if (! is_file($package['packagefile'])) {
                $config                 = JFactory::getConfig();
                $package['packagefile'] = $config->get('tmp_path') . '/' . $package['packagefile'];
            }

            // Cleanup the package file
            JInstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']);
        } else {
            $result  = false;
            $msgType = 'error';
            $msg     = 'Could not download package from URL: ' . $url;
        }

        $res = array(
            'result'            => $result,
            'msgType'           => $msgType,
            'message'           => $msg,
            'extension_message' => $installer->get('extension_message'),
            'redirect_url'      => $installer->get('redirect_url'),
        );

        bfEncrypt::reply($res['result'], $res);
    }

    /**
     * Get a JSON formatted list of installed extensions Needs to be public so we can call it from the audit.
     *
     * @return string
     */
    public function getExtensions()
    {
        // connect/get the Joomla db object
        $this->db = JFactory::getDbo();

        // crazy way of handling Joomla 1.5.x legacy :-(
        $one5 = false;

        // Get Joomla 2.0+ Extensions
        $this->db->setQuery('SELECT e.extension_id, e.name, e.type, e.element, e.enabled, e.folder,
                                (
                                 SELECT title
                                 FROM #__menu AS m
                                 WHERE m.component_id = e.extension_id
                                 AND parent_id = 1
                                 ORDER BY ID ASC LIMIT 1
                                 )
                                 AS title
                                FROM #__extensions AS e
                                WHERE protected = 0');
        $installedExtensions = $this->db->loadObjectList();

        // ok if we have none maybe we are Joomla < 1.5.26
        if (! $installedExtensions) {
            // Yooo hoo I'm on a crap old, out of date, probably hackable Joomla version!
            $one5 = true;

            // Get the extensions - used to be called components
            $this->db->setQuery(
                'SELECT "component" as "type", name, `option` as "element", enabled FROM #__components WHERE iscore != 1 and parent = 0'
            );
            $components = $this->db->loadObjectList();

            // Get the plugins
            $this->db->setQuery('SELECT "plugin" as "type", name, element, folder, published as enabled FROM #__plugins WHERE iscore != 1');
            $plugins = $this->db->loadObjectList();

            // get the modules
            $this->db->setQuery(
                'SELECT  "module" as "type", module, module as name, client_id, published as enabled FROM #__modules WHERE iscore != 1'
            );
            $modules = $this->db->loadObjectList();

            /**
             * Get the templates - I n Joomla 1.5.x the templates are not in the db unless published so we need to read
             * the folders from the /templates folders Note in Joomla 1.5.x there was no such think as admin templates.
             */
            $folders   = array_merge(scandir(JPATH_BASE . '/templates'), scandir(JPATH_ADMINISTRATOR . '/templates'));
            $templates = array();
            foreach ($folders as $templateFolder) {
                $f = JPATH_BASE . '/templates/' . trim($templateFolder);
                $a = JPATH_ADMINISTRATOR . '/templates/' . trim($templateFolder);

                // We dont want index.html etc...
                if (! is_dir($f) && ! is_dir($a) || ('.' == $templateFolder || '..' == $templateFolder)) {
                    continue;
                }

                if (is_dir($a)) {
                    $client_id = 1;
                } else {
                    $client_id = 0;
                }

                // make it look like we want like Joomla 2.5+ would
                $template = array(
                    'type'      => 'template',
                    'template'  => $templateFolder,
                    'client_id' => $client_id,
                    'enabled'   => 1,
                );

                // Convert to an obj
                $templates[] = json_decode(json_encode($template));
            }

            // Merge all the "extensions" we have found all over the place
            $installedExtensions = array_merge($components, $plugins, $modules, $templates);
        }

        $lang = JFactory::getLanguage();

        // Load all the language strings up front incase any strings are shared
        foreach ($installedExtensions as $k => $ext) {
            $lang->load(strtolower($ext->element) . '.sys', JPATH_ADMINISTRATOR, 'en-GB', true);
            $lang->load(strtolower($ext->element) . '.sys', JPATH_SITE, 'en-GB', true);
            $lang->load(strtolower($ext->name) . '.sys', JPATH_ADMINISTRATOR, 'en-GB', true);
            $lang->load(strtolower($ext->name) . '.sys', JPATH_SITE, 'en-GB', true);
            $lang->load(strtolower($ext->title) . '.sys', JPATH_ADMINISTRATOR, 'en-GB', true);
            $lang->load(strtolower($ext->title) . '.sys', JPATH_SITE, 'en-GB', true);

            $lang->load(strtolower($ext->element), JPATH_ADMINISTRATOR, 'en-GB', true);
            $lang->load(strtolower($ext->element), JPATH_SITE, 'en-GB', true);
            $lang->load(strtolower($ext->name), JPATH_ADMINISTRATOR, 'en-GB', true);
            $lang->load(strtolower($ext->name), JPATH_SITE, 'en-GB', true);
            $lang->load(strtolower($ext->title), JPATH_ADMINISTRATOR, 'en-GB', true);
            $lang->load(strtolower($ext->title), JPATH_SITE, 'en-GB', true);

            $element = str_replace('_TITLE', '', strtoupper($ext->element));
            $name    = str_replace('_TITLE', '', strtoupper($ext->name));
            $title   = str_replace('_TITLE', '', strtoupper($ext->title));

            $lang->load(strtolower($element) . '.sys', JPATH_ADMINISTRATOR, 'en-GB', true);
            $lang->load(strtolower($element) . '.sys', JPATH_SITE, 'en-GB', true);
            $lang->load(strtolower($name) . '.sys', JPATH_ADMINISTRATOR, 'en-GB', true);
            $lang->load(strtolower($name) . '.sys', JPATH_SITE, 'en-GB', true);
            $lang->load(strtolower($title) . '.sys', JPATH_ADMINISTRATOR, 'en-GB', true);
            $lang->load(strtolower($title) . '.sys', JPATH_SITE, 'en-GB', true);

            $lang->load(strtolower($element), JPATH_ADMINISTRATOR, 'en-GB', true);
            $lang->load(strtolower($element), JPATH_SITE, 'en-GB', true);
            $lang->load(strtolower($name), JPATH_ADMINISTRATOR, 'en-GB', true);
            $lang->load(strtolower($name), JPATH_SITE, 'en-GB', true);
            $lang->load(strtolower($title), JPATH_ADMINISTRATOR, 'en-GB', true);
            $lang->load(strtolower($title), JPATH_SITE, 'en-GB', true);

            // templates
            $lang->load('tpl_' . strtolower($name), JPATH_ADMINISTRATOR, 'en-GB', true);
            $lang->load('tpl_' . strtolower($name), JPATH_SITE, 'en-GB', true);

            // Joomla 1.5.x modules
            $lang->load('mod_' . strtolower($name), JPATH_ADMINISTRATOR, 'en-GB', true);
            $lang->load('mod_' . strtolower($name), JPATH_SITE, 'en-GB', true);

            // tut tut Akeeba - bad naming!
            $lang->load(strtolower('PLG_SYSTEM_SRP'), JPATH_ADMINISTRATOR, 'en-GB', true); // should be plg_srp
            $lang->load(strtolower('PLG_SYSTEM_ONECLICKACTION'), JPATH_SITE, 'en-GB', true); // should be plg_oneclickaction
            $lang->load(strtolower('PLG_SYSTEM_ONECLICKACTION'), JPATH_ADMINISTRATOR, 'en-GB', true); // should be plg_oneclickaction

            // Joomla 1.5 plugins
            if ('plugin' == $ext->type) {
                $plg = 'plg_' . $ext->folder . '_' . $ext->element;
                $lang->load(strtolower($plg), JPATH_SITE, 'en-GB', true);
                $lang->load(strtolower($plg), JPATH_ADMINISTRATOR, 'en-GB', true);
            }

            if ('template' == $ext->type) {
                $plg = 'tpl_' . $ext->name;
                $lang->load(strtolower($plg), JPATH_SITE, 'en-GB', true);
                $lang->load(strtolower($plg), JPATH_ADMINISTRATOR, 'en-GB', true);
            }
        }

        // ok now we have the extensions - get the xml for further offline crunching
        foreach ($installedExtensions as $k => $ext) {
            // remove not supported types :-(
            if ('file' == $ext->type || 'package' == $ext->type) {
                unset($installedExtensions[$k]);
                continue;
            }
            $ext->xmlFile = $this->findManifest($ext);

            try {
                if (false !== $ext->xmlFile) {
                    $parts = explode('/', $ext->xmlFile);
                    array_pop($parts);
                    $ext->path = implode('/', $parts);
                    bfLog::log('Loading XML file = ' . str_replace(JPATH_BASE, '', $ext->xmlFile));
                    $xml   = trim(file_get_contents($ext->xmlFile));
                    $myXML = new SimpleXMLElement($xml);
                    if (property_exists($myXML, 'description')) {
                        $ext->desc = $myXML->description;
                    }
                    $ext->xmlFileContents = base64_encode(gzcompress($xml));
                    $ext->xmlFileCreated  = filemtime($ext->xmlFile);
                } else {
                    $ext->MANIFESTERROR = true;
                }
            } catch (Exception $e) {
                bfLog::log('EXCEPTION = ' . $ext->xmlFile . ' ' . $e->getMessage());
                exit('Could not process XML file at: ' . str_replace(JPATH_BASE, '', $ext->xmlFile));
            }

            $ext->name  = JText::_($ext->name);
            $ext->title = JText::_($ext->title);
            $ext->desc  = base64_encode(gzcompress(JText::_($ext->desc)));

            // remove base paths - we dont want to leak data :)
            $ext->xmlFile = $this->removeBase($ext->xmlFile);
            $ext->path    = $this->removeBase($ext->path);

            // Sort so its pretty - not that anyone sees, but debugging is easier
            $ext = (array) $ext;
            ksort($ext);

            // push to the result
            $installedExtensions[$k] = $ext;
        }

        return json_encode($installedExtensions);
    }

    /**
     * Find the XML file to parse.
     *
     * @param stdClass $ext
     *
     * @return bool
     */
    private function findManifest($ext)
    {
        $prefixes = array('com_',
            'ext_',
            'plg_content_',
            'plg_system_',
            'plg_user_',
            'plg_authentication_',
            'plg_authentication_',
            'plg_captcha_',
            'plg_content_',
            'plg_editors_',
            'plg_editors-xtd_',
            'plg_extension_',
            'plg_finder_',
            'plg_quickicon_',
            'plg_search_',
            'plg_system_',
            'plg_twofactorauth_',
            'plg_user_',
            'plg_',
        );

        if (property_exists($ext, 'element')) {
            $shortName = str_replace($prefixes, '', strtolower($ext->element));
        } else {
            $shortName = str_replace($prefixes, '', strtolower($ext->option));
        }

        $try  = array(); // need to support PHP 5.3 :-(
        $last = array(); // need to support PHP 5.3 :-(

        // Let the UGLY code begin
        switch ($ext->type) {
            case 'component':
                $try[]  = JPATH_ADMINISTRATOR . '/components/' . $ext->element . '/' . $shortName . '.xml';
                $last[] = JPATH_ADMINISTRATOR . '/components/' . $ext->element . '/';
                break;
            case 'module':
                $try[]  = JPATH_ADMINISTRATOR . '/modules/' . $ext->element . '/' . $shortName . '.xml';
                $try[]  = JPATH_BASE . '/modules/' . $ext->element . '/' . $shortName . '.xml';
                $try[]  = JPATH_ADMINISTRATOR . '/modules/' . $ext->module . '/' . $ext->module . '.xml';
                $try[]  = JPATH_BASE . '/modules/' . $ext->module . '/' . $ext->module . '.xml';
                $last[] = JPATH_ADMINISTRATOR . '/modules/' . $ext->module . '/';
                $last[] = JPATH_BASE . '/modules/' . $ext->module . '/';
                break;
            case 'template':
                $try[] = JPATH_ADMINISTRATOR . '/templates/' . $ext->element . '/templateDetails.xml';
                $try[] = JPATH_BASE . '/templates/' . $ext->element . '/templateDetails.xml';
                if (property_exists($ext, 'template')) {
                    $try[] = JPATH_ADMINISTRATOR . '/templates/' . $ext->template . '/templateDetails.xml';
                    $try[] = JPATH_BASE . '/templates/' . $ext->template . '/templateDetails.xml';
                }
                if (property_exists($ext, 'name')) {
                    $try[] = JPATH_ADMINISTRATOR . '/templates/' . $ext->name . '/templateDetails.xml';
                    $try[] = JPATH_BASE . '/templates/' . $ext->name . '/templateDetails.xml';
                }
                break;
            case 'language':
                $try[] = JPATH_ADMINISTRATOR . '/language/' . $ext->element . '/' . $ext->element . '.xml';
                $try[] = JPATH_BASE . '/language/' . $ext->element . '/' . $ext->element . '.xml';
                break;
            case 'plugin':
                $try[] = JPATH_ADMINISTRATOR . '/plugins/' . $ext->element . '/' . $shortName . '.xml';
                $try[] = JPATH_BASE . '/plugins/' . $ext->folder . '/' . $ext->element . '/' . $shortName . '.xml';
                $try[] = JPATH_BASE . '/plugins/' . $ext->element . '/' . $shortName . '.xml';
                $try[] = JPATH_BASE . '/plugins/' . $ext->folder . '/' . $shortName . '.xml';
                $try[] = JPATH_BASE . '/plugins/' . $ext->option . '/' . $shortName . '.xml';

                $last[] = JPATH_ADMINISTRATOR . '/plugins/' . $ext->element . '/';
                $last[] = JPATH_BASE . '/plugins/' . $ext->folder . '/' . $ext->element . '/';
                $last[] = JPATH_BASE . '/plugins/' . $ext->element . '/';
                $last[] = JPATH_BASE . '/plugins/' . $ext->folder . '/';
                $last[] = JPATH_BASE . '/plugins/' . $ext->option . '/';
                break;
        }

        if (count($try)) {
            foreach ($try as $tryThisFile) {
                if (file_exists($tryThisFile)) {
                    return $tryThisFile;
                }
            }
        }

        // argh! still no xml file! - ok lets get tough!
        foreach ($last as $tryThisFolder) {
            $foldersAndFiles = scandir($tryThisFolder);
            foreach ($foldersAndFiles as $f) {
                if (preg_match('/\.xml/', $f)) {
                    $fileContents = file_get_contents($tryThisFolder . '/' . $f);
                    if (preg_match('/(\<install|\<extension )/', $fileContents)) {
                        return realpath($tryThisFolder . '/' . $f);
                    }
                }
            }
            // look for ANY xml files in this folder

            // If you find an xml file - look inside it to see if its a manifest/install
        }

        return false;
    }

    /**
     * Remove the JPATH_BASE from a file with path to prevent leaking absolute paths.
     *
     * @param string $path The full path to a file
     *
     * @return string The absolute path to the file
     */
    private function removeBase($path)
    {
        return str_replace(JPATH_BASE, '', $path);
    }

    /**
     * Updates an extension.
     *
     * @return bool
     */
    public function doUpdate($extensionId)
    {
        // Manipulate the base Uri in the Joomla Stack to provide compatibility with some 3pd extensions like ACL Manager!
        try {
            $uri = Uri::getInstance();

            $reflection   = new \ReflectionClass($uri);
            $baseProperty = $reflection->getProperty('base');
            $baseProperty->setAccessible(true);
            $base           = $baseProperty->getValue();
            $base['prefix'] = $uri->toString(array('scheme', 'host'));
            $base['path']   = '/';
            $baseProperty->setValue($base);
        } catch (ReflectionException $e) {
        }

        /*
         * Unfortunately, we need to load the installer and system plugins so that extensions like 4SEO and others with
         * libraries can access their own code. This builds a bigger stack which we did not really want to do.
         */
        $cache = JFactory::getCache('_system');
        $cache->clean();
        JPluginHelper::importPlugin('installer');
        JPluginHelper::importPlugin('system');
        JFactory::getApplication()->triggerEvent('onAfterInitialise');

        require JPATH_BASE . '/administrator/components/com_installer/models/update.php';

        $model = new InstallerModelUpdate();
        $this->legacyForceDownloadIdIfNeeded($extensionId);

        if ($model->update(array($extensionId))) {
            $cache = JFactory::getCache('mod_menu');
            $cache->clean();
            $cache = JFactory::getCache('_system');
            $cache->clean();
        }

        return $model->getState('result');
    }

    /**
     * THIS IS CRAP FOR A CRAP REASON - BADLY handle when akeeba keys are not in the right place This is not a
     * mySites.guru bug This is not an Akeeba bug This is not an Akeeba backup issue This is not an admin tools
     * bug/issue This is Joomla 3 - still - badly handling extra_query This is a badly written method that makes wild
     * assumptions - but in the context of mySites.guru updates, its enough!
     *
     * @see https://www.akeeba.com/news/1746-working-around-joomla-s-broken-extensions-updater.html
     *
     * @param int $extensionId
     */
    private function legacyForceDownloadIdIfNeeded($extensionId)
    {
        $db = JFactory::getDbo();

        // Clean up our mess when we put a dlid in FOF 4.x by mistake. Sorry.
        $db->setQuery('UPDATE #__update_sites SET extra_query = "" where `name` = "FOF 4.x"');
        $db->execute();

        $db->setQuery('SELECT * FROM #__updates where update_id = ' . (int) $extensionId);
        $update = $db->loadObject();

        if (! $update->extra_query || 'dlid=' == $update->extra_query) {
            if ($key = ComponentHelper::getParams($update->element)->get('update_dlid', '')) {
                if ($key) {
                    return $key;
                }
            }

            $try = array(array('akeeba backup', 'com_akeeba'), array('akeeba backup', 'com_akeebabackup'), array('admin tools', 'com_admintools'));

            $key = null;
            foreach ($try as $pkg) {
                if ($key) {
                    continue;
                }

                if (! $key && preg_match('/' . $pkg[0] . '/i', $update->name)) {
                    $key = ComponentHelper::getParams($pkg[1])->get('update_dlid', '');

                    if (! $key) {
                        $key = ComponentHelper::getParams($pkg[1])->get('downloadid', '');
                    }
                }
            }

            if ($key) {
                $db->setQuery(sprintf('UPDATE #__updates SET extra_query = "dlid=%s" where update_id = %s', $key, $extensionId));
                $db->execute();

                $db->setQuery(
                    sprintf('UPDATE #__update_sites SET extra_query = "dlid=%s" where update_site_id = %s', $key, $update->update_site_id)
                );
                $db->execute();
            }
        }
    }
}

© 2025 Cubjrnet7