By: Jonathan M. Cameron
Warning
This manual has been updated for Attachments 3.1 for Joomla 2.5.11+ but has not been tested for correctness.
This version includes updates for some API changes in the attachments plugin framework. The manual is specific to Attachments version 3.1 or later (which should be released by the end of May; contact the author for a pre-release version).
If you follow this manual and have difficulties creating an attachments plugin, or have any suggestions for improving this manual, please email your corrections, additions, or suggestions to the the author (see the email address at the end of this document).
2013-05-19
Contents
The Attachments extension for Joomla! lets users attach files to content items such as articles. The original purpose of the Attachments extension was to add attachments to articles and it was not possible to add attachments to anything else. Attachments extension version 2.0 was upgraded and refactored so that files can be attached to a variety of content items provided by various Joomla! components. The Attachments extension now accepts its own plugins to make this possible for new types of content items.
Content items correspond to a component. For instance, articles are part of the com_content component. Plugins for the Attachments extension provide the necessary functions to allow attaching files to new types of content items.
The purpose of this manual is describe how to create a new Attachments plugin. There are several steps involved in the process. First, you need to create a set of files for the plugin. A template is provided that includes the necessary files. The files need to be adapted to the new component including adding appropriate functionality. Then the plugin can be installed and tested.
The Attachments plugins are a new type of Joomla! plugin that provide extra functionality to the the Attachments extensions. They are installed like other Joomla! plugins and are visible in the plugin manager.
In order to add attachments to a content item, the content item must invoke the Joomla! content plugin ‘onContentPrepare’ when that item is rendered. To determine if that is the case, we need to do a little diagnostic work. Install the Attachments extension and temporarily edit the main attachments plugin file:
plugins/content/attachments/attachments.php
Edit this file and find the onContentPrepare function near the beginning of the file. Uncomment several lines at the beginning of the function to generate some diagnostic output. The uncommented lines should look like this:
// Enable the following four diagnostic lines to see if a component uses onContentPrepare
$msg = "<br/>onContentPrepare: CONTEXT: $context, OBJ: " . get_class($row) . ", VIEW: " . JRequest::getCmd('view');
$row->text .= $msg;
$row->introtext .= $msg;
return;
where the ‘CONTEXT’ tag is for the Parent Component or parent type, ‘OBJ’ is the class of the the content item, and ‘VIEW’ is the name of the view. Note that some versions of Attachments have these lines aready present, but commented out. Just uncomment these lines.
Refresh the frontpage (or whichever page contains the content item). Look for the diagnostic line beginning with ‘CONTEXT’ just after your content item. Make a note of what appears after the CONTENT, OBJ, and VIEW tags. You may need it when you implement the getParentId() function (see section 4.2 Functions you may not need to implement). It may be useful to insert a command to dump the entire $row object (e.g. var_dump($row); ). Note that the display of any existing attachments will be superceded by this output; when these two lines are removed the display of attachments will return to normal.
If you do not see any output after your item, it may not be possible to attach files to your type of content items using the Attachemnts extension. Note that some components have settings that control whether the ‘onContentPrepare’ is called by the component code during the rendering process. Check the extension’s documentation. Make sure the setting is enabled, if available.
Warning
Once you have determined if the ‘onContentPrepare’ plugin is called for your content item, don’t forget to restore the onContentPrepare() function to its normal operation!
The next step is to identify two things: (1) the parent type and (2) any parent entities that you intend to handle in the new Attachments plugin.
From the diagnostic display you saw in the previous step, you can clearly identify the parent type as the component name to the right of the ‘CONTENT:’ just after the item you want to attach files to. It should look something like com_newcomp. (Obviously, the ‘newcomp’ would be replaced with the actual name of your component.) This may not come as a surprise since this should correspond to the type of content you are interested in.
If you are interested in only one type of content item for the new component, then this phase is complete. The parent type is com_newcomp. The entity corresponds to the name of type of content item. It will also be the default one, called default. So the default entity will have two names: default and whatever entity name you want for the content item (in com_content, this was article).
If there is more than one type of entity that you wish to handle for the component, pay special attention to the other two items (OBJ and VIEW) for each item from the diagnostic display. More than likely the entities will correspond to the primary types of content in the new component.
Each entity name needs be alphanumeric token without spaces. Entity names will be used in the code and URLs and will be general to all languages. You can use the translation file to create alternate names that have spaces and capitalization.
For instance, for the basic Joomla! content, the parent type is com_content and the entities are article (and default for articles), and category. These are all basic Joomla! content items that can have descriptions or textual content associated with them.
Warning
The entity names must be unique and not be the same as any other entity name in other components.
The next thing you need to do is create the basic set of files you need for your new Attachments plugin. First, create a directory for your files and create a set of files like this inside that directory:
attachments_for_newcomp.xml
attachments_for_newcomp.php
en-GB.plg_attachments_attachments_for_newcomp.ini
en-GB.plg_attachments_attachments_for_newcomp.sys.ini
where you should replace all occurrences of newcomp with the name of your component (the part after the com_ prefix) you are building the Attachments plugin for.
Here is what the installation file attachments_for_newcomp.xml should contain:
<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" group="attachments" version="2.5" method="upgrade">
<name>Attachments - For Newcomp</name>
<creationDate>???</creationDate>
<author>???</author>
<authorEmail>???</authorEmail>
<authorUrl>???</authorUrl>
<copyright>???</copyright>
<license>http://www.gnu.org/licenses/gpl-3.0.html GNU/GPL</license>
<version>???</version>
<description>ATTACHMENTS_FOR_NEWCOMP_PLUGIN_INSTALLED</description>
<files>
<filename plugin="attachments_for_newcomp">attachments_for_newcomp.php</filename>
<filename>index.html</filename>
<folder>language</folder>
</files>
</extension>
where you should fill in for all of the ??? items as well as change all occurrences of ‘newcomp’ to the name of your new component. Note that the description field is a translation token and should include no spaces.
The main code for the plugin is in the file attachments_for_newcomp.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | <?php
// no direct access
defined('_JEXEC') or die('Restricted access');
// Load the attachments plugin class
if (!JPluginHelper::importPlugin('attachments', 'attachments_plugin_framework'))
{
// Fail gracefully if the Attachments plugin framework plugin is disabled
return;
}
class AttachmentsPlugin_Com_Newcomp extends AttachmentsPlugin
{
/**
* Constructor
*/
public function __construct(&$subject, $config = array())
{
parent::__construct($subject, $config);
// Configure the plugin
$this->_name = 'attachments_for_newcomp';
// Set basic attachments defaults
$this->parent_type = 'com_newcomp';
$this->default_entity = 'thing';
// Add the information about the default entity (thing)
$this->entities[] = 'thing';
$this->entity_name['thing'] = 'thing';
$this->entity_name['default'] = 'thing';
$this->entity_table['thing'] = 'things';
$this->entity_id_field['thing'] = 'id';
$this->entity_title_field['thing'] = 'title';
// Configure additional entities
...
// Always load the language
$this->loadLanguage();
}
... OTHER FUNCTIONS DESCRIBED IN APPENDEX A BELOW
}
// Register this attachments type
$apm = getAttachmentsPluginManager();
$apm->addParentType('com_newcomp');
?>
|
where many functions have been omitted for clarity. Each function that may need implementing is described in Appendix A. Replace newcomp with the appropriate component name for your component throughout this code. The configuration code in the constructor will be described in the next section.
Notice lines 48-49 at the end of the file. These two lines are necessary in order to automatically register your new plugin with the Attachments plugin framework. Do not leave them out!
You can refer to the the com_content component configuration file plugins/attachments/attachments_for_content/attachments_for_content.php for a more involved example with multiple blocks and aliases. (Check after the Attachments extension is installed).
Your new class extends the AttachmentsPlugin class that can be found in the file:
- plugins/attachments/attachments_plugin_framework/attachments_plugin.php
in your Joomla! installation.
Now consider the code in the constructor. It is important to get the constructor exactly right in order for the plugin to work properly.
Lines 22-27 configure the new plugin as a whole. In line 23, define the name component. Simply replace ‘newcomp’ with the name of your component (with the com_ prefix):
$this->_name = 'attachments_for_newcomp';
In line 26, set the name of the component to be supported (use the form with the com_ prefix):
$this->_parent_type = 'com_newcomp';
In line 18, set the name for the default entity. This is the raw, untranslated entity name in lowercase:
$this->_default_entity = 'thing';
As was mentioned before, every entity name (including this one) must be a single alphanumeric token without spaces (because it may be used in URLs). The same token entity token is used in all languages.
The next section of code (lines 29-35), configures the information about the default entity. For most of these lines, simply replace ‘thing’ with the name of your entity.
In line 33, define the name of the database table where the entities can be looked up (remove the leading #__ prefix). We will refer to this as the entity_table.
$this->_entity_table['thing'] = 'things';
For example, if your site uses jos_ as the table prefix, then the full entity_table name would be jos_things and you would strip off the jos_ prefix to get the entity table name used in this line.
In line 34, define which field in the entity_table contains the primary ID. This is normally ‘id’, but some components may use a different name for the primary ID field:
$this->_entity_id_field['thing'] = 'id';
Note
By default, the AttachmentsPlugin base class (which your code will extend) supports content items that appear in database tables, which usually means that they are defined in Joomla! components. If your entity is not defined in a Joomla! database table, you will have to override several of the base class functions, particularly the function to retrieve a content item’s title.
In the next line, 35, define which field in the entity_table contains the entity title (or comparable name of the entity):
$this->_entity_title_field['thing'] = 'title';
Finally, if there is more than one entity, a block of code similar to lines 29-35 would be added where lines 37-38 currently are for a different entity name (if only one entity is supported, you may delete lines 37-38).
Note that secondary (non-default) entities must not include a line like line 32 since there can only be one default entity. For blocks describing secondary entities, replace ‘thing’ with the appropriate entity name and update the database table name and the entity ID and title fields.
You can refer to the the com_content component configuration file plugins/attachments/plugins/com_content.ini for a more involved example with multiple blocks and aliases. (Check after the Attachments extension is installed).
In the code above, line 44 is a placeholder for several functions that will need to be added before the plugin can be functional.
Please see Appendix A for a listing of which functions need to be implemented.
The main translations .ini file should look like this:
# en-GB.plg_attachments_for_newcomp.ini
# Attachments for Joomla! newcomp extension
# Copyright (C) ??? ???, All rights reserved.
# License http://www.gnu.org/licenses/gpl-3.0.html GNU/GPL
# Note : All ini files need to be saved as UTF-8 - No BOM
# English translation
ATTACHMENTS_FOR_NEWCOMP_PLUGIN_INSTALLED=This plugin enables adding attachments to Newcomp 'Things'
THING=Thing
THINGS=Things
This file should define any translation item created in this plugin. Note that the item ATTACHMENTS_FOR_NEWCOMP_PLUGIN_INSTALLED must be exactly the same as the one in the <description> item in the installation .xml file. We have also added a translation item for “thing”, the basic entity of com_newcomp as well as its pluralized version. Note that the pluralization in the translation item token on the left is always done by simply adding a ‘S’ on the end of the translation item; the translation on the right can be spelled appropriately. All translation keys (on the left of the equals sign) must be alphanumeric without spaces.
Each supported entity name should be given with an appropriate translation that may include spaces, etc.
Don’t forget to add translation items for any error messages you may include in the code our write.
Tip
It is correct that ‘attachments’ appears twice in the .ini filename.
The system translations .ini file should look like this:
# en-GB.plg_attachments_for_newcomp.sys.ini
# Attachments for Joomla! newcomp extension
# Copyright (C) ??? ???, All rights reserved.
# License http://www.gnu.org/licenses/gpl-3.0.html GNU/GPL
# Note : All ini files need to be saved as UTF-8 - No BOM
# English translation
ATTACH_ATTACHMENTS_FOR_NEWCOMP_PLUGIN_DESCRIPTION="The Attachments for newcomp plugin enables adding attachments to newcomp things."
PLG_ATTACHMENTS_FOR_NEWCOMP="Attachments - For Newcomp"
These are the ‘system’ language tokens for displaying information about your new attachments plugin in the extension manager and plugin manager.
Note
The translation on the right side of each language token must be in double-quotes and must be all one one line (no matter how long).
Once the files have been created (see 3.3 Create the set of files for the plugin) and edited to provide the necessary functionality, you will need to create a zip file for installation. Use your favorite zip tool to create a zip file with the 4 files. Note that top level files and hierarchy of the zip file should look like this:
├── attachments_for_newcomp.php
├── attachments_for_newcomp.xml
├── index.html
└── language
├── en-GB
│ ├── en-GB.plg_attachments_attachments_for_newcomp.ini
│ ├── en-GB.plg_attachments_attachments_for_newcomp.sys.ini
│ └── index.html
└── index.html
These files should appear in the zip file directly as shown and not in a nested directory. Notice the added empty index.html files to prevent directory browsing.
To see where these files go when installed, please see Appendix B.
Once you have created your zip file, you should be able to install it into Joomla! using the regular installer (under the Extensions > Install/Uninstall menu item in the administrative back end). You will then need to enable the plugin.
DO NOT FORGET TO ENABLE YOUR NEW PLUGIN!
Once the new attachments plugin is installed and enabled, you should be able to test it.
Go to the front end and log in as a user with adequate permissions to edit the content item you are interested in. You should see a red Add Attachment link just below the item. Click on it to add an attachment to make sure it works.
You should also try adding an attachment to a content item in the administrative back end. Click on the ‘Attachments’ item under the Components menu. Then click on the [New] button on the task bar. On the top right of the form, you will see a row of buttons corresponding to the supported types of content entities. Click on the one corresponding to your new content entity. Then click on the [Select] entity button at the right end of the first field in the form. You should see a list of the entities. Select one and try adding the attachment to it.
Once an attachment has been added to a content item, the usual functions to edit, delete, download, etc., should work properly.
If your new code does not work properly, you will need to review the functions described in section Appendix A. You may need to fix the code or add functions that you may have omitted.
You may wish to implement simplified versions of the permission checking functions first (e.g., userMayAddAttachment(), userMayEditAttachment(), and userMayAccessAttachment()) first. It may be more productive to get the rest of the functionality working, then implement the permissions functions afterwards.
In your attachments plugin file com_newcomp.php, you will probably need to implement some or all of the following functions.
/**
* Get a URL to view the entity
*
* @param int $parent_id the ID for this parent object
* @param string $parent_entity the type of entity for this parent type
*
* @return a URL to view the entity (non-SEF form)
*/
public function getEntityViewURL($parent_id, $parent_entity = 'default')
{
...
}
This function constructs and returns a URL that will view or visit a specific entity. This is specific to each type of component and each implemented type of entity. In your component, find the URL for a view for each entity supported and implement them here. Try to trim anything extra from the URL; often extra fields can be eliminated from the URL without affecting its operation (eg, dates, category IDs, etc).
You will need to implement this function.
/**
* Check to see if a custom title applies to this parent
*
* Note: this public function assumes that the parent_id's match
*
* @param string $parent_entity the parent entity for the parent of the list
* @param string $rtitle_parent_entity the entity of the candidate attachment list title (from params)
*
* @return true if the custom title should be used
*/
public function checkAttachmentsListTitle($parent_entity, $rtitle_parent_entity)
{
if ( $rtitle_parent_entity == 'newcomp' )
{
return true;
}
return false;
}
This function checks to see if custom titles for attachments list might apply to this parent. In the options, there is a ‘custom titles for attachments lists’ option that allows the admin to define custom titles for attachments lists on a system wide level or on a entity-by-entity basis (eg, for a specific article with ‘article:23’). When this function is called, rtitle_parent_entity will be ‘article’ (or an what ever entity name you specify to the left of the colon in the custom title list).
If you wish this functionality to be available for your new content type, you should implement this function. If this function is not re-implemented, custom titles for specific component entities will never be applied to your new component attachments.
The code shown above is typical if only one type of parent entity is supported for the new content type. If more are supported, your function will need to be more sophisticated; see the attachments attachments_for_content plugin file for an example.
You should implement this function.
/**
* Check to see if the parent is published
*
* @param int $parent_id the ID for this parent object
* @param string $parent_entity the type of entity for this parent type
*
* @return true if the parent is published
*/
public function isParentPublished($parent_id, $parent_entity = 'default')
{
...
}
This function checks to see if the parent entity is published. Your code will need to check the component tables for the parent entity to see if it is published and return true if it is (and false if not)
You will need to implement this function.
/**
* May the parent be viewed by the user?
*
* This public function should be called by derived class functions.
*
* Note that this base class function only determines necessary
* conditions. If this function returns FALSE, then viewing is definitely
* not permitted. If this function returns TRUE, then the derived classes
* also need to check whether viewing the specific content item (eg,
* article) is permitted.
*
* @param int $parent_id the ID for this parent object
* @param string $parent_entity the type of entity for this parent type
* @param object $user_id the user_id to check (optional, primarily for testing)
*
* @return true if the parent may be viewed by the user
*/
public function userMayViewParent($parent_id, $parent_entity = 'default', $user_id = null)
{
...
}
This function checks to see if the parent may be viewed by the current user. This function defaults to true (meaning anyone can see the parent). In most cases, each parent object will have its own access rules controlling whether the user has adequate privileges to view the parent. You will need to use the authorization functions provided by the parents extension/class to implement this function.
You will probably want to implement this function.
/**
* Return true if the user may add an attachment to this parent
*
* (Note that all of the arguments are assumed to be valid; no sanity checking is done.
* It is up to the caller to validate these objects before calling this function.)
*
* @param int $parent_id the ID of the parent the attachment is attached to
* @param string $parent_entity the type of entity for this parent type
* @param bool $new_parent if true, the parent is being created and does not exist yet
* @param object $user_id the user_id to check (optional, primarily for testing)
*
* @return true if this user add attachments to this parent
*/
public function userMayAddAttachment($parent_id, $parent_entity, $new_parent = false, $user_id = null)
{
...
}
Checks to see if the current user may add attachments to this entity.
The simplest implementation would be to always return true. This would mean than anyone can add an attachment to your new component. This is obviously not recommended for production but would make it easier to get your attachments plugin working quickly for testing purposes.
If this function is not re-implemented, the default is that no users may add attachments for the specified type of parent. Effectively, this means that only admin/superadmin should be able to add attachments (since the code assumes they always can).
You will need to implement this function.
/**
* Return true if this user may edit (modify/delete/update) this attachment for this parent
*
* (Note that all of the arguments are assumed to be valid; no sanity checking is done.
* It is up to the caller to validate the arguments before calling this function.)
*
* @param &record &$attachment database record for the attachment
* @param object $user_id the user_id to check (optional, primarily for testing)
*
* @return true if this user may edit this attachment
*/
public function userMayEditAttachment(&$attachment, $user_id = null)
{
...
}
Check the attachment and see if the current user may edit it. For attachments, ‘Edit’ means edit/modify or delete.
The simplest implementation would be to always return true. This would mean than anyone can edit all attachments to your new component. This is obviously not recommended for production but would make it easier to get your attachments plugin working quickly for testing purposes.
If this function is not re-implemented, the default is that no users may edit attachments for the specified type of parent. Effectively, this means that only admin/superadmin should be able to edit attachments (since the code assumes they always can).
You will need to implement this function.
/** Check to see if the user may access (see/download) the attachments
*
* @param &record &$attachment database record for the attachment
* @param object $user_id the user_id to check (optional, primarily for testing)
*
* @return true if access is okay (false if not)
*/
public function userMayAccessAttachment(&$attachment, $user_id = null)
{
...
}
Check the attachment and see if the current user may access the attachment. By ‘access’, we mean to see the attachments in attachments list and to be able to download it.
The simplest implementation would be to always return true. This would mean than anyone can access (see/download) an attachment to your new component. This is obviously not recommended for production but would make it easier to get your attachments plugin working quickly for testing purposes.
Currently, this is only checked in searches. But it is likely that it will be used elsewhere in the Attachments plugin in the future.
You will need to implement this function.
/**
* Determine the parent entity
*
* From the view and the class of the parent (row of onContentPrepare plugin),
* determine what the entity type is for this entity.
*
* Derived classes MUST overrride this
*
* @param &object &$parent The object for the parent (row) that onContentPrepare gets
*
* @return the correct entity (eg, 'default', 'category', etc) or false if this entity should not be displayed.
*/
public function determineParentEntity(&$parent)
{
...
}
If the component does not have more than one type of entity, you will not need to define this function; the one in the AttachmentsPlugin base class will be fine.
If there is more than one type of entity, you will need to write code here to distinguish them based on the OBJ and VIEW values you determined for each entity in the diagnostic section 3.1 Make sure an attachment plugin will work. See the attachments attachments_for_content plugin file for an example.
In your attachments plugin file attachments_for_newcomp.php, you may not need to implement the following functions:
/**
* Return the parent entity / row ID
*
* This will only be called by the main attachments 'onContentPrepare'
* plugin if $attachment does not have an id
*
* @param object &$attachment the attachment
*
* @return id if found, false if this is not a valid parent
*/
public function getParentId(&$attachment)
{
...
}
When the regular attachments plugin is called from the front end when the ‘onContentPrepare’ plugin function is invoked, an object for the article or content item is passed in as $row. Normally $row has an ID field $row->id. If your component has the field $row->id, then you will probably not need to implement this function. If $row does not have an $row->id field, the ID should be some field of the $row object. This function should extract the entity ID and return it. Note that the onContentPrepare callback function may be invoked several times for each entity on the page. You may need to examine the other data about the entity (retrieved in the diagnostic section 3.1 Make sure an attachment plugin will work) to determine which call you want to process and which ones you want to ignore. Return false for the ones you want to ignore.
/**
* Does the parent exist?
*
* @param int $parent_id the ID for this parent object
* @param string $parent_entity the type of entity for this parent type
*
* @return true if the parent exists
*/
public function parentExists($parent_id, $parent_entity = 'default')
{
...
}
This function checks to see if the parent entity exists. If you have defined a table for the entity in the configuration, you probably will not need to redefine this function.
/**
* Get a URL to add an attachment to a specific entity
*
* @param int $parent_id the ID for the parent entity object (null if the parent does not exist)
* @param string $parent_entity the type of entity for this parent type
* @param string $from where the call should return to
*
* @return the url to add a new attachments to the specified entity
*/
public function getEntityAddUrl($parent_id, $parent_entity = 'default', $from = 'closeme')
{
...
}
This function constructs and returns a URL to add an attachment to a specific entity. You probably will not need to redefine it.
/**
* Get the path for the uploaded file (on the server file system)
*
* Note that this does not include the base directory for attachments.
*
* @param string $parent_entity the type of entity for this parent type
* @param int $parent_id the ID for the parent object
* @param int $attachment_id the ID for the attachment
*
* @return string the directory name for this entity (with trailing '/'!)
*/
public function getAttachmentPath($parent_entity, $parent_id, $attachment_id)
{
...
}
This function constructs the path for a newly uploaded attachment file.
You probably will not need to define this function. If you are satisfied with the default attachment file path scheme (see Appendix C for details), then you can use the version already defined in the AttachmentsPlugin base class.
/**
* Return the URL that can be called to select a specific content item.
*
* @param string $parent_entity the type of entity to select from
*
* @return the URL that can be called to select a specific content item
*/
public function getSelectEntityURL($parent_entity = 'default')
{
...
}
This function builds and returns a URL that will construct a list of a particular type of entity and allow the user to select a specific one from the list. For example, in the Joomla! base component com_content, this is the function that allows users to select an article.
You probably will not need to implement this function.
Once these files are installed in your Joomla! installation, they will go into the following locations:
plugins/attachments/atttachments_for_newcomp
├── attachments_for_newcomp.php
├── attachments_for_newcomp.xml
├── index.html
└── language
├── en-GB
│ ├── en-GB.plg_attachments_attachments_for_newcomp.ini
│ ├── en-GB.plg_attachments_attachments_for_newcomp.sys.ini
│ └── index.html
└── index.html
When the attachment files are uploaded, they are stored in paths with the following form
<joomla>/attachments/<entity-name>/<entity-ID>/<filename>
where:
- <joomla>
- is the top directory in which your Joomla! installation is installed
- <entity-name>
- is the name of the entity type (e.g., article). Note that ‘default’ is never used here since all entity names must be unique.
- <entity-ID>
- is the ID of the specific entity to which the files are attached
- <filename>
- is the name of the file (without any associated path)
So for an article, the path might look like this:
<joomla>/attachments/article/23/attachmentFile.txt
Please report corrections and suggestions to jmcameron@jmcameron.net