5

I'm attempting to write a Joomla CLI-script that automatically upgrades the site to the current version. In Joomla this appears to be done through *com_joomlaupdate*. The idea is to be able to upgrade any Joomla site on a server from an admin frontend.

I have written the following for testing, attempting to mimic the controller in com_joomlaupdate by directly accessing methods in its model. I'm unfamiliar with the joomla framework so I may be doing some silly things here.

<?php

const _JEXEC = 1;

error_reporting(E_ALL | E_NOTICE);
ini_set('display_errors', 1);

define('JPATH_BASE', dirname(__DIR__));

require_once JPATH_BASE . '/includes/defines.php';
require_once JPATH_BASE . '/includes/framework.php';

require_once JPATH_LIBRARIES . '/import.legacy.php';
require_once JPATH_LIBRARIES . '/cms.php';

// Load the configuration
require_once JPATH_CONFIGURATION . '/configuration.php';

define('JPATH_COMPONENT_ADMINISTRATOR', JPATH_ADMINISTRATOR . '/components/com_joomlaupdate');

require_once JPATH_COMPONENT_ADMINISTRATOR . '/models/default.php';

class Upgradejoomla extends JApplicationCli
{

        public function doExecute()
        {
                $app = JFactory::getApplication('administrator');
                $app->initialise();
                $app->input->set('method', 'direct');

                $this->out('Fetching updates...');

                $updater = JModelLegacy::getInstance('JoomlaupdateModelDefault');

                $updater->refreshUpdates();

                $updater->applyUpdateSite();

                $basename = $updater->download();

                $app->setUserState('com_joomlaupdate.file', $basename);

                $updater->createRestorationFile($basename);

                echo ($updater->finaliseUpgrade());

                $updater->cleanUp();
        }
}

JApplicationCli::getInstance('Upgradejoomla')->execute();

download() works fine, I do get the latest file, and its placed in the tmp directory. createRestorationFile() appears to work too, i get a restoration.php file inside the com_joomlaupdate directory.

The issue seems to be with finaliseUpgrade(). It calls setupInstall() in Installer, which attempts to look for a manifest file. What I'm missing (among other things) I suppose is the step where that file (or the entire contents of the update) is unpacked somewhere. The issue is I can't find any code that does that in com_joomlaupdate?

I have attempted to manually unpack the updatefile inside /tmp. When I do this, finaliseUpgrade() actually returns true, but the site remains at its old version still.

3 Answers3

0

Okay I figured it out.

Part of the update happens using restore.php (apparently taken from Akeeba backup). Joomla makes 2 ajax calls to restore.php, the first returns a factory object, which you have to pass back. There's also AES CTR encryption to worry about. What I did is use curl to mimic Joomla's ajax calls.

Here's the code as it sits currently. It could probably use a lot of cleaning up (Don't hardcode the URL, more errorchecking etc), but it does work.

const _JEXEC = 1;

error_reporting(E_ALL | E_NOTICE);
ini_set('display_errors', 1);

define('JPATH_BASE', dirname(__DIR__));

require_once JPATH_BASE . '/includes/defines.php';
require_once JPATH_BASE . '/includes/framework.php';

require_once JPATH_LIBRARIES . '/import.legacy.php';
require_once JPATH_LIBRARIES . '/cms.php';

// Load the configuration
require_once JPATH_CONFIGURATION . '/configuration.php';

define('JPATH_COMPONENT_ADMINISTRATOR', JPATH_ADMINISTRATOR . '/components/com_joomlaupdate');

require_once JPATH_COMPONENT_ADMINISTRATOR . '/models/default.php';

// restore.php is loud, using ob_end_clean() to shut it up, we need it for decryption
ob_start();
require_once JPATH_COMPONENT_ADMINISTRATOR . '/restore.php';
ob_end_clean();

class Upgradejoomla extends JApplicationCli
{

    // Used to talk to restore.php, since it only comunicates using Ajax.
    public function curlCall($data) {
        $url = 'http://localhost/administrator/components/com_joomlaupdate/restore.php';
        $ch = curl_init ($url);
        curl_setopt ($ch, CURLOPT_POST, true);
        curl_setopt ($ch, CURLOPT_POSTFIELDS, $data);
        curl_setopt ($ch, CURLOPT_RETURNTRANSFER, true);
        $ret = curl_exec ($ch);
        return $ret;
    }

    // Decrypt the JSON return.php passes back to us, and make it a php array while we're at it.
    public function deCrypt($str, $password) {
        $result = AKEncryptionAES::AESDecryptCtr($str, $password, 128);
        return json_decode($result, true);
    }

    public function doExecute() {
        // Get a Joomla-instance so createResorationFile() doesn't complain too much.
        $app = JFactory::getApplication('site');
        $app->initialise();
        // Make sure we're not in FTP mode
        $app->input->set('method', 'direct');

        // com_joomlaupdate's model
        $updater = JModelLegacy::getInstance('JoomlaupdateModelDefault');

        // Make sure we know what the latest version is
        $updater->refreshUpdates();
                $updater->applyUpdateSite();

        // Let's see if we're already on the latest version, the model always returns a null-object if this is the case
        $version_check = $updater->getUpdateInformation();
        if (is_null($version_check['object'])) {
            echo 'No new updates available' . "\n";
            return 0;
        }

        $this->out('Fetching updates...');

        // Grab the update (ends up in /tmp)
        $basename = $updater->download();

        // Create restoration.php (ends up in /com_joomlaupdate)
        $updater->createRestorationFile($basename);

        // Grab the password that was generated and placed in restoration.php so we can decrypt later
        $password = $app->getUserState('com_joomlaupdate.password', null);

        // Ask restore.php to start
        $first_pass = array (
            "task" => "startRestore",
        );

        $result = $this->curlCall($first_pass);
        $result = $this->deCrypt($result, $password);

        // Now we can pass back the factory-object we got and let restore.php do its thing
        $second_pass = array (
            "task" => "stepRestore",
            "factory" => $result['factory']
        );

        $result = $this->curlCall($second_pass);
        $result = $this->deCrypt($result, $password);

        if ($result['done'] == 1) {
            echo "Success!". "\n";
        }
        // Update SQL etc based on the manifest file we got with the update     
        $updater->finaliseUpgrade();

        $updater->cleanUp();
    }
}
JApplicationCli::getInstance('Upgradejoomla')->execute();
                                                             
mickmackusa
  • 43,625
  • 12
  • 83
  • 136
0

Well, this certainly doesn't work:

wget -O j3.zip \
    https://github.com/joomla/joomla-cms/releases/download/3.3.6/Joomla_3.3.6-Stable-Full_Package.zip
unzip -o j3.zip

This prety much worked for previous versions of joomla :(

Des Cent
  • 111
  • 6
-1

im interested in this update script. In your newer listing the opening bracket after "class Upgradejoomla extends JApplicationCli" is missing, So a error occured. I have tested it and put in some lines for put out status lines. It is tested with joomla 3.4.0 Update to 3.4.6. The download to /tmp is working, but after that the status lines are appearing but nothing happens. i put in some $_SERVER vars to prevent the joomla to go to errors. But it is hard to me to understand what is really going on...

<?php
const _JEXEC = 1;

error_reporting(E_ALL | E_NOTICE);
ini_set('display_errors', 1);

define('JPATH_BASE', dirname(__DIR__));

require_once JPATH_BASE . '/includes/defines.php';
require_once JPATH_BASE . '/includes/framework.php';

require_once JPATH_LIBRARIES . '/import.legacy.php';
require_once JPATH_LIBRARIES . '/cms.php';

// Load the configuration
require_once JPATH_CONFIGURATION . '/configuration.php';

define('JPATH_COMPONENT_ADMINISTRATOR', JPATH_ADMINISTRATOR . '/components/com_joomlaupdate');

require_once JPATH_COMPONENT_ADMINISTRATOR . '/models/default.php';

// restore.php is loud, using ob_end_clean() to shut it up, we need it for decryption
ob_start();
require_once JPATH_COMPONENT_ADMINISTRATOR . '/restore.php';
ob_end_clean();

$_SERVER['HTTP_HOST'] = 'http://domainname.de';
$_SERVER['REQUEST_URI'] = '/administrator/components/com_joomlaupdate/restore.php';
$_SERVER['PHP_SELF'] = '/administrator/components/com_joomlaupdate/restore.php';

class Upgradejoomla extends JApplicationCli
        {


        // Used to talk to restore.php, since it only comunicates using Ajax.
        public function curlCall($data) {


                $url = 'http://domainname.de/administrator/components/com_joomlaupdate/restore.php';
                $ch = curl_init ($url);
                curl_setopt ($ch, CURLOPT_POST, true);
                curl_setopt ($ch, CURLOPT_POSTFIELDS, $data);
                curl_setopt ($ch, CURLOPT_RETURNTRANSFER, true);
                $ret = curl_exec ($ch);

                return $ret;

        }

        // Decrypt the JSON return.php passes back to us, and make it a php array while we're at it.
        public function deCrypt($str, $password) {

                $result = AKEncryptionAES::AESDecryptCtr($str, $password, 128);
                return json_decode($result, true);
}

        public function doExecute() {

                // Get a Joomla-instance so createResorationFile() doesn't complain too much.
                $app = JFactory::getApplication('site');
                $app->initialise();
                // Make sure we're not in FTP mode
                $app->input->set('method', 'direct');

                // com_joomlaupdate's model
                $updater = JModelLegacy::getInstance('JoomlaupdateModelDefault');

                // Make sure we know what the latest version is
                $updater->refreshUpdates();
                $updater->applyUpdateSite();

                // Let's see if we're already on the latest version, the model always returns a null-object if this is the case
                $version_check = $updater->getUpdateInformation();
                if (is_null($version_check['object'])) {
                        echo 'No new updates available' . "\n";
                        return 0;
                }

                $this->out('Fetching updates...');

                // Grab the update (ends up in /tmp)
                $basename = $updater->download();

                // Create restoration.php (ends up in /com_joomlaupdate)
                $updater->createRestorationFile($basename);
                $this->out('creating restoration...');

                // Grab the password that was generated and placed in restoration.php so we can decrypt later
                $password = $app->getUserState('com_joomlaupdate.password', null);
                $this->out('get password...');


                // Ask restore.php to start
                $first_pass = array (
                "task" => "startRestore",
                );

                $this->out('start restore...');

                $result = $this->curlCall($first_pass);
                $result = $this->deCrypt($result, $password);

                // Now we can pass back the factory-object we got and let restore.php do its thing
                $second_pass = array (
                "task" => "stepRestore",
                "factory" => $result['factory']
                );
                $this->out('pass factory object...');


                $result = $this->curlCall($second_pass);
                $result = $this->deCrypt($result, $password);

                if($result['done'] == 1) {
                        echo "Success!". "\n";
                }
                $this->out('sucess...');

                // Update SQL etc based on the manifest file we got with the update
                $updater->finaliseUpgrade();
                $this->out('finalize...');

                $updater->cleanUp();
                $this->out('cleanup...');

        }
}


JApplicationCli::getInstance('Upgradejoomla')->execute();

?>
herms
  • 1
  • 2