/* 
 * RoLF - The Roxen LDAP File system
 *
 * Copyright (C) 2004 Stefan Pfetzing <dreamind@dreamind.de>
 *
 * Portions of this code are based on the sqlfs.pike module for Roxen.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.  
 *
 */

/* The TODO stuff:
 *
 * clean the code up                    * work in progress
 * check ldap_errors.h                  * linked the file to
                                          /opt/roxen/server-4.0.172/etc/include
 * split it into multiple files         * canceled (for now)
 * tag documentation                    * work in progress
 * rework TagRolfUrl. the code is ugly. * work in progress
 * implement an emit plugin or something
 *   similar for getting a list of url's
 *   from the LDAP tree                 * work in progress
 * implement a real image objectClass   * not started
 *  - implement a gallery for this      * not started
 *
 */

inherit "module";

#include <module.h>
#include <roxen.h>
#include <stat.h>
#include <ldap_errors.h>

// Some local used constants
constant rolf_version_string = "RoLF - Roxen LDAP Filesystem 0.1";

constant objectclasses_query_string = "(|" +
  "(objectClass=webDocument)" +
  "(objectClass=webTemplatedDocument)" +
  "(objectClass=webRewrite))";

constant objectclasses =
  ({ "webDocument", "webTemplatedDocument", "webRewrite" });

// Variables for the Roxen module interface
constant thread_safe=1;
constant module_type = MODULE_LOCATION | MODULE_TAG;
constant module_unique = 0;
LocaleString module_name = "File systems: RoLF - The Roxen LDAP File system";
LocaleString module_doc =
#"<p>This is a file system module, which allows you to access
html/xhtml/css/etc. objects, stored in an LDAP tree.</p>
<p>Whats kinda new, is that it has built in support for some kind of RXML
templates, stored in LDAP.</p>";
// some variables for this Module
private int disabled;
// if we have threads, use a Mutex.  (dunno how to use this yet...)
private Thread.Mutex mutex = Thread.Mutex();
private Protocols.LDAP.client ldap_client;

// some predefined strings from configuration
private string location, ldap_host, ldap_base_dn, ldap_auth_dn,
  ldap_auth_password, mime_type, charset;
private int ldap_use_ssl;

/* The create function. it will be called upon creation of the module object.
   This will happen, before the real LDAP connections take place, just when
   configuring the module related stuff. */
void create ()
{
  defvar ("location", Variable.Location ("/",
    VAR_INITIAL|VAR_NO_DEFAULT, "Mount point",
    "Where the module will be mounted in the site's virtual file system."));

  defvar ("ldap_host", Variable.String ("localhost",
    VAR_INITIAL, "LDAP Hostname",
    "The LDAP Server, which will be used for all LDAP queries."));

  defvar ("ldap_base_dn", Variable.String ("",
    VAR_INITIAL|VAR_NO_DEFAULT, "LDAP base dn",
    "The LDAP base dn, under which the objects will be searched."));

  defvar ("ldap_auth_dn", Variable.String ("",
    VAR_INITIAL|VAR_NO_DEFAULT, "LDAP auth dn",
    "The LDAP dn, which will be used for authentication."));

  defvar ("ldap_auth_password", Variable.String ("",
    VAR_INITIAL|VAR_NO_DEFAULT, "LDAP auth password",
    "The password, which will be used for LDAP authentication."));

  defvar ("ldap_use_ssl", Variable.Flag (0,
    VAR_INITIAL, "Use SSL for LDAP",
    "Wether to use SSL for the LDAP connection or not."));

  defvar ("charset", Variable.String ("iso-8859-1",
    0, "File contents charset",
    "The charset of the contents of this file system. This "
    "variable makes it possible for Roxen to use any object from "
    "the LDAP tree, no matter what charset it is written in. If "
    "necessary, Roxen will convert the file to Unicode before "
    "processing the file."));

defvar ("mime_type", Variable.String ("text/html",
0, "File contents Mime-Type",
"This variable allows you to specify the default mime-type of "
"any objects stored in your LDAP tree, if there is no one "
"explicitly specified."));
}

/* This method gets called, when the module is configured already and should be
 * started.  It will connect to the ldap server and verify if everything is ok.
 */
void start ()
{
  location = query ("location");
  ldap_use_ssl = query ("ldap_use_ssl");
  ldap_host = query ("ldap_host");
  ldap_base_dn = query ("ldap_base_dn");
  ldap_auth_dn = query ("ldap_auth_dn");
  ldap_auth_password = query ("ldap_auth_password");
  mime_type = query ("mime_type");
  charset = query ("charset");
  ldap_client = Protocols.LDAP.client (
    (ldap_use_ssl ? "ldaps://" : "ldap://" ) + ldap_host + "/");
  ldap_client->set_basedn (ldap_base_dn);
  ldap_client->set_scope ("base");

  if (!ldap_client->bind (ldap_auth_dn, ldap_auth_password)) {

    report_error ("Could not bind: " +
      ldap_client->error_string() +
      " - RoLF disabled");

    disabled = 1;
  } else
    disabled = 0;
}

/* Disconnects from the ldap server. */
void stop ()
{
  ldap_client->unbind();
}

/* This function gets called to compute the name for the site menu in the roxen
 * webinterface.  To make it better readeable, it prints out the location of
 * this module (where its mounted.)
 */
string query_name ()
{
  return (location + " from LDAP");
}

Stat stat_file( string path, RequestID id )
{
  string base_dn = path_to_dn(path);
  mapping(string:array(string))|int results_map =
    do_fetch_ldap_search(objectclasses_query_string, path, base_dn,
      "base", ({ "cn" }));

  if (intp(results_map))
    return 0;
  else
    return ({ 0755, 1, 0, 0, 0, 0, 0 });
}

mapping|Stdio.File|int(-1..0) find_file (string path, RequestID id)
{
  if (!ldap_client)
    disabled = 1;

  /* when the module is disabled, try to re-enabled it, and if its still
     disabled return nothing. */
  if (disabled) {
    report_notice ("Trying to reconnect...");
    Thread.MutexKey mtk = mutex->lock(1);
    start ();
    if (disabled)
      return -1;
  }

  // do not cache anything!
  NOCACHE();

  // deny cn's with ()
  if ((search(path,")") >= 0) || (search(path, "(") >= 0)) {
    return -1;
  // The normal request
  } else {
    // compute the dn to be used.
    string base_dn = path_to_dn(path);
    // the fields, returned by a normal search
    array(string) search_fields = ({
      "objectClass",
      "cn",
      "webTitle",
      "description",
      "documentBody",
      "o",
      "ou",
      "documentAuthor",
      "documentPublisher",
      "documentLocation",
      "documentVersion",
      "documentRefresh",
      "documentExpires",
      "documentMimeType",
      "documentEncoding",
      "htmlDocumentType",
      "css",
      "icon",
      "javaScript",
      "modifyTimestamp",
      "createTimestamp",
      "headerComments",
      "url",
      "templateCN"
      });

    mapping(string:array(string))|int results_map =
      do_fetch_ldap_search(objectclasses_query_string, path, base_dn,
        "base", search_fields);

    if (intp(results_map))
      return results_map;

    mapping(string:string) metadata =
      result_to_metadata(results_map, search_fields);

    switch (metadata["objectClass"]) {
      case "webTemplatedDocument":
        // nu hier das eigentliche template fetchen, und in 'body' einfügen.
        // selbiges für die metadaten.

        results_map = do_fetch_ldap_search("&(cn=" + metadata["templateCN"] +
          ")(objectClass=webTemplate)", path, ldap_base_dn, "sub",
            search_fields);

        if (intp(results_map))
          return results_map;

        mapping(string:string) template_metadata =
          result_to_metadata(results_map, search_fields);

        metadata = join_metadata (metadata, template_metadata);

      case "webDocument":

        // set the rolf path inside the RequestID.
        id->misc->rolf = (["path":path]);
        // add all the other metadata...
        foreach (indices(metadata), string index) {
          id->misc->rolf += ([lower_case(index):metadata[index]]);
        }

        if (!metadata["htmlDocumentType"])
          switch (metadata["documentMimeType"]) {
            case "text/html":
              metadata += (["htmlDocumentType":
                "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 " +
                  "Transitional//EN\">"]);
              break;
            case "text/xhtml":
              metadata += (["htmlDocumentType":
                "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\"\n\"" +
                  "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">"]);
            break;
          }

        string end_token = ">";

        switch (metadata["documentMimeType"]) {
          case "text/xhtml":
            end_token = " />";
          case "text/html":
            // first create all of the header stuff:
            string header = "";

            // set the encoding
            if (id->set_output_charset)
              id->set_output_charset (metadata["documentEncoding"], 2);
            id->misc->input_charset = "UTF8";

            foreach (({"htmlDocumentType", "headerComments", "headSTARTToken",
              "webTitle", "css", "icon", "javaScript", "documentEncoding",
              "documentExpires", "documentRefresh", "documentPublisher",
              "generator", "createTimestamp", "modifyTimestamp", "headENDToken"}),
              string head_element) {

              if (metadata[head_element] ||
                head_element == "generator" ||
                head_element == "headSTARTToken" ||
                head_element == "headENDToken")

                switch (head_element) {

                  case "htmlDocumentType":
                    header += metadata[head_element] + "\n";
                    break;

                  case "documentEncoding":
                    header +=
                      "<meta http-equiv=\"content-type\" content=\"text/html;" +
                        "charset=" + upper_case(metadata[head_element]) + "\"" +
                        end_token + "\n";
                    break;

                  case "webTitle":
                    header += "<title>" + metadata[head_element] + "</title>\n";
                    break;

                  case "css":
                    header += "<link rel=\"stylesheet\" type=\"text/css\" " +
                      "href=\"" + metadata[head_element] + "\"" + end_token + "\n";
                    break;

                  case "icon":
                    header += "<link rel=\"shortcut icon\" " +
                      "href=\"" + metadata[head_element] + "\"" + end_token + "\n" +
                        "<link rel=\"icon\" href=\"" + metadata[head_element] +
                        "\" type=\"image/ico\"" + end_token + "\n";
                    break;

                  case "javaScript":
                    header += "<script src=\"" + metadata[head_element] +
                      "\" type=\"text/javascript\"" + end_token + "\n";
                    break;

                  case "documentExpires":
                    header += "<meta http-equiv=\"expires\" " +
                      "content=\"" + metadata[head_element] + "\"" +
                        end_token + "\n";
                    break;

                  case "documentRefresh":
                    header += "<meta http-equiv=\"refresh\" " +
                      "content=\"" + metadata[head_element] + "\"" +
                        end_token + "\n";
                    break;

                  case "documentPublisher":
                    header += "<meta name=\"author\" " +
                      "content=\"" + metadata[head_element] + "\"" +
                        end_token + "\n";
                    break;

                  case "generator":
                    header += "<meta name=\"generator\" " +
                      "content=\"" + rolf_version_string + "\"" +
                        end_token + "\n";
                    break;

                  case "createTimestamp":
                    if (metadata["modifyTimestamp"])
                      break;

                  case "modifyTimestamp":
                    header += "<meta name=\"date\" " +
                      "content=\"" + timestamp_to_iso (metadata[head_element]) +
                        "\"" + end_token + "\n";
                    break;

                  case "headSTARTToken":
                    if (metadata["documentMimeType"] == "text/xhtml")
                      header += "<html xmlns=\"http://www.w3.org/1999/xhtml\" " +
                        "xml:lang=\"en\" >\n<head>\n";
                    else
                      header += "<html>\n<head>\n";
                    break;

                  case "headENDToken":
                    header += "</head>\n<body>\n";
                    break;

                  default:
                    header += metadata[head_element] + "\n";
                }
            }

            return Roxen.http_rxml_answer ("<rolf>" + header + metadata["body"] +
              "\n</body>\n</html></rolf>", id);

          case "image/jpg":
          case "image/png":
          case "image/gif":
          case "application/octet-stream":
            id->misc->input_charset = "ISO-8859-1";
            id->set_output_charset ("ISO-8859-1", 2);
            return Roxen.http_string_answer (MIME.decode_base64(metadata["body"]),
              metadata["documentMimeType"]);

          default:
            return Roxen.http_string_answer (metadata["body"],
              metadata["documentMimeType"]);
        }

      case "webRewrite":
        return Roxen.http_redirect (metadata["url"], id);
    }
  }

  return 0;
}

string path_to_dn (string path)
{
  // the return value
  string retval = "";
  // tokenize the string
  array(string) path_elements = reverse (path / "/");

  foreach (path_elements, string element) {
    retval = element == "" ? "" : retval + "cn=" + element + ",";
  }

  return retval + ldap_base_dn;
}

string dn_to_path (string dn)
{
  // the return value
  string retval = "";
  // minus 2, because it starts by zero and one more to remove the ","
  int len = strlen (dn) - strlen (ldap_base_dn) - 2;
  dn = dn[0..len];
  array(string) dn_elements = reverse (dn / ",");

  foreach (dn_elements, string element)
    retval += ( retval == "" ? "" : "/" ) + (element - "cn=");

  return location + retval;
}


mapping(string:array(string))|int find_by_cn (string cn, string|void filter)
{
  mapping(string:array(string))|int results_map = do_fetch_ldap_search("(&(" +
    objectclasses_query_string + ")(" +
      (filter ?
        ("&(cn=" + cn + ")(" + filter + ")" ) :
        ("cn=" + cn )
      ) + "))", "cn: " + cn, ldap_base_dn, "sub", ({ "cn", "webTitle" }));

  return results_map;
}

string path_by_cn (string cn)
{
  mapping(string:array(string))|int results_map = find_by_cn(cn);

  if (intp (results_map))
    return 0;

  return dn_to_path (results_map["dn"][0]);
}

string timestamp_to_iso (string timestamp)
{
  // first comest 4 digits of the year
  return sprintf("%s%s-%s-%sT%s:%s:%s+00:00", @(timestamp/2));
}

mapping(string:array(string))|int do_fetch_ldap_search(string search_string,
  string path, string base_dn, string scope, array(string) search_fields)
{
  object(Protocols.LDAP.client.result)|int search_result =
    ldap_search(search_string, path, base_dn, scope, search_fields);
  if (intp (search_result))
    return search_result;
  else
    return search_result->fetch();
}

object(Protocols.LDAP.client.result)|int ldap_search(string search_string,
  string path, string base_dn, string scope, array(string) search_fields)
{
  // the error which will be catched
  array(mixed) error;

  object(Protocols.LDAP.client.result)|int search_result;

  // lock the mutex
  Thread.MutexKey mtk = mutex->lock(1);

  // now comes the first ldap search.
  ldap_client->set_basedn (base_dn);
  ldap_client->set_scope (scope);

  /* the search can throw an error, so catch it here and disable the module
     upon the error */
  error = catch {
    search_result =
    ldap_client->search (search_string, search_fields);
  };

  if (error) {
    report_error (error[0]);
    disabled = 1;
    return -1;
  }

  // search_result is an int, so an error occured.
  if (intp (search_result)) {
    report_error ("Query error [" + search_result + "] [" + path + "] " +
      "[" + base_dn + "]");
    return 0;
  // the Object was not found, return null
  } else if (search_result->error_number() == LDAP_NO_SUCH_OBJECT) {
    return 0;
  // there happened a strange error, log it and return null
  } else if (search_result->error_number()) {
    report_error ("Query error [" + ldap_client->error_string() + "] " +
      "[" + path + "] [" + base_dn + "]");
    return 0;
  // the object was not found, return null
  } else if (search_result->count_entries() <= 0)
    return 0;
  // else, the object was found, return it.
  else
    return search_result;
}

mapping(string:string) result_to_metadata(
  mapping(string:array(string)) results_map, array(string) search_fields)
{

  mapping(string:string) metadata = (["body":""]);

  foreach (search_fields, string search_element) {

    switch (search_element) {

      case "htmlDocumentType":
        if (results_map[search_element])
          metadata += ([search_element:results_map[search_element][0]]);
        break;

      case "documentBody":
        if (results_map[search_element])
          foreach (results_map["documentBody"], string body_element)
            metadata["body"] += body_element;
        break;

      case "javaScript":
      case "css":
      case "icon":
        if (results_map[search_element])
          if (search(results_map[search_element][0],"/") >= 0)
            metadata += ([search_element:results_map[search_element][0]]);
          else
            metadata += ([search_element:
              path_by_cn (results_map[search_element][0])]);
        break;

      case "documentMimeType":
        if (!results_map[search_element])
          metadata += ([search_element:this->mime_type]);
        else
          metadata += ([search_element:results_map["documentMimeType"][0]]);
        break;

      case "documentEncoding":
        if (!results_map[search_element])
          metadata += ([search_element:charset]);
        else
          metadata += ([search_element:results_map["documentEncoding"][0]]);
        break;

      case "objectClass":
        foreach (results_map[search_element], string element)
          foreach (objectclasses, string objclass_element)
            if (element == objclass_element)
              metadata += ([search_element:element]);
        break;

      default:
        if (results_map[search_element]) {
          string tmp;

          foreach (results_map[search_element], string element)
            if (tmp)
              tmp += "\n" + element;
            else
              tmp = element;

          metadata += ([search_element:tmp]);
        }
    }
  }
  return metadata;
}

mapping(string:string) join_metadata (mapping(string:string) metadata,
  mapping(string:string) template_metadata)
{
  foreach (indices(template_metadata), string element) {
    switch (element) {

      case "body":
        metadata[element] = template_metadata[element] + "<template-body>" +
          metadata[element] + "</template-body>";
        break;

      default:
        if (!metadata[element])
          metadata += ([ element:template_metadata[element] ]);
        else
          metadata += ([ "template_" + element:template_metadata[element] ]);
    }
  }
  return metadata;
}

// Define the rolf-link tag class. It must begin with "Tag".
class TagRolfLink {
  inherit RXML.Tag;

  // This constant tells the parser that the tag should be called "rolf-link".
  constant name  = "rolf-link";

  mapping(string:RXML.Type) arg_types = ([ "cn" : RXML.t_text(RXML.PEnt) ]);

  class Frame {
    inherit RXML.Frame;

    array do_enter(RequestID id)
    {
      mapping(string:array(string))|int results_map = find_by_cn(args->cn);

      if (intp(results_map)) {
      RXML.run_error("cn:" + args->cn + " not found!");
      return 0;
      }

      result="<a href=\"" + dn_to_path (results_map["dn"][0]) + "\">" +
      ((content == "") ?
        results_map["cn"][0] :
        content
      ) + "</a>";

      result = Roxen.parse_rxml(result, id);

      return 0;
    }
  }
}

class TagRolfUrl {
  inherit RXML.Tag;

  // This constant tells the parser that the tag should be called "rolf-url".
  constant name  = "rolf-url";

  mapping(string:RXML.Type) arg_types = ([ "search" : RXML.t_text(RXML.PEnt) ]);
  mapping(string:RXML.Type) opt_arg_types = ([
    "delimiter" : RXML.t_text(RXML.PEnt),
    "description" : RXML.t_text(RXML.PEnt),
    "list" : RXML.t_text(RXML.PEnt),
    "sort" : RXML.t_text(RXML.PEnt) ]);

  class Frame {
    inherit RXML.Frame;

    array do_enter(RequestID id)
    {
      if (!id->misc->rolf) {
        RXML.run_error("not running with rolf!");
      } else {
        string delimiter = args->delimiter ? args->delimiter : " - ";

        object(Protocols.LDAP.client.result)|int results =
          ldap_search("(&(objectClass=webLink)(" + args->search + "))",
          "search: " + args->search, ldap_base_dn, "sub",
          ({ "cn", "url", "o", "ou", "documentPublisher", "version",
          "description" }));

        if (intp(results)) {
          RXML.run_error("searchstring:" + args->search + " not found!");
          return 0;
        }

        if(args->list) {
          result = "<ul>\n";

          mapping(string:mapping(string:array(string))) sort_results_map = ([]);

          do {

            mapping(string:array(string)) results_map = results->fetch();

            string sort_by = (
              ( !args->sort || args->sort == "") ?
                "cn" :
                args->sort );

            if (results_map[sort_by]) {
              string key = results_map[sort_by][0];
              sort_results_map += ([key:results_map]);
            }

          } while(results->next());

          array(string) results_keys =
            args->sort ?
              sort(indices(sort_results_map)) :
              indices(sort_results_map);

          foreach (results_keys, string results_key) {

            mapping(string:array(string)) results_map =
              sort_results_map[results_key];
            string description =
            args->description ?
            args->description :
            results_map["description"][0];

            result += "<li><a href=\"" + results_map["url"][0] + "\">" +
              (( content == "" ) ?
                results_map["cn"][0] :
                content ) + "</a>" +
              (( description == "" ) ?
                "" :
                delimiter + description ) + "</li>\n";
          }

          result += "</ul>\n";

        } else {
          mapping(string:array(string)) results_map = results->fetch();
          string description =
            args->description ?
              args->description :
              results_map["description"][0];
          result = "<a href=\"" + results_map["url"][0] + "\">" +
            (( content == "" ) ?
              results_map["cn"][0] :
              content ) + "</a>" +
            (( description == "" ) ?
              "" :
              delimiter + description);
        }
      }

      result = Roxen.parse_rxml(result, id);

      return 0;
    }
  }
}

// Define the rolf-getvars tag class. It must begin with "Tag".
class TagRolf {
  inherit RXML.Tag;

  // This constant tells the parser that the tag should be called "rolf-getvars".
  constant name  = "rolf";

  mapping(string:RXML.Type) opt_arg_types = ([ ]);

  class Frame {
    inherit RXML.Frame;

    string scope_name;
    mapping|object vars;
    mapping oldvar;

    array do_enter(RequestID id)
    {
      scope_name = "rolf";
      vars = ([]);

      if (!id->misc->rolf)
        RXML.run_error("not running with rolf!");
      else
        vars = id->misc->rolf;

      return 0;
      }

    array do_return(RequestID id)
    {
      result=content;
      return 0;
    }
  }
}

// Define the rolf-path tag class. It must begin with "Tag".
class TagRolfPath {
  inherit RXML.Tag;

  // This constant tells the parser that the tag should be called "rolf-getvars".
  constant name  = "rolf-path";

  mapping(string:RXML.Type) opt_arg_types = ([
    "delimiter" : RXML.t_text(RXML.PEnt),
    "path" : RXML.t_text(RXML.PEnt)
    ]);

  class Frame {
    inherit RXML.Frame;

    array do_enter(RequestID id)
    {
      if (!id->misc->rolf && !args->path) {
        RXML.run_error("not running with rolf!");
      } else {
        string delimiter = args->delimiter ? args->delimiter : "/";
        string used_path = args->path ? args->path : id->misc->rolf["path"];
        string file;

        if (args->path) {
          array(string) path_parts = args->path / "/";
          file = path_parts[sizeof(path_parts)-1];
          if (file == "")
          file = path_parts[sizeof(path_parts)-2];
        } else
          file = id->misc->rolf["cn"];

        result = "<a href=\"" + location + "\">home</a>";
        string sub_path;
        foreach (used_path / "/", string path_element) {
          if (path_element != "") {
            sub_path = sub_path ? sub_path + "/" + path_element : path_element;
            if (path_element == file)
              result += delimiter + path_element;
            else
              result += delimiter + "<a href=\"" + location + sub_path + "\">" +
                path_element + "</a>";
          }
        }
      }
      return 0;
    }
  }
}

// Define the rolf-image tag class. It must begin with "Tag".
class TagRolfImage {
  inherit RXML.Tag;

  // This constant tells the parser that the tag should be called "rolf-image".
  constant name  = "rolf-image";

  mapping(string:RXML.Type) arg_types = ([ "cn" : RXML.t_text(RXML.PEnt) ]);

  class Frame {
    inherit RXML.Frame;

    array do_enter(RequestID id)
    {
      mapping(string:array(string))|int results_map =
        find_by_cn (args->cn, "documentMimeType=image/*");

      if (intp(results_map)) {
        RXML.run_error("cn:" + args->cn + " not found!");
        return 0;
      }

      result="<img src=\"" + dn_to_path (results_map["dn"][0]) + "\" " +
        "alt=\"" +
        (( content == "" ) ?
          results_map["webTitle"][0] :
          content ) + "\" />";

      return 0;
    }
  }
}

TAGDOCUMENTATION;
#ifdef manual
constant tagdoc = ([
"rolf":#"<desc type='tag'><p>
  <short>
    This tag is used internlly to generate the rolf scope.
  </short>
  It will simply make everything which is availible under id->misc->rolf
  availible in this new scope.
</p>
<p>
  You can access nearly all ldap attributes through this scope, for example the
  cn: <b>&amp;rolf.cn;</b>.
</p>
</desc>",

"rolf-link":#"<desc type='tag'><p><short>
Allows you to have internal references by common name.</short>
</p></desc>

<attr name='cn' value'string'>
<p>The cn of another webDocument stored in an LDAP tree.</p>
</attr>",

"rolf-url":#"<desc type='tag'><p><short>
Create a link to an url stored in the LDAP tree.</short>
<p></desc>

<attr name='cn' value='string'>
<p>The cn of the webLink object stored in the LDAP tree.</p>
</attr>",

"rolf-path":#"<desc type='tag'><p><short>
Returns the current path with links to every element in the path.</short>
</p></desc>

<attr name='delimiter' value'string'>
<p>The delimiter being used to create this path.</p>
<p>For example:
<ex-box><rolf-link delimiter=\":\" /></ex-box>
  will create the following for /foo/bar:
<ex-box><a href='/'>home</a>:<a href='/foo'>foo</a>:bar</ex-box>
</attr>",

"rolf-image":#"<desc type='tag'><p><short>
Creats a reference to an image object stored inside of RoLF.<short>
</p></desc>

<attr name='cn' value'string'>
<p>The cn of a Base64 encoded image in the LDAP tree.</p>
</attr>"

]);
#endif

// vim:set sts=2 sw=2 tw=82: