+csv dep for PG'OCaml.
[cocanwiki.git] / scripts / lib / cocanwiki_pages.ml
1 (* COCANWIKI - a wiki written in Objective CAML.
2  * Written by Richard W.M. Jones <rich@merjis.com>.
3  * Copyright (C) 2004 Merjis Ltd.
4  * $Id: cocanwiki_pages.ml,v 1.12 2006/12/07 15:46:54 rich Exp $
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; see the file COPYING.  If not, write to
18  * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
19  * Boston, MA 02111-1307, USA.
20  *)
21
22 open Apache
23
24 open Cocanwiki
25 open Cocanwiki_strings
26
27 type pt = Page of string | Title of string
28
29 type section =
30     string option * string option * string option * string option * string
31     (* (sectionname, divname, divclass, jsgo, content) *)
32
33 type model = {
34   id : int32;                           (* Original page ID (0 = none). *)
35   pt : pt;                              (* Page of title (only used if id=0) *)
36   description : string;                 (* Description. *)
37   keywords : string option;             (* Keywords. *)
38   noodp : bool option;                  (* NOODP per-page override. *)
39   redirect : string option;             (* Redirect to. *)
40   (* NB. Don't call this 'contents' because that clashes with the
41    * Pervasives.contents fields of the ref type.
42    *)
43   contents_ : section list;
44 }
45
46 exception SaveURLError
47 exception SaveConflict of int32 * int32 * string * string option
48
49 let new_page pt =
50   let description =
51     match pt with
52     | Page page -> page
53     | Title title -> title in
54
55   let model = { id = 0l;
56                 pt = pt;
57                 description = description;
58                 keywords = None;
59                 noodp = None;
60                 redirect = None;
61                 contents_ = [] } in
62   model
63
64 let new_page_with_title title =
65   (* Initial page contents. *)
66   let contents = [ None, None, None, None, "<b>" ^ title ^ "</b> is " ] in
67   let model = { id = 0l;
68                 pt = Title title;
69                 description = title;
70                 keywords = None;
71                 noodp = None;
72                 redirect = None;
73                 contents_ = contents } in
74   model
75
76 let load_page dbh hostid ~url ?version () =
77   (* Pull out the page itself from the database. *)
78   let rows =
79     match version with
80     | None ->
81         PGSQL(dbh) "select id, title, description, keywords, noodp, redirect
82                       from pages
83                      where hostid = $hostid and url = $url"
84     | Some version ->
85         PGSQL(dbh) "select id, title, description, keywords, noodp, redirect
86                       from pages
87                      where hostid = $hostid and id = $version and
88                            (url = $url or url_deleted = $url)" in
89
90   let pageid, title, description, keywords, noodp, redirect =
91     match rows with
92     | [row] -> row
93     | _ -> raise Not_found in
94
95   (* Get the sections. *)
96   let contents = PGSQL(dbh)
97     "select sectionname, divname, divclass, jsgo, content
98        from contents
99       where pageid = $pageid
100       order by ordering" in
101
102   let model = { id = pageid;
103                 pt = Page url;
104                 description = description;
105                 keywords = keywords;
106                 noodp = noodp;
107                 redirect = redirect;
108                 contents_ = contents } in
109   model
110
111 let save_page r dbh hostid ?user model =
112   (* Logging information, if available. *)
113   let logged_user =
114     match user with
115         None -> None
116       | Some user ->
117           match user with
118             | User (id, _, _, _) -> Some id
119             | _ -> None in
120
121   let logged_ip =
122     try Some (Connection.remote_ip (Request.connection r))
123     with Not_found -> None in
124
125   let url, pageid =
126     (* Creating a new page (id = 0)?  If so, we're just going to insert
127      * a new row, which is easy.
128      *)
129     if model.id = 0l then (
130       (* Create the page title or URL. *)
131       let url, title =
132         match model.pt with
133             Page url -> url, url
134           | Title title ->
135               match Wikilib.generate_url_of_title r dbh hostid title with
136                   Wikilib.GenURL_OK url -> url, title
137                 | _ ->
138                     raise SaveURLError in
139
140       let description = model.description in
141       let keywords = model.keywords in
142       let noodp = model.noodp in
143       let redirect = model.redirect in
144       PGSQL(dbh) "insert into pages (hostid, url, title,
145                                      description, keywords, noodp,
146                                      logged_ip, logged_user,
147                                      redirect)
148                   values ($hostid, $url, $title, $description, $?keywords,
149                           $?noodp,
150                           $?logged_ip, $?logged_user, $?redirect)";
151
152       let pageid = PGOCaml.serial4 dbh "pages_id_seq" in
153
154       (* Create the page contents. *)
155       let ordering = ref 0 in   (* Creating new ordering. *)
156       List.iter (
157         fun (sectionname, divname, divclass, jsgo, content) ->
158           incr ordering; let ordering = Int32.of_int !ordering in
159           PGSQL(dbh)
160             "insert into contents (pageid, ordering, sectionname, divname,
161                                    divclass, jsgo, content)
162              values ($pageid, $ordering,
163                      $?sectionname, $?divname, $?divclass, $?jsgo, $content)"
164       ) model.contents_;
165
166       url, pageid
167     )
168       (* Otherwise it's an old page which we're updating. *)
169     else (
170       (* Pull out fields from the database. *)
171       let model_id = model.id in
172       let rows =
173         PGSQL(dbh)
174         "select creation_date, coalesce (url, url_deleted),
175                 title, css
176            from pages
177           where hostid = $hostid and id = $model_id" in
178
179       let creation_date, url, title, css =
180         match rows with
181         | [ row ] -> row
182         | _ -> assert false in
183       let url = Option.get url in
184
185       (* Title changed? *)
186       let title =
187         match model.pt with
188         | Title new_title when title <> new_title -> new_title
189         | _ -> title in
190
191       (* Has someone else edited this page in the meantime? *)
192       let max_id = Option.get (
193         List.hd (
194           PGSQL(dbh) "select max(id) from pages
195                        where hostid = $hostid and url = $url"
196         )
197       ) in
198
199       let edited = max_id <> model_id in
200
201       if edited then
202         raise (SaveConflict (max_id, model_id, url, css));
203
204       (* Defer the pages_redirect_cn constraint because that would
205        * temporarily fail on the next UPDATE.
206        *)
207       PGSQL(dbh)
208         "set constraints
209                pages_redirect_cn, sitemenu_url_cn,
210                page_emails_url_cn, links_from_cn, recently_visited_url_cn
211              deferred";
212
213       (* Lock the pages table to avoid bogus 404 errors.  The
214        * lock is released at the end of the current transaction.
215        *)
216       PGSQL(dbh) "lock table pages";
217
218       (* Mark the old page as deleted. *)
219       PGSQL(dbh) "update pages set url_deleted = url, url = null
220                    where hostid = $hostid and id = $model_id";
221
222       let description = model.description in
223       let keywords = model.keywords in
224       let noodp = model.noodp in
225       let redirect = model.redirect in
226       PGSQL(dbh)
227         "insert into pages (hostid, url, title,
228                             description, keywords, noodp,
229                             creation_date, logged_ip,
230                             logged_user, redirect, css)
231          values ($hostid, $url, $title, $description, $?keywords, $?noodp,
232                  $creation_date,
233                  $?logged_ip, $?logged_user, $?redirect, $?css)";
234
235       (* New page ID <> old page ID model.id. *)
236       let pageid = PGOCaml.serial4 dbh "pages_id_seq" in
237
238       (* Create the page contents. *)
239       let ordering = ref 0 in   (* Creating new ordering. *)
240       List.iter (
241         fun (sectionname, divname, divclass, jsgo, content) ->
242           incr ordering; let ordering = Int32.of_int !ordering in
243           PGSQL(dbh) "insert into contents (pageid,
244                          ordering, sectionname, divname, divclass,
245                          jsgo, content)
246                       values ($pageid, $ordering, $?sectionname,
247                               $?divname, $?divclass, $?jsgo, $content)"
248       ) model.contents_;
249
250       url, pageid
251     ) in
252
253   (* Keep the links table in synch. *)
254   Cocanwiki_links.update_links_for_page r dbh hostid url;
255
256   url, pageid