/*
* 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>&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: