Updated MANIFEST.
[cocanwiki.git] / scripts / page.ml
index 7f7c0bf..db74684 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: page.ml,v 1.45 2006/03/28 16:24:07 rich Exp $
+ * $Id: page.ml,v 1.59 2006/12/06 09:46:57 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
@@ -33,11 +33,15 @@ open Cocanwiki_ok
 open Cocanwiki_date
 open Cocanwiki_server_settings
 open Cocanwiki_links
+open Cocanwiki_extensions
+open Cocanwiki_strings
 
-type fp_status = FPOK of int32 * string * string * Calendar.t * bool
-              | FPInternalRedirect of string
-              | FPExternalRedirect of string
-              | FPNotFound
+type fp_status =
+  | FPOK of int32 * string * string * string option * Calendar.t * bool
+      * bool option
+  | FPInternalRedirect of string
+  | FPExternalRedirect of string
+  | FPNotFound
 
 (* Referer strings which help us decide if the user came from
  * a search engine and highlight terms in the page appropriately.
@@ -69,21 +73,22 @@ let run r (q : cgi) dbh hostid
    *)
 
   let template_page_header =
-    get_template ~page dbh hostid "page_header.html" in
-  let template_page = get_template ~page dbh hostid "page.html" in
+    get_template ~page dbh hostid "page_header.html" in
+  let template_page = get_template ~page dbh hostid "page.html" in
 
   (* This is the simpler template for 404 pages. *)
-  let template_404  = get_template dbh hostid "page_404.html" in
+  let template_404_header = get_template r dbh hostid "page_404_header.html" in
+  let template_404  = get_template r dbh hostid "page_404.html" in
 
   (* Host-specific fields. *)
-  let rows = PGSQL(dbh)
-    "select css is not null, feedback_email is not null, mailing_list, navigation
-       from hosts where id = $hostid" in
-  let has_host_css, has_feedback_email, mailing_list, navigation =
+  let rows =
+    PGSQL(dbh)
+      "select feedback_email is not null, mailing_list, navigation
+         from hosts where id = $hostid" in
+  let has_feedback_email, mailing_list, navigation =
     match rows with
-      | [Some has_host_css, Some has_feedback_email,
-        mailing_list, navigation] ->
-         has_host_css, has_feedback_email, mailing_list, navigation
+      | [Some has_feedback_email, mailing_list, navigation] ->
+         has_feedback_email, mailing_list, navigation
       | _ -> assert false in
 
   (* User permissions. *)
@@ -175,16 +180,77 @@ let run r (q : cgi) dbh hostid
        ) in
       Some (List.assoc name !extensions)
     with
-      Not_found | ExtList.List.Empty_list -> None
+      Not_found | ExtList.List.Empty_list | Failure "hd" -> None
   in
 
   (* This code generates ordinary pages. *)
-  let make_page title description pageid last_modified_date has_page_css
+  let make_page title description keywords
+      pageid last_modified_date has_page_css noodp
       version page page' extension =
     let t = template_page in
     let th = template_page_header in
-    t#set "title" title;
+    (*t#set "title" title; - nothing uses ::title:: on page.html - removed *)
+
+    (* Page title, h1 and superdirs (if any). *)
     th#set "title" title;
+
+    let superdirs, h1 =
+      match String.nsplit title "/" with
+      | [] -> [], ""
+      | [h1] -> [], h1
+      | xs ->
+         let xs = List.rev xs in
+         let h1 = List.hd xs in
+         let superdirs = List.rev (List.tl xs) in
+
+         (* Check the superdirs are reasonable, then convert them
+          * into paths or redlinks.
+          * If any of this fails, then there are no superdirs.
+          *)
+         try
+           let pathsofar = ref "" in
+           let superdirs =
+             List.mapi (
+               fun i name ->
+                 (* Path will be something like "Dir1/Dir2".  We want
+                  * a URL like "dir1/dir2".
+                  *)
+                 let path =
+                   if i = 0 then name else !pathsofar ^ "/" ^ name in
+                 (* Path so far reasonable? *)
+                 let url, redlink =
+                   match Wikilib.generate_url_of_title r dbh hostid path with
+                   | Wikilib.GenURL_Duplicate url -> url, None
+                   | Wikilib.GenURL_OK url ->
+                       (* Is it an extension page? *)
+                       (match get_extension url with
+                        | Some _ -> url, None (* extension page *)
+                        | None -> url, Some path (* redlink *))
+                   | Wikilib.GenURL_BadURL | Wikilib.GenURL_TooShort ->
+                       raise Exit in
+                 pathsofar := path;
+                 name, url, redlink
+             ) superdirs in
+           superdirs, h1
+         with
+           Exit -> [], title in
+
+    let superdirs = List.map (
+      fun (name, url, redlink) ->
+       let is_redlink, redlink_title =
+         match redlink with
+         | None -> false, ""
+         | Some title -> true, title in
+       [ "url", Template.VarString url;
+         "name", Template.VarString name;
+         "is_redlink", Template.VarConditional is_redlink;
+         "redlink_title", Template.VarString redlink_title ]
+    ) superdirs in
+
+    th#conditional "has_superdirs" (superdirs <> []);
+    th#table "superdirs" superdirs;
+    th#set "h1" h1;
+
     t#set "last_modified_date" last_modified_date;
 
     (match description with
@@ -193,6 +259,12 @@ let run r (q : cgi) dbh hostid
           th#conditional "has_description" true;
           th#set "description" description);
 
+    (match keywords with
+        None -> th#conditional "has_keywords" false
+       | Some keywords ->
+          th#conditional "has_keywords" true;
+          th#set "keywords" keywords);
+
     if page <> page' then (* redirection *) (
       t#set "page" page';
       th#set "page" page';
@@ -204,9 +276,16 @@ let run r (q : cgi) dbh hostid
       t#conditional "redirected" false
     );
 
-    th#conditional "has_host_css" has_host_css;
     th#conditional "has_page_css" has_page_css;
 
+    (* If the per-page noodp is not null, set the noodp flag here.  Otherwise
+     * we will use the default (from hosts.global_noodp) which was set
+     * in Cocanwiki_template.
+     *)
+    (match noodp with
+     | None -> ()
+     | Some b -> th#conditional "noodp" b);
+
     (* Are we showing an old version of the page?  If so, warn. *)
     (match version with
         None ->
@@ -218,6 +297,12 @@ let run r (q : cgi) dbh hostid
           t#set "old_version" (Int32.to_string pageid);
           th#set "old_version" (Int32.to_string pageid));
 
+    (* Just before we show the header, call any registered pre-page
+     * handlers.  They might want to send cookies.
+     *)
+    List.iter (fun handler ->
+                handler r q dbh hostid page') !pre_page_handlers;
+
     (* At this point, we can print out the header and flush it back to
      * the user, allowing the browser to start fetching stylesheets
      * and background images while we compose the page.
@@ -240,15 +325,33 @@ let run r (q : cgi) dbh hostid
          None -> []
        | Some pageid ->
            let rows = PGSQL(dbh)
-             "select ordering, sectionname, content, divname
+             "select ordering, sectionname, content, divname, divclass, jsgo
                  from contents where pageid = $pageid order by ordering" in
 
            List.map
-             (fun (ordering, sectionname, content, divname) ->
+             (fun (ordering, sectionname, content, divname, divclass, jsgo) ->
                 let divname, has_divname =
                   match divname with
                   | None -> "", false
                   | Some divname -> divname, true in
+                let divclass, has_divclass =
+                  match divclass with
+                  | None -> "", false
+                  | Some divclass -> divclass, true in
+                let jsgo, has_jsgo =
+                  match jsgo with
+                  | None -> "", false
+                  | Some jsgo -> jsgo, true in
+
+                let has_divclass, divclass =
+                  if has_jsgo then
+                    (true,
+                     if divclass = "" then "jsgo_div"
+                     else divclass ^ " jsgo_div")
+                  else
+                    has_divclass, divclass in
+                let has_div = has_divname || has_divclass in
+
                 let sectionname, has_sectionname =
                   match sectionname with
                   | None -> "", false
@@ -261,16 +364,21 @@ let run r (q : cgi) dbh hostid
                   "linkname", Template.VarString linkname;
                   "content",
                     Template.VarString
-                      (Wikilib.xhtml_of_content dbh hostid content);
+                      (Wikilib.xhtml_of_content dbh hostid content);
                   "has_divname", Template.VarConditional has_divname;
-                  "divname", Template.VarString divname ]) rows in
+                  "divname", Template.VarString divname;
+                  "has_divclass", Template.VarConditional has_divclass;
+                  "divclass", Template.VarString divclass;
+                  "has_div", Template.VarConditional has_div;
+                  "has_jsgo", Template.VarConditional has_jsgo;
+                  "jsgo", Template.VarString jsgo ]) rows in
 
     (* Call an extension to generate the first section in this page? *)
     let sections =
       match extension with
          None -> sections
        | Some extension ->
-           let content = extension dbh hostid page' in
+           let content = extension dbh hostid page' in
            let section = [
              "ordering", Template.VarString "0";
              "has_sectionname", Template.VarConditional false;
@@ -278,6 +386,11 @@ let run r (q : cgi) dbh hostid
              "content", Template.VarString content;
              "has_divname", Template.VarConditional true;
              "divname", Template.VarString "form_div";
+             "has_divclass", Template.VarConditional false;
+             "divclass", Template.VarString "";
+             "has_div", Template.VarConditional true;
+             "has_jsgo", Template.VarConditional false;
+             "jsgo", Template.VarString "";
            ] in
            section :: sections in
 
@@ -291,17 +404,31 @@ let run r (q : cgi) dbh hostid
           t#conditional "user_logged_in" true;
           t#set "username" username);
 
+    (* Can anonymous users create accounts?  If not them we don't
+     * want to offer to create accounts for them.
+     *)
+    t#conditional "create_account_anon" host.create_account_anon;
+
     (* If logged in, we want to update the recently_visited table. *)
     if pageid <> None then (
       match user with
        | User (userid, _, _, _) ->
-           PGSQL(dbh)
-             "delete from recently_visited
-                where hostid = $hostid and userid = $userid and url = $page'";
-           PGSQL(dbh)
-             "insert into recently_visited (hostid, userid, url)
-               values ($hostid, $userid, $page')";
-           PGOCaml.commit dbh;
+           (try
+              PGSQL(dbh)
+                "delete from recently_visited
+                   where hostid = $hostid and userid = $userid
+                     and url = $page'";
+              PGSQL(dbh)
+                "insert into recently_visited (hostid, userid, url)
+                  values ($hostid, $userid, $page')";
+              PGOCaml.commit dbh;
+            with
+              exn ->
+                (* Exceptions here are non-fatal.  Just print them. *)
+                prerr_endline "exception updating recently_visited:";
+                prerr_endline (Printexc.to_string exn);
+                PGOCaml.rollback dbh;
+           );
            PGOCaml.begin_work dbh;
        | _ -> ()
     );
@@ -377,8 +504,8 @@ let run r (q : cgi) dbh hostid
   let make_404 () =
     Request.set_status r 404;          (* Return a 404 error code. *)
 
-    let t = template_404 in
-    t#set "page" page;
+    let th = template_404_header in
+    th#set "page" page;
 
     let search_terms =
       String.map
@@ -386,13 +513,129 @@ let run r (q : cgi) dbh hostid
              ('a'..'z' | 'A'..'Z' | '0'..'9') as c -> c
            | _ -> ' ') page in
 
-    t#set "search_terms" search_terms;
+    th#set "search_terms" search_terms;
 
-    t#conditional "can_edit" can_edit;
-    t#conditional "can_manage_users" can_manage_users;
-    t#conditional "has_stats" has_stats;
+    (* Flush out the header while we start the search. *)
+    q#header ();
+    ignore (print_string r th#to_string);
+    ignore (Request.rflush r);
+
+    let t = template_404 in
+    t#set "query" search_terms;
+    t#set "canonical_hostname" host.canonical_hostname;
+
+    (* This is a simplified version of the code in search.ml. *)
+    let have_results =
+      (* Get the keywords from the query string. *)
+      let keywords = Pcre.split ~rex:split_words search_terms in
+      let keywords =
+       List.filter (fun s -> not (string_is_whitespace s)) keywords in
+      let keywords = List.map lowercase keywords in
+
+      (* Turn the keywords into a tsearch2 ts_query string. *)
+      let tsquery = String.concat "&" keywords in
+
+      (* Search the titles first. *)
+      let rows =
+       PGSQL(dbh)
+           "select url, title, last_modified_date,
+                    (lower (title) = lower ($search_terms)) as exact
+               from pages
+              where hostid = $hostid
+               and url is not null
+               and redirect is null
+                and title_description_fti @@ to_tsquery ('default', $tsquery)
+              order by exact desc, last_modified_date desc, title" in
+
+      let titles =
+       List.map (function
+                 | (Some url, title, last_modified, _) ->
+                     url, title, last_modified
+                 | _ -> assert false) rows in
+
+      let have_titles = titles <> [] in
+      t#conditional "have_titles" have_titles;
+
+      (* Search the contents. *)
+      let rows =
+       PGSQL(dbh)
+         "select c.id, p.url, p.title, p.last_modified_date
+             from contents c, pages p
+            where c.pageid = p.id
+              and p.hostid = $hostid
+              and url is not null
+              and p.redirect is null
+              and c.content_fti @@ to_tsquery ('default', $tsquery)
+            order by p.last_modified_date desc, p.title
+            limit 50" in
+
+      let contents =
+       List.map (function
+                 | (contentid, Some url, title, last_modified) ->
+                     contentid, url, title, last_modified
+                 | _ -> assert false) rows in
+
+      let have_contents = contents <> [] in
+      t#conditional "have_contents" have_contents;
+
+      (* Pull out the actual text which matched so we can generate a summary.
+       * XXX tsearch2 can actually do better than this by emboldening
+       * the text which maps.
+       *)
+      let content_map =
+       if contents = [] then []
+       else (
+         let rows =
+           let contentids =
+             List.map (fun (contentid, _,_,_) -> contentid) contents in
+           PGSQL(dbh)
+             "select id, sectionname, content from contents
+                where id in $@contentids" in
+         List.map (fun (id, sectionname, content) ->
+                     id, (sectionname, content)) rows
+       ) in
 
-    q#template t
+      (* Generate the final tables. *)
+      let table =
+       List.map (fun (url, title, last_modified) ->
+                   let last_modified = printable_date last_modified in
+                   [ "url", Template.VarString url;
+                     "title", Template.VarString title;
+                     "last_modified", Template.VarString last_modified ]
+                ) titles in
+      t#table "titles" table;
+
+      let table =
+       List.map
+         (fun (contentid, url, title, last_modified) ->
+            let sectionname, content = List.assoc contentid content_map in
+            let have_sectionname, sectionname =
+              match sectionname with
+                None -> false, ""
+              | Some sectionname -> true, sectionname in
+            let content =
+              truncate 160
+                (Wikilib.text_of_xhtml
+                   (Wikilib.xhtml_of_content r dbh hostid content)) in
+            let linkname = linkname_of_sectionname sectionname in
+            let last_modified = printable_date last_modified in
+            [ "url", Template.VarString url;
+              "title", Template.VarString title;
+              "have_sectionname", Template.VarConditional have_sectionname;
+              "sectionname", Template.VarString sectionname;
+              "linkname", Template.VarString linkname;
+              "content", Template.VarString content;
+              "last_modified", Template.VarString last_modified ]
+         ) contents in
+      t#table "contents" table;
+
+      (* Do we have any results? *)
+      let have_results = have_titles || have_contents in
+      have_results in
+    t#conditional "have_results" have_results;
+
+    (* Deliver the rest of the page. *)
+    ignore (print_string r t#to_string)
   in
 
   (* Fetch a page by name.  This function can give three answers:
@@ -405,50 +648,50 @@ let run r (q : cgi) dbh hostid
       | None ->
          if allow_redirect then (
            let rows = PGSQL(dbh)
-             "select url, redirect, id, title, description,
-                      last_modified_date, css is not null
+             "select url, redirect, id, title, description, keywords,
+                      last_modified_date, css is not null, noodp
                  from pages
                 where hostid = $hostid and lower (url) = lower ($page)" in
            match rows with
-           | [Some page', _, _, _, _, _, _]
+           | [Some page', _, _, _, _, _, _, _, _]
                when page <> page' -> (* different case *)
                FPExternalRedirect page'
-           | [ _, None, id, title, description,
-               last_modified_date, has_page_css ] ->
+           | [ _, None, id, title, description, keywords,
+               last_modified_date, has_page_css, noodp ] ->
                let has_page_css = Option.get has_page_css in
-               FPOK (id, title, description, last_modified_date,
-                     has_page_css)
-           | [_, Some redirect, _, _, _, _, _] ->
+               FPOK (id, title, description, keywords, last_modified_date,
+                     has_page_css, noodp)
+           | [_, Some redirect, _, _, _, _, _, _, _] ->
                FPInternalRedirect redirect
            | [] -> FPNotFound
            | _ -> assert false
          ) else (* redirects not allowed ... *) (
            let rows = PGSQL(dbh)
-             "select id, title, description, last_modified_date,
-                      css is not null
+             "select id, title, description, keywords, last_modified_date,
+                      css is not null, noodp
                  from pages where hostid = $hostid and url = $page" in
            match rows with
-           | [ id, title, description,
-               last_modified_date, has_page_css ] ->
+           | [ id, title, description, keywords,
+               last_modified_date, has_page_css, noodp ] ->
                let has_page_css = Option.get has_page_css in
-               FPOK (id, title, description, last_modified_date,
-                     has_page_css)
+               FPOK (id, title, description, keywords, last_modified_date,
+                     has_page_css, noodp)
            | [] -> FPNotFound
            | _ -> assert false
          )
       | Some version ->
          let rows = PGSQL(dbh)
-           "select id, title, description, last_modified_date,
-                    css is not null
+           "select id, title, description, keywords, last_modified_date,
+                    css is not null, noodp
                from pages
               where hostid = $hostid and id = $version and
                     (url = $page or url_deleted = $page)" in
          match rows with
-         | [ id, title, description,
-             last_modified_date, has_page_css ] ->
+         | [ id, title, description, keywords,
+             last_modified_date, has_page_css, noodp ] ->
              let has_page_css = Option.get has_page_css in
-             FPOK (id, title, description, last_modified_date,
-                   has_page_css)
+             FPOK (id, title, description, keywords, last_modified_date,
+                   has_page_css, noodp)
          | [] -> FPNotFound
          | _ -> assert false
   in
@@ -465,34 +708,34 @@ let run r (q : cgi) dbh hostid
   let rec loop page' i =
     if i > max_redirect then (
       error ~title:"Too many redirections" ~back_button:true
-        dbh hostid q
+        dbh hostid q
        ("Too many redirects between pages.  This may happen because " ^
         "of a cycle of redirections.");
       return ()
     ) else
       match fetch_page page' version allow_redirect with
-       | FPOK (pageid, title, description, last_modified_date, has_page_css)->
+       | FPOK (pageid, title, description, keywords,
+               last_modified_date, has_page_css, noodp)->
            (* Check if the page is also a template. *)
            let extension = get_extension page' in
-           make_page title (Some description) (Some pageid)
-             (printable_date last_modified_date) has_page_css
+           make_page title (Some description) keywords (Some pageid)
+             (printable_date last_modified_date) has_page_css noodp
              version page page' extension
        | FPInternalRedirect page' ->
            loop page' (i+1)
        | FPExternalRedirect page' ->
-           (* This normally happens when a user has request an uppercase
+           (* This normally happens when a user has requested an uppercase
             * page name.  We redirect to the true (lowercase) version.
             *)
-           q#redirect ("http://" ^ host.hostname ^ "/" ^ page');
-           return ()
+           q#redirect ("http://" ^ host.hostname ^ "/" ^ page')
        | FPNotFound ->
            (* Might be a templated page with no content in it. *)
            let extension = get_extension page' in
            (match extension with
               | (Some _) as extension ->
                   let title = page' in
-                  make_page title None None
-                    "Now" false None page page'
+                  make_page title None None None
+                    "Now" false None None page page'
                     extension
               | None ->
                   make_404 ())