Friday, June 4, 2010

Joomla Developer

Introduction


Joomla! plugins serve a variety of purposes.  As modules enhance the presentation of the final output of the Web site, plugins enhance the data and can also provide additional, installable functionality.


This tutorial looks at the general principles used to design and build a plugin.





Plugin Types


While the number of possible types of plugins is almost limitless, there are a number of core plugin types that are used by Joomla!.  These core types are grouped into directories under /plugins/.  They are:




  • authentication

  • content

  • editors

  • editors-xtd

  • search

  • system


  • user

  • xmlrpc


Authentication plugins allow you to authenticate (to allow you to login) against different sources.  By default you will authenticate against the Joomla! user database when you try to login.  However, there are other methods available such as by OpenID, by a Google account, LDAP, and many others.  Wherever a source has a public API, you can write an authentication plugin to verify the login credentials against this source.  For example, you could write a plugin to authenticate against Twitter accounts because they have a public API.



Content plugins modify and add features to displayed content.  For example, content plugins can cloak email address or can convert URL's into SEF format.  Content plugins can also look for markers in content and replace them with other text or HTML.  For example, the Load Module plugin will take {*loadmodule banner1*} (you would remove the *'s in practice.  They are included to actually prevent the plugin from working in this article), load all the modules in the banner1 position and replace the marker with that output.


Editor plugins allow you to add new content editors (usually WYSIYWG).


Editor-XTD (extended) plugins allow you to add additional buttons to the editors.  For example, the Image, Pagebreak and Read more buttons below the default editor are actually plugins.



Search plugins allow you to search different content from different components.  For example, search plugins for Articles, Contacts and Weblinks are already provided in Joomla!.


System plugins allow you to perform actions at various points in the execution of the PHP code that runs a Joomla! Web site.


User plugins allow you to perform actions at different times with respect to users.  Such times include logging in and out and also saving a user.  User plugins are typically user to "bridge" between web applications (such as creating a Joomla! to phpBB bridge).



XML-RPC plugins allow you to provide additional XML-RPC web services for your site.  When your Web site exposes web services, it gives you the ability to interact remotely, possibly from a desktop application.  Web services are a fairly advanced topic and will not be covered in much detail here.


The Basic Files


While a plugin can have any number of files, there are two you need as a minimum and there are specific naming conventions you must follow.  Before we look at the files, we must decide what sort of plugin we are going to create.  It must either fall under one of the built-in types (authentication, content, editors, editors-xtd, search, system, user or xmlrpc) or your can create your own type by adding a new folder under /plugins/.  So, files for an authentication plugin will be saved under /plugins/authentication/, files for a system plugin will be saved under /plugins/system/, and so on.



Let's look at an example creating the basic skeleton for a system plugin called "Test".  There is no restriction on the file name for the plugin (although we recommend sticking with alpha-numeric characters and underscores only), but once you decide on the file name, it will set the naming convention for other parts of the plugin.


For this plugin you will need to create a PHP file, test.php, which is the file actually loaded by Joomla! and an XML file, text.xml, which contains meta and installation information for the plugin as well as the definition of the plugin parameters.


test.php


The skeleton test.php has the following source:



<?php
// no direct access
defined( '_JEXEC' ) or die( 'Restricted access' );

jimport( 'joomla.plugin.plugin' );

/**
* Example system plugin
*/
class plgSystemTest extends JPlugin
{
/**
* Constructor
*
* For php4 compatibility we must not use the __constructor as a constructor for plugins
* because func_get_args ( void ) returns a copy of all passed arguments NOT references.
* This causes problems with cross-referencing necessary for the observer design pattern.
*
* @access protected
* @param object $subject The object to observe
* @param array $config An array that holds the plugin configuration
* @since 1.0
*/
function plgSystemTest( &$subject, $config )
{
parent::__construct( $subject, $config );

// Do some extra initialisation in this constructor if required
}

/**
* Do something onAfterInitialise
*/
function onAfterInitialise()
{
// Perform some action
}
}

Let's look at this file in detail.  Please note that the usual Docblock (the comment block you normally see at the top of most PHP files) has been omitted for clarity.


The file starts with the normal check for defined( '_JEXEC' ) which ensures that the file will fail to execute if access directly via the URL.  This is a very important security feature and the line must be placed before any other executable PHP in the file (it's fine to go after all the initial comment though).  The importance of having this check your PHP files cannot be overemphasised.


Next we use the jimport function to load the library file with the definition of the JPlugin class.



You will notice that a plugin is simply a class derived from JPlugin (this differs from previous versions of Joomla!).  The naming convention of this class is very important.  The formula for this name is:


plg + Proper case name of the plugin directory + Proper case name of the plugin file without the extension.


Proper case simply means that we capitalise the first letter of the name.  When we join them altogether it's then referred to as "Camel Case".  The case is not that important as PHP classes are not case-sensitive but it's the convention Joomla! uses and generally makes the code a little more readable.



For our test system plugin, the formula gives us a class name of:


plg + System + Test = plgSystemTest


Let's move on to the methods in the class.


The first method, which is called the constructor, is completely optional.  You only require this is you want to do some work when the plugin is actually loaded by Joomla!.  This happens with a call to the helper method JPluginHelper::importPlugin( <plugin_type> ).  This means that you even if the plugin is never triggered, for whatever reason, you still have an opportunity to execute code if you need to in the constructor.



In PHP 4 the name of the constructor method is the same as the name of the class.  If you were designing only for PHP 5 you could replace this with the name of __constructor instead.


The remaining methods will take on the name of "events" that are trigger throughout the execution of the Joomla! code.  In the example, we know there is an event called onAfterInitialise which is the first event called after the Joomla! application sets itself up for work.  For more information on when some events are triggered, see the API Execution Order page on the Documentation Wiki.



The naming rule here is simple: the name of the method must be the same as the event on which you want it triggered.  The Joomla! Framework will auto-register all the methods in the class for you.


That's the basics of the plugin PHP file.  It's location, name and methods will depend on what you want to use the plugin for.  One thing to note about system plugins is that they are not limited to handling just system events.  Because the system plugins are always loaded on each run of the Joomla! PHP, you can include any triggered event in a system plugin.


The events triggered in Joomla! are:


Authentication




  • onAuthenticate


Content



  • onPrepareContent

  • onAfterDisplayTitle

  • onBeforeDisplayContent

  • onBeforeContentSave (new in 1.5.4)


  • onAfterContentSave (new in 1.5.4)


Editors



  • onInit

  • onGetContent

  • onSetContent

  • onSave


  • onDisplay

  • onGetInsertMethod


Editors XTD (Extended)



  • onDisplay


Seach




  • onSearch

  • onSearchAreas


System



  • onAfterInitialise

  • onAfterRoute

  • onAfterDispatch


  • onAfterRender


User



  • onLoginUser

  • onLoginFailure

  • onLogoutUser

  • onLogoutFailure


  • onBeforeStoreUser

  • onAfterStoreUser

  • onBeforeDeleteUser

  • onAfterDeleteUser


XML-RPC



  • onGetWebServices



For more detailed information on how to create specific plugins, visit the Plugins Category on the Documentation Wiki.


text.xml


The skeleton test.xml has the following source:


<?xml version="1.0" encoding="utf-8"?>
<install version="1.5.2" type="plugin" group="system" method="upgrade">

<name>System - Test</name>
<author>Author</author>
<creationDate>Month 2008</creationDate>
<copyright>Copyright (C) 2008 Holder. All rights reserved.</copyright>

<license>GNU General Public License</license>
<authorEmail>email</authorEmail>
<authorUrl>url</authorUrl>
<version>1.0.1</version>

<description>A test system plugin</description>
<files>
<filename plugin="example">example.php</filename>
</files>
<params>

<param name="example"
type="text"
default=""
label="Example"
description="An example text parameter" />
</params>
</install>

This is a very typcial format for most meta XML files (sometimes called manifests).  Let's go through some of the most important tags:


INSTALL


The install tag has several key attributes.  The type must be "plugin" and you must specify the group.  The group attribute is required and is the name of the directory you saved your files in (for example, system, content, etc).  We use the method="upgrade" attribute to allow us to install the extension without uninstalling.  In other words, if you are sharing this plugin with other, they can just install the new version over the top of the old one.



NAME


We usually start the name with the type of plugin this is.  Our example is a system plugin and it has some some nebulous test purpose.  So we have named the plugin "System - Test".  You can name the plugins in any way, but this is a common format.


FILES


The files tag includes all of the files that will will be installed with the plugin.  Plugins can also support be installed with subdirectories.  To specify these just all a FOLDER tag, <folder>test</folder>.  It is common practice to have only one subdirectory and name it the same as the plugin PHP file (without the extension of course).



PARAMS


Any number of parameters can be specified for a plugin.  Please note there is no "advanced" group for plugins as there is in modules and components.


Packaging Plugins


Packaging a plugin is easy.  If you only have the two files (the PHP file and the XML file), just "zip" them up into a compressed archive file.  If your plugin uses a subdirectory then simply include that in the archive as well. 

Secure coding guidelines

Getting data from the request


All input originating from a user must be considered potentially dangerous and must be cleaned before being used. You should always use the Joomla Framework JRequest class to retrieve data from the request, rather than the raw $_GET, $_POST or $_REQUEST variables as the JRequest methods apply input filtering by default. JRequest deals with all aspects of the user request in a way that is independent of the request method used. It can also be used to retrieve cookie data and even server and environment variables. However, it is important to use the correct JRequest method to ensure maximum security. It is very easy to just use the JRequest::getVar method with default parameters and ignore the fact that in many cases it is possible to apply a more stringent requirement on user input.



It very important to understand that the JRequest methods are not SQL-aware and further work is required to guard against SQL injection attacks.There is no default value that will be returned if no default is specified in the call the JRequest::getVar. If no default is specified and the argument is not present in the request variable then it will return undefined.


Using JRequest also obviates the need to pay attention to the setting of magic_quotes_gpc. JRequest does the right thing, regardless of whether magic_quotes_gpc is on or off. See http://php.net/manual/en/security.magicquotes.php for further information.



When considering user input you should think about the data type you are expecting to retrieve and apply the most stringent form of JRequest that is applicable in each case. In particular, avoid the lazy approach of using JRequest::get as this will return an array that may contain entries that you did not expect and although each of those entries will have been cleaned, it is often the case that additional filtering could have been applied to some individual arguments. For example, the get method treats all arguments as strings, whereas it may be possible to restrict some arguments to be integers.


The first three parameters of each of the JRequest get methods are the same. Only the first parameter is mandatory. In general, the format is



    JRequest::get<type>( <name>, <default>, <data-source> )


where




















<type>the data type to be retrieved (see below for the types available).
<name>the name of the variable to be retrieved (for example, the name of an argument in a URL).
<default>the default value.
<data-source>specifies where the variable is to be retrieved from (see below).

The following values for <data-source> are supported:






































GETData submitted in the query part of the URL.
POSTData submitted from form fields.
METHODThe same as either GET or POST depending on how the request was made.
COOKIEData submitted in cookies.
REQUESTAll the GET, POST and COOKIE data combined. This is the default.
FILESInformation about files uploaded as part of a POST request.
ENVEnvironment variables (platform-specific).
SERVERWeb server variables (platform-specific).

Notice that the default is REQUEST, which includes cookie data.


The following sections look at each of the data types in more detail.




Integer


The following will accept an integer. An integer can include a leading minus sign, but a plus sign is not permitted.



$integer = JRequest::getInt( 'id' );


will return the value of the "id" argument from the request (which by default includes all GET, POST and COOKIE data). The default value is zero.



$integer = JRequest::getInt( 'myId', 12, 'COOKIE' );


will return the value of the "myId" variable from a cookie, with a default value of 12.



Floating point number


A floating point number can include a leading minus sign, but not a plus sign. If the number includes a decimal point, then there must be at least one digit before the decimal point. For example,



$float = JRequest::getFloat( 'price' );


will return the value of the 'price' argument from the request. The default is "0.0".



$float = JRequest::getFloat( 'total', 100.00, 'POST' );


will retrieve the value of the 'total' argument from a POST request (but not a GET), with a default value of 100.00.



Boolean value


Any non-zero value is regarded as being true; zero is false.



$boolean = JRequest::getBool( 'show' );


will return false if the value of the 'show' argument in the request is zero, or 1 (true) if the argument is anything else. The default is false. Note that any string argument will result in a return value of true, so calling the above with a URL containing "?show=false" will actually return true!



$boolean = JRequest::getBool( 'hide', true, 'GET' );


will retrieve the value of the 'hide' argument from a GET request (but not a POST), with a default value of true.



Word


A word is defined as being a string of alphabetic characters. The underscore character is permitted as part of a word.



$word = JRequest::getWord( 'search-word' );


will retrieve the value of the 'search-word' argument from the request. The default is an empty string.



$word = JRequest::getWord( 'keyword', '', 'COOKIE' );


will retrieve the value of the 'keyword' variable from a cookie, with the default being an empty string.



Command


A command is like a word but a wider range of characters is permitted. Allowed characters are: all alphanumeric characters, dot, dash (hyphen) and underscore.



$command = JRequest::getCmd( 'option' );


will retrieve the value of the "option" argument from the request. The default value is an empty string.



$command = JRequest::getCmd( 'controller', 'view', 'POST' );


will retrieve the value of the "controller" argument from a POST request (but not a GET), with a default value of 'view'.



String


The string type allows a much wider range of input characters. It also takes an optional fourth argument specifying some additional mask options. See #Filter options for information on the available masks.



$string = JRequest::getString( 'description' );


will retrieve the value of the "description" argument from the request. The default value is an empty string. The input will have whitespace removed from the left and right ends and any HTML tags will be removed.



$string = JRequest::getString( 'text', '', 'METHOD', JREQUEST_NOTRIM );


will retrieve the value of the "text" argument from the request.. The default value is an empty string. Leading and trailing whitespace will not be removed.



$string = JRequest::getString( 'template', '<html />', 'METHOD', JREQUEST_ALLOWHTML );


will retrieve the value of the "template" argument from the request. The default value is '<html />'. Leading and trailing whitespace will be removed, but HTML will be permitted.



Generic and other data types


If the above methods do not meet your needs, there is a small number of additional filter types which you can use by calling the JRequest::getVar method directly. The syntax is:



JRequest::getVar( <name>, <default>, <data-source>, <type>, <options> );


where:

























<name>the name of the variable to be retrieved (for example, the name of an argument in a URL).
<default>the default value. There is no default value that will be returned if no default is specified in the call the JRequest::getVar. If no default is specified and the argument is not present in the request variable then it will return undefined.
<data-source>specifies where the variable is to be retrieved from (one of GET, POST, METHOD, COOKIE, REQUEST, ENV, SERVER; default is REQUEST).
<type>specifies the data type expected (see below).
<options>an optional bit-field used to specify options for some of the input filters (see below).

The first three arguments are the same as for the more specific methods described earlier. Only the first argument is mandatory.


Allowed values of the <type>, which is case-insensitive, are as follows:




















































INT, INTEGEREquivalent to JRequest::getInt.
FLOAT, DOUBLEEquivalent to JRequest::getFloat.
BOOL, BOOLEANEquivalent to JRequest::getBool.
WORDEquivalent to JRequest::getWord.
ALNUMAllow only alphanumeric characters (a-z, A-Z, 0-9).
CMDEquivalent to JRequest::getCmd.
BASE64Allow only those characters that could be present in a base64-encoded string (ie. a-z, A-Z, 0-9, /, + and =).
STRINGEquivalent to JRequest::getString.
ARRAYSource is not filtered but is cast to array type.
PATHValid pathname regex that filters out common attacks. For example, any path beginning with a "/" will return an empty string. Simliarly, any path containing "/./" or "/../" will return an empty string. Dots within filenames are okay though.
USERNAMERemoves control characters (0x00 - 0x1F), 0x7F, <, >, ", ', % and &.


Filter options


Allowed values of <options> are as follows (none of these are applied by default):
















JREQUEST_NOTRIMDoes not remove whitespace from the start and ends of strings.
JREQUEST_ALLOWRAWDoes not do any filtering at all. Use with extreme caution.
JREQUEST_ALLOWHTMLDoes not remove HTML from string inputs.

Masks can be combined by logically OR'ing them. If no filter options are specified, then by default, whitespace is trimmed and HTML is removed.



File uploads



Web servers already have a good deal of security around handling file uploads, but it is still necessary to take additional steps to ensure that file names and paths cannot be abused. A simplified form which requests a file to be uploaded looks like this:



<form action="index.php?option=com_mycomponent/form_handler.php"  method="post" enctype="multipart/form-data">

<input type="file" name="Filedata" />
<input type="submit" />

</form>

On clicking the submit button, the browser will upload the file in a POST request, passing control to Joomla which will call "components/com_mycomponent/form_handler.php". This will include code like the following. The variable $somepath must be set to some path where the web server has permission to create files.



// Check to ensure this file is included in Joomla!
defined('_JEXEC') or die( 'Restricted access' );


// Get the file data array from the request.
$file = JRequest::getVar( 'Filedata', '', 'files', 'array' );


// Make the file name safe.
jimport('joomla.filesystem.file');
$file['name'] = JFile::makeSafe($file['name']);


// Move the uploaded file into a permanent location.
if (isset( $file['name'] )) {


// Make sure that the full file path is safe.
$filepath = JPath::clean( $somepath.'/'.strtolower( $file['name'] ) );

// Move the uploaded file.

JFile::upload( $file['tmp_name'], $filepath );
}



Saving a request variable into user state


Because setting a user state variable from a variable in the request is such a common operation, there is an API method to make the task easier. This is generally safe to use because it calls [JRequest/getVar|JRequest::getVar]] to obtain the input from the request, but remember that none of the input filtering calls will protect against SQL injection attempts.



$app =& JFactory::getApplication();
$app->getUserStateFromRequest( <key>, <name>, <default>, <type> );


where




















<key>the name of the variable in the user state.
<name>the name of the request variable (same as the first argument of a JRequest::getVar call).
<default>the default value to be assigned to the user state variable if the request variable is absent. The default is null.
<type>the type of variable expected (same as the fourth argument of a JRequest::getVar call).


For example, getting an integer variable called 'id' from the request with a default value of 0, then saving it into a session variable called 'myid' can be done like this:



$app =& JFactory::getApplication();
$app->getUserStateFromRequest( 'myid', 'id, 0, 'int' );


instead of something like this:



$app =& JFactory::getApplication();
$app->setUserState( 'myid', JRequest::getInt( 'id', 0 ) );



Constructing SQL queries


One of the most common forms of attack on web applications is SQL injection, where the aim of the attacker is to change a database query by exploiting a poorly filtered input variable. Injecting modified SQL statements into the database can damage data or reveal private information. It is important to ensure that when SQL statements are constructed, they are correctly escaped and quoted so that bad input data cannot result in a bad SQL statement. You cannot rely on the JRequest methods to do this as they are not SQL-aware.


With the MySQL database, numeric fields should not be quoted, so it is important that they be typecast instead. Failure to do this will leave your code vulnerable to an attacker inserting a string containing SQL data.


Depending on the type, numeric types are cast like this:



// For SQL data types: INT, INTEGER, TINYINT, SMALLINT, MEDIUMINT, BIGINT, YEAR
$query = 'SELECT * FROM #__table WHERE `id`=' . (int) $id;

// For SQL data types: FLOAT, DOUBLE
$query = 'SELECT * FROM #__table WHERE `id`=' . (float) $id;

It's a good idea to get into the habit of always typecasting integers like this even if the variable was previously obtained using [[Further information on SQL injection attacks can be found here: http://uk2.php.net/manual/en/security.database.sql-injection.php and here: JRequest::getInt.



In the examples that follow it is assumed that $db is an instance of a Joomla database object. This can always be obtained from JFactory using



$db =& JFactory::getDBO();

Strings should always be escaped before being used in an SQL statement. This is actually very simple as the [[JDatabase->quote]] method escapes everything for you. You can also use the [[JDatabase->getEscaped]] method directly. The following statements are equivalent:




$query = 'SELECT * FROM #__table WHERE `field` = ' . $db->quote( $db->getEscaped( $field ), false );


$query = 'SELECT * FROM #__table WHERE `field` = ' . $db->quote( $field );

Special attention should be paid to LIKE clauses which contain the % wildcard character as these require special escaping in order to avoid possible denial of service attacks. LIKE clauses can be handled like this:




// Construct the search term by escaping the user-supplied string and, if required, adding the % wildcard characters manually.
$search = '%' . $db->getEscaped( $search, true ) . '%' );


// Construct the SQL query, being careful to suppress the default behaviour of Quote so as to prevent double-escaping.
$query = 'SELECT * FROM #__table WHERE `field` LIKE ' . $db->quote( $search, false );


If data is to be entered into a datetime column then you can use the Joomla API to ensure a valid date format:



$date =& JFactory::getDate( $mydate );
$query = 'UPDATE #__table SET `date` = ' . $db->quote( $date->toMySQL(), false );


Note that it is necessary to suppress database escaping as legitimate dates may contain characters that should not be escaped.


In the comparatively rare case where a field name is a variable, that should also be quoted using an API call:



$query = 'SELECT * FROM #__table WHERE ' . $db->NameQuote( $field-name ) . '=' . $db->quote( $field-value );



Securing forms


Apart from cleaning input variables as described above, you can also implement a simple technique which makes it more difficult for a cross-site request forgery attack (CSRF) to succeed. This involves adding a randomly-generated unique token to the form which is checked against a copy of the token held in the user's session. By checking that the submitted token matches the one contained in the stored session, it is possible to tie a rendered form to the request variables presented.


In POST forms you should add a hidden token field using:



echo JHTML::_( 'form.token' );


This outputs the token as a hidden form field looking like this:



<input type="hidden" name="8cb24ae69ffd7828ccecbcf06056e6fc" value="1" />


and places a copy of the token into the user's session, for later checking.


If you need to add the token to a URL rather than a form then you can use something like this:



echo JRoute::_( 'index.php?option=com_mycomponent&' . JUtility::getToken() . '=1' );


In the most common scenario, you will want to check the token following a POST to the form handler. This can be done by adding this line of code to form handler:



JRequest::checkToken() or die( JText::_( 'Invalid Token' ) );


If you need to pass the token in a GET request then you can check it like this:



JRequest::checkToken( 'get' ) or die( JText::_( 'Invalid Token' ) );


In both cases the code will die if the token is omitted from the request, or the submitted token does not match the session token. If the token is correct but has expired, then JRequest::checkToken will automatically redirect to the site front page.



Cleaning filesystem paths


If there is any possibility that a filesystem path might be constructed using data that originated from user input, then the path must be cleaned and checked before being used. This can be done quite simply like this:



JPath::check( $path );


This will raise an error and terminate Joomla if the path contains a ".." or leads to a location outside the Joomla root directory. If you want to deal with the error yourself without terminating the application, then you can use code like this:



$path = JPath::clean( $path );
if (strpos( $path, JPath::clean( JPATH_ROOT ) ) !== 0) {

// Handle the error here.
}

The JPath:clean method can be used in your own code too. It merely removes leading and trailing whitespace and replace double slashes and backslashes with the standard directory separator.



Cleaning filesystem file names


As with filesystem paths, if there is any possibility that a file name might be constructed using user-originated data, then the file name must be cleaned and checked before use. This can be done like this:




jimport('joomla.filesystem.file');
$clean = JFile::makeSafe( $unclean );

This method removes sequences of two or more "." characters and any character that is not alphabetic, numeric or a dot, dash or underscore character. If there is a leading dot then that is removed too.

How to add tabs to joomla component

mosTabs is no longer available in Joomla 1.5. Use this method instead to add tabbed content to your component or module in Joomla 1.5 native mode.








  • Include this line of code before you start your tabs

     1 

    jimport ( 'joomla.html.pane');






  • Then get the instance for the tabs and assign it to a variable:

     1 

    $myTabs = & JPane::getInstance ( 'tabs' );





  • Use the code below to define your tabs:

      1 
      2 
      3 
      4 
      5 
      6 
      7 
      8 
      9 
     10 
     11 
     12 
     13 
     14 
     15 
     16 
     17 
     18 
     19 
     20 
     21 
     22 


      // Start the tabs
      echo $myTabs->startPane( "my_tabs" );

      // Start the first Tab definition
      echo $myTabs->startPanel(JText::_('First Tab'),'tab1_id');

      // Add the content for the First Tab here...
      
      // Close the First Tab
      echo $myTabs->endPanel();
      
     
      // Start the Tab2 definition
      echo $myTabs->startPanel(JText::_('Second Tab'),'tab2_id');

      // Add the content for the Second Tab here...

      // Close the Second Tab
      echo $myTabs->endPanel();

      // Close the tabs
      echo $myTabs->endPane();






  • An alternative to tabs is the slider. To display your content in sliders just get the instance for 'sliders' instead of 'tabs' as shown in the following line of code:

     1 

    $myTabs =& JPane::getInstance( 'sliders' );


Develop Model-View-Controller component





Developing a Model-View-Controller (MVC) Component for Joomla!1.6 - Part 09



From Joomla! Documentation




Jump to: navigation, search







Warning - Joomla! 1.6 has not been released yet.



This article contains preliminary information which is subject to change.







Contents


[hide]





Articles in this series






Introduction


This tutorial is part of the Developing a Model-View-Controller (MVC) Component for Joomla!1.6 tutorial. You are encouraged to read the previous parts of the tutorial before reading this.



Adding a toolbar


In Joomla!1.6, the administrator interacts generally with components through the use of a toolbar. In the file admin/views/helloworldlist/view.html.php put this content. It will create a basic toolbar and a title for the component.


admin/views/helloworldlist/view.html.php




<?php
// No direct access to this file
defined('_JEXEC') or die('Restricted access');
// import Joomla view library

jimport('joomla.application.component.view');
/**
* HelloWorldList View
*/

class HelloWorldViewHelloWorldList extends JView {
/**
* items to be displayed
*/


protected $items;
/**
* pagination for the items
*/

protected $pagination;
/**
* HelloWorldList view display method
* @return void
*/

function display($tpl = null)

{
// Get data from the model
$items = $this->get('Items');
$pagination = $this->get('Pagination');
// Assign data to the view

$this->items = $items;
$this->pagination = $pagination;
// Set the toolbar

$this->_setToolBar();
// Display the template
parent::display($tpl);
}

/**
* Setting the toolbar
*/

protected function _setToolBar()
{
JToolBarHelper::title(JText::_('com_helloworld_Manager'));
JToolBarHelper::deleteListX('com_helloworld_HelloWorldList_Are_you_sure_you_want_to_delete_these_greetings', 'helloworldlist.remove');
JToolBarHelper::editListX('helloworld.edit');
JToolBarHelper::addNewX('helloworld.add');
}

}


You can find others classic backend actions in the administrator/includes/toolbar.php file of your Joomla!1.6 installation.



Adding specific controllers


Three actions has been added:



  • helloworldlist.remove

  • helloworld.edit


  • helloworld.add


These are compound tasks (controller.task). So a remove task has to be coded in a new HelloWorldControllerHelloworldList controller and edit and add tasks have to be coded in a new HelloWorldControllerHelloWorld controller.



admin/controllers/helloworldlist.php



<?php
// No direct access to this file
defined('_JEXEC') or die('Restricted access');

// import Joomla controller library
jimport('joomla.application.component.controller');

class HelloWorldControllerHelloWorldList extends JController
{
/**
* remove record(s)
* @return void
*/


function remove()
{
$model = $this->getModel('HelloWorldList');
if ($model->remove())
{

$msg = JText::_('com_helloworld_HelloWorldList_Greetings_removed');
$type = 'message';
}
else

{
$msg = JText::sprintf('com_helloworld_HelloWorldList_One_or_more_greetings_could_not_be_deleted', implode("<br />", $model->getErrors()));
$type = 'error';
}

$this->setRedirect('index.php?option=com_helloworld', $msg, $type);
}
}



admin/controllers/helloworld.php



<?php
// No direct access to this file
defined('_JEXEC') or die('Restricted access');

// import Joomla controller library
jimport('joomla.application.component.controller');
/**
* HelloWorld Controller
*/

class HelloWorldControllerHelloWorld extends JController
{

/**
* constructor (registers additional tasks to methods)
* @return void
*/

function __construct($config=array())
{

parent::__construct($config);
// Register Extra tasks
$this->registerTask('add', 'edit');
}

/**
* display the edit form
* @return void
*/

function edit()
{
$model = & $this->getModel();
$view = & $this->getView('HelloWorld','html');
$view->setModel($model, true);
$view->display();
}

/**
* save a record (and redirect to main page)
* @return void
*/

function save()
{
$model = $this->getModel();
if ($model->save())
{

$msg = JText::_('com_helloworld_HelloWorld_Greeting_saved');
$type = 'message';
$this->setRedirect('index.php?option=com_helloworld', $msg, $type);
}

else
{
$msg = JText::sprintf('com_helloworld_HelloWorld_Error_Saving_greeting', implode("<br />", $model->getError()));
$type = 'error';
$app = & JFactory::getApplication();
$app->enqueueMessage($msg, $type);
$view = & $this->getView('HelloWorld','html');
$view->setModel($model, true);
$view->display();
}

}
/**
* cancel editing a record
* @return void
*/

function cancel()
{
$msg = JText::_('com_helloworld_HelloWorld_edit_cancelled');
$this->setRedirect('index.php?option=com_helloworld', $msg);
}

}


In the previous controller we have introduced two new tasks: cancel and save. They will be used in the new view for editing message. Note that add and edit tasks have same code (see registerTask in the constructor).




Adding an editing view


With your favorite file manager and editor, put a file admin/views/helloworld/view.html.php containing:


admin/views/helloworld/view.html.php



<?php
// No direct access to this file
defined('_JEXEC') or die('Restricted access');

// import Joomla view library
jimport('joomla.application.component.view');
/**
* HelloWorld View
*/

class HelloWorldViewHelloWorld extends JView
{

/**
* View form
*
* @var form
*/

protected $form = null;
/**
* display method of Hello view
* @return void
*/

public function display($tpl = null)
{

// get the Form
$form = & $this->get('Form');
// get the Data

$data = & $this->get('Data');
// Bind the Data
$form->bind($data);
// Assign the form

$this->form = $form;
// Set the toolbar
$this->_setToolBar();
// Display the template

parent::display($tpl);
}
/**
* Setting the toolbar
*/

protected function _setToolBar()
{

JRequest::setVar('hidemainmenu', 1);
$isNew = ($this->form->getValue('id') < 1);
JToolBarHelper::title(JText::_('com_helloworld_Manager') . ': <small><small>[ ' . ($isNew ? JText::_('JToolBar_New') : JText::_('JToolBar_Edit')) . ' ]</small></small>');
JToolBarHelper::save('helloworld.save');
JToolBarHelper::cancel('helloworld.cancel', $isNew ? 'JToolBar_Cancel' : 'JToolBar_Close');
}

}


This view will display data using a layout.


Put a file admin/views/helloworld/tmpl/default.php containing


admin/views/helloworld/tmpl/default.php



<?php
// No direct access

defined('_JEXEC') or die('Restricted access');
JHTML::_('behavior.tooltip');
?>

<form action="<?php echo JRoute::_('index.php?option=com_helloworld'); ?>" method="post" name="adminForm" id="adminForm">
<fieldset class="adminform">

<legend><?php echo JText::_( 'com_helloworld_HelloWorld_Details' ); ?></legend>
<?php foreach($this->form->getFieldset() as $field): ?>

<?php if (!$field->hidden): ?>
<?php echo $field->label; ?>

<?php endif; ?>
<?php echo $field->input; ?>

<?php endforeach; ?>
</fieldset>
<input type="hidden" name="task" value="helloworld.edit" />

</form>



Adding a model and modifying the existing one


The HelloWorldViewHelloWorld view asks form and data from a model. This model has to provide a getForm, a getData method and a save method (called from the HelloWorldControllerHelloWorld controller)



admin/models/helloworld.php



<?php
// No direct access to this file
defined('_JEXEC') or die('Restricted access');

// import Joomla modelform library
jimport('joomla.application.component.modelform');
/**
* HelloWorld Model
*/

class HelloWorldModelHelloWorld extends JModelForm
{

/**
* @var array data
*/

protected $data = null;
/**
* Method to get the data.
*
* @access public
* @return array of string
* @since 1.0
*/

public function &getData()
{

if (empty($this->data))
{
$app = & JFactory::getApplication();
$data = & JRequest::getVar('jform');
if (empty($data))
{

$selected = & JRequest::getVar('cid', 0, '', 'array');
$db = JFactory::getDBO();
$query = $db->getQuery(true);
// Select all fields from the hello table.

$query->select('*');
$query->from('`#__helloworld`');
$query->where('id = ' . (int)$selected[0]);
$db->setQuery((string)$query);
$data = & $db->loadAssoc();
}

if (empty($data))
{
// Check the session for previously entered form data.
$data = $app->getUserState('com_helloworld.edit.helloworld.data', array());
unset($data['id']);
}

$app->setUserState('com_helloworld.edit.helloworld.data', $data);
$this->data = $data;
}

return $this->data;
}
/**
* Method to get the HelloWorld form.
*
* @access public
* @return mixed JForm object on success, false on failure.
* @since 1.0
*/

public function &getForm()
{

$form = $this->loadForm('com_helloworld.helloworld', 'helloworld', array('control' => 'jform', 'load_data' => $loadData));
return $form;
}

/**
* Method to save a record
*
* @access public
* @return boolean True on success
*/

function save()
{
$data = & $this->getData();
// Database processing

$row = & $this->getTable();
// Bind the form fields to the hello table
if (!$row->save($data))
{

$this->setError($row->getErrorMsg());
return false;
}

return true;
}
}


This model inherits from the JModelForm class and uses its getForm method. This method searches for forms in the forms folder. With your favorite file manager and editor, put a file admin/models/forms/helloworld.xml containing:



admin/models/forms/helloworld.xml



<?xml version="1.0" encoding="utf-8"?>
<form>
<fields>

<field
id="id"
name="id"
type="hidden"

/>

<field
id="greeting"
name="greeting"
type="text"

size="40"
class="inputbox"
default=""
label="com_helloworld_HelloWorld_Greeting"

description="com_helloworld_HelloWorld_Greeting_Desc"
/>

</fields>
</form>


The HelloWorldModelHelloWorldList model has to provide a remove method (called from the HelloWorldControllerHelloWorldList controller). Modify the admin/models/helloworldlist.php file:



admin/models/helloworldlist.php



<?php
// No direct access to this file
defined('_JEXEC') or die('Restricted access');

// import the Joomla modellist library
jimport('joomla.application.component.modellist');
/**
* HelloWorldList Model
*/

class HelloWorldModelHelloWorldList extends JModelList
{

/**
* Model context string.
*
* @var string
*/

protected $_context = 'com_helloworld.helloworldlist';
/**
* Method to remove the selected items
*
* @return boolean true of false in case of failure
*/

public function remove()
{

// Get the selected items
$selected = $this->getState('selected');
// Get a weblink row instance
$table = $this->getTable('HelloWorld');
foreach($selected as $id)
{

// Load the row and check for an error.
if (!$table->load($id))
{

$this->setError($table->getError());
return false;
}

// Delete the row and check for an error.
if (!$table->delete())
{

$this->setError($table->getError());
return false;
}

}
return true;
}
/**
* Method to build an SQL query to load the list data.
*
* @return string An SQL query
*/

protected function getListQuery()
{

// Create a new query object.
$db = JFactory::getDBO();
$query = $db->getQuery(true);
// Select some fields

$query->select('id,greeting');
// From the hello table
$query->from('#__helloworld');
return $query;
}

/**
* Method to auto-populate the model state.
*
* This method should only be called once per instantiation and is designed
* to be called on the first call to the getState() method unless the model
* configuration flag to ignore the request is set.
*
* @return void
*/

protected function populateState()
{
// Initialize variables.
$app = JFactory::getApplication('administrator');
// Load the list state.

$this->setState('list.start', $app->getUserStateFromRequest($this->_context . '.list.start', 'limitstart', 0, 'int'));
$this->setState('list.limit', $app->getUserStateFromRequest($this->_context . '.list.limit', 'limit', $app->getCfg('list_limit', 25) , 'int'));
$this->setState('selected', JRequest::getVar('cid', array()));
}

}



Packaging the component


Content of your code directory



Create a compressed file of this directory or directly download the archive and install it using the extension manager of Joomla!1.6. You can add a menu item of this component using the menu manager in the backend.


helloworld.xml



<?xml version="1.0" encoding="utf-8"?>

<extension type="component" version="1.6.0" method="upgrade">
<name>Hello World!</name>

<creationDate>November 2009</creationDate>
<author>John Doe</author>
<authorEmail>john.doe@example.org</authorEmail>
<authorUrl>http://www.example.org</authorUrl>

<copyright>Copyright Info</copyright>
<license>License Info</license>
<version>0.0.9</version>
<description>com_helloworld_Description</description>


<install> <!-- Runs on install -->
<sql>
<file driver="mysql" charset="utf8">sql/install.mysql.utf8.sql</file>

</sql>
</install>
<uninstall> <!-- Runs on uninstall -->
<sql>
<file driver="mysql" charset="utf8">sql/uninstall.mysql.utf8.sql</file>

</sql>
</uninstall>
<update> <!-- Runs on update -->
<sql>
<file driver="mysql" charset="utf8">sql/update.mysql.utf8.sql</file>

</sql>
</update>

<files folder="site">
<filename>index.html</filename>

<filename>helloworld.php</filename>
<filename>controller.php</filename>
<folder>views</folder>
<folder>models</folder>

<folder>language</folder>
</files>

<administration>
<menu>Hello World!</menu>
<files folder="admin">

<filename>index.html</filename>
<filename>helloworld.php</filename>
<filename>controller.php</filename>
<folder>sql</folder>

<folder>tables</folder>
<folder>models</folder>
<folder>views</folder>
<folder>controllers</folder>

</files>
<languages folder="admin">
<language tag="en-GB">language/en-GB/en-GB.com_helloworld.ini</language>

<language tag="en-GB">language/en-GB/en-GB.com_helloworld.menu.ini</language>
</languages>
</administration>
</extension>




Contributors


















Personal tools