This article is a tutorial to create a plugin step by step using a small example plugin. You will learn about the general plugin structure and about some features of the new classes in CONTENIDO 4.9.

Before you start, you should install the most recent version of CONTENIDO with the example client.

Creating the plugin.xml

In your CONTENIDO plugin folder (contenido/plugins/) you have to create a new folder for the plugin. Make the name descriptive! Our example plugin will be called "Messages", so the name of the folder will be "messages".

Inside of that folder we can create a new file called "plugin.xml". This file is used to store meta information about the plugin, its requirements and the menus that CONTENIDO should create for it.

<?xml version="1.0" encoding="UTF-8"?>
<plugin>
	<general active="1">
        <plugin_name>Messages</plugin_name>
        <plugin_foldername>messages</plugin_foldername>
        <uuid>A76F6359-B6DE-246B-25D5-5EB5CF025E42</uuid>
        <description>Send messages to other users</description>
        <author>Mischa Holz (4fb)</author>
        <copyright>four for business AG</copyright>
        <mail>mischa.holz@4fb.de</mail>
        <website>http://www.4fb.de</website>
        <version>1.0.0</version>
	</general>
	
    <requirements php="5.3">
        <contenido minversion="4.9.0" />
    </requirements>
	
	<contenido>
        <areas>
            <area menuless="1">messages</area>
        </areas>
        <actions>
            <action area="messages">send_message</action>
            <action area="messages">delete_message</action>
        </actions>
        <frames>
            <frame area="messages" filetype="main" name="messages/includes/include.messages.php" frameId="4" />
		</frames>
		
        <nav_sub>
            <nav area="messages" level="0" navm="3">messages/xml/lang_en_US.xml;navigation/extra/messages/main</nav>
		</nav_sub>
		
	</contenido>
</plugin>

The root tag of the XML file is plugin. The general tag contains information about the plugin like the display name, the folder, description and so on. The UUID is used to identify your plugin. You can generate one based on the copyright and foldername field here: http://www.contenido.org/deutsch/technik/tools/uuid-generator/index.html

Next up is the requirements tag, Here we can list what software has to be installed in order for this plugin to work. There are lots of things you can check for, but we only need to make sure that the CONTENIDO version is 4.9.0 or greater and that the PHP version is 5.3 or newer.

Last but not least, the contenido tag. This is where we can store all the new area, actions files and navigation links our plugin adds. We need one area "messages" which is menuless (that means it has no content in the left two frames). This area has 2 actions which we will use later in our code. The frames tag tells CONTENIDO what PHP file to execute if the user wants to load the area. In nav_sub we define the anchors in the menu of CONTENIDO. In this case we will add a new menu point in the "Extras" tab (which has the id "3"). To localize this menu point we add a file called "lang_en_US.xml" in the plugin's xml folder:

<?xml version="1.0" encoding="UTF-8"?>
<language>
    <navigation>
        <extra>
            <messages>
                <main>Messages</main>
            </messages>
        </extra>
    </navigation>
</language>

This just tells CONTENIDO that the new menu point is called "Messages" in English. You might want to add more language files and include them in your plugin.xml.

To find out more about the plugin.xml, read its own section in this article here.

In addition to the lang_en_US.xml we will also create a file called "include.messages.php" in the folder includes:

<?php
die("hi");

If you followed all these steps correctly, you should be able to install the plugin. After logging out and back in you should see the menu point "Messages" under "Extras". Clicking on it will simply print "hi"':

plugin_install.sql and plugin_uninstall.sql

These two files can contain SQL statements that will be executed when the plugin is installed or uninstalled. We will use these files to create a new table called *_messages to store the messages that users send each other. It is recommend that you read their section here.

CREATE TABLE IF NOT EXISTS !PREFIX!_messages (idmessage int NOT NULL PRIMARY KEY auto_increment, recipient_name text, sender_name text, text text, date_sent timestamp DEFAULT CURRENT_TIMESTAMP, `read` int)

In our plugin_uninstall.sql we will simply drop this table:

DROP TABLE !PREFIX!_messages

After reinstalling (make sure to uncheck "execute plugin_uninstall.sql"!) your plugin, there should be a new table in your database with the 6 columns we described in the plugin_install.sql.

Using GenericDB

The next step is to write two classes (Message and MessageCollection) to make dealing with the database easier. We can simply extend the Item and ItemCollection class from the core and add our own functions. Both classes will reside in one file in "classes/class.message.php":

<?php

class MessageCollection extends ItemCollection {
    
}
class Message extends Item {
}

These classes need to be filled with logic:

<?php

class MessageCollection extends ItemCollection {
    public function __construct() {
        global $cfg;
        parent::__construct($cfg['sql']['sqlprefix'] . "_pi_messages", 'idmessage');
        $this->_setItemClass('Message');
    }
    public function sendMessage($recName, $text) {
        $item = parent::createNewItem();
        $item->set("sender_name", $this->getUserName());
        $item->set("recipient_name", $recName);
        $item->set("text", $text);
        $item->set("read", 0);
        return $item->store();
    }
    public function selectMyMessages() {
        return $this->select("recipient_name = '" . $this->getUserName() . "' OR sender_name = '" . $this->getUserName() . "'", "", "date_sent DESC");
    }
    public function markMessagesAsRead() {
        $db = cRegistry::getDb();
        $db->query("UPDATE " . $cfg['sql']['sqlprefix'] . "_pi_messages" . " SET `read`=1 WHERE recipient_name = '" . $this->getUserName() . "'");
        return $db->affectedRows() > 0;
    }
    protected function getUserName() {
        $user = new cApiUser(cRegistry::getAuth()->getAuthInfo()['uid']);
        return $user->get("username");
    }
}
class Message extends Item {
    public function __construct($mId = false) {
        global $cfg;
        parent::__construct($cfg['sql']['sqlprefix'] . "_pi_messages", 'idmessage');
        $this->setFilters(array(
            'addslashes'
        ), array(
            'stripslashes'
        ));
        if ($mId !== false) {
            $this->loadByPrimaryKey($mId);
        }
    }
}

The collection just has some shortcut functions for sending messages, marking messages as read and selecting your own messages. For the message class, we don't really need anything more than what Item gives us already.

In order to allow CONTENIDO to find our classes we will add another new file called "config.plugin.php"

<?php
plugin_include("messages", "classes/class.message.php");

With this in place we can finally start working on the actual backend page!

Creating the template, CSS and JavaScript files

We will use a couple of classes that are provided by contenido.css. The class cGuiPage will include JavaScript, CSS and templates from special folders inside our plugin folder. Let's create the template first:

<table class="generic" cellspacing="0" cellpadding="2" border="0">
	<tr>
		<th>
			Sender
		</th>
		<th>
			Content
		</th>
		<th>
			Date
		</th>
		<th>
			Read
		</th>
		<th>
			Actions
		</th>
	</tr>
	<!-- BEGIN:BLOCK -->
	<tr>
		<td>
			{SENDER_NAME}-&gt;{RECIPIENT_NAME}
		</td>
		<td class="shortenedText">
			{TEXT}
		</td>
		<td>
			{DATE}
		</td>
		<td>
			<img src="{READ}">
		</td>
		<td>
    		<form method="post" action="main.php?area=messages&frame=4&action=delete_message">
    			<input type="hidden" name="message_id" value="{MESSAGE_ID}">
    			<input type="image" src="images/delete.gif" title="Delete message">
    		</form>
		</td>
	</tr>
	<!-- END:BLOCK -->
	<form method="post" action="main.php?area=messages&frame=4&action=send_message">
	<tr>
		<td>
			{USER_NAME}-&gt;<input type="text" name="message_recipient" placeholder="Recipient..." size="10">
		</td>
		<td>
			<input class="messageText" type="text" name="message_text" placeholder="Message...">
		</td>
		<td>
			{CURRENT_DATE}
		</td>
		<td>
			&nbsp;
		</td>
		<td>
   			<input type="image" src="images/newsletter_dispatch_on.gif" title="Send message">
		</td>
	</tr>
	</form>
</table>

This is just a simple table to view all messages. Note how we use the actions we defined earlier in the plugin.xml.

In order to see if everything works correctly we can now add some logic to the include.message.php:

<?php
$page = new cGuiPage("messages", "messages");
$messageCollection = new MessageCollection();
if($action == "delete_message") {
    if($messageCollection->delete($_REQUEST["message_id"])) {
        $page->displayInfo("Message successfully deleted!");
    } else {
        $page->displayWarning("Message could not have been deleted!");
    }
} else if($action == "send_message") {
    if($messageCollection->sendMessage($_REQUEST["message_recipient"], $_REQUEST["message_text"])) {
        $page->displayInfo("Message sent!");
    } else {
        $page->displayWarning("Could not send message. Are you sure the recipient exists?");
    }
}
$messageCollection->selectMyMessages();
$page->set("s", "USER_NAME", $messageCollection->getUserName());
$page->set("s", "CURRENT_DATE", date(getEffectiveSetting("dateformat", "full", "Y-m-d H:i:s")));
while(($message = $messageCollection->next()) !== false) {
    $page->set("d", "SENDER_NAME", $message->get("sender_name"));
    $page->set("d", "RECIPIENT_NAME", $message->get("recipient_name"));
    $page->set("d", "TEXT", $message->get("text"));
    $page->set("d", "DATE", date(getEffectiveSetting("dateformat", "full", "Y-m-d H:i:s"), strtotime($message->get("date_sent"))));
    if($message->get("read") > 0) {
        $page->set("d", "READ", "images/but_ok.gif");
    } else {
        $page->set("d", "READ", "images/but_warn.gif");
    }
    $page->set("d", "MESSAGE_ID", $message->get("idmessage"));
    $page->next();
}
$page->render();
$messageCollection->markMessagesAsRead();

Now you should be able to send and receive messages. The class cGuiPage is intelligent enough to find our template using the page name and the plugin name. Our page name is "messages" so the template is called "template.messages.html". The 2nd parameter is used to specify a plugin name, so that the class knows to look in the "messages" plugin folder.

There is one more little thing that I would like to add. Currently, if a user sends a really long message we'll get something like this:

In order to prevent this from happening we will use JavaScript and CSS to limit the visible length of the message. For that we create a new folder: "scripts/". Inside we will create a new file called "messages.js":

$(document).ready(function() {
    $(".shortenedText").each(function() {
        if($(this).html().length > 140) {
            $(this).attr("data-collapsed", "1");
            $(this).attr("data-text", $(this).html());
            $(this).html($(this).html().substring(0, 140) + "...");
        }
    });
    $(".shortenedText").click(function() {
        if($(this).attr("data-collapsed") == "1") {
            $(this).attr("data-collapsed", "0");
            $(this).html($(this).attr("data-text"));
        } else if($(this).html().length > 137) {
            $(this).attr("data-collapsed", "1");
            $(this).attr("data-text", $(this).html());
            $(this).html($(this).html().substring(0, 140) + "...");
        }
    });
})

After reloading the page, you will see results directly. Long messages are now shortened. This is because cGuiPage is able to find and include the JS file, since it has the same name as the page name. Now we'll just add some CSS to make it more obvious that you can click on the text. For that we create a folder called "styles/" and a file called "messages.css":

@CHARSET "UTF-8";
.shortenedText {
	cursor: pointer;
}
.messageText {
	width: 90%;
} 

And again, without changing anything else, the CSS file is included.

Closing Words

The plugin is now complete. Of course, this is just a small example of what you can do with plugins in CONTENIDO (and we haven't even covered Chains yet).

In case you are interested in the source code, here is the plugin zip, ready to be installed by the plugin manager:

messages.zip

This plugin is released under the same license as the CONTENIDO core.

If you have any more questions make sure to visit the CONTENIDO forum, take a look at the API documentation and of course the pages about extending the system.