/[drupal]/contributions/modules/loginticket/loginticket.module
ViewVC logotype

Contents of /contributions/modules/loginticket/loginticket.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.4 - (show annotations) (download) (as text)
Sun Jun 28 02:16:54 2009 UTC (5 months ago) by crdant
Branch: MAIN
CVS Tags: HEAD
Changes since 1.3: +37 -13 lines
File MIME type: text/x-php
Added alter hook to allow modules to modify the ticket that was generated.  Used on a client site to create a more secure ticket rather than the one in the module that seemed to be focused on usability/readability.  Also refactored ticket expiration into it's own function so that it could be called by other modules.  Need to backport these changes to 5.x and roll a release for 5.x as well.
1 <?php
2 // $Id: loginticket.module,v 1.3.2.1 2009/04/30 02:51:05 crdant Exp $
3
4 /**
5 * @file
6 * Login Ticket API - Facilities for other modules to let people log in
7 * as Drupal user as long as their login ticket is valid.
8 *
9 * Copyright 2006 by D R Pratten <http://www.davidpratten.com>
10 * Copyright 2007 by Jakob Petsovits <jpetso@gmx.at>
11 * Distributed under the GNU General Public Licence version 2 or higher,
12 * as published by the FSF on http://www.gnu.org/copyleft/gpl.html
13 */
14
15
16 /**
17 * Invokes hook_loginticket() for all modules that implemented it.
18 *
19 * @param $op
20 * What kind of action is being performed. Possible values:
21 * - 'insert': The ticket has been created and is readily accessible.
22 * - 'login': The user associated with the ticket has just logged in
23 * by using the login ticket.
24 * - 'delete': The ticket is about to be deleted from the database, either
25 * because it has expired or because the user himself has been
26 * deleted. In the latter case, the user object cannot be
27 * loaded anymore.
28 * @param $ticket
29 * A ticket object, as returned by loginticket_load().
30 * @param $passcode
31 * If $op == 'insert' and $op == 'login', this contains the passcode
32 * as plaintext. For all other hook invocations this is NULL.
33 */
34 function loginticket_invoke_all($op, $ticket, $passcode = NULL) {
35 module_invoke_all('loginticket', $op, $ticket, $passcode);
36 }
37
38 /**
39 * Create a login ticket that is valid for a given timespan.
40 * There can only be one ticket at a time for any user, so if there already
41 * exists a ticket for the given user then the existing ticket is replaced
42 * by the newly generated one.
43 *
44 * After the ticket has been generated, two hooks are available:
45 * hook_loginticket_alter(&$ticket) called to allow other modules to change the ticket
46 * hook_loginticket($op='insert', $ticket, $passcode) call for modules to address the new ticket being created
47 *
48 * @param $purpose
49 * The purpose of this ticket, as a short string. This argument is there
50 * to make it possible for tickets from several different modules to exist
51 * side by side. E.g., $purpose could be 'sitepass' or 'guest_invitation'.
52 * @param $account
53 * The user account that will be logged in with this ticket.
54 * Obviously, this may not be the anonymous user.
55 * @param $expiration_time
56 * A Unix timestamp specifying when this ticket will expire.
57 *
58 * @return The passcode that the user can enter in order to log in.
59 */
60 function loginticket_create($purpose, $account, $expiration_time) {
61 if ($account->uid == 0) {
62 return;
63 }
64
65 // generate a passcode, and check if it doesn't clash with an already existing passcode in the database
66 do {
67 $passcode = user_password(6);
68 // allow other modules to change the ticket generation algorithm by changing or replacing the ticket
69 drupal_alter("loginticket", $passcode, $account->name);
70
71 $passcode_md5 = md5($passcode);
72 $existing_passcodes = db_result(db_query("SELECT count(ticket_id) FROM {loginticket}
73 WHERE passcode_md5 = '%s'",
74 $passcode_md5, $purpose)
75 );
76 } while ($existing_passcodes > 0);
77
78 // If the current user already has a ticket for this purpose, we'll expire it, so that we get
79 // only one active ticket per user and purpose. We're not deleting it or replacing it to make
80 // sure tickets aren't reused too quickly (see comment in loginticket_check_expiration()).
81 loginticket_expire($account->uid, $purpose);
82
83 db_query("INSERT INTO {loginticket} (passcode_md5, purpose, expires, uid)
84 VALUES ('%s', '%s', %d, %d)",
85 $passcode_md5, $purpose, $expiration_time, $account->uid
86 );
87
88 $ticket = loginticket_load($purpose, array('passcode_md5' => $passcode_md5), TRUE);
89
90 loginticket_invoke_all('insert', $ticket, $passcode);
91 return $passcode;
92 }
93
94 /**
95 * Expire tickets granted to a user.
96 * @param $user
97 * the user for whom to expire tickets
98 * @param $purpose
99 * the purpose of the ticket, as specified in loginticket_create, if not provided, all tickets will be expired
100 * for the user
101 */
102 function loginticket_expire($uid, $purpose = NULL) {
103
104 if ( isset($purpose) ) {
105 db_query("UPDATE {loginticket}
106 SET expires = %d
107 WHERE uid = %d AND purpose = '%s' AND expires > %d",
108 time() - 1, $uid, $purpose, time()
109 );
110 } else {
111 db_query("UPDATE {loginticket}
112 SET expires = %d
113 WHERE uid = %d AND expires > %d",
114 time() - 1, $uid, time()
115 );
116 }
117 }
118
119 /**
120 * Log the user account in that corresponds to the given passcode.
121 * If there is already a user logged in, the session will be replaced,
122 * and the user corresponding to the passcode will be logged in afterwards.
123 *
124 * If the user corresponding to the passcode is the same user that is
125 * already logged in, nothing happens and TRUE is returned.
126 *
127 * If the passcode is invalid (= expired or non-existant), nothing happens
128 * except some watchdog logging, and FALSE is returned.
129 *
130 * If the user has been logged in successfully, the standard logs are written
131 * and hook_user() is invoked, like in user.module's user_login_submit().
132 * Naturally, the global $user object then contains the newly logged in user.
133 * Directly afterwards, hook_loginticket($op='login') is invoked as well.
134 *
135 * @param $purpose
136 * The purpose of the ticket, as specified in loginticket_create().
137 *
138 * @return The user object of the successfully logged in user,
139 * or FALSE if the user could not be logged in successfully.
140 */
141 function loginticket_login($purpose, $passcode) {
142 global $user;
143
144 $ticket = loginticket_load($purpose, array('passcode' => $passcode));
145 if ($ticket === FALSE) {
146 _loginticket_login_failed($purpose, $passcode);
147 return FALSE;
148 }
149
150 if ($user->uid == $ticket->uid) {
151 loginticket_invoke_all('login', $ticket, $passcode);
152 return $user; // yay, the user is already logged in
153 }
154
155 $account = user_load(array('uid' => $ticket->uid, 'status' => 1));
156 if ($account === FALSE) {
157 _loginticket_login_failed($purpose, $passcode);
158 return FALSE;
159 }
160
161 $user = $account;
162
163 watchdog('user', 'Session opened for %name (using a "%purpose" login ticket.)',
164 array('%name' => $user->name, '%purpose' => $purpose)
165 );
166
167 // Update the user table timestamp noting that the user has logged in.
168 db_query("UPDATE {users} SET login = %d WHERE uid = %d", time(), $user->uid);
169
170 // Make sure there's a session to write, otherwise sess_write()
171 // might not do anything and for direct login URLS the next loaded page
172 // is back to the anonymous user.
173 if (empty($_SESSION)) {
174 $_SESSION['loginticket'] = TRUE;
175 }
176
177 sess_regenerate();
178
179 $edit = array();
180 user_module_invoke('login', $edit, $user);
181 loginticket_invoke_all('login', $ticket, $passcode);
182
183 return $user;
184 }
185
186 function _loginticket_login_failed($purpose, $passcode) {
187 watchdog('user', 'Login attempt (using the "%purpose" login ticket "%passcode" failed.)',
188 array('%purpose' => $purpose, '%passcode' => $passcode)
189 );
190 }
191
192
193 /**
194 * Retrieve a single matching ticket object.
195 *
196 * @param $purpose
197 * The purpose of the ticket, as specified in loginticket_create().
198 * @param $array
199 * An associative array of attributes to search for in selecting the ticket.
200 * Allowed keys: 'passcode', 'passcode_md5', and 'uid'.
201 * @param $include_expired_tickets
202 * If TRUE, this function also considers tickets that are already expired.
203 * Otherwise (by default), only a valid ticket is included in the result.
204 *
205 * @return A ticket that fulfills the given constraints, or FALSE if there is
206 * no such ticket, or if there is more than one matching ticket.
207 * The ticket itself is an object with attributes named
208 * 'purpose', 'passcode_md5', 'expires', and 'uid'.
209 */
210 function loginticket_load($purpose, $array = array(), $include_expired_tickets = FALSE) {
211 $tickets = loginticket_load_array($purpose, $array, $include_expired_tickets);
212 if (count($tickets) != 1) {
213 return FALSE;
214 }
215 return $tickets[0];
216 }
217
218 /**
219 * Retrieve all matching ticket objects.
220 *
221 * @param $purpose
222 * The purpose of the tickets, as specified in loginticket_create().
223 * @param $array
224 * An associative array of attributes to search for in selecting the tickets.
225 * Allowed keys: 'ticket_id', 'passcode', 'passcode_md5', and 'uid'.
226 * @param $include_expired_tickets
227 * If TRUE, this function also returns tickets that are already expired.
228 * Otherwise (by default), only valid tickets are included in the result.
229 *
230 * @return An array of tickets that fulfill the given constraints, or an empty
231 * array if there are no such tickets. The tickets themselves are
232 * objects with attributes named 'ticket_id', 'purpose', 'passcode_md5', 'expires',
233 * and 'uid'.
234 */
235 function loginticket_load_array($purpose, $array = array(), $include_expired_tickets = FALSE) {
236 $condition = '';
237 $conds_and = array();
238 $conds_or = array();
239 $args = array();
240
241 if ($purpose != '<all>') {
242 $conds_and[] = "purpose = '%s'";
243 $args[] = $purpose;
244 }
245 if (!$include_expired_tickets) {
246 $conds_and[] = "expires > %d";
247 $args[] = time();
248 }
249
250 if (array_key_exists('ticket_id', $array)) {
251 $conds_or[] = "ticket_id = %d";
252 $args[] = $array['ticket_id'];
253 }
254 if (array_key_exists('passcode', $array)) {
255 $conds_or[] = "passcode_md5 = '%s'";
256 $args[] = md5($array['passcode']);
257 }
258 if (array_key_exists('passcode_md5', $array)) {
259 $conds_or[] = "passcode_md5 = '%s'";
260 $args[] = $array['passcode_md5'];
261 }
262 if (array_key_exists('uid', $array)) {
263 $conds_or[] = "uid = %d";
264 $args[] = $array['uid'];
265 }
266
267 if (count($conds_or) > 0) {
268 $conds_and[] = '('. implode(' OR ', $conds_or) .')';
269 }
270 if (count($conds_and) > 0) {
271 $condition = ' WHERE '. implode(' AND ', $conds_and);
272 }
273
274 $result = db_query('SELECT * FROM {loginticket}'. $condition, $args);
275 $tickets = array();
276
277 while ($ticket = db_fetch_object($result)) {
278 $tickets[] = $ticket;
279 }
280 return $tickets;
281 }
282
283 /**
284 * Delete login tickets from the database, and call
285 * hook_loginticket($op='delete', $ticket, NULL) before doing so.
286 *
287 * @param $tickets
288 * A ticket object or an array of ticket objects, preferably loaded
289 * with loginticket_load() or loginticket_load_array().
290 */
291 function loginticket_delete($tickets) {
292 if (is_object($tickets)) {
293 $tickets = array($tickets);
294 }
295 if (!is_array($tickets) || empty($tickets)) {
296 return;
297 }
298
299 $placeholders = array();
300 $params = array();
301
302 foreach ($tickets as $ticket) {
303 $placeholders[] = "%d";
304 $params[] = $ticket->ticket_id;
305 loginticket_invoke_all('delete', $ticket);
306 }
307
308 // delete all the processed tickets
309 db_query('DELETE FROM {loginticket}
310 WHERE ticket_id = ('. implode(',', $placeholders) .')',
311 $params);
312 }
313
314
315 /**
316 * Implementation of hook_user():
317 * Clean up tickets for deleted users.
318 * Before the tickets are actually deleted from the database,
319 * hook_loginticket($op='delete', $ticket, NULL) is called for each of them.
320 */
321 function loginticket_user($op, &$edit, &$account, $category = NULL) {
322 if ($op == 'delete') {
323 $tickets = loginticket_load_array('<all>', array('uid' => $account->uid), TRUE);
324 loginticket_delete($tickets);
325 }
326 }
327
328 /**
329 * Implementation of hook_cron():
330 * Check for expired login tickets and clean them up.
331 */
332 function loginticket_cron() {
333 loginticket_check_expiration();
334 }
335
336 /**
337 * Check for expired login tickets, and delete those from the database.
338 * Before the tickets are actually deleted from the database,
339 * hook_loginticket($op='delete', $ticket, NULL) is called for each of them.
340 */
341 function loginticket_check_expiration() {
342 // Wait two months before deleting expired tickets, so that newly generated
343 // tickets don't possibly get recently used passcodes.
344 // Because then, the previous users could accidentally log in
345 // as the new user with the same ticket.
346 $expire = strtotime('-2 months');
347 $result = db_query("SELECT * FROM {loginticket} WHERE expires < %d", $expire);
348 $tickets = array();
349
350 while ($ticket = db_fetch_object($result)) {
351 $tickets[] = $ticket;
352 }
353 loginticket_delete($tickets);
354 }

  ViewVC Help
Powered by ViewVC 1.1.2