Logging in and logging out.
[cocanwiki.git] / scripts / cocanwiki.ml
1 (* COCANWIKI scripts.
2  * Written by Richard W.M. Jones <rich@merjis.com>.
3  * Copyright (C) 2004 Merjis Ltd.
4  * $Id: cocanwiki.ml,v 1.4 2004/09/07 16:19:43 rich Exp $
5  *)
6
7 open Apache
8 open Registry
9 open Cgi
10 open Printf
11
12 open Cocanwiki_ok
13
14 module Pool = DbiPool (Dbi_postgres)
15
16 (* This function is used to grab a database handle.  It's used in a couple
17  * of very special places, and is not for general consumption.
18  *)
19 let _get_dbh r = Pool.get r "cocanwiki"
20
21 (* The [CgiExit] exception should be folded back into the base
22  * mod_caml code at some point.  It just causes the 'run' function to
23  * return at that point safely.  (XXX)
24  *)
25 exception CgiExit
26
27 (* Permissions and restrictions.
28  *
29  * Use the optional ~restrict parameter to register_script to restrict
30  * who can use the script.  For example:
31  *   register_script ~restrict:[CanEdit ; CanManageUsers] run
32  *)
33 type permissions_t = CanEdit | CanManageUsers
34
35 (* The "user object". *)
36 type user_t = Anonymous                 (* Not logged in. *)
37             | User of int * string * permissions_t list
38                                         (* Userid, name, permissions. *)
39
40 let test_permission edit_anon perm user =
41   if perm = CanEdit && edit_anon then true
42   else match user with
43       Anonymous -> false
44     | User (_, _, perms) -> List.mem perm perms
45
46 let can_edit edit_anon = test_permission edit_anon CanEdit
47 let can_manage_users = test_permission false CanManageUsers
48
49 (* Our wrapper around the standard [register_script] function.
50  *
51  * The optional ~restrict and ~anonymous parameters work as follows:
52  *
53  * By default (neither parameter given), anonymous or logged-in users
54  * at any level are permitted to run the script.
55  *
56  * If ~anonymous:false then a user must be logged in to use the script.
57  *
58  * If ~restrict contains a list of permissions (eg. CanEdit, etc.) then
59  * the user must have the ability to do AT LEAST ONE of those actions.
60  * (Note that this does not necessarily imply that the user must be
61  * logged in, because in some circumstances even anonymous users have
62  * the CanEdit permission - very typical for a wiki).
63  *
64  * If ~anonymous:false and ~restrict is given then the user must be
65  * logged in AND have the ability to do AT LEAST ONE of those actions.
66  *)
67 let register_script ?(restrict = []) ?(anonymous = true) run =
68   (* Actually register the script with the real [Registry] module. *)
69   register_script
70     (fun r ->
71        let q = new cgi r in
72        let dbh = _get_dbh r in
73
74        (* Get the host ID, by comparing the Host: header with the hostnames
75         * table in the database.
76         *)
77        let hostid, hostname, edit_anon =
78          let hostname = try Request.hostname r
79          with Not_found -> failwith "No ``Host:'' header in request" in
80          let hostname = String.lowercase hostname in
81
82          let sth =
83            dbh#prepare_cached
84              "select h.id, h.canonical_hostname, h.edit_anon
85                 from hostnames hn, hosts h
86                where hn.name = ? and hn.hostid = h.id" in
87          sth#execute [`String hostname];
88
89          try
90            (match sth#fetch1 () with
91                 [ `Int id; `String hostname; `Bool edit_anon ] ->
92                   id, hostname, edit_anon
93               | _ -> assert false)
94          with
95              Not_found ->
96                failwith ("Hostname ``" ^ hostname ^ "'' not found in " ^
97                          "the hosts/hostnames tables in the database.") in
98
99        (* Look for the user's cookie, and determine from this the user
100         * object.
101         *)
102        let user =
103          try
104            let cookie =
105              (* Allow the user to deliberately specify an extra "cookie"
106               * parameter, which we will send back as a cookie.  This is
107               * useful for "mail my password"-type scripts.
108               *)
109              if q#param_exists "cookie" then (
110                let value = q#param "cookie" in
111                let cookie = Cookie.cookie ~name:"auth" ~value ~path:"/" () in
112                Table.set (Request.headers_out r) "Set-Cookie" cookie#as_string;
113                value
114              ) else (
115                (* Normal cookie, from the headers. *)
116                let header = Table.get (Request.headers_in r) "Cookie" in
117                let cookies = Cookie.parse header in
118                let cookie =
119                  List.find (fun cookie -> cookie#name = "auth") cookies in
120                cookie#value
121              ) in
122
123            let sth =
124              dbh#prepare_cached
125                "select u.id, u.name, u.can_edit, u.can_manage_users
126                   from usercookies uc, users u
127                  where uc.cookie = ? and uc.userid = u.id and u.hostid = ?" in
128            sth#execute [`String cookie; `Int hostid];
129            (match sth#fetch1 () with
130                 [ `Int userid; `String name;
131                   `Bool can_edit; `Bool can_manage_users ] ->
132                   let perms =
133                     (if can_edit then [ CanEdit ] else []) @
134                     (if can_manage_users then [ CanManageUsers ] else []) in
135                   User (userid, name, perms)
136               | _ -> assert false)
137          with
138              Not_found -> Anonymous
139        in
140
141        (* If the ~restrict parameter is given, then we want to check that
142         * the user has sufficient permission to run this script.
143         *)
144        let permitted =
145          if not anonymous && user = Anonymous then false
146          else
147            match restrict with
148                [] -> true               (* empty list = no restrictions *)
149              | rs ->
150                  List.fold_left ((||)) false
151                    (List.map (fun r -> test_permission edit_anon r user) rs) in
152
153        if permitted then (
154          (* Call the actual CGI script. *)
155          try
156            run r q dbh (hostid, hostname, edit_anon) user
157          with
158              CgiExit -> ()
159        ) else
160          error ~back_button:true
161            ~title:"Access denied"
162            q "You do not have permission to access this part of the site."
163     )