Developers

Introduction

This page exists to help developers who wish to find out how to write mod_caml programs, or who want to modify COCANWIKI itself.

You should at least download the source code for COCANWIKI and unpack it. Ideally you should install it and create a working wiki because that will allow you to see the results of your modifications.

Directory structure

Once you have unpacked the source tarball, you will find the following directory structure:

Configuration and database

A very good place to start is conf/cocanwiki.conf which controls the somewhat complex mapping between URLs which the user sees and scripts which actually run.

You should also familiarise yourself with the mod_rewrite documentation.

The basic Wiki page is mapped to the script /_bin/page.cmo?page=xxx where xxx is the name of the page. For the root page (/) the name is always index.

Special pages such as /mypage/edit and /mypage/history get mapped respectively to /_bin/edit.cmo?page=mypage and /_bin/history.cmo?page=mypage.

Scripts under /_bin are compiled from sources in scripts directory. Notice there are files called scripts/page.ml, scripts/edit.ml, scripts/history.ml and so on.

(Don't attempt to understand these large scripts yet - they are amongst the most complicated in the application).

Database

Another good file to read is cocanwiki.sql which is the PostgreSQL database schema. You may prefer to actually load it into a PostgreSQL database and examine it interactively with psql or pgaccess.

The database name is cocanwiki. It must be set up as a UNICODE database, thus implying that all strings which we read and write from the database will be in UTF-8 format.

The central table is hosts. There is one row in this table for every wiki on the server.

The other important tables are pages and contents which are explained in more detail below.

Simple script and template

Let's start by examining a script and its associated template.

The files scripts/00-TEMPLATE.ml and templates/00-TEMPLATE.html contain an example "do-nothing" script.

In fact, when you are going to create a new script and/or template, you begin with a copy of these files.

[Note to 1.0 readers: another simple script which you can examine is scripts/upload_file_form.ml and its associated template templates/upload_file_form.html.]

The script is very simple. The guts of the script are:

open Cocanwiki
open Cocanwiki_template

let run r (q : cgi) (dbh : Dbi.connection) hostid _ _ =
  let template = get_template dbh hostid "00-TEMPLATE.html" in

  q#template template

let () =
  register_script ~restrict:[CanManageUsers] run

Starting at the end, all scripts must provide a run function which is what is called each time the script is invoked by a browser. All scripts must also register the run function by calling register_script.

The code for register_script is in the module Cocanwiki. It's a rather complex function which provides a lot of features automatically, like:

You don't need to worry about it right now.

Inside the run function we call Cocanwiki_template.get_template to load the HTML template from the templates directory. This function is also quite complex, and handles caching templates and prefills a lot of fields in the template.

Then (in this trivial script) we simply print the template out (q#template template) and finish.

A more realistic script would actually do stuff between getting the template and printing it out!

More complex scripts

The concepts below will explain what happens in more complex, realistic scripts. You should probably start to read scripts in order of line count. In the current codebase:

$ wc -l scripts/*.ml | sort -n | less
[...]
   36 scripts/forgot_password_form.ml
   36 scripts/upload_file_form.ml
   36 scripts/upload_image_form.ml
   40 scripts/00-TEMPLATE.ml
   40 scripts/preview.ml
   41 scripts/search.ml
[...]
   47 scripts/delete_file_form.ml
   51 scripts/edit_page_css_form.ml
   52 scripts/hoststyle.ml
   52 scripts/undelete_file_form.ml

are the simpler scripts, and these are the most complex:

  140 scripts/upload_image.ml
  249 scripts/page.ml
  315 scripts/edit_sitemenu.ml
  498 scripts/edit.ml

From now on I'm going to explain individual concepts (eg. "how to check arguments and raise an error", "how to change the database"), rather than going through scripts. You'll find plenty of examples of these concepts by grepping the scripts themselves.

Script naming conventions

There are some informal conventions I use when naming scripts.

Most "actions" come in two parts: in the first part you present a form to the user who fills it in and submits it. The second part is the form submission. For these actions, the pattern is normally:

Some other conventions

Information pages like history.ml and diff.ml have a simple name.

edit_object_form and edit_object to edit an "object". Similarly create_... for creating "objects", and delete_... for deleting "objects".

Libraries (common code)

If you examine the Makefile, you'll see that much common code shared between scripts has been moved into various libraries. These libraries are linked together into cocanwiki.cma which is loaded separately into the webserver (using CamlLoad Apache directive).

This is done to save space and avoid needless duplication of code. In some cases the library is actually necessary, eg. where there is a shared data structure such as a cache.

Generally speaking, as a beginner you should be aware of the libraries, but you do not need to examine them in detail. You should, however, look at their interfaces (the symbols they export).

The data model

Hosts

Each web server may serve several wikis/web sites. The hosts table lists each site, and the global properties for that site.

Pages and contents

The pages table contains a row for every page, and a row for each old version of the page.

Each live page has the URL in the url column. For deleted pages, the url column is null and url_deleted contains the original URL.

This allows us to add a unique index on pages (hostid, url) to ensure URLs are unique. (There is, of course, no uniqueness constraint for url_deleted because there may be several copies of old pages with the same URL).

Each page consists of a list of sections (corresponding to the editable paragraphs that you see in the editor). The sections are stored in the contents table. The contents.ordering field orders the sections for display.

Each section is stored in "wiki markup" format and converted to XHTML on the fly for display.

Images and files

Images and files are stored in the corresponding tables with and we use a name/name_deleted scheme similar to that used for pages.

Parameters passed to 'run'

The parameters passed to run are:

let run r (q : cgi) (dbh : Dbi.connection) hostid host user =
  ...

which are in order:

Database access

We use the Dbi module from ocamldbi to access the database. The database is a PostgreSQL database.

The Cocanwiki.register_script function arranges for persistent database connections, which considerably reduces the time taken to start a script.

Basic queries look something like this:

 let sth =
   dbh#prepare_cached
     "select id, name, email, registration_date, can_edit, can_manage_users
        from users where hostid = ? order by name" in
 sth#execute [`Int hostid];
 
 let table =
   sth#map
     (function
          [`Int userid; `String name; (`Null | `String _) as email;
           `Date registration_date;
           `Bool can_edit; `Bool can_manage_users] ->
            let email = match email with `Null -> "" | `String s -> s in
            [ "userid", Template.VarString (string_of_int userid);
              "name", Template.VarString name;
              "email", Template.VarString email;
              "registration_date",
                Template.VarString (printable_date' registration_date);
              "can_edit", Template.VarConditional can_edit;
              "can_manage_users", Template.VarConditional can_manage_users ]
        | _ -> assert false) in
 
 template#table "users" table;

In this real example we select users from the database. We then map over the result [sth#map] and produce a table which can be inserted directly into the template.

Note: In almost every case, you should be passing the hostid to the database query.

Updates

For INSERT, UPDATE and DELETE statements, you must commit the changes at the end of the script (specifically: after doing all error checking, after doing all updating, but before confirming the page by calling the ok function).

 dbh#commit ();
 
 let buttons = [ ok_button "/_files" ] in
 ok ~title:"File uploaded" ~buttons
   q "File was uploaded successfully."

'error' and 'ok' functions