Sunday 23 January 2011

Unistalling multimple extensions in Joomla!

Here's the uninstaller code. It depends on the extensions.txt file written by the installer script.

<?php
/**
 * Uninstall a set of previously installed extensions
 * @package Packager
 */
// No direct access
defined( '_JEXEC' ) or die( 'Restricted access' );
define( 'PACKAGER','Packager');
$exts = array();
$types = array();
// 1. read in the extensions.txt file
$extensions_file = $this->parent->_paths[
  'extension_administrator'].DS."extensions.txt";
$file_len = filesize( $extensions_file );
$handle = fopen( $extensions_file, "r" );
if ( $handle )
{
  $contents = fread( $handle, $file_len );
  if ( $contents )
  {
    $lines = split( "\n", $contents );
    foreach ( $lines as $line )
    {
      $extension = split( "\t", $line );
      if ( count($extension)==2 )
      {
        $types[] = $extension[0];
        $exts[] = $extension[1];
      }
    }
  }
  fclose( $handle );
  // 2. for each extension, retrieve its ID from the database
  $db = JFactory::getDBO();
  $prefix = $db->getPrefix();
  $installer = new JInstaller();
  for ( $i=0;$i<count($types);$i++ )
  {
    $table = $prefix.$types[$i]."s";
    $name = ($types[$i]=="module")?"title":"name";
    $query = "select id from $table where $name='".$exts[$i]."';";
    $db->setQuery( $query );
  $result = $db->loadObjectList();
  if ( count($result) == 1 )
  {
      // 3. uninstall it
      $installer->uninstall( $types[$i], $result[0]->id, 0 );
    }
  }
}
else
  error_log(PACKAGER.": failed to find extensions.txt");
?>

Friday 21 January 2011

Installing multiple Joomla! extensions in one go

The problem

Joomla! has a fairly good component architecture but what it lacks is a way to break up a custom component into smaller modules, plugins and other components. This is essential for the development of complex components. An example is my own mvd-gui, which has multiple views of the same data. I realise that I can supply multiple controllers, models and view files and coordinate them all from a master controller. That's what I did before, but it gets unmanageable after more than a few views. I had eight with more planned.

However, breaking up a Joomla! application into many small components requires the end-user to manually install multiple components, modules and plugins. Since scripting of the installation isn't possible, how can many separately developed extensions be unified into a single package?

The Solution

I have seen two other approaches to this problem. One is a relatively low level customisation of the JInstaller code used in the Joomdle package. Another is this post on "Jeff Channel" which is higher level if also incomplete. It's very simple and I used it as the basis for my own approach. But whereas Jeff adds the components to the manifest file, so they get copied to the installation directory of one of the components to be installed, I decided not to copy any of them and instead created an empty Packager component that would just take care of the installation. The user drops any number of components, plugins and modules into an "extensions" folder within a generic Packager component, zips it up, and installs it. The install script then copies them directly from the tmp directory during installation.

Uninstalling

Likewise, when the user wants to uninstall the package, removal of the Packager component will trigger removal of all sub-extensions automatically. To do this the installer script saves a list of the extensions it originally installed in "extensions.txt" in the administrator directory. It will also need a small uninstall script for each sub-extension to warn (but not prohibit) the user from uninstalling it separately. The master Packager uninstall script can always skip any extensions it can't find so this doesn't have to generate an error.

So far I've got the install part working well, but not uninstall yet. Here is the manifest file of the Packager component. The current state of this package in on Googlecode.

<install type="component" version="1.5.0">
 <name>packager</name>
 <creationDate>2011-01-21</creationDate>
 <author>Desmond Schmidt</author>
 <copyright>Desmond Schmidt 2011</copyright>
 <license>GPL v3</license>
 <version>1.01</version>
 <description>Packager to install/uninstall multiple 
 components/modules/plugins in one go</description>
 <installfile>install.packager.php</installfile>
 <files>
     <filename>index.html</filename>
     <filename>packager.php</filename>
 </files>
 <administration>
 <menu>packager</menu>
 <files folder="admin">
    <filename>index.html</filename>
 </files>
 </administration>
</install>

Simple eh? Other than the "install.packager.php" file the rest are more or less empty files. Here's that installer script:

<?php
/**
 * All-in-one installer/uninstaller component. To install 
 * a set of components, modules and plugins just drop them 
 * into the "extensions" directory within this component. 
 * Then zip up the component and install it via the Joomla 
 * interface. Similarly when uninstalling the packager
 * all previously installed components and modules etc will
 * be uninstalled (by the uninstaller script).
 * Copyright 2011 Desmond Schmidt
 * License: GPLv3.
 * @package packager
 */
// No direct access
defined( '_JEXEC' ) or die( 'Restricted access' );
// rename this to something you like
define( 'PACKAGER','Packager');
jimport('joomla.installer.helper');
$installer = new JInstaller();
$installer->_overwrite = true;
$config =& JFactory::getConfig();
$tmp_dir = $config->getValue('config.tmp_path');
$dir_handle = opendir( $tmp_dir );
$jroot = JURI::root( true );
// look for the installation directory of Packager in tmp
if ( $dir_handle )
{
  $found_dirs = array();
  while ( $file = readdir($dir_handle) )
  {
    if ( strncmp("install_",$file,8)==0 )
    {
      if ( file_exists($tmp_dir.DS.$file.DS.PACKAGER) )
        $found_dirs[] = $tmp_dir.DS.$file;
    }
  }
  if ( count($found_dirs) > 0 )
  {
    $best_dir = $found_dirs[0];
    $best_ctime = filectime( $found_dirs[0] );
    for ($i=1;$i<count($found_dirs);$i++ )
    {
      if ( filectime($found_dirs[$i])>$best_ctime )
      {
        $best_dir = $found_dirs[$i];
        $best_ctime = filectime( $found_dirs[$i] );
      }
    }
    // so $best_dir is our best candidate directory
    $extensions_dir = $best_dir.DS.PACKAGER.DS."extensions";
    if ( file_exists($extensions_dir) )
    {
      // save record of installed extensions
      $exts = array();
      $types = array();
      // look for and install all extensions
      $zip_handle = opendir($extensions_dir);
      if ( $zip_handle )
      {
        while ( $zip = readdir($zip_handle) )
        {
          // ends in ".zip"?
          if ( strrpos($zip,".zip")==strlen($zip)-4 )
          {
            $zip_file = $extensions_dir.DS.$zip;
            $package = JInstallerHelper::unpack( $zip_file );
            $msgtext = "";
            $msgcolor = "";
            $pkgname = substr( $zip, 0, strlen($zip)-4 );
            $image = $jroot."/administrator/images/tick.png";
            if( $installer->install( $package['dir'] ) )
            {
              $msgcolor = "#E0FFE0";
              $msgtext  = "$pkgname successfully installed.";
              if ( count($installer->_adapters)>0 )
              {
                $type = $package['type'];
                $exts[] = $installer->_adapters[$type]->name;
                $types[] = $type;
              }
            }
            else
            {
              $msgcolor = "#FFD0D0";
              $msgtext  = "ERROR: Could not install the ".
                $pkgname.". Please install manually.";
              $image = $jroot
                ."/administrator/images/publish_x.png";
            }
            echo "<table bgcolor=\"$msgcolor\" width =\"100%\">";
            echo "<tr style=\"height:30px\">";
            echo "<td width=\"50px\"><img src=\"$image\" height="
              ."\"20px\" width=\"20px\"></td>";
            echo "<td><font size=\"2\"><b>$msgtext</b></font></td>";
            echo "</tr></table>";
            JInstallerHelper::cleanupInstall(
               $package['packagefile'], $package['extractdir'] );
          }
        }
        closedir( $zip_handle );
      }
      // save record of installed extensions
      if ( count($exts)> 0 )
      {
        $handle = fopen( $this->parent->_paths[
          'extension_administrator'].DS."extensions.txt", "w" );
        if ( $handle )
        {
          for ( $i=0;$i<count($exts)&&$i<count($types);$i++ )
            fwrite( $handle, $types[$i]."\t".$exts[$i]."\n" );
          fclose( $handle );
        }
      }
    }
    else
      error_log(PACKAGER.": missing extensions directory!");
  }
  else
    error_log(PACKAGER.": couldn't find a suitable install directory!");
  closedir( $dir_handle );
}
else
  error_log(PACKAGER.": couldn't open $tmp_dir!");
?>

You also need to create a folder called "extensions" within the Packager component where you can put sub-components and plugins etc. You will also probably want to rename the "Packager" component as something else. I'll post the uninstall script and any modifications to the installer script in my next post.

Friday 14 January 2011

mvd-core component complete

It's not tested yet fully but the backend component that is meant to govern how the mvd-gui works as a whole is finished. Here's a picture of it:

I've come to the conclusion that the only way to write a successful Joomla GUI is to break it up into small components and modules. Otherwise it rapidly gets too complex to handle. This way I can add new views at will, or take them away and it will all still work. Each view hangs off the mvd-core component, which only has this simple backend, and provides access to nmerge. And that's it. With components this small fixing bugs should be a breeze.

Monday 10 January 2011

Writing an admin backend for a Joomla! component

I wanted to add an admin back-end to my mvd-gui component. There are few instructions about how to do this and all of them are messy. Basically you are supposed to define a model-view-controller interface as in the site part of the component. But in the Joomla application they have shortcuts that only require a few lines of code, so I wanted to do it that way. Also it would look more consistent and reformat properly when the user changed the admin template.

So I took the massemail component's admin interface, which was quite simple, and deconstructed it. Customising the icons that appear in the toolbar was easy: just modify the calls to JToolBarHelper in toolbar.massemail.html.php. (You should rename this file as something else). I then got stuck on changing the icon that is displayed in the toolbar. Basically you call JToolBarHelper::title with 2 arguments: the first is the text you want displayed. The 2nd argument is supposed to be an image, but in reality it has to be one of the images in the images directory of the admin template. Since I can't add to that without making my changes non-portable I decided just to use the generic icon. It looks OK:

Or, in code-form:

class TOOLBAR_mvdcore
{
 /**
 * Draws the menu for a New Contact
 */
 function _DEFAULT() {
  JToolBarHelper::title( 'mvd-core', 'generic.png' );
  JToolBarHelper::cancel();
  JToolBarHelper::save();
  JToolBarHelper::help( 'screen.users.massmail' );
 }

Next step is to supply a help file. That's the last line in the code above. Then I still have to provide code for the save button and reformat the HTML form so that it displays my stuff. I'll save that for tomorrow's post.

Sunday 2 January 2011

Debugging php in Netbeans on Ubuntu

I used to be an avid fan of Eclipse. But setting it up to debug php drove me to try Netbeans yet again. And no it is not much easier in that IDE either. What they don't seem to understand is that all the programmer really wants is to do is install the package and it just works. So here are the problems I had getting the debugger to work and how I overcame them.

  1. First, you have to install the xdebug package and apache2 with php etc. This is straightforward using Synaptic or apt-get etc.
  2. Next you can just run a php application you create in Netbeans and it will tell you to add certain lines to your php.ini file. Cool. Do that.
  3. Third, if it still does not work, and inexplicably gives the same error, the reason is probably that you haven't specified the script to debug. Right click on the project and check that the "index file" in the "run configuration" section is set to your root script.
  4. Fourth, if you created a source folder in your project, remember to set the "source folder" in the project properties under "Sources". And no, of course you can't edit it directly in the GUI, Silly, but you can in the "project.properties" file in the nbproject folder (look under the "files" tab on the left of the IDE). Now that's what I call a useful feature.
  5. If it debugs but the line-numbers are wrong, remember that the executing script is the one copied to the server. And the files visible in your debugger are the ones in the project. Cool. To keep them in sync click on "sources" in the project properties and select "Copy sources from Sources folder to another location". And remember to specify the folder on your web server. Of course you'll have to set the creator of that folder to your user name not www-data (if you're using Apache). But you knew that.
  6. So it all works but you don't see any local variables, just global ones and the current object? The problem is with xdebug. You need to upgrade it manually. There are some instructions here.

So now it all should work. Thank heaven for Linux or geeks would be extinct.