+csv dep for PG'OCaml.
[cocanwiki.git] / scripts / lib / cocanwiki.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.ml,v 1.15 2006/12/06 09:46:57 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 open Registry
24 open Cgi
25 open Printf
26
27 open Cocanwiki_ok
28 open Cocanwiki_strings
29
30 (* The "host object". *)
31 type host_t = { hostname : string;
32                 canonical_hostname : string;
33                 edit_anon : bool;
34                 view_anon : bool;
35                 create_account_anon : bool; }
36
37 (* Permissions and restrictions.
38  *
39  * Use the optional ~restrict parameter to register_script to restrict
40  * who can use the script.  For example:
41  *   register_script ~restrict:[CanEdit ; CanManageUsers] run
42  *)
43 type permissions_t = CanView | CanEdit | CanManageUsers | CanManageContacts
44                    | CanManageSite | CanEditGlobalCSS | CanImportMail
45                    | CanEditMacros
46
47 (* User preferences and other settings (some cannot be changed by the user). *)
48 type prefs_t = {
49   email : string option;                (* Email address. *)
50   email_notify : bool;                  (* Email notification. *)
51 }
52
53 (* The "user object". *)
54 type user_t = Anonymous                 (* Not logged in. *)
55             | User of int32 * string * permissions_t list * prefs_t
56                                         (* Userid, name, perms, prefs. *)
57
58 let test_permission {edit_anon = edit_anon; view_anon = view_anon} perm user =
59   if perm = CanEdit && edit_anon then true
60   else if perm = CanView && view_anon then true
61   else match user with
62       Anonymous -> false
63     | User (_, _, perms, _) -> List.mem perm perms
64
65 let can_edit host = test_permission host CanEdit
66 let can_manage_users host = test_permission host CanManageUsers
67 let can_manage_contacts host = test_permission host CanManageContacts
68 let can_manage_site host = test_permission host CanManageSite
69 let can_edit_global_css host = test_permission host CanEditGlobalCSS
70 let can_import_mail host = test_permission host CanImportMail
71 let can_edit_macros host = test_permission host CanEditMacros
72
73 let get_uri_from_request r =
74   try
75     (* If we passed through mod_rewrite, then it saved the
76      * unmodified original URL in a subprocess environment
77      * variable called SCRIPT_URL:
78      *)
79     let tbl = Request.subprocess_env r in
80     Some (Table.get tbl "SCRIPT_URL")
81   with
82     Not_found ->
83       try
84         (* Otherwise try the ordinary uri field
85          * in request_rec.
86          *)
87         Some (Request.uri r)
88       with Not_found ->
89         None
90
91 (* Our wrapper around the standard [register_script] function.
92  *
93  * The optional ~restrict and ~anonymous parameters work as follows:
94  *
95  * By default (neither parameter given), anonymous or logged-in users
96  * at any level are permitted to run the script.
97  *
98  * If ~anonymous:false then a user must be logged in to use the script.
99  *
100  * If ~restrict contains a list of permissions (eg. CanEdit, etc.) then
101  * the user must have the ability to do AT LEAST ONE of those actions.
102  * (Note that this does not necessarily imply that the user must be
103  * logged in, because in some circumstances even anonymous users have
104  * the CanEdit permission - very typical for a wiki).
105  *
106  * If ~anonymous:false and ~restrict is given then the user must be
107  * logged in AND have the ability to do AT LEAST ONE of those actions.
108  *)
109 let register_script ?(restrict = []) ?(anonymous = true) run =
110   (* Actually register the script with the real [Registry] module. *)
111   register_script
112     (fun r ->
113        let q = new cgi r in
114
115        (* XXX Database pooling. *)
116        let dbh = PGOCaml.connect ~database:"cocanwiki" () in
117        PGOCaml.begin_work dbh;
118
119        let exn =
120          try
121            (* Get the host ID, by comparing the Host: header with the hostnames
122             * table in the database.
123             *)
124            let hostid, hostname, canonical_hostname, edit_anon, view_anon,
125              create_account_anon =
126              let hostname =
127                try Request.hostname r
128                with Not_found ->
129                  error ~back_button:true
130                    ~title:"Browser problem" r dbh (-1l) q
131                    ("Your browser didn't send a \"Host\" header as part of " ^
132                       "the HTTP request.  Unfortunately this web server " ^
133                       "cannot handle HTTP requests without a \"Host\" " ^
134                       "header.");
135                  return () in
136              let hostname = lowercase hostname in
137
138              let rows =
139                PGSQL(dbh)
140                  "select h.id, h.canonical_hostname, h.edit_anon, h.view_anon,
141                          h.create_account_anon
142                     from hostnames hn, hosts h
143                    where hn.name = $hostname and hn.hostid = h.id" in
144
145              match rows with
146              | [id, canonical_hostname, edit_anon, view_anon,
147                 create_account_anon] ->
148                  id, hostname, canonical_hostname, edit_anon, view_anon,
149                  create_account_anon
150              | [] ->
151                error ~back_button:true
152                  ~title:"Unknown website" r dbh (-1l) q
153                  ("No website called \"" ^ hostname ^ "\" can be found.  " ^
154                   "If you are the administrator of this site, check that " ^
155                   "the hostname is listed in the \"hostnames\" table " ^
156                   "in the database.");
157                  return ()
158              | _ -> assert false in
159
160            (* Create the host object. *)
161            let host = { hostname = hostname;
162                         canonical_hostname = canonical_hostname;
163                         edit_anon = edit_anon;
164                         view_anon = view_anon;
165                         create_account_anon = create_account_anon; } in
166
167            (* Look for the user's cookie, and determine from this the user
168             * object.
169             *)
170            let user =
171              try
172                let cookie =
173                  (* Allow the user to deliberately specify an extra "cookie"
174                   * parameter, which we will send back as a cookie.  This is
175                   * useful for "mail my password"-type scripts.
176                   *)
177                  if q#param_exists "cookie" then (
178                    let value = q#param "cookie" in
179                    let cookie = Cookie.cookie "auth" value ~path:"/" in
180                    Table.set (Request.headers_out r)
181                      "Set-Cookie" cookie#to_string;
182                    value
183                  ) else (
184                    (* Normal cookie, from the headers. *)
185                    let header = Table.get (Request.headers_in r) "Cookie" in
186                    let cookies = Cookie.parse header in
187                    let cookie =
188                      List.find (fun cookie -> cookie#name = "auth") cookies in
189                    cookie#value
190                  ) in
191
192                let rows =
193                  PGSQL(dbh)
194                    "select u.id, u.name, u.can_edit, u.can_manage_users,
195                            u.can_manage_contacts, u.can_manage_site,
196                            u.can_edit_global_css, u.can_import_mail,
197                            u.can_edit_macros,
198                            u.email, u.email_notify
199                       from usercookies uc, users u
200                      where uc.cookie = $cookie
201                        and uc.userid = u.id
202                        and u.hostid = $hostid" in
203                match rows with
204                | [userid, name, can_edit, can_manage_users,
205                   can_manage_contacts, can_manage_site,
206                   can_edit_global_css, can_import_mail,
207                   can_edit_macros,
208                   email, email_notify] ->
209                    (* Every logged in user can view. *)
210                    let perms = [CanView] in
211                    let perms =
212                      if can_edit then CanEdit :: perms
213                      else perms in
214                    let perms =
215                      if can_manage_users then CanManageUsers :: perms
216                      else perms in
217                    let perms =
218                      if can_manage_contacts then CanManageContacts :: perms
219                      else perms in
220                    let perms =
221                      if can_manage_site then CanManageSite :: perms
222                      else perms in
223                    let perms =
224                      if can_edit_global_css then CanEditGlobalCSS :: perms
225                      else perms in
226                    let perms =
227                      if can_import_mail then CanImportMail :: perms
228                      else perms in
229                    let perms =
230                      if can_edit_macros then CanEditMacros :: perms
231                      else perms in
232                    (* Preferences. *)
233                    let prefs = { email = email;
234                                  email_notify = email_notify; } in
235                    User (userid, name, perms, prefs)
236                | [] -> raise Not_found
237                | _ -> assert false
238              with
239                Not_found -> Anonymous in
240
241            (* If the ~restrict parameter is given, then we want to check that
242             * the user has sufficient permission to run this script.
243             *)
244            let permitted =
245              if not anonymous && user = Anonymous then false
246              else
247                match restrict with
248                | [] -> true             (* empty list = no restrictions *)
249                | rs ->
250                    List.fold_left (||) false
251                      (List.map (fun r -> test_permission host r user) rs) in
252
253            if permitted then (
254              (* Call the actual CGI script. *)
255              run r q dbh hostid host user
256            ) else (
257              if user = Anonymous then (
258                (* Not logged in and no permission to do the requested action,
259                 * so redirect to the login script.  If possible set the
260                 * redirect parameter so that we return to the right URL.
261                 *)
262                let redirect = get_uri_from_request r in
263
264                let url =
265                  "http://" ^ hostname ^ "/_login" ^
266                    match redirect with
267                    | None -> ""
268                    | Some url -> "?redirect=" ^ Cgi_escape.escape_url url in
269                q#redirect url
270              ) else
271                error ~back_button:true
272                  ~title:"Access denied"
273                  r dbh hostid q
274                  "You do not have permission to access this part of the site."
275            );
276
277            None (* no exception *)
278          with
279            exn -> Some exn in
280
281        (* XXX Connection pooling - see above. *)
282        PGOCaml.close dbh;
283
284        (* To help with debugging, if there is an exception, print some
285         * extended details.
286         *)
287        (match exn with
288         | Some exn ->
289             fprintf stderr "COCANWIKI exception: %S\n" (Std.dump exn);
290             fprintf stderr "Time: %s\n"
291               (Printer.CalendarPrinter.to_string (Calendar.now ()));
292             let hostname =
293               try Some (Request.hostname r) with Not_found -> None in
294             fprintf stderr "Host: ";
295             (match hostname with
296              | None -> fprintf stderr "not available\n"
297              | Some hostname -> fprintf stderr "%S\n" hostname
298             );
299             let uri = get_uri_from_request r in
300             fprintf stderr "Request: ";
301             (match uri with
302              | None -> fprintf stderr "not available\n"
303              | Some uri -> fprintf stderr "%S\n" uri
304             );
305         | _ -> ()
306        );
307
308        (* May re-raise the caught exception. *)
309        Option.may raise exn
310     )
311
312 (* Convert a section name into something valid for use in <a name="..."> *)
313 let linkname_of_sectionname str =
314   let buf = UTF8.Buf.create (String.length str) in
315   UTF8.iter (
316     fun c ->
317       if iswebsafe c then UTF8.Buf.add_char buf c
318       else UTF8.Buf.add_char buf (UChar.of_char '_')
319   ) str;
320   UTF8.Buf.contents buf
321
322 (* Maximum degree of redirection. *)
323 let max_redirect = 4