+csv dep for PG'OCaml.
[cocanwiki.git] / scripts / lib / cocanwiki_pages.ml
index 24e2291..baba26e 100644 (file)
@@ -1,7 +1,7 @@
 (* COCANWIKI - a wiki written in Objective CAML.
  * Written by Richard W.M. Jones <rich@merjis.com>.
  * Copyright (C) 2004 Merjis Ltd.
- * $Id: cocanwiki_pages.ml,v 1.3 2004/11/22 11:07:32 rich Exp $
+ * $Id: cocanwiki_pages.ml,v 1.12 2006/12/07 15:46:54 rich Exp $
  *
  * 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
@@ -26,251 +26,231 @@ open Cocanwiki_strings
 
 type pt = Page of string | Title of string
 
+type section =
+    string option * string option * string option * string option * string
+    (* (sectionname, divname, divclass, jsgo, content) *)
+
 type model = {
-  id : int;                            (* Original page ID (0 = none). *)
+  id : int32;                          (* Original page ID (0 = none). *)
   pt : pt;                             (* Page of title (only used if id=0) *)
   description : string;                        (* Description. *)
-  redirect : string;                   (* Redirect to ("" = none). *)
-  contents : (string * string * string) list;
-                                       (* (sectionname, divname, content)
-                                        * for each section. *)
+  keywords : string option;            (* Keywords. *)
+  noodp : bool option;                 (* NOODP per-page override. *)
+  redirect : string option;            (* Redirect to. *)
+  (* NB. Don't call this 'contents' because that clashes with the
+   * Pervasives.contents fields of the ref type.
+   *)
+  contents_ : section list;
 }
 
 exception SaveURLError
-exception SaveConflict of int * int * string * string
+exception SaveConflict of int32 * int32 * string * string option
 
 let new_page pt =
   let description =
     match pt with
-       Page page -> page
-      | Title title -> title in
+    | Page page -> page
+    | Title title -> title in
 
-  let model = { id = 0;
+  let model = { id = 0l;
                pt = pt;
                description = description;
-               redirect = "";
-               contents = [] } in
+               keywords = None;
+               noodp = None;
+               redirect = None;
+               contents_ = [] } in
   model
 
 let new_page_with_title title =
   (* Initial page contents. *)
-  let contents = [ "", "", "<b>" ^ title ^ "</b> is " ] in
-  let model = { id = 0;
+  let contents = [ None, None, None, None, "<b>" ^ title ^ "</b> is " ] in
+  let model = { id = 0l;
                pt = Title title;
                description = title;
-               redirect = "";
-               contents = contents } in
+               keywords = None;
+               noodp = None;
+               redirect = None;
+               contents_ = contents } in
   model
 
-let load_page (dbh : Dbi.connection) hostid ~url ?version () =
+let load_page dbh hostid ~url ?version () =
   (* Pull out the page itself from the database. *)
-  let sth =
+  let rows =
     match version with
-       None ->
-         let sth = dbh#prepare_cached "select id, title, description,
-                                               coalesce (redirect, '')
-                                          from pages
-                                         where hostid = ? and url = ?" in
-         sth#execute [`Int hostid; `String url];
-         sth
-      | Some version ->
-         let sth = dbh#prepare_cached "select id, title, description,
-                                               coalesce (redirect, '')
-                                          from pages
-                                         where hostid = ? and id = ? and
-                                               (url = ? or url_deleted = ?)" in
-         sth#execute [`Int hostid; `String url; `String url];
-         sth in
-
-  let pageid, title, description, redirect =
-    match sth#fetch1 () with
-       [`Int pageid; `String title; `String description; `String redirect] ->
-         pageid, title, description, redirect
-      | _ -> assert false in
+    | None ->
+       PGSQL(dbh) "select id, title, description, keywords, noodp, redirect
+                      from pages
+                     where hostid = $hostid and url = $url"
+    | Some version ->
+       PGSQL(dbh) "select id, title, description, keywords, noodp, redirect
+                      from pages
+                     where hostid = $hostid and id = $version and
+                           (url = $url or url_deleted = $url)" in
+
+  let pageid, title, description, keywords, noodp, redirect =
+    match rows with
+    | [row] -> row
+    | _ -> raise Not_found in
 
   (* Get the sections. *)
-  let sth = dbh#prepare_cached "select coalesce (sectionname, ''),
-                                       content,
-                                       coalesce (divname, '')
-                                  from contents
-                                 where pageid = ?
-                                 order by ordering" in
-  sth#execute [`Int pageid];
-
-  let contents =
-    sth#map (function
-              | [`String sectionname; `String content; `String divname] ->
-                  sectionname, divname, content
-              | _ -> assert false) in
+  let contents = PGSQL(dbh)
+    "select sectionname, divname, divclass, jsgo, content
+       from contents
+      where pageid = $pageid
+      order by ordering" in
 
   let model = { id = pageid;
                pt = Page url;
                description = description;
+               keywords = keywords;
+               noodp = noodp;
                redirect = redirect;
-               contents = contents; } in
+               contents_ = contents } in
   model
 
-let save_page (dbh : Dbi.connection) hostid ?user ?r model =
+let save_page r dbh hostid ?user model =
   (* Logging information, if available. *)
   let logged_user =
     match user with
-       None -> `Null
+       None -> None
       | Some user ->
          match user with
-           | User (id, _, _, _) -> `Int id
-           | _ -> `Null in
+           | User (id, _, _, _) -> Some id
+           | _ -> None in
 
   let logged_ip =
-    match r with
-       None -> `Null
-      | Some r ->
-         try `String (Connection.remote_ip (Request.connection r))
-         with Not_found -> `Null in
-
-  (* Get redirect. *)
-  let redirect =
-    if model.redirect = "" then `Null
-    else `String model.redirect in
+    try Some (Connection.remote_ip (Request.connection r))
+    with Not_found -> None in
 
   let url, pageid =
     (* Creating a new page (id = 0)?  If so, we're just going to insert
      * a new row, which is easy.
      *)
-    if model.id = 0 then (
+    if model.id = 0l then (
       (* Create the page title or URL. *)
       let url, title =
        match model.pt with
            Page url -> url, url
          | Title title ->
-             match Wikilib.generate_url_of_title dbh hostid title with
+             match Wikilib.generate_url_of_title dbh hostid title with
                  Wikilib.GenURL_OK url -> url, title
                | _ ->
                    raise SaveURLError in
 
-      let sth = dbh#prepare_cached "insert into pages (hostid, url, title,
-                                      description, logged_ip, logged_user,
-                                      redirect)
-                                      values (?, ?, ?, ?, ?, ?, ?)" in
-      sth#execute [`Int hostid; `String url; `String title;
-                  `String model.description; logged_ip; logged_user;
-                  redirect];
+      let description = model.description in
+      let keywords = model.keywords in
+      let noodp = model.noodp in
+      let redirect = model.redirect in
+      PGSQL(dbh) "insert into pages (hostid, url, title,
+                                     description, keywords, noodp,
+                                     logged_ip, logged_user,
+                                     redirect)
+                  values ($hostid, $url, $title, $description, $?keywords,
+                          $?noodp,
+                          $?logged_ip, $?logged_user, $?redirect)";
 
-      let pageid = sth#serial "pages_id_seq" in
+      let pageid = PGOCaml.serial4 dbh "pages_id_seq" in
 
       (* Create the page contents. *)
-      let sth = dbh#prepare_cached "insert into contents (pageid,
-                                      ordering, sectionname, divname,
-                                      content)
-                                      values (?, ?, ?, ?, ?)" in
       let ordering = ref 0 in  (* Creating new ordering. *)
-      List.iter (fun (sectionname, divname, content) ->
-                  let divname =
-                    if string_is_whitespace divname then `Null
-                    else `String divname in
-                  let sectionname =
-                    if string_is_whitespace sectionname then `Null
-                    else `String sectionname in
-                  incr ordering; let ordering = !ordering in
-                  sth#execute [`Int pageid; `Int ordering;
-                               sectionname; divname;
-                               `String content])
-       model.contents;
+      List.iter (
+       fun (sectionname, divname, divclass, jsgo, content) ->
+         incr ordering; let ordering = Int32.of_int !ordering in
+         PGSQL(dbh)
+           "insert into contents (pageid, ordering, sectionname, divname,
+                                   divclass, jsgo, content)
+             values ($pageid, $ordering,
+                     $?sectionname, $?divname, $?divclass, $?jsgo, $content)"
+      ) model.contents_;
 
       url, pageid
     )
       (* Otherwise it's an old page which we're updating. *)
     else (
       (* Pull out fields from the database. *)
-      let sth = dbh#prepare_cached "select creation_date,
-                                           coalesce (url, url_deleted),
-                                           title, css
-                                      from pages
-                                     where hostid = ? and id = ?" in
-      sth#execute [`Int hostid; `Int model.id];
+      let model_id = model.id in
+      let rows =
+       PGSQL(dbh)
+       "select creation_date, coalesce (url, url_deleted),
+                title, css
+           from pages
+          where hostid = $hostid and id = $model_id" in
 
       let creation_date, url, title, css =
-       match sth#fetch1 () with
-           [ creation_date; `String url; `String title; css ] ->
-             creation_date, url, title, css
-         | _ -> assert false in
+       match rows with
+       | [ row ] -> row
+       | _ -> assert false in
+      let url = Option.get url in
 
       (* Title changed? *)
       let title =
        match model.pt with
-           Title new_title when title <> new_title -> new_title
-         | _ -> title in
+       | Title new_title when title <> new_title -> new_title
+       | _ -> title in
 
       (* Has someone else edited this page in the meantime? *)
-      let sth = dbh#prepare_cached "select max(id) from pages
-                                     where hostid = ? and url = ?" in
-      sth#execute [`Int hostid; `String url];
+      let max_id = Option.get (
+       List.hd (
+         PGSQL(dbh) "select max(id) from pages
+                       where hostid = $hostid and url = $url"
+       )
+      ) in
 
-      let max_id = sth#fetch1int () in
-      let edited = max_id <> model.id in
+      let edited = max_id <> model_id in
 
-      if edited then (
-       let css = match css with
-           `Null -> "" | `String css -> css
-         | _ -> assert false in
-       raise (SaveConflict (max_id, model.id, url, css))
-      );
+      if edited then
+       raise (SaveConflict (max_id, model_id, url, css));
 
       (* Defer the pages_redirect_cn constraint because that would
        * temporarily fail on the next UPDATE.
        *)
-      let sth =
-       dbh#prepare_cached
-         "set constraints pages_redirect_cn, sitemenu_url_cn,
+      PGSQL(dbh)
+       "set constraints
+               pages_redirect_cn, sitemenu_url_cn,
                page_emails_url_cn, links_from_cn, recently_visited_url_cn
-               deferred" in
-      sth#execute [];
-
-      (* Mark the old page as deleted.  NB. There is a small race
-       * condition here because PostgreSQL doesn't do isolation
-       * properly.  If a user tries to visit this page between the
-       * delete and the creation of the new page, then they'll get
-       * a page not found error. (XXX)
+             deferred";
+
+      (* Lock the pages table to avoid bogus 404 errors.  The
+       * lock is released at the end of the current transaction.
        *)
-      let sth = dbh#prepare_cached "update pages set url_deleted = url,
-                                                     url = null
-                                     where hostid = ? and id = ?" in
-      sth#execute [`Int hostid; `Int model.id];
-
-      (* Create the new page. *)
-      let sth = dbh#prepare_cached "insert into pages (hostid, url, title,
-                                    description, creation_date, logged_ip,
-                                    logged_user, redirect, css)
-                                    values (?, ?, ?, ?, ?, ?, ?, ?, ?)" in
-      sth#execute [`Int hostid; `String url; `String title;
-                  `String model.description; creation_date; logged_ip;
-                  logged_user; redirect; css];
+      PGSQL(dbh) "lock table pages";
+
+      (* Mark the old page as deleted. *)
+      PGSQL(dbh) "update pages set url_deleted = url, url = null
+                   where hostid = $hostid and id = $model_id";
+
+      let description = model.description in
+      let keywords = model.keywords in
+      let noodp = model.noodp in
+      let redirect = model.redirect in
+      PGSQL(dbh)
+       "insert into pages (hostid, url, title,
+                            description, keywords, noodp,
+                            creation_date, logged_ip,
+                            logged_user, redirect, css)
+         values ($hostid, $url, $title, $description, $?keywords, $?noodp,
+                 $creation_date,
+                 $?logged_ip, $?logged_user, $?redirect, $?css)";
 
       (* New page ID <> old page ID model.id. *)
-      let pageid = sth#serial "pages_id_seq" in
+      let pageid = PGOCaml.serial4 dbh "pages_id_seq" in
 
       (* Create the page contents. *)
-      let sth = dbh#prepare_cached "insert into contents (pageid,
-                                    ordering, sectionname, divname,
-                                    content)
-                                    values (?, ?, ?, ?, ?)" in
       let ordering = ref 0 in  (* Creating new ordering. *)
-      List.iter (fun (sectionname, divname, content) ->
-                  let divname =
-                    if string_is_whitespace divname then `Null
-                    else `String divname in
-                  let sectionname =
-                    if string_is_whitespace sectionname then `Null
-                    else `String sectionname in
-                  incr ordering; let ordering = !ordering in
-                  sth#execute [`Int pageid; `Int ordering;
-                               sectionname; divname;
-                               `String content])
-       model.contents;
+      List.iter (
+       fun (sectionname, divname, divclass, jsgo, content) ->
+         incr ordering; let ordering = Int32.of_int !ordering in
+         PGSQL(dbh) "insert into contents (pageid,
+                         ordering, sectionname, divname, divclass,
+                         jsgo, content)
+                      values ($pageid, $ordering, $?sectionname,
+                              $?divname, $?divclass, $?jsgo, $content)"
+      ) model.contents_;
 
       url, pageid
     ) in
 
   (* Keep the links table in synch. *)
-  Cocanwiki_links.update_links_for_page dbh hostid url;
+  Cocanwiki_links.update_links_for_page dbh hostid url;
 
   url, pageid