How to get plugged in

This is a small HowTo about writing your own FusionDirectory plugin. The plugin will be added to the 'Addons' sections of the FusionDirectory menu and will present some dummy content. Later we will see how to manipulate ldap attributes using the plugin class, creating input checks, using templates and how to specify a set of permissions.

The Dummy Plugin

Create plugin's directory

The first step is to create a new directory within the './plugins/addons/', we use 'dummyplug'.

# cd plugins/addons
# mkdir dummyplug && cd dummyplug

The main.inc

FusionDirectory expects a file named “main.inc” inside of this plugin directory. This file handles the plugin's lifecycle, it knowns when to reset the plugin, restore it from the session and may initialize acl related variables.

Tip : Avoid sending output directly

All page output must be placed inside the $display variable. Please do not output anything from within your plugin directly, this will may break the session handling due to the gact that the header was already was already sent. FusionDirectory will take the display variable after things got processed and assemble the final page after that.

<?php

/* Remove locks created by this plugin.
 *
 */
if ($remove_lock){
  if(session::is_set('dummyplug')){
    // Nothing to unlock here
  }
}


/* Cleanup is 'true' if the user selects another plugin from the FusionDirectory menu.
 * This plugin will then no longer be required an can be removed from the session.
 */
if ( $cleanup ){
  session::un_set('dummyplug');
}else{


    /* All plugin objects get stored inside the PHP session. Here we create the object if it doesn't
     *  exist yet. It is recreated if "reset" is set.
     */
    if (!session::is_set('dummyplug')){

        /* The second parameter is null, due to the fact that we do not want to edit an ldap object right now. 
         * If you are going to write a plugin for the 'MyAccount' section of FusionDirectory then you should pass '$ui->dn' 
         *  as parameter, $ui->dn is the dn of the currently logged in user. 
         * Your plugin can then use this dn to initialize your plugin with the objects attributes.
         */ 
        session::set('dummyplug',new dummyplug($config, null));
    }
    $dummyplug = session::get('dummyplug');


    /* The function 'save_object' is used to act on HTML _POST and _GET variables.
     * E.g. The plugin may provide an input field.
     */
    $display= $dummyplug->save_object();


    /* Execute plugin. This calls your plugin class and appends its output to the display variable.
     */
    $display= $dummyplug->execute ();


    /* FusionDirectory avoids switching plugins until the user has saved it's modifications.
     * We ignore this check for the current plugin.
     */
    $display.= "<input type=\"hidden\" name=\"ignore\">\n";


    /* print_header generates the page header including the headline and the icon, get template
     * path looks for eventually overloaded files. You don't have to take care of this - it's only important
     * for theming artists. Copy the icon (as .png) to the html/images directory, if you don't care about
     * the image, just copy the personal.png to dummy.png.


     */
    $display= print_header(get_template_path('plugins/log/images/plugin.png'), _("System log view")).$display;


    /* After executing the plugin, some data may have changed, so we need to put the plugin back
     *  to the session.
     */
    session::set('dummyplug',$dummyplug);
}

?>

The plugin class

The basic starter for your plugin is ready now. We're missing the plugin itself currently. To create it, you need to create a file starting with class_ and ending with .inc. These files get sourced automatically.

Here's the class_dummy.inc:

<?php

/* Create a class (name must be unique inside FusionDirectory) which extends plugin. The plugin base
 *  class contains all methods that are used by FusionDirectory and it provides the mechanism to load data
 *  from LDAP to local variables and prepare the save to ldap routine for you. 
 */
class dummyplug extends plugin
{

  /* These contain attributes to be loaded. We're not creating an LDAP plugin currently, so we don't
   *  care... 
   */
  var $attributes= array();
  var $objectclasses= array();

  var $plHeadline = "Dummy plugin";
  var $plDescription = "Dummy plugin";

  /* The constructor just saves a copy of the config. You may add what ever you need. 
   */
  public function __construct ($config, $dn= NULL)
  {
        /* Include config object 
         */
        $this->config= $config;
  }


  /* Execute is the function all plugins need. It fills the plugin with life and produces the output. 
   */
  function execute()
  {
        /* Initialize the smarty engine.
         */
        $smarty= get_smarty();

        /* Let smarty fetch and process the page. Always seperate PHP and HTML as much as
         * you can. 
         * If you've problems telling smarty the correct location of your template file
         *  try to use it the this way 'get_template_path('contents.tpl', TRUE, dirname(__FILE__))'
         *  the template will then be found in the current folder too.
         */
        return ($smarty->fetch (get_template_path('contents.tpl', TRUE)));
  }

 
}
?>

A smarty template

There are two things missing now. The template file and an entry in your fusiondirectory.conf. Lets finish the work in the plugin directory, first.

Here is the contents.tpl file:

<h2>{t}Hello World{/t}</h2>
<p>
  {t}This is a dummy plugin. It really does nothing. The tags around this text do automatic translations to the desired         language. Please don't include linefeeds here, or it will not work at all!{/t}
</p>

<p class="plugbottom">
  &nbsp;
</p>

The fusiondirectory.conf

Now add the following entry to your Addons section in fusiondirectory.conf:

                        <plugin acl="dummyplug" class="dummyplug" />

(More about the acl tag in the Adding ACLs part.)

After logging into FusionDirectory, you'll see your plugin in the addons section. If the plugin is not visible, then verify that you are logged in with an administrative account (admin).

Download the example addon

Download and extract the example in the 'plugins' directory in the FusionDirectory base path, this should then look like this: '/usr/share/fusiondirectory/plugins/dummyplug/main.inc …'

Then run ./update-fusiondirectory it is also located in the FusionDirectory base directory. This script will recreate the list of all known classes, thus allow FusionDirectory to load the dummy plugin.

Do not forget to modify your fusiondirectory.conf!

Download: See Attachment dummyplug_1

Further plugin extensions

Ldap modifications

We've a working plugin now, but it just displays a simple message, what about ldap modifications?.

FusionDirectory uses the 'class_ldap.inc' class to perform actions on the ldap database, this class can easily be instanciated by using the config object, … yes it is the $config object given in the constructor of our dummy plugin.

A simple call of $this→config→get_ldap_link() and the config class does all steps required to manage ldap connections, from loggin in to the ldap database to multiplexing of several connection instances.

Here is a small example, listing all users of your ldap database:

    /* Get an ldap handle and set the initial context to the ldap base. 
     */
    $ldap = $this->config->get_ldap_link();
    $ldap->cd($this->config->current['BASE']);

    /* Search for all objects, recursive, that match the given ldap filter.
     */
    $ldap->search("(&(objectClass=gosaAccount)(objectClass=person))",array("uid","cn"));

    /* Print out each returned object.
     * print_a is a debugging function and is not for productive use!
     */
    while($attrs = $ldap->fetch()){
      print_a($attrs);
    }

Here is a small example, adding a new dummy user:

  /* Get an ldap handle and set the initial context to the ldap base.
    */
   $ldap = $this->config->get_ldap_link();
   $ldap->cd($this->config->current['BASE']);

   /* the dummy user object
    */
   $user = array();
   $user['objectClass'] = array("top","person","organizationalPerson","inetOrgPerson","gosaAccount");
   $user['sn'] = "Wusel";
   $user['givenName'] = "Herbert";
   $user['cn']   = "Herbert Wusel";
   $user['uid']  = "herb123";
   $user_dn = "cn=".$user['cn'].",".get_people_ou().$this->config->current['BASE'];

   /* Ensure that the organizational structure is created if needed.
    * In this case we want to write a new user to 'ou=people,...',
    *  create_missing_trees now ensures that the organizationalUnit 'ou=people' is
    *  available.
    */
   $ldap->create_missing_trees(preg_replace("/^[^,]+,/","",$user_dn));

   /* Create the new user
    */
   $ldap->cd($user_dn);
   $ldap->add($user);

   echo $ldap->get_error();

The class ldap provides operations like: read, add, modify, delete, delete recursive, rename, match, exists and some more, just have a look at the file 'include/class_ldap.inc' in the FusionDirectory source.

Download the example addon 2

Download and extract the example in the 'plugins' directory in the fusiondirectory base path, this should then look like this: '/usr/share/fusiondirectory/plugins/dummyplug/main.inc …'

Then run ./update-fusiondirectory it is also located in the fusiondirectory base directory. This script will recreate the list of all known classes, thus allow FusionDirectory to load the dummy plugin.

Do not forget to modify your fusiondirectory.conf!

Download: See Attachment dummyplug_2

Ldap and the plugin class

The plugin class allows to modify, add and remove ordinary ldap attributes out of the box. This allows the user to write plugins without having a single line of ldap actions coded, except the saving back to the ldap database required 3-4 lines of code.

So what modifications are required to let the dummy plugin write the users 'description' and mail 'attribute'?

Main.inc modifications

The first step is to initialize the plugin class with the dn of the user object we want to modify. You can specify the dn of every object you want, the plugin class will then internally open the object and read the required attributes, checks whether the object exists or not and check if the given objectclasses are part object, but more about this later. We simply use the dn of the currently logged in user.

Edit the 'main.inc' and modify the plugin initialization like this:

  ...

  /* All plugin objects get stored inside the PHP session. Here we create the object if it doesn't
   *  exist yet. It is recreated if "reset" is set.
   */
  if (!session::is_set('dummyplug') || isset($_GET['reset'])){
    session::set('dummyplug',new dummyplug($config, $ui->dn));
  }
  $dummyplug = session::get('dummyplug');


  /* The function 'save_object' is used to act on HTML _POST and _GET variables.
   * E.g. The plugin may provide an input field.
   */
  $display= $dummyplug->save_object();


  /* Save plugin was requested, check possible input validation problems
   */
  if(isset($_POST['save_dummyplug'])){
    $msgs = $dummyplug->check();
    if(!count($msgs)){
      $dummyplug->save();
    }else{
      msg_dialog::displayChecks($msgs);
    }
  }

  ...

The complete plugin can be downloaded, see Attachment section → dummyplug_3

Class and constructor modifications

Now we have to tell the plugin class in which attributes we are interested by specifying an array of attributes. We will also specify some default values for 'mail' and 'description'.

  var $attributes = array('''"description","mail"''');
  var $mail ="";
  var $description = "";

And the plugin constructor:

  /* The constructor just saves a copy of the config. You may add what ever you need.
   */
  public function __construct ($config, $dn)
  {
    /* Include config object
     */
    $this->config= $config;
    plugin::plugin($config,$dn);
  }

The plugin class will now automatically read the mail and the description value and will put them in the same-named class variables.

Execute modifications

Now modify the execute() function in class_dummyplug.inc to present the values in an HTML input field using the smarty template engine.

  /* Execute is the function all plugins need. It fills the plugin with life and produces the output.
   */
  function execute()
  {

    /* Initialize the smarty engine.
     */
    $smarty= get_smarty();
    foreach($this->attributes as $attr){
      $smarty->assign($attr,$this->$attr);
    }
    return ($smarty->fetch (get_template_path('contents.tpl', TRUE)));
  }

Template modifications

And the contents.tpl template:

 <h2>{t}Hello World{/t}</h2>
 <p>
  {t}Mail{/t}&nbsp;<input type="text" name="mail" value="{$mail}">
 </p>
 <p>
  {t}Description{/t}&nbsp;<input type="text" name="description" value="{$description}">
 </p>
 <input type='submit' name='submit' value='{t}Reload{/t}'>
 <input type='submit' name='save_dummyplug' value='{t}Save{/t}'>

Save the modifications

To save the modifications back to the ldap server we have to add a save() method to the dummyplug class.

 /* Save attribute modifications back to the ldap
   */
  function save()
  {
    /* Call the save method of the mother plugin, to generate a new ldap entry
     */
    plugin::save();

    /* Write modifications bak to the ldap, uncomment these lines to save to the ldap database.
     */
#   $ldap = $this->config->get_ldap_link();
#   $ldap->cd($this->dn);
#   $ldap->modify($this->attrs);
#   if($ldap->success()){
#     // "Ok!"
#   }else{
#     msgPool::ldaperror($ldap->get_error(), $this->dn,LDAP_MOD, get_class());
#   }
  }

Adding checks

Your plugin is working now and can write the user input back to the ldap, but we should add some checks to avoid saving incorrect values.

Checks can be implemented by using the check() method, this method should return an array containing all error messages or just an empty array if everything is fine.

  /* Validate user input
   */
  function check()
  {
    $messages = plugin::check();
    if(!tests::is_email($this->mail)){
      $messages[] = msgPool::invalid($this->mail,"","","user@example.de");
    }
    return($messages);
  }

Download the example addon 3

Download and extract the example in the 'plugins' directory in the fusiondirectory base path, this should then look like this: '/usr/share/fusiondirectory/plugins/dummyplug/main.inc …'

Then run ./update-fusiondirectory it is also located in the fusiondirectory base directory. This script will recreate the list of all known classes, thus allow FusionDirectory to load the dummy plugin.

Do not forget to modify your fusiondirectory.conf!

Download: See Attachment dummyplug_3

Ldap performance tips

Only request the information you really need

Avoid searches like this: $ldap→search(“(objectClass=*)”,array(“*”));

Always specify an objectClass

For example to search for all groups use something like this:

$ldap→search(“(objectClass=posixGroup)”,array(“cn”,“description”)

or

$ldap→search(“(&(objectClass=posixGroup)(cn=a*))”,array(“cn”,“description”)

Do not search for the attribute only'''

E.g. Do not! $ldap→search(“mail=test@…”,array(“cn”));

Always specify a list of attributes that should be requested

Some objects maybe very large, for example a group with hundreds of member objects.

For example, if you want to find all groups containing user 'peter' do the following:

$ldap→search(“(&(objectClass=posixGroup)(memberUid=peter))”,array(“cn”));

Adding ACLs

Now that we have a working plugin which is able to write ldap attributes, we should think about permissions. Permissions are realized by a static function named 'plInfo' which should be part of every plugin that require ACLs.

Open the 'class_dummyplug.inc' again and insert the function as shown below:

static function plInfo()
{

   $ret = array(
        "plShortName" => _("Dummyplug"),
        "plDescription" => _("A simple dummy plugin"),
        "plSelfModify"  => TRUE,
        "plDepends"     => array(),
        "plPriority"    => 1,
        "plSection"     => array("addon"),
        "plCategory"    => array("dummyplug" => array("description" => _("Dummy plug"),
                                                  "objectClass" => "dummyObjectClass")),

        "plProvidedAcls" => array(
            "mail"       =>  _("Mail address"),
            "description"=>  _("Description"))
   );
  return($ret);
}

Attribute description

  • plShortName Is a short name used in lists.
  • plDescription A description that will be shown in detailed views.
  • plSelfModify Enables the 'Grant permission to owner' flag, see 'GOsa ACLs' documentation.
  • plDepends Unused currently.
  • plPriority Used to sort plugin ACLs of a specific category.
  • plSection The section, in our case 'addon'. 'administration' and 'personal' are also available.
  • plCategory The permission category with an identifier and an objectClass.
  • plProvidedAcls The permission itself. Do not use special chars like '_' here.

If everything is done correctly you should now be able to assign ACLs for the dummyplug as decribed here: FusionDirectory Acls.

Test the ACLs

Now create a new user named 'dummyuser' and login into FusionDirectory with this user, the dummyplug should not be visible, in fact there is no plugin visible.

Login as admin user again and select 'department' in the 'admin' section, switch to the 'ACL' tab and click on the 'New ACL' button. Then select 'Complete Subtree' for 'ACL Type' and add the user 'dummyuser'. Select 'dummyplug' from the category list below and add some ACLs, e.g. global read and write, then save the settings. For more details see FusionDirectory Acls.

If you login as 'dummyuser' again, then the 'dummyplug' should be visible, if not then check your fusiondirectory.conf it should look like this:

  <plugin acl="dummyplug" class="dummyplug" />

The attribute acl=“…” specifies when a plugin is visible for a user, in our case the currently logged in user requires permissions for the 'dummyplug'.

Adding ACLs to the template and the plugin class

FusionDirectory can automatically disable or gray-out HTML input fields if the user has insufficient permissions. In order to use this feature, we have to use a special smarty tag, the {render} tag. This example demonstrates the usage of the tag.

  • 1.Modify the contents.tpl as follows:
    <h2>{t}Hello World{/t}</h2>
    <p>
     {t}Mail{/t}&nbsp;
     {render acl=$mailACL}
        <input type="text" name="mail" value="{$mail}">
     {/render}
    </p>
    <p>
     {t}Description{/t}&nbsp;
     {render acl=$descriptionACL}
        <input type="text" name="description" value="{$description}">
     {/render}
    </p>
    <input type='submit' name='submit' value='{t}Reload{/t}'>
    <input type='submit' name='save_dummyplug' value='{t}Save{/t}'>
    
    <p class="plugbottom">
      &nbsp;
    </p>


    The input fields will then automatically be grayed-out or disabled depending on the ACLs.

  • 2.We have to modify the 'main.inc' too, to tell the plugin in which ACL context it was opened:
     ...
      /* All plugin objects get stored inside the PHP session. Here we create the object if it doesn't
       *  exist yet. It is recreated if "reset" is set.
       */
      if (!session::is_set('dummyplug') || isset($_GET['reset'])){
        session::set('dummyplug',new dummyplug($config, $ui->dn));
      }
      $dummyplug = session::get('dummyplug');
      $dummyplug->set_acl_category("dummyplug");
     ...


    And at least we have to assign the smarty variables for $mailACL and $descriptionACL.

  • 3.modify the 'class_plugin.inc' as follows:
      /* Execute is the function all plugins need. It fills the plugin with life and produces the output.
       */
      function execute()
      {
    
        /* Initialize the smarty engine.
          */
        $smarty= get_smarty();
        foreach($this->attributes as $attr){
          $smarty->assign($attr,$this->$attr);
          $smarty->assign($attr."ACL",$this->getacl($attr));
        }
        echo $this->dn;
        return ($smarty->fetch (get_template_path('contents.tpl', TRUE)));
      }


    If you now assign only read permissions to the 'dummyuser' then the fields should automatically be grayed out. Additionally the plugin class will now use this permissions internally, this means posted values for 'description' and 'mail' will only be accepted if the user has write access to the attribute, otherwise the posted value will be ignored.

Manual permission checks

The following methods can be used to request permissions directly:

  • plugin→getacl(String attribute) Get the permission for a specific attribute.
  • plugin→acl_is_readable(String attribute) Returns TRUE if the attribute is readable else FALSE
  • plugin→acl_is_writeable(String attribute) Returns TRUE if the attribute is writeable es FALSE
  • plugin→acl_is_createable() Returns TRUE if we are allowed to create the plugin
  • plugin→acl_is_removeable() Returns TRUE if the plugin is createable.

This is a small example, demonstrating the usage of the above methods, you can paste it into the execute() function of your class_dummyplug.inc' if you want.

    $ui = get_userinfo();
    echo "Global plugin permissions : ";
    echo $ui->get_permissions($this->dn,$this->acl_category.get_class($this),"0")."<br>";

    echo "Plugin overall permissions, global permissions merged with attribute permissions: ";
    echo $ui->get_permissions($this->dn,$this->acl_category.get_class($this),"")."<br>";

    $mailACL = $this->acl_is_readable("mail") ? "'Mail' is readable" : "'Mail' is NOT readable";
    echo $mailACL."<br>";

    $mailACL = $this->acl_is_writeable("mail") ? "'Mail' is writeable" : "'Mail' is NOT writeable";
    echo $mailACL."<br>";

    $descriptionACL = $this->acl_is_readable("description") ? "'description' is readable" : "'description' is NOT readable";
    echo $descriptionACL."<br>";

    $descriptionACL = $this->acl_is_writeable("description") ? "'description' is writeable" : "'description' is NOT writeable";
    echo $descriptionACL."<br>";

    $pluginCreateable = $this->acl_is_createable() ? "This plugin is createable" : "This plugin is NOT createable";
    echo $pluginCreateable."<br>";

    $pluginRemoveable = $this->acl_is_removeable() ? "This plugin is removeable" : "This plugin is NOT removeable";
    echo $pluginRemoveable."<br>";

Download the example addon 4

Download and extract the example in the 'plugins' directory in the fusiondirectory base path, this should then look like this: '/usr/share/fusiondirectory/plugins/dummyplug/main.inc …'

Then run ./update-fusiondirectory it is also located in the fusiondirectory base directory. This script will recreate the list of all known classes, thus allow FusionDirectory to load the dummy plugin.

Do not forget to set permissions for the user you test the plugin with.

Download: See Attachment dummyplug_4

Detailed plugin descriptions

plugin::plInfo - Setting up ACLs, properties, requirements for plugins

This is an article about how to use the plInfo function to populate the plugins properties, permissions and so on. This is dedicated to those who want to write their own plugins or want to edit/extend plugins.

How does it looks like

Here is an example plInfo, as you can see, it simply returns an array containing settings for this plugin. This will then be used by FusionDirectory to set up ACLs, menus, properties, ldap schema requirements and maybe more in the future.

class myClass extends plugin {
    ...
    public static function plInfo()
    {
        return (array(
                    "plShortName"   => _("MyPlug"),
                    "plDescription" => _("A very nice plugin I made"),
                    "plSelfModify"  => TRUE,
                    "plDepends"     => array(),
                    "plPriority"    => 0,
                    "plProperties"  => array(
                          array(
                                "name"          => "dummy",
                                "type"          => "bool",
                                "default"       => "true",
                                "description"   => _("You can toggle this flag."),
                                "check"         => "gosaProperty::isBool",
                                "migrate"       => "",
                                "group"         => "myGroup",
                                "mandatory"     => FALSE)
                          ),

                    "plSection"     => array("administration"),
                    "plCategory"    => array("all" => array("description" => '*&nbsp;'._("All"))),
                    "plProvidedAcls"    => array())
               );
    }
}

Generic settings

plShortName

The plugins name in short, just one or two words. For example 'Posix' or 'Mail'. Do not forget to add the translation marks _().

plDescription

A short but meaningful description for the plugin. For example 'User posix account' or 'Imap mail service for servers'. Do not forget to add the translation marks _()

plSection

The section of the plugin, at the moment there are the following sections available 'addon', 'administration' and 'personal'. It has no functional use at the moment it just some kind of plugin grouping.

You can define your own if you want.

plPriority

A priority which is used when several plugins of the same 'plCategory' gets listed, for example in the ACL editor plugin. This priority does NOT affect the permissions. The priority is mostly used to display the plugins, which are shown in the ACL editor, in the same order in which they are displayed in the plugin-tabs.

Permission settings

plCategory

The statement 'plCategory' specifies a category for the plugin, it is some kind of functional grouping, for example all user related plugins share the category 'users'. A plugin can be part of multiple categories too, for example the 'environment' plugin uses 'users' and 'groups', because it is shown in both group-tabs and user-tabs.

FusionDirectory uses this category in combination with the plugins class name to create unique ACLs, for example 'users/environment' and 'groups/environment'. This allows FusionDirectory to use the same plugin for several objects, without having extra permission definitions.

This attribute has two modes, one to just add the plugin to a category and one to create a new category, which can then be used by other plugins.

  • Using existing categories:
        "plCategories" => array('users','groups'),
  • Defining a new category:
        "plCategories" => array(
            'mycategory' => array(
        		'description =>  _("My category"),
    			"objectClass" => "gosaAccount"),
    		'groups'),

Please have a look at 'FusionDirectory ACLs and its usage' to get an idea about how to use these ACLs.

FusionDirectory ACLs and its usage

fusiondirectory.conf

You can restrict the access to plugins defined in your fusiondirectory.conf, by using the acl attribute of a <plugin> tag.

Here is an example which demonstrates the usage:

    <plugin class='user' acl='users/user,users/something'>
	<plugin class='password' acl='users/posix:self'>

The first <plugin> tag enables the plugin 'user' if we've have at least permissions for 'users/user' or 'users/something', the second tag enforces permissions to the users own entry, which means the currently logged in user requires permissions to his own posix plugin. This is done by adding ':self' to the acl attribute.

For details about how to set up plugins using the self modification have a look at the plugin::plInfo #plSelfModify documentation.

Attachment

en/documentation_dev/writing_plugins.txt · Last modified: 2017/10/31 10:32 (external edit)
CC Attribution-Share Alike 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0