wbTeamPro Plugin Architecture

System Plugins allow you to integrate code directly into wbTeamPro via a set of events triggered during runtime. Included with your installation package is a plugin example that can be used as reference while learning to develop within wbTeamPro.

Plugin File Structure

Plugins are stored within the wbTeamPro addons folder /modules/addons/wbteampro/plugins, with each plugin being entirely contained within a sub-folder.

The plugin model requires that the following files be included (where "example" is the unique plugin key):

  • example/example.php
    This is the core plugin file that will be loaded and examined for plugin events.
  • example/example.xml
    This is the plugin package information.
  • example/lang/english.php
    This is the default language file for the plugin.

Plugin Information XML

The plugin package XML defines several important aspects of a plugin to the wbTeamPro system. Plugins may include parameter fields that can be used to allow customization of the plugin once installed. The example.xml file shows how the XML file should be constructed, and examples of the available parameter fields.


<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" version="1.0" zone="all" method="install">
  <name>plugin_example.xml_name</name>
  <author>Webuddha, Holodyn, Inc.</author>
  <created>2013-01-01</created>
  <version>3.0.0</version>
  <description>plugin_example.xml_description</description>
  <files>
    <folder>lang</folder>
    <folder>widgets</folder>
    <file>example.php</file>
    <file>example.xml</file>
  </files>
  <params>
    <param
      type="text"
      name="example_text"
      default="0"
      label="plugin_example.params.example_text-def"
      description="plugin_example.params.example_text-desc"
      />
    <param
      type="selectlist"
      name="example_selectlist"
      default="0"
      label="plugin_example.params.example_selectlist-def"
      description="plugin_example.params.example_selectlist-desc"
      >
      <option value="0">plugin_example.params.opt_no</option>
      <option value="1">plugin_example.params.opt_yes</option>
    </param>
    <param
      type="radiolist"
      name="example_radiolist"
      default="0"
      label="plugin_example.params.example_radiolist-def"
      description="plugin_example.params.example_radiolist-desc"
      >
      <option value="0">plugin_example.params.opt_no</option>
      <option value="1">plugin_example.params.opt_yes</option>
    </param>
    <param
      type="checklist"
      name="example_checklist"
      default="1"
      label="plugin_example.params.example_checklist-def"
      description="plugin_example.params.example_checklist-desc"
      >
      <option value="0">plugin_example.params.opt_no</option>
      <option value="1">plugin_example.params.opt_yes</option>
    </param>
    <param
      type="textarea"
      name="example_textarea"
      default=""
      label="plugin_example.params.example_textarea-def"
      description="plugin_example.params.example_textarea-desc"
      />
  </params>
</extension>

Plugin Event PHP

The plugin package Core PHP file defines the event methods that will be triggered during runtime.

  • Plugins are zoned for either client, admin, or all and are loaded upon wbTeamPro initialization
  • Every plugin must have a unique plugin "key" name, such as "example" for the Example plugin
  • The plugin class is defined by appending the plugin key to the prefix "wbTeamPro_Plugin_"
  • Plugin events are methods named for the appropriate event trigger
  • All event methods will receive the same two parameters ( $data, $params )

The example.php file shows how to to properly construct the plugin.


<?php

/**

  Webuddha wbTeamPro
  (c)2010 Webuddha.com, The Holodyn Corporation - All Rights Reserved

**/

// ********************************************************************
// Check Valid Access
// ********************************************************************

  defined('WHMCS_ADMIN') or defined('WHMCS_CLIENT') or die('Invalid Access');

// ********************************************************************
// Plugin Class
// ********************************************************************

  class wbTeamPro_Plugin_Example extends wbTeamPro_Plugin {

    /******************************************************************************
      PUBLIC FUNCTIONS
    *******************************************************************************/

    //---------------------------------------------------------------------------
    // Class Constructor
    //---------------------------------------------------------------------------
    public function __construct( $extension ){

      // Import Plugin Language
      wbTeamPro_Lang::import( dirname(__FILE__).DS.'lang'.DS );

      // Initialize Plugin
      parent::__construct( $extension );

    }

    //---------------------------------------------------------------------------
    // System Event Functions
    //---------------------------------------------------------------------------
    public function system_onRunCron( &$data, &$params ){}
    public function system_onAfterUpgrade( &$data, &$params ){
      $previous_version = $data['previous_version'];  // Before Upgrade - ie: 3.0.1
      $current_version  = $data['current_version'];   // After Upgrade - ie: 3.0.2
      wbTeamPro_Common::IsOlderVersion($current_version, '3.0.2'); // Is older than 3.0.2 (returns 1 if major, 2 if minor, 3 if build, 4 if stage)
    }

    //---------------------------------------------------------------------------
    // APIv1 Event Functions
    //---------------------------------------------------------------------------
    public function apiv1_getResourceList( &$data, &$params ){
      $resourceList = &$data['resourceList'];
      $resourceList['plg_example'] = array(
        'label' => 'Example Plugin',
        'methods' => array(
          'plgExampleFindConfigs' => array(
            'synopsis'    => 'Return a Example Plugin Config Option List',
            'action'      => 'plg_example.configs',
            'http_method' => 'get',
            'params'      => array()
            ),
          'plgExampleGetConfig' => array(
            'synopsis'    => 'Return a Example Plugin Config Option',
            'action'      => 'plg_example.config.{option}',
            'http_method' => 'get',
            'params'      => array(
              'option'         => array('type' => 'text', 'required' => 1),
              'example_string' => array('type' => 'string', 'default' => 'example', 'note' => 'This is a String'),
              'example_int'    => array('type' => "int"),
              'example_enum'   => array('type' => "enum('a','b')")
              )
            ),
          'plgExampleGetUser' => array(
            'synopsis'    => 'Return an Admin user',
            'action'      => 'plg_example.user.{id}',
            'http_method' => 'get',
            'params'      => array(
              'id' => array('type' => 'int')
            )
          ),
          'plgExampleFindActions' => array(
            'synopsis'    => 'Return a Custom Action List',
            'action'      => 'plg_example.actions',
            'http_method' => 'get',
            'params'      => array(
              'limit'         => array('type' => 'int', 'default' => wbTeamPro_Config::_('page_listing_limit')),
              'page'          => array('type' => 'int', 'default' => 1),
              'offset'        => array('type' => 'int', 'default' => 0),
              'order'         => array('type' => 'string', 'default' => 'project.project_name.asc'),
              'search_string' => array('type' => 'string'),
              'project_id'    => array('type' => 'int')
              )
            ),

          ),
        'fields' => array()
        );
    }
    function apiv1_processAction( &$data, &$params ){
      $request =& $data['request'];
      $route = explode('/', $request['action']);
      if (reset($route) == 'plg_example')
      {
        $api =& $data['api'];
        $response =& $data['response'];
        array_shift($route);
        switch (reset($route)) {
          case 'configs':
            $request['code'] = 200;
            $response['config'] = $this->extension->extension_params;
            break;
          case 'config':
            $filter = new wbTeamPro_Params($api->parseMethodParams('plgExampleGetConfig'));
            if (!isset($this->extension->extension_params[ $filter->get('option') ])) {
              $request['code'] = 400;
              $request['message'] = 'Invalid Configuration Value';
            }
            else {
              $request['code'] = 200;
              $response['config'] = array(
                'option'  => $option,
                'value'   => $this->extension->extension_params[ $filter->get('option') ]
                );
            }
            break;
          case 'user':
            // Request from Database
              $filter = new wbTeamPro_Params($api->parseMethodParams('plgExampleGetUser'));
              if ((int)$filter->get('id')) {
                $dbh = wbDatabase::getInstance();
                $record = $dbh->runQuery("
                  SELECT id, firstname
                  FROM `tbladmins`
                  WHERE `id` = '". $dbh->getEscaped($filter->get('id')) ."'
                  ")->getRow();
              }
            // Response
              if (@$record) {
                $data['request']['code'] = 200;
                $data['response']['data']['filters'] = (array)$filter;
                $data['response']['client'] = $record;
              }
              else {
                $data['request']['code'] = 404;
                $data['request']['message'] = 'Not Found';
              }
            break;
          case 'actions':
            // Request from Model
              $model      = wbTeamPro_Model::getInstance('action');
              $filter     = $api->parseMethodParams('plgExampleFindActions');
              $order      = preg_replace('/^(.*)\.(asc|desc)$/','$1 $2',wbTeamPro_Request::get('order','action.action_complete.asc'));
              $page       = wbTeamPro_Request::get('page',1);
              $limitstart = wbTeamPro_Request::get('offset',0);
              $limit      = wbTeamPro_Request::get('limit',wbTeamPro_Config::_('page_listing_limit'));
              $result     = $model->getList(array(
                              'filter'      => (array)$filter,
                              'order'       => $order,
                              'page'        => $page,
                              'limitstart'  => $limitstart,
                              'limit'       => $limit
                            ));
            // Response
              $request['code'] = 200;
              $response['data']['total']   = $result['total'];
              $response['data']['page']    = $result['page'];
              $response['data']['pages']   = $result['pages'];
              $response['data']['limit']   = $result['limit'];
              $response['data']['offset']  = $result['limitstart'];
              $response['data']['filters'] = $result['filters'];
              $response['actions']         = &$result['records'];
            break;
        }
      }
    }

    //---------------------------------------------------------------------------
    // Table Event Functions
    //---------------------------------------------------------------------------
    public function table_onAfterStore_Project( &$data, &$params ){

      // Return TRUE to continue (default)
      return true;

      // You can inspect the class & data
      wbTeamPro_Common::inspect('table_onAfterStore_Project Event', $this, $data, $params); die('ABORT');

      // You can modify the recently stored object
      $openProject =& $data['openProject'];
      if( strpos($openProject->project_name, '*Example Plugin*') === false ){
        // Set the active object for use by following operations
        $openProject->project_name = $openProject->project_name . ' ** Example Plugin';
        // Store your changes (Within the store function you must save directly)
        $openProject->_dbh->runQuery("
          UPDATE `".$openProject->_tbl."`
          SET `project_name` = '". $openProject->_dbh->getEscaped($openProject->project_name) ."'
          WHERE `project_id` = '". $openAction->_dbh->getEscaped($openProject->project_id) ."'
          LIMIT 1
          ");
      }

    }
    public function table_onBeforeDelete_Project( &$data, &$params ){

      // Return TRUE to continue (default)
      return true;

      // You can inspect the class & data
      wbTeamPro_Common::inspect('table_onBeforeDelete_Project Event', $this, $data, $params); die('ABORT');

      // Assign a Message and Return FALSE to abort
      $data['openProject']->setError( wbTeamPro_Lang::_('plugin_example.deleteCancel','Delete Cancelled by Example Plugin') );
      return false;

    }
    public function table_onAfterCopy_Project( &$data, &$params ){}
    public function table_onAfterStore_Action( &$data, &$params ){}
    public function table_onBeforeDelete_Action( &$data, &$params ){}
    public function table_onAfterStore_Topic( &$data, &$params ){}
    public function table_onBeforeDelete_Topic( &$data, &$params ){}
    public function table_onAfterStore_File( &$data, &$params ){}
    public function table_onBeforeDelete_File( &$data, &$params ){}
    public function table_onAfterStore_TicketXref( &$data, &$params ){}
    public function table_onBeforeDelete_TicketXref ( &$data, &$params ){}
    public function table_onAfterStore_Timelog( &$data, &$params ){}
    public function table_onBeforeDelete_Timelog( &$data, &$params ){}

    //---------------------------------------------------------------------------
    // View Event Functions
    // view_onBeforeRender_Path_File
    //---------------------------------------------------------------------------
    public function view_onBeforeRender_Blocks_Menu( &$data, &$params ){

      // Get Menu
        $menu = wbTeamPro_Menu::getMenu('mainmenu');

      // Primary Menu
        $custom = $menu->addChild('menu.custom_plugin', array(
          'label' => wbTeamPro_Lang::_('menu.custom_plugin', 'Custom Plugin'),
          'uri'   => 'wbteampro.php?call=plugin&target=example'
          )
        );
        $custom->addChild('menu.custom_plugin_link', array(
          'label' => wbTeamPro_Lang::_('menu.custom_plugin_link', 'Custom Plugin Link'),
          'uri'   => 'wbteampro.php?call=plugin&target=example'
          ));

    }
    public function view_onBeforeRender_Blocks_Sidebar( &$data, &$params ){

      // Get Menu
        $sidebar = wbTeamPro_Menu::getMenu('sidebar');

      // Primary Menu
        $custom = $sidebar->addChild(
          'menu.custom_plugin',
          array(
            'label'      => '<i class="fa fa-cogs"></i> ' . wbTeamPro_Lang::_('menu.custom_plugin', 'Custom Plugin'),
            'extras'     => array('safe_label' => true),
            'attributes' => array('class' => 'nav-setupmanager'),
          )
        );
        $custom->addChild('menu.custom_plugin_link', array(
          'label' => wbTeamPro_Lang::_('menu.custom_plugin_link', 'Custom Plugin Link'),
          'uri'   => 'wbteampro.php?call=plugin&target=example'
          ));

    }

    //---------------------------------------------------------------------------
    // Client Event Functions
    //---------------------------------------------------------------------------
    public function client_onInitialize( &$data, &$params ){}
    public function client_onCallPlugin( &$data, &$params ){

      // Examples of Direct Call
      // wbteampro.php?call=plugin&target=example&view=html
      // wbteampro.php?call=plugin&target=example&view=raw

      echo "Process & Produce Client Output...";

    }
    public function client_onPrepareView( &$data, &$params ){}
    public function client_onBeforeRender_ProjectEditTabs( &$data, &$params ){

      // Example of Tab Injection
      /*
      $data['pageTabs']['unique_taskid'] =
        array(
          'task'  => 'blank',
          'link'  => 'wbteampro.php?task=blank',
          'label' => wbTeamPro_Lang::_('tabs.label','Example Tab'),
        );
      */

    }
    public function client_onBeforeLoadTask( &$data, &$params ){}
    public function client_onAfterLoadTask( &$data, &$params ){}
    public function client_onFinalizeView( &$data, &$params ){}
    public function client_onBeforeHeaderOutput( &$data, &$params ){}
    public function client_onHeaderOutput( &$data, &$params ){}
    public function client_onAfterStore_Action( &$data, &$params ){}
    public function client_onAfterStore_File( &$data, &$params ){}
    public function client_onAfterStore_Topic( &$data, &$params ){}

    //---------------------------------------------------------------------------
    // Admin Event Functions
    //---------------------------------------------------------------------------
    public function admin_onInitialize( &$data, &$params ){}
    public function admin_onCallPlugin( &$data, &$params ){

      // Examples of Direct Call
      // wbteampro.php?call=plugin&target=example&view=html
      // wbteampro.php?call=plugin&target=example&view=raw

      echo "Process & Produce Admin Output...";

    }
    public function admin_onPrepareView( &$data, &$params ){}
    public function admin_onBeforeLoadTask( &$data, &$params ){}
    public function admin_onAfterLoadTask( &$data, &$params ){}
    public function admin_onFinalizeView( &$data, &$params ){}
    public function admin_onBeforeHeaderOutput( &$data, &$params ){}
    public function admin_onHeaderOutput( &$data, &$params ){}
    public function admin_onBeforeRender_ProjectEditTabs( &$data, &$params ){

      // Example of Tab Injection
      /*
      $data['pageTabs']['unique_taskid'] =
        array(
          'task'  => 'blank',
          'link'  => 'wbteampro.php?task=blank',
          'label' => wbTeamPro_Lang::_('tabs.label','Example Tab'),
        );
      */

    }
    public function admin_onDuringRender_ProjectEditForm( &$data, &$params ){}
    public function admin_onBeforeRender_ActionEditTabs( &$data, &$params ){

      // Example of Tab Injection
      /*
      $data['subTabs']['unique_tabkey'] = array(
        'label'   => 'Tab Label',     // tab label
        'title'   => 'Tab Title',     // tab link title
        'css'     => '',              // tab css rules
        'onclick' => '',              // tab onclick js code
        'content' => '',              // tab content
        'src'     => '',              // url to call for tab content

        // Future Consideration - not in use
        'event' => array(
          'click' => null             // tab onclick js code
          )
        'content' => array(           // use one of the three options
          'include' => null,          // path to include for tab innerHTML
          'html'    => null,          // html content for tab innerHTML
          'raw'     => null,          // raw code to replace tab wrapper
          'src'     => null           // url to load on tab click
          )

        );
      */

    }
    public function admin_onDuringRender_TimelogEditForm( &$data, &$params ){}
    public function admin_onAfterStore_Project( &$data, &$params ){}
    public function admin_onAfterStore_Action( &$data, &$params ){}
    public function admin_onAfterStore_File( &$data, &$params ){}
    public function admin_onAfterStore_Topic( &$data, &$params ){}
    public function admin_onRenderTicketProjectTabForm( &$data, &$params ){}
    public function admin_onAfterStore_TicketXref( &$data, &$params ){}

    //---------------------------------------------------------------------------
    // Plugin Management Functions
    //---------------------------------------------------------------------------
    public function admin_onDuringRender_PluginEditForm( &$data, &$params ){

      // Localize the Extension Data
      $ext_data = $data['extension']['extension_data'];

      // Render a form on the Plugin Edit Form
      ?>
      <h2><?php echo wbTeamPro_Lang::_('plugin_example.dataStorage.header') ?></h2>
      <table class="form" width="100%" border="0" cellspacing="2" cellpadding="3">
        <tr>
          <td class=fieldlabel width="15%"><?php echo wbTeamPro_Lang::_('plugin_example.dataStorage.example1-def') ?>:</td>
          <td class=fieldarea><input type="text" name="extension_data[example1]" value="<?php echo htmlspecialchars($ext_data['example1']) ?>" style="width:99%;" ></td>
          <td class="fieldlabel desc"><?php echo wbTeamPro_Lang::_('plugin_example.dataStorage.example1-desc') ?></td>
        </tr>
        <tr>
          <td class=fieldlabel width="15%"><?php echo wbTeamPro_Lang::_('plugin_example.dataStorage.example2-def') ?></td>
          <td class=fieldarea width="35%"><input type="text" name="extension_data[example2]" value="<?php echo htmlspecialchars($ext_data['example2']) ?>" style="width:99%;" ></td>
          <td class="fieldlabel desc"><?php echo wbTeamPro_Lang::_('plugin_example.dataStorage.example1-desc') ?></td>
        </tr>
      </table>
      <?php

    }
    public function admin_onAfterSave_PluginEditForm( &$data, &$params ){

      // Overwrite the Extension Data
      $this->extension->extension_data->example1 = 'Test Value Overwrite';

      // Store the extension data changes
      $this->extension->store();

    }

    //---------------------------------------------------------------------------
    // Plugin Management Functions
    //---------------------------------------------------------------------------
    public function plugin_onBeforeUninstall( &$data, &$params ){

      // Allow Uninstall
      return true;

      // Example of how to prevent uninstall
      $data['openExtension']->setError('Cannot Uninstall Example Plugin');
      return false;

    }

  }

Plugin Language PHP

The plugin package optionally allows for language translation files to be included. Language files should each be named after the language options available in WHMCS.


<?php

/**

  Webuddha wbTeamPro
  (c)2010 Webuddha.com, The Holodyn Corporation - All Rights Reserved

  CHANGELOG ************

**/

defined('WHMCS_CLIENT') or defined('WHMCS_ADMIN') or die('Invalid Access');

/************** SHARED LANGUAGE */
$_PLUGINLANG["wbteampro"]["plugin_example"]["xml_name"] = 'Example Plugin';
$_PLUGINLANG["wbteampro"]["plugin_example"]["xml_description"] = 'This example plugin can be used to learn about wbTeamPro development';
$_PLUGINLANG["wbteampro"]["plugin_example"]["widget_title"] = 'Example Widget';

/************** ADMIN LANGUAGE */
if( defined('WHMCS_ADMIN') ){

  $_ADMINLANG["wbteampro"]["plugin_example"]["params"]["example_text-def"] = 'Example Input';
  $_ADMINLANG["wbteampro"]["plugin_example"]["params"]["example_text-desc"] = 'TEXT Input Field';
  $_ADMINLANG["wbteampro"]["plugin_example"]["params"]["example_selectlist-def"] = 'Select Input';
  $_ADMINLANG["wbteampro"]["plugin_example"]["params"]["example_selectlist-desc"] = 'SELECT Options Field';
  $_ADMINLANG["wbteampro"]["plugin_example"]["params"]["example_radiolist-def"] = 'Radio Input';
  $_ADMINLANG["wbteampro"]["plugin_example"]["params"]["example_radiolist-desc"] = 'RADIO Input Single Select List';
  $_ADMINLANG["wbteampro"]["plugin_example"]["params"]["example_checklist-def"] = 'Checkbox Input';
  $_ADMINLANG["wbteampro"]["plugin_example"]["params"]["example_checklist-desc"] = 'CHECKBOX Input Multiple Select List';
  $_ADMINLANG["wbteampro"]["plugin_example"]["params"]["example_textarea-def"] = 'Textarea Input';
  $_ADMINLANG["wbteampro"]["plugin_example"]["params"]["example_textarea-desc"] = 'Textarea Block Field';
  $_ADMINLANG["wbteampro"]["plugin_example"]["params"]["opt_yes"] = 'Yes';
  $_ADMINLANG["wbteampro"]["plugin_example"]["params"]["opt_no"] = 'No';

  $_ADMINLANG["wbteampro"]["plugin_example"]["welcome"] = 'This is a Language Example';
  $_ADMINLANG["wbteampro"]["plugin_example"]["deleteCancel"] = 'Delete Cancelled by Example Plugin';

  $_ADMINLANG["wbteampro"]["plugin_example"]["dataStorage"]["header"] = 'Custom Plugin Form / Extension Data Storage';
  $_ADMINLANG["wbteampro"]["plugin_example"]["dataStorage"]["example1-def"] = 'Date Example 1';
  $_ADMINLANG["wbteampro"]["plugin_example"]["dataStorage"]["example1-desc"] = 'Custom Description Example';
  $_ADMINLANG["wbteampro"]["plugin_example"]["dataStorage"]["example2-def"] = 'Date Example 2';
  $_ADMINLANG["wbteampro"]["plugin_example"]["dataStorage"]["example2-desc"] = 'Custom Description Example';

}

/************** CLIENT LANGUAGE */
else {

  $_LANG["wbteampro"]["plugin_example"]["welcome"] = "This is a Client Language";

}

Plugin Event Reference

The following is a complete list of the plugin event triggers currently available.

  system_onRunCron - During cron after validating session
  system_onAfterUpgrade - After performing a wbTeamPro upgrade

  apiv1_getResourceList - Inject API resources

  table_onAfterCopy_Project -> After performing copy function
  table_onAfterStore_Action -> After storing database record
  table_onAfterStore_File -> After storing database record
  table_onAfterStore_Project -> After storing database record
  table_onAfterStore_TicketXref -> After storing database record
  table_onAfterStore_Timelog -> After storing database record
  table_onAfterStore_Topic -> After storing database record
  table_onBeforeDelete_Action -> Before deleting database record
  table_onBeforeDelete_File -> Before deleting database record
  table_onBeforeDelete_Project -> Before deleting database record
  table_onBeforeDelete_TicketXref -> Before deleting database record
  table_onBeforeDelete_Timelog -> Before deleting database record
  table_onBeforeDelete_Topic -> Before deleting database record

  plugin_onAfterInstall -> After plugin installed
  plugin_onBeforeUninstall -> Before plugin un-installed
  plugin_onAfterEnable -> After plugin enabled
  plugin_onBeforeDisable -> Before plugin disabled

  admin_onCallPlugin - Injection point for custom plugin events
  admin_onAfterLoadTask - During controller after loading task
  admin_onBeforeLoadTask - During controller before loading task
  admin_onFinalizeView - During controller after collecting output
  admin_onHeaderOutput - Executes after output of html header
  admin_onInitialize - During controller startup after startup
  admin_onPrepareView - During controller before preparing ouput
  admin_onBeforeHeaderOutput - Before rendering header ouput

  admin_onDuringRender_PluginEditForm -> During plugin detail form display
  admin_onAfterSave_PluginEditForm -> After saving plugin detail form

  admin_onBeforeRender_ActionEditTabs - Before action edit tabs display
  admin_onBeforeRender_ProjectEditTabs - Before project edit tabs display
  admin_onDuringRender_ProjectEditForm - During project detail form display
  admin_onDuringRender_TicketProjectTabForm - During project tab form on Support Ticket display
  admin_onDuringRender_TimelogEditForm - During timelog edit form display

  admin_onAfterStore_Action - After saving before rendering
  admin_onAfterStore_File - After saving before rendering
  admin_onAfterStore_Project - After saving before rendering
  admin_onAfterStore_TicketXref - After saving before rendering
  admin_onAfterStore_Topic - After saving before rendering
  admin_onAfterStore_Timelog - After saving before rendering

  client_onAfterLoadTask - During controller after loading task
  client_onBeforeLoadTask - During controller before loading task
  client_onFinalizeView - During controller after collecting output
  client_onHeaderOutput - Executes after output of html header
  client_onInitialize - During controller startup after startup
  client_onPrepareView - During controller before preparing ouput
  client_onBeforeRender_ProjectEditTabs - During project edit tabs display (added v3.0.16)
  client_onAfterStore_Action - After saving before rendering
  client_onAfterStore_File - After saving before rendering
  client_onAfterStore_Topic - After saving before rendering
  client_onBeforeHeaderOutput - Before rendering header ouput
  client_onCallPlugin - Injection point for custom plugin events