Source for file installer.php
Documentation is available at installer.php
* @package Joomla.Libraries
* @copyright Copyright (C) 2005 - 2013 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
jimport('joomla.filesystem.folder');
* Joomla base installer class
* @package Joomla.Libraries
* Array of paths needed by the installer
* True if package is an upgrade
* The manifest trigger class
* True if existing files can be overwritten
* Stack of installation steps
* - Used for installation rollback
* The output from the install/uninstall scripts
* The installation manifest XML object
* The extension message that appears
* The redirect URL if this extension (can be null if no redirect)
* JInstaller instance container.
protected static $instance;
parent::__construct(__DIR__
, 'JInstallerAdapter', __DIR__ .
'/adapter');
// Override the default adapter folder
* Returns the global Installer object, only creating it
* if it doesn't already exist.
* @return JInstaller An installer object
if (!isset
(self::$instance))
self::$instance =
new JInstaller;
* Get the allow overwrite switch
* @return boolean Allow overwrite switch
* Set the allow overwrite switch
* @param boolean $state Overwrite switch state
* @return boolean True it state is set, false if it is not
* Get the redirect location
* @return string Redirect location (or null)
* Set the redirect location
* @param string $newurl New redirect location
* @param boolean $state Upgrade switch state
* @return boolean True if upgrade, false otherwise
* Get the installation manifest object
* @return object Manifest object
* Get an installer path by name
* @param string $name Path name
* @param string $default Default value
public function getPath($name, $default =
null)
return (!empty($this->paths[$name])) ?
$this->paths[$name] :
$default;
* Sets an installer path by name
* @param string $name Path name
* @param string $value Path
public function setPath($name, $value)
$this->paths[$name] =
$value;
* Pushes a step onto the installer stack for rolling back steps
* @param array $step Installer step
* Installation abort method
* @param string $msg Abort message from the installer
* @param string $type Package type if defined
* @return boolean True if successful
* @throws RuntimeException
public function abort($msg =
null, $type =
null)
// Placeholder in case this is necessary in the future
// $stepval is always false because if this step was called it invariably failed
// Get database connector object
$query =
$db->getQuery(true);
// Remove the entry from the #__extensions table
$query->delete($db->quoteName('#__extensions'))
->where($db->quoteName('extension_id') .
' = ' . (int)
$step['id']);
$stepval =
$db->execute();
// Build the name of the custom rollback method for the type
$method =
'_rollback_' .
$step['type'];
// Custom rollback method handler
$stepval =
$this->_adapters[$type]->$method($step);
// Only set the return value if it is false
// Get the next step and continue
$debug =
$conf->get('debug');
throw
new RuntimeException('Installation unexpectedly terminated: ' .
$msg, 500);
* Package installation method
* @param string $path Path to package source folder
* @return boolean True if successful
public function install($path =
null)
$this->abort(JText::_('JLIB_INSTALLER_ABORT_NOINSTALLPATH'));
$this->abort(JText::_('JLIB_INSTALLER_ABORT_DETECTMANIFEST'));
$type = (string)
$this->manifest->attributes()->type;
// Add the languages from the package itself
$this->_adapters[$type]->loadLanguage($path);
// Fire the onExtensionBeforeInstall event.
'onExtensionBeforeInstall',
array('method' =>
'install', 'type' =>
$type, 'manifest' =>
$this->manifest, 'extension' =>
0)
$result =
$this->_adapters[$type]->install();
// Fire the onExtensionAfterInstall
'onExtensionAfterInstall',
array('installer' =>
clone $this, 'eid' =>
$result)
// Refresh versionable assets cache
* Discovered package installation method
* @param integer $eid Extension ID
* @return boolean True if successful
$this->abort(JText::_('JLIB_INSTALLER_ABORT_LOAD_DETAILS'));
$this->abort(JText::_('JLIB_INSTALLER_ABORT_ALREADYINSTALLED'));
// Add the languages from the package itself
// Fire the onExtensionBeforeInstall event.
'onExtensionBeforeInstall',
'method' =>
'discover_install',
'extension' =>
$this->extension->get('extension_id')
// Fire the onExtensionAfterInstall
'onExtensionAfterInstall',
array('installer' =>
clone $this, 'eid' =>
$result)
// Refresh versionable assets cache
$this->abort(JText::_('JLIB_INSTALLER_ABORT_METHODNOTSUPPORTED'));
$this->abort(JText::_('JLIB_INSTALLER_ABORT_EXTENSIONNOTVALID'));
* Extension discover method
* Asks each adapter to find extensions
* @return array JExtension
// Joomla! 1.5 installation adapter legacy support
$tmp =
$adapter->discover();
// If its an array and has entries
// Merge it into the system
* @param string $path Path to package source folder
* @return boolean True if successful
public function update($path =
null)
$this->abort(JText::_('JLIB_INSTALLER_ABORT_NOUPDATEPATH'));
$this->abort(JText::_('JLIB_INSTALLER_ABORT_DETECTMANIFEST'));
$type = (string)
$this->manifest->attributes()->type;
// Add the languages from the package itself
$this->_adapters[$type]->loadLanguage($path);
// Fire the onExtensionBeforeUpdate event.
$dispatcher->trigger('onExtensionBeforeUpdate', array('type' =>
$type, 'manifest' =>
$this->manifest));
// Fire the onExtensionAfterUpdate
'onExtensionAfterUpdate',
array('installer' =>
clone $this, 'eid' =>
$result)
* Package uninstallation method
* @param string $type Package type
* @param mixed $identifier Package identifier for adapter
* @param integer $cid Application ID; deprecated in 1.6
* @return boolean True if successful
public function uninstall($type, $identifier, $cid =
0)
// We failed to get the right adapter
// We don't load languages here, we get the extension adapter to work it out
// Fire the onExtensionBeforeUninstall event.
$dispatcher->trigger('onExtensionBeforeUninstall', array('eid' =>
$identifier));
$result =
$this->_adapters[$type]->uninstall($identifier);
// Fire the onExtensionAfterInstall
'onExtensionAfterUninstall',
array('installer' =>
clone $this, 'eid' =>
$identifier, 'result' =>
$result)
// Refresh versionable assets cache
* Refreshes the manifest cache stored in #__extensions
* @param integer $eid Extension ID
* @return mixed void on success, false on error @todo missing return value ?
$this->abort(JText::_('JLIB_INSTALLER_ABORT_LOAD_DETAILS'));
$this->abort(JText::_('JLIB_INSTALLER_ABORT_REFRESH_MANIFEST_CACHE'));
$this->abort(JText::_('JLIB_INSTALLER_ABORT_REFRESH_MANIFEST_CACHE_VALID'));
* Prepare for installation: this method sets the installation directory, finds
* and checks the installation file and verifies the installation type.
* @return boolean True on success
// We need to find the installation manifest file
// Load the adapter(s) for the install manifest
$type = (string)
$this->manifest->attributes()->type;
* Backward compatible method to parse through a queries element of the
* installation manifest file and take appropriate action.
* @param SimpleXMLElement $element The XML node to process
* @return mixed Number of queries processed or False on error
// Get the database connector object
if (!$element ||
!count($element->children()))
// Either the tag does not exist or has no children therefore we return zero files processed.
// Get the array of query nodes to process
$queries =
$element->children();
if (count($queries) ==
0)
// Process each query in the $queries array (children of $tagName).
foreach ($queries as $query)
$db->setQuery($query->data());
return (int)
count($queries);
* Method to extract the name of a discreet installation sql file from the installation manifest file.
* @param object $element The XML node to process
* @return mixed Number of queries processed or False on error
if (!$element ||
!count($element->children()))
// The tag does not exist.
if ($dbDriver ==
'mysqli')
// Get the name of the sql file to process
foreach ($element->children() as $file)
$fCharset =
(strtolower($file->attributes()->charset) ==
'utf8') ?
'utf8' :
'';
$fDriver =
strtolower($file->attributes()->driver);
if ($fDriver ==
'mysqli')
if ($fCharset ==
'utf8' &&
$fDriver ==
$dbDriver)
$sqlfile =
$this->getPath('extension_root') .
'/' .
$file;
// Check that sql files exists before reading. Otherwise raise error for rollback
// Graceful exit and rollback if read not successful
// Create an array of queries from the sql file
if (count($queries) ==
0)
// Process each query in the $queries array (split out of sql file).
foreach ($queries as $query)
if ($query !=
'' &&
$query{0} !=
'#')
return (int)
count($queries);
* Set the schema version for an extension by looking at its latest update
* @param SimpleXMLElement $schema Schema Tag
* @param integer $eid Extension ID
$schemapaths =
$schema->children();
if ($dbDriver ==
'mysqli')
foreach ($schemapaths as $entry)
$attrs =
$entry->attributes();
if ($attrs['type'] ==
$dbDriver)
usort($files, 'version_compare');
$query =
$db->getQuery(true)
->where('extension_id = ' .
$eid);
->insert($db->quoteName('#__schemas'))
->columns(array($db->quoteName('extension_id'), $db->quoteName('version_id')))
->values($eid .
', ' .
$db->quote(end($files)));
* Method to process the updates for an item
* @param SimpleXMLElement $schema The XML node to process
* @param integer $eid Extension Identifier
* @return boolean Result of the operations
// Ensure we have an XML element and a valid extension id
$schemapaths =
$schema->children();
if ($dbDriver ==
'mysqli')
foreach ($schemapaths as $entry)
$attrs =
$entry->attributes();
if ($attrs['type'] ==
$dbDriver)
usort($files, 'version_compare');
$query =
$db->getQuery(true)
->where('extension_id = ' .
$eid);
$version =
$db->loadResult();
foreach ($files as $file)
// Graceful exit and rollback if read not successful
// Create an array of queries from the sql file
if (count($queries) ==
0)
// Process each query in the $queries array (split out of sql file).
foreach ($queries as $query)
if ($query !=
'' &&
$query{0} !=
'#')
$queryString = (string)
$query;
$queryString =
str_replace(array("\r", "\n"), array('', ' '), substr($queryString, 0, 80));
$query =
$db->getQuery(true)
->where('extension_id = ' .
$eid);
->insert($db->quoteName('#__schemas'))
->columns(array($db->quoteName('extension_id'), $db->quoteName('version_id')))
->values($eid .
', ' .
$db->quote(end($files)));
* Method to parse through a files element of the installation manifest and take appropriate
* @param SimpleXMLElement $element The XML node to process
* @param integer $cid Application ID of application to install to
* @param array $oldFiles List of old files (SimpleXMLElement's)
* @param array $oldMD5 List of old MD5 sums (indexed by filename with value as MD5)
* @return boolean True on success
public function parseFiles(SimpleXMLElement $element, $cid =
0, $oldFiles =
null, $oldMD5 =
null)
// Get the array of file nodes to process; we checked whether this had children above.
if (!$element ||
!count($element->children()))
// Either the tag does not exist or has no children (hence no files to process) therefore we return zero files processed.
* Here we set the folder we are going to remove the files from.
$pathname =
'extension_' .
$client->name;
$destination =
$this->getPath($pathname);
$pathname =
'extension_root';
$destination =
$this->getPath($pathname);
* Here we set the folder we are going to copy the files from.
* Does the element have a folder attribute?
* If so this indicates that the files are in a subdirectory of the source
* folder and we should append the folder attribute to the source path when
$folder = (string)
$element->attributes()->folder;
$source =
$this->getPath('source') .
'/' .
$folder;
$source =
$this->getPath('source');
// Work out what files have been deleted
if ($oldFiles &&
($oldFiles instanceof
SimpleXMLElement))
$oldEntries =
$oldFiles->children();
foreach ($deletions['folders'] as $deleted_folder)
foreach ($deletions['files'] as $deleted_file)
// Copy the MD5SUMS file if it exists
$path['src'] =
$source .
'/MD5SUMS';
$path['dest'] =
$destination .
'/MD5SUMS';
// Process each file in the $files array (children of $tagName).
foreach ($element->children() as $file)
$path['src'] =
$source .
'/' .
$file;
$path['dest'] =
$destination .
'/' .
$file;
// Is this path a file or folder?
$path['type'] =
($file->getName() ==
'folder') ?
'folder' :
'file';
* Before we can add a file to the copyfiles array we need to ensure
* that the folder we are copying our file to exits and if it doesn't,
if (basename($path['dest']) !=
$path['dest'])
// Add the file to the copyfiles array
* Method to parse through a languages element of the installation manifest and take appropriate
* @param SimpleXMLElement $element The XML node to process
* @param integer $cid Application ID of application to install to
* @return boolean True on success
// TODO: work out why the below line triggers 'node no longer exists' errors with files
if (!$element ||
!count($element->children()))
// Either the tag does not exist or has no children therefore we return zero files processed.
// Here we set the folder we are going to copy the files to.
// 'languages' Files are copied to JPATH_BASE/language/ folder
$destination =
$client->path .
'/language';
* Here we set the folder we are going to copy the files from.
* Does the element have a folder attribute?
* If so this indicates that the files are in a subdirectory of the source
* folder and we should append the folder attribute to the source path when
$folder = (string)
$element->attributes()->folder;
$source =
$this->getPath('source') .
'/' .
$folder;
$source =
$this->getPath('source');
// Process each file in the $files array (children of $tagName).
foreach ($element->children() as $file)
* Language files go in a subfolder based on the language code, ie.
* <language tag="en-US">en-US.mycomponent.ini</language>
* would go in the en-US subdirectory of the language folder.
// We will only install language files where a core language pack
if ((string)
$file->attributes()->tag !=
'')
$path['src'] =
$source .
'/' .
$file;
if ((string)
$file->attributes()->client !=
'')
$path['dest'] =
$langclient->path .
'/language/' .
$file->attributes()->tag .
'/' .
basename((string)
$file);
// Use the default client
$path['dest'] =
$destination .
'/' .
$file->attributes()->tag .
'/' .
basename((string)
$file);
// If the language folder is not present, then the core pack hasn't been installed... ignore
$path['src'] =
$source .
'/' .
$file;
$path['dest'] =
$destination .
'/' .
$file;
* Before we can add a file to the copyfiles array we need to ensure
* that the folder we are copying our file to exits and if it doesn't,
if (basename($path['dest']) !=
$path['dest'])
// Add the file to the copyfiles array
* Method to parse through a media element of the installation manifest and take appropriate
* @param SimpleXMLElement $element The XML node to process
* @param integer $cid Application ID of application to install to
* @return boolean True on success
public function parseMedia(SimpleXMLElement $element, $cid =
0)
if (!$element ||
!count($element->children()))
// Either the tag does not exist or has no children therefore we return zero files processed.
// Here we set the folder we are going to copy the files to.
// Default 'media' Files are copied to the JPATH_BASE/media folder
$folder =
((string)
$element->attributes()->destination) ?
'/' .
$element->attributes()->destination :
null;
// Here we set the folder we are going to copy the files from.
* Does the element have a folder attribute?
* If so this indicates that the files are in a subdirectory of the source
* folder and we should append the folder attribute to the source path when
$folder = (string)
$element->attributes()->folder;
$source =
$this->getPath('source') .
'/' .
$folder;
$source =
$this->getPath('source');
// Process each file in the $files array (children of $tagName).
foreach ($element->children() as $file)
$path['src'] =
$source .
'/' .
$file;
$path['dest'] =
$destination .
'/' .
$file;
// Is this path a file or folder?
$path['type'] =
($file->getName() ==
'folder') ?
'folder' :
'file';
* Before we can add a file to the copyfiles array we need to ensure
* that the folder we are copying our file to exits and if it doesn't,
if (basename($path['dest']) !=
$path['dest'])
// Add the file to the copyfiles array
* Method to parse the parameters of an extension, build the INI
* string for its default parameters, and return the INI string.
* @return string INI string of parameter values
// Validate that we have a fieldset to use
if (!isset
($this->manifest->config->fields->fieldset))
// Getting the fieldset tags
$fieldsets =
$this->manifest->config->fields->fieldset;
// Creating the data collection variable:
// Iterating through the fieldsets:
foreach ($fieldsets as $fieldset)
if (!count($fieldset->children()))
// Either the tag does not exist or has no children therefore we return zero files processed.
// Iterating through the fields and collecting the name/default values:
foreach ($fieldset as $field)
// Check against the null value since otherwise default values like "0"
// cause entire parameters to be skipped.
if (($name =
$field->attributes()->name) ===
null)
if (($value =
$field->attributes()->default) ===
null)
$ini[(string)
$name] = (string)
$value;
* Copy files from source directory to the target directory
* @param array $files Array with filenames
* @param boolean $overwrite True if existing files can be replaced
* @return boolean True on success
public function copyFiles($files, $overwrite =
null)
* To allow for manual override on the overwriting flag, we check to see if
* the $overwrite flag was set and is a boolean value. If not, use the object
* $files must be an array of filenames. Verify that it is an array with
* at least one file to copy.
foreach ($files as $file)
// Get the source and destination paths
* The source file does not exist. Nothing to copy so set an error
elseif (($exists =
file_exists($filedest)) &&
!$overwrite)
// It's okay if the manifest already exists
if ($this->getPath('manifest') ==
$filesource)
// The destination file already exists and the overwrite flag is false.
// Set an error and return false.
// Copy the folder or file to the new location.
if ($filetype ==
'folder')
if (!(JFolder::copy($filesource, $filedest, null, $overwrite)))
JLog::add(JText::sprintf('JLIB_INSTALLER_ERROR_FAIL_COPY_FOLDER', $filesource, $filedest), JLog::WARNING, 'jerror');
$step =
array('type' =>
'folder', 'path' =>
$filedest);
if (!(JFile::copy($filesource, $filedest, null)))
// In 3.2, TinyMCE language handling changed. Display a special notice in case an older language pack is installed.
if (strpos($filedest, 'media/editors/tinymce/jscripts/tiny_mce/langs'))
$step =
array('type' =>
'file', 'path' =>
$filedest);
* Since we copied a file/folder, we want to add it to the installation step stack so that
* in case we have to roll back the installation we can remove the files copied.
// The $files variable was either not an array or an empty array
* Method to parse through a files element of the installation manifest and remove
* the files that were installed
* @param object $element The XML node to process
* @param integer $cid Application ID of application to remove from
* @return boolean True on success
if (!$element ||
!count($element->children()))
// Either the tag does not exist or has no children therefore we return zero files processed.
// Get the client info if we're using a specific client
// Get the array of file nodes to process
$files =
$element->children();
* Here we set the folder we are going to remove the files from. There are a few
* special cases that need to be considered for certain reserved tags.
switch ($element->getName())
if ((string)
$element->attributes()->destination)
$folder = (string)
$element->attributes()->destination;
$source =
$client->path .
'/media/' .
$folder;
$lang_client = (string)
$element->attributes()->client;
$source =
$client->path .
'/language';
$source =
$client->path .
'/language';
$pathname =
'extension_' .
$client->name;
$source =
$this->getPath($pathname);
$pathname =
'extension_root';
$source =
$this->getPath($pathname);
// Process each file in the $files array (children of $tagName).
foreach ($files as $file)
* If the file is a language, we must handle it differently. Language files
* go in a subdirectory based on the language code, ie.
* <language tag="en_US">en_US.mycomponent.ini</language>
* would go in the en_US subdirectory of the languages directory.
if ($file->getName() ==
'language' && (string)
$file->attributes()->tag !=
'')
$path =
$source .
'/' .
$file->attributes()->tag .
'/' .
basename((string)
$file);
$path =
$target_client->path .
'/language/' .
$file->attributes()->tag .
'/' .
basename((string)
$file);
// If the language folder is not present, then the core pack hasn't been installed... ignore
$path =
$source .
'/' .
$file;
// Actually delete the files/folders
JLog::add('Failed to delete ' .
$path, JLog::WARNING, 'jerror');
* Copies the installation manifest file to the extension folder in the given client
* @param integer $cid Where to copy the installfile [optional: defaults to 1 (admin)]
* @return boolean True on success, False on error
$path['src'] =
$this->getPath('manifest');
$pathname =
'extension_' .
$client->name;
$pathname =
'extension_root';
* Tries to find the package manifest file
* @return boolean True on success, False on error
// Main folder manifests (higher priority)
// Search for children manifests (lower priority)
// Create an unique array of files ordered by priority
// If at least one XML file exists
foreach ($xmlfiles as $file)
// Is it a valid Joomla installation manifest file?
// If the root method attribute is set to upgrade, allow file overwrite
if ((string)
$manifest->attributes()->method ==
'upgrade')
// If the overwrite option is set, allow file overwriting
if ((string)
$manifest->attributes()->overwrite ==
'true')
// Set the manifest object and path
// Set the installation source path to that of the manifest file
// None of the XML files found were valid install files
JLog::add(JText::_('JLIB_INSTALLER_ERROR_NOTFINDJOOMLAXMLSETUPFILE'), JLog::WARNING, 'jerror');
// No XML files were found in the install folder
JLog::add(JText::_('JLIB_INSTALLER_ERROR_NOTFINDXMLSETUPFILE'), JLog::WARNING, 'jerror');
* Is the XML file a valid Joomla installation manifest file.
* @param string $file An xmlfile path to check
* @return mixed A SimpleXMLElement, or null if the file failed to parse
// If we cannot load the XML file return null
// Check for a valid XML root tag.
if ($xml->getName() !=
'extension')
// Valid manifest file return the object
* Generates a manifest cache
* @return string serialised manifest data
* Cleans up discovered extensions if they're being installed some other way
* @param string $type The type of extension (component, etc)
* @param string $element Unique element identifier (e.g. com_content)
* @param string $folder The folder of the extension (plugins; e.g. system)
* @param integer $client The client application (administrator or site)
* @return object Result of query
$query =
$db->getQuery(true)
->delete($db->quoteName('#__extensions'))
->where('type = ' .
$db->quote($type))
->where('element = ' .
$db->quote($element))
->where('folder = ' .
$db->quote($folder))
->where('client_id = ' . (int)
$client)
* Compares two "files" entries to find deleted files/folders
* @param array $old_files An array of SimpleXMLElement objects that are the old files
* @param array $new_files An array of SimpleXMLElement objects that are the new files
* @return array An array with the delete files and folders in findDeletedFiles[files] and findDeletedFiles[folders] respectively
// The magic find deleted files function!
// The files that are new
// The folders that are new
// The folders of the files that are new
// A list of files to delete
$files_deleted =
array();
// A list of folders to delete
$folders_deleted =
array();
foreach ($new_files as $file)
switch ($file->getName())
// Add any folders to the list
$folders[] = (string)
$file; // add any folders to the list
// Add any files to the list
$files[] = (string)
$file;
// Now handle the folder part of the file to ensure we get any containers
// Break up the parts of the directory
// Make sure this is clean and empty
foreach ($container_parts as $part)
// Iterate through each part
// Add a slash if its not empty
// Aappend the folder part
// Add the container if it doesn't already exist
$containers[] =
$container;
foreach ($old_files as $file)
switch ($file->getName())
if (!in_array((string)
$file, $folders))
// See whether the folder exists in the new list
if (!in_array((string)
$file, $containers))
// Check if the folder exists as a container in the new list
// If it's not in the new list or a container then delete it
$folders_deleted[] = (string)
$file;
// Look if the file exists in the new list
// Look if the file is now potentially in a folder
$files_deleted[] = (string)
$file; // not in a folder, doesn't exist, wipe it out!
return array('files' =>
$files_deleted, 'folders' =>
$folders_deleted);
* Loads an MD5SUMS file into an associative array
* @param string $filename Filename to load
* @return array Associative array with filenames as the index and the MD5 as the value
// Bail if the file doesn't exist
$data =
file($filename, FILE_IGNORE_NEW_LINES |
FILE_SKIP_EMPTY_LINES);
// Cull any potential prefix
$retval[$results[1]] =
$results[0];
* Parse a XML install manifest file.
* XML Root tag should be 'install' except for languages which use meta file.
* @param string $path Full path to XML file.
* @return array XML metadata.
// Read the file to see if it's a valid component XML file
// Check for a valid XML root tag.
// Extensions use 'extension' as the root tag. Languages use 'metafile' instead
if ($xml->getName() !=
'extension' &&
$xml->getName() !=
'metafile')
$data['name'] = (string)
$xml->name;
// Check if we're a language. If so use metafile.
$data['type'] =
$xml->getName() ==
'metafile' ?
'language' : (string)
$xml->attributes()->type;
$data['creationDate'] =
((string)
$xml->creationDate) ? (string)
$xml->creationDate :
JText::_('Unknown');
$data['author'] =
((string)
$xml->author) ? (string)
$xml->author :
JText::_('Unknown');
$data['copyright'] = (string)
$xml->copyright;
$data['authorEmail'] = (string)
$xml->authorEmail;
$data['authorUrl'] = (string)
$xml->authorUrl;
$data['version'] = (string)
$xml->version;
$data['description'] = (string)
$xml->description;
$data['group'] = (string)
$xml->group;
Documentation generated on Tue, 19 Nov 2013 15:05:47 +0100 by phpDocumentor 1.4.3