Show all changed fields in the diff report.
[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.8 2006/08/04 12:20:07 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
31     (* (sectionname, divname, 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   redirect : string option;             (* Redirect to. *)
38   (* NB. Don't call this 'contents' because that clashes with the
39    * Pervasives.contents fields of the ref type.
40    *)
41   contents_ : section list;
42 }
43
44 exception SaveURLError
45 exception SaveConflict of int32 * int32 * string * string option
46
47 let new_page pt =
48   let description =
49     match pt with
50         Page page -> page
51       | Title title -> title in
52
53   let model = { id = 0l;
54                 pt = pt;
55                 description = description;
56                 redirect = None;
57                 contents_ = [] } in
58   model
59
60 let new_page_with_title title =
61   (* Initial page contents. *)
62   let contents = [ None, None, None, "<b>" ^ title ^ "</b> is " ] in
63   let model = { id = 0l;
64                 pt = Title title;
65                 description = title;
66                 redirect = None;
67                 contents_ = contents } in
68   model
69
70 let load_page dbh hostid ~url ?version () =
71   (* Pull out the page itself from the database. *)
72   let rows =
73     match version with
74     | None ->
75         PGSQL(dbh) "select id, title, description, redirect
76                       from pages
77                      where hostid = $hostid and url = $url"
78     | Some version ->
79         PGSQL(dbh) "select id, title, description, redirect
80                       from pages
81                      where hostid = $hostid and id = $version and
82                            (url = $url or url_deleted = $url)" in
83
84   let pageid, title, description, redirect =
85     match rows with
86     | [row] -> row
87     | _ -> raise Not_found in
88
89   (* Get the sections. *)
90   let contents = PGSQL(dbh)
91     "select sectionname, divname, jsgo, content
92        from contents
93       where pageid = $pageid
94       order by ordering" in
95
96   let model = { id = pageid;
97                 pt = Page url;
98                 description = description;
99                 redirect = redirect;
100                 contents_ = contents } in
101   model
102
103 let save_page r dbh hostid ?user model =
104   (* Logging information, if available. *)
105   let logged_user =
106     match user with
107         None -> None
108       | Some user ->
109           match user with
110             | User (id, _, _, _) -> Some id
111             | _ -> None in
112
113   let logged_ip =
114     try Some (Connection.remote_ip (Request.connection r))
115     with Not_found -> None in
116
117   let url, pageid =
118     (* Creating a new page (id = 0)?  If so, we're just going to insert
119      * a new row, which is easy.
120      *)
121     if model.id = 0l then (
122       (* Create the page title or URL. *)
123       let url, title =
124         match model.pt with
125             Page url -> url, url
126           | Title title ->
127               match Wikilib.generate_url_of_title r dbh hostid title with
128                   Wikilib.GenURL_OK url -> url, title
129                 | _ ->
130                     raise SaveURLError in
131
132       let description = model.description in
133       let redirect = model.redirect in
134       PGSQL(dbh) "insert into pages (hostid, url, title,
135                                      description, logged_ip, logged_user,
136                                      redirect)
137                   values ($hostid, $url, $title, $description,
138                           $?logged_ip, $?logged_user, $?redirect)";
139
140       let pageid = PGOCaml.serial4 dbh "pages_id_seq" in
141
142       (* Create the page contents. *)
143       let ordering = ref 0 in   (* Creating new ordering. *)
144       List.iter (
145         fun (sectionname, divname, jsgo, content) ->
146           incr ordering; let ordering = Int32.of_int !ordering in
147           PGSQL(dbh)
148             "insert into contents (pageid, ordering, sectionname, divname,
149                                    jsgo, content)
150              values ($pageid, $ordering,
151                      $?sectionname, $?divname, $?jsgo, $content)"
152       ) model.contents_;
153
154       url, pageid
155     )
156       (* Otherwise it's an old page which we're updating. *)
157     else (
158       (* Pull out fields from the database. *)
159       let model_id = model.id in
160       let rows =
161         PGSQL(dbh)
162         "select creation_date, coalesce (url, url_deleted),
163                 title, css
164            from pages
165           where hostid = $hostid and id = $model_id" in
166
167       let creation_date, url, title, css =
168         match rows with
169         | [ row ] -> row
170         | _ -> assert false in
171       let url = Option.get url in
172
173       (* Title changed? *)
174       let title =
175         match model.pt with
176         | Title new_title when title <> new_title -> new_title
177         | _ -> title in
178
179       (* Has someone else edited this page in the meantime? *)
180       let max_id = Option.get (
181         List.hd (
182           PGSQL(dbh) "select max(id) from pages
183                        where hostid = $hostid and url = $url"
184         )
185       ) in
186
187       let edited = max_id <> model_id in
188
189       if edited then
190         raise (SaveConflict (max_id, model_id, url, css));
191
192       (* Defer the pages_redirect_cn constraint because that would
193        * temporarily fail on the next UPDATE.
194        *)
195       PGSQL(dbh)
196         "set constraints
197                pages_redirect_cn, sitemenu_url_cn,
198                page_emails_url_cn, links_from_cn, recently_visited_url_cn
199              deferred";
200
201       (* Mark the old page as deleted.  NB. There is a small race
202        * condition here because PostgreSQL doesn't do isolation
203        * properly.  If a user tries to visit this page between the
204        * delete and the creation of the new page, then they'll get
205        * a page not found error. (XXX)
206        *)
207       PGSQL(dbh) "update pages set url_deleted = url, url = null
208                    where hostid = $hostid and id = $model_id";
209
210       let description = model.description in
211       let redirect = model.redirect in
212       PGSQL(dbh)
213         "insert into pages (hostid, url, title,
214                             description, creation_date, logged_ip,
215                             logged_user, redirect, css)
216          values ($hostid, $url, $title, $description, $creation_date,
217                  $?logged_ip, $?logged_user, $?redirect, $?css)";
218
219       (* New page ID <> old page ID model.id. *)
220       let pageid = PGOCaml.serial4 dbh "pages_id_seq" in
221
222       (* Create the page contents. *)
223       let ordering = ref 0 in   (* Creating new ordering. *)
224       List.iter (
225         fun (sectionname, divname, jsgo, content) ->
226           incr ordering; let ordering = Int32.of_int !ordering in
227           PGSQL(dbh) "insert into contents (pageid,
228                          ordering, sectionname, divname, jsgo, content)
229                       values ($pageid, $ordering, $?sectionname,
230                               $?divname, $?jsgo, $content)"
231       ) model.contents_;
232
233       url, pageid
234     ) in
235
236   (* Keep the links table in synch. *)
237   Cocanwiki_links.update_links_for_page r dbh hostid url;
238
239   url, pageid