user.module 98.3 KB
Newer Older
Dries's avatar
   
Dries committed
1
2
3
<?php
// $Id$

Dries's avatar
   
Dries committed
4
5
6
7
8
/**
 * @file
 * Enables the user registration and login system.
 */

9
10
11
/**
 * Maximum length of username text field.
 */
12
define('USERNAME_MAX_LENGTH', 60);
13
14
15
16

/**
 * Maximum length of user e-mail text field.
 */
17
18
define('EMAIL_MAX_LENGTH', 64);

19

Dries's avatar
Dries committed
20
21
22
/**
 * Invokes hook_user() in every module.
 *
23
 * We cannot use module_invoke() for this, because the arguments need to
Dries's avatar
Dries committed
24
25
 * be passed by reference.
 */
26
function user_module_invoke($type, &$array, &$user, $category = NULL) {
27
  foreach (module_implements('user_' . $type) as $module) {
28
    $function = $module . '_user_' . $type;
29
    $function($array, $user, $category);
Dries's avatar
   
Dries committed
30
31
32
  }
}

33
/**
34
 * Implementation of hook_theme().
35
36
37
38
39
 */
function user_theme() {
  return array(
    'user_picture' => array(
      'arguments' => array('account' => NULL),
40
      'template' => 'user-picture',
41
42
    ),
    'user_profile' => array(
43
      'arguments' => array('account' => NULL),
44
      'template' => 'user-profile',
45
      'file' => 'user.pages.inc',
46
47
48
    ),
    'user_profile_category' => array(
      'arguments' => array('element' => NULL),
49
      'template' => 'user-profile-category',
50
      'file' => 'user.pages.inc',
51
52
53
    ),
    'user_profile_item' => array(
      'arguments' => array('element' => NULL),
54
      'template' => 'user-profile-item',
55
      'file' => 'user.pages.inc',
56
57
58
59
60
61
    ),
    'user_list' => array(
      'arguments' => array('users' => NULL, 'title' => NULL),
    ),
    'user_admin_perm' => array(
      'arguments' => array('form' => NULL),
62
      'file' => 'user.admin.inc',
63
64
65
    ),
    'user_admin_new_role' => array(
      'arguments' => array('form' => NULL),
66
      'file' => 'user.admin.inc',
67
68
69
    ),
    'user_admin_account' => array(
      'arguments' => array('form' => NULL),
70
      'file' => 'user.admin.inc',
71
72
73
    ),
    'user_filter_form' => array(
      'arguments' => array('form' => NULL),
74
      'file' => 'user.admin.inc',
75
76
77
    ),
    'user_filters' => array(
      'arguments' => array('form' => NULL),
78
      'file' => 'user.admin.inc',
79
    ),
80
81
82
    'user_signature' => array(
      'arguments' => array('signature' => NULL),
    ),
83
84
85
  );
}

Dries's avatar
   
Dries committed
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/**
 * Implementation of hook_fieldable_info().
 */
function user_fieldable_info() {
  $return = array(
    'user' => array(
      'name' => t('User'),
      'id key' => 'uid',
    ),
  );
  return $return;
}

/**
 * Implementation of hook_field_build_modes().
 */
function user_field_build_modes($obj_type) {
  $modes = array();
  if ($obj_type == 'user') {
    $modes = array(
      'full' => t('User account'),
    );
  }
  return $modes;
}

Dries's avatar
   
Dries committed
112
function user_external_load($authname) {
Dries's avatar
   
Dries committed
113
  $result = db_query("SELECT uid FROM {authmap} WHERE authname = '%s'", $authname);
Dries's avatar
   
Dries committed
114

115
  if ($user = db_fetch_array($result)) {
Dries's avatar
   
Dries committed
116
    return user_load($user);
Dries's avatar
   
Dries committed
117
118
  }
  else {
119
    return FALSE;
Dries's avatar
   
Dries committed
120
121
122
  }
}

123
/**
124
125
126
 * Perform standard Drupal login operations for a user object.
 *
 * The user object must already be authenticated. This function verifies
127
 * that the user account is not blocked and then performs the login,
128
 * updates the login timestamp in the database, invokes hook_user('login'),
129
 * and regenerates the session.
130
131
132
133
134
135
 *
 * @param $account
 *    An authenticated user object to be set as the currently logged
 *    in user.
 * @param $edit
 *    The array of form values submitted by the user, if any.
136
 *    This array is passed to hook_user op login.
137
138
139
140
 * @return boolean
 *    TRUE if the login succeeds, FALSE otherwise.
 */
function user_external_login($account, $edit = array()) {
141
142
  $form = drupal_get_form('user_login');

143
144
145
146
147
  $state['values'] = $edit;
  if (empty($state['values']['name'])) {
    $state['values']['name'] = $account->name;
  }

148
  // Check if user is blocked.
149
150
  user_login_name_validate($form, $state, (array)$account);
  if (form_get_errors()) {
151
    // Invalid login.
152
153
    return FALSE;
  }
154

155
  // Valid login.
156
157
  global $user;
  $user = $account;
158
  user_authenticate_finalize($state['values']);
159
160
161
  return TRUE;
}

Dries's avatar
Dries committed
162
163
164
165
166
/**
 * Fetch a user object.
 *
 * @param $array
 *   An associative array of attributes to search for in selecting the
167
 *   user, such as user name or e-mail address.
Dries's avatar
Dries committed
168
 *
169
170
171
 * @return
 *   A fully-loaded $user object upon successful user load or FALSE if user
 *   cannot be loaded.
Dries's avatar
Dries committed
172
 */
Dries's avatar
   
Dries committed
173
function user_load($array = array()) {
Dries's avatar
Dries committed
174
  // Dynamically compose a SQL query:
175
  $query = array();
176
  $params = array();
177

178
179
180
  if (is_numeric($array)) {
    $array = array('uid' => $array);
  }
181
182
183
  elseif (!is_array($array)) {
    return FALSE;
  }
184

Dries's avatar
   
Dries committed
185
  foreach ($array as $key => $value) {
186
187
    if ($key == 'uid' || $key == 'status') {
      $query[] = "$key = %d";
188
      $params[] = $value;
189
    }
190
    elseif ($key == 'pass') {
191
      $query[] = "pass = '%s'";
192
      $params[] = $value;
193
    }
Dries's avatar
   
Dries committed
194
    else {
195
      $query[]= "LOWER($key) = LOWER('%s')";
196
      $params[] = $value;
Dries's avatar
   
Dries committed
197
198
    }
  }
199
  $result = db_query('SELECT * FROM {users} u WHERE ' . implode(' AND ', $query), $params);
Dries's avatar
   
Dries committed
200

201
  if ($user = db_fetch_object($result)) {
202
    $user = drupal_unpack($user);
Dries's avatar
   
Dries committed
203

204
    $user->roles = array();
205
206
207
208
209
210
    if ($user->uid) {
      $user->roles[DRUPAL_AUTHENTICATED_RID] = 'authenticated user';
    }
    else {
      $user->roles[DRUPAL_ANONYMOUS_RID] = 'anonymous user';
    }
211
212
213
214
    $result = db_query('SELECT r.rid, r.name FROM {role} r INNER JOIN {users_roles} ur ON ur.rid = r.rid WHERE ur.uid = %d', $user->uid);
    while ($role = db_fetch_object($result)) {
      $user->roles[$role->rid] = $role->name;
    }
215

Dries's avatar
   
Dries committed
216
217
218
219
    // Attach fields.
    // TODO D7 : not sure the 3rd param ($types) is needed.
    field_attach_load('user', array($user->uid => $user));

220
221
222
223
224
225
226
    if (!empty($user->picture) && ($file = file_load($user->picture))) {
      $user->picture = $file;
    }
    else {
      $user->picture = NULL;
    }

227
    user_module_invoke('load', $array, $user);
228
229
  }
  else {
230
    $user = FALSE;
Dries's avatar
   
Dries committed
231
  }
Dries's avatar
   
Dries committed
232
233
234
235

  return $user;
}

236
/**
237
 * Save changes to a user account or add a new user.
238
239
 *
 * @param $account
240
241
 *   The $user object for the user to modify or add. If $user->uid is
 *   omitted, a new user will be added.
242
 *
243
 * @param $edit
244
 *   An array of fields and values to save. For example array('name'
245
 *   => 'My name'). Keys that do not belong to columns in the user-related
246
247
248
 *   tables are added to the a serialized array in the 'data' column
 *   and will be loaded in the $user->data array by user_load().
 *   Setting a field to NULL deletes it from the data column.
249
250
251
 *
 * @param $category
 *   (optional) The category for storing profile information in.
252
253
 *
 * @return
254
 *   A fully-loaded $user object upon successful save or FALSE if the save failed.
255
 */
256
function user_save($account, $edit = array(), $category = 'account') {
257
258
259
  $table = drupal_get_schema('users');
  $user_fields = $table['fields'];

260
  if (!empty($edit['pass'])) {
261
    // Allow alternate password hashing schemes.
262
    require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc');
263
    $edit['pass'] = user_hash_password(trim($edit['pass']));
264
    // Abort if the hashing failed and returned FALSE.
265
    if (!$edit['pass']) {
266
267
      return FALSE;
    }
268
269
270
  }
  else {
    // Avoid overwriting an existing password with a blank password.
271
    unset($edit['pass']);
272
273
  }

Dries's avatar
   
Dries committed
274
275
276
277
278
279
280
281
282
283
284
285
  // Get the fields form so we can recognize the fields in the $edit
  // form that should not go into the serialized data array.
  $field_form = array();
  $field_form_state = array();
  $edit = (object) $edit;
  field_attach_form('user', $edit, $field_form, $field_form_state);

  // Presave fields.
  field_attach_presave('user', $edit);

  $edit = (array) $edit;

286
  if (is_object($account) && $account->uid) {
287
    user_module_invoke('update', $edit, $account, $category);
Dries's avatar
Dries committed
288
    $data = unserialize(db_result(db_query('SELECT data FROM {users} WHERE uid = %d', $account->uid)));
289
290
    // Consider users edited by an administrator as logged in, if they haven't
    // already, so anonymous users can view the profile (if allowed).
291
    if (empty($edit['access']) && empty($account->access) && user_access('administer users')) {
292
      $edit['access'] = REQUEST_TIME;
293
    }
294
    foreach ($edit as $key => $value) {
Dries's avatar
   
Dries committed
295
296
297
298
      // Form fields that don't pertain to the users, user_roles, or
      // Field API are automatically serialized into the users.data
      // column.
      if ($key != 'roles' && empty($user_fields[$key]) && empty($field_form[$key])) {
299
300
        if ($value === NULL) {
          unset($data[$key]);
Dries's avatar
   
Dries committed
301
        }
302
303
        else {
          $data[$key] = $value;
Dries's avatar
   
Dries committed
304
        }
Dries's avatar
   
Dries committed
305
306
307
      }
    }

308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324

    // Process picture uploads.
    if (!empty($edit['picture']->fid)) {
      $picture = $edit['picture'];
      // If the picture is a temporary file move it to its final location and
      // make it permanent.
      if (($picture->status & FILE_STATUS_PERMANENT) == 0) {
        $info = image_get_info($picture->filepath);
        $destination = file_create_path(variable_get('user_picture_path', 'pictures') . '/picture-' . $account->uid . '.' . $info['extension']);
        if ($picture = file_move($picture, $destination, FILE_EXISTS_REPLACE)) {
          $picture->status |= FILE_STATUS_PERMANENT;
          $edit['picture'] = file_save($picture);
        }
      }
    }
    $edit['picture'] = empty($edit['picture']->fid) ? 0 : $edit['picture']->fid;

325
326
    $edit['data'] = $data;
    $edit['uid'] = $account->uid;
327
    // Save changes to the users table.
328
329
    $success = drupal_write_record('users', $edit, 'uid');
    if (!$success) {
Dries's avatar
   
Dries committed
330
331
332
333
334
335
336
337
      // The query failed - better to abort the save than risk further
      // data loss.

      // TODO: Fields change: I think this is a bug.  If no columns in
      // the users table are changed, drupal_write_record returns
      // FALSE because rowCount() (rows changed) is 0.  However,
      // non-users data may have been changed, e.g. fields.
      // return FALSE;
338
    }
Dries's avatar
   
Dries committed
339

340
341
342
343
344
345
346
    // If the picture changed or was unset, remove the old one. This step needs
    // to occur after updating the {users} record so that user_file_references()
    // doesn't report it in use and block the deletion.
    if (!empty($account->picture->fid) && ($edit['picture'] != $account->picture->fid)) {
      file_delete($account->picture);
    }

347
    // Reload user roles if provided.
348
    if (isset($edit['roles']) && is_array($edit['roles'])) {
Dries's avatar
Dries committed
349
      db_query('DELETE FROM {users_roles} WHERE uid = %d', $account->uid);
Dries's avatar
   
Dries committed
350

351
      foreach (array_keys($edit['roles']) as $rid) {
352
353
354
        if (!in_array($rid, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) {
          db_query('INSERT INTO {users_roles} (uid, rid) VALUES (%d, %d)', $account->uid, $rid);
        }
355
      }
Dries's avatar
   
Dries committed
356
357
    }

358
    // Delete a blocked user's sessions to kick them if they are online.
359
    if (isset($edit['status']) && $edit['status'] == 0) {
360
      drupal_session_destroy_uid($account->uid);
361
362
    }

363
364
    // If the password changed, delete all open sessions and recreate
    // the current one.
365
    if (!empty($edit['pass'])) {
366
      drupal_session_destroy_uid($account->uid);
367
368
369
      if ($account->uid == $GLOBALS['user']->uid) {
        drupal_session_regenerate();
      }
370
371
    }

Dries's avatar
   
Dries committed
372
373
374
375
    // Save Field data.
    $obj = (object) $edit;
    field_attach_update('user', $obj);

376
    // Refresh user object.
Dries's avatar
   
Dries committed
377
    $user = user_load(array('uid' => $account->uid));
378
379

    // Send emails after we have the new user object.
380
    if (isset($edit['status']) && $edit['status'] != $account->status) {
381
      // The user's status is changing; conditionally send notification email.
382
      $op = $edit['status'] == 1 ? 'status_activated' : 'status_blocked';
383
384
385
      _user_mail_notify($op, $user);
    }

386
    user_module_invoke('after_update', $edit, $user, $category);
Dries's avatar
   
Dries committed
387
388
  }
  else {
389
    // Allow 'created' to be set by the caller.
390
    if (!isset($edit['created'])) {
391
      $edit['created'] = REQUEST_TIME;
392
    }
393
394
    // Consider users created by an administrator as already logged in, so
    // anonymous users can view the profile (if allowed).
395
    if (empty($edit['access']) && user_access('administer users')) {
396
      $edit['access'] = REQUEST_TIME;
397
    }
398

399
400
401
402
403
404
    $success = drupal_write_record('users', $edit);
    if (!$success) {
      // On a failed INSERT some other existing user's uid may be returned.
      // We must abort to avoid overwriting their account.
      return FALSE;
    }
405

406
    // Build the initial user object.
407
    $user = user_load(array('uid' => $edit['uid']));
Dries's avatar
   
Dries committed
408

Dries's avatar
   
Dries committed
409
410
411
    $obj = (object) $edit;
    field_attach_insert('user', $obj);

412
    user_module_invoke('insert', $edit, $user, $category);
413

414
415
    // Note, we wait with saving the data column to prevent module-handled
    // fields from being saved there.
416
    $data = array();
417
    foreach ($edit as $key => $value) {
Dries's avatar
   
Dries committed
418
419
420
421
      // Form fields that don't pertain to the users, user_roles, or
      // Field API are automatically serialized into the users.data
      // column.
      if (($key != 'roles') && (empty($user_fields[$key]) && empty($field_form[$key])) && ($value !== NULL)) {
422
423
424
        $data[$key] = $value;
      }
    }
425
426
427
428
    if (!empty($data)) {
      $data_array = array('uid' => $user->uid, 'data' => $data);
      drupal_write_record('users', $data_array, 'uid');
    }
429

430
    // Save user roles (delete just to be safe).
431
432
433
    if (isset($edit['roles']) && is_array($edit['roles'])) {
      db_query('DELETE FROM {users_roles} WHERE uid = %d', $edit['uid']);
      foreach (array_keys($edit['roles']) as $rid) {
434
        if (!in_array($rid, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) {
435
          db_query('INSERT INTO {users_roles} (uid, rid) VALUES (%d, %d)', $edit['uid'], $rid);
436
        }
437
438
439
      }
    }

440
    // Build the finished user object.
441
    $user = user_load(array('uid' => $edit['uid']));
Dries's avatar
   
Dries committed
442
443
444
445
446
  }

  return $user;
}

Dries's avatar
Dries committed
447
448
449
/**
 * Verify the syntax of the given name.
 */
Dries's avatar
   
Dries committed
450
function user_validate_name($name) {
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
  if (!$name) {
    return t('You must enter a username.');
  }
  if (substr($name, 0, 1) == ' ') {
    return t('The username cannot begin with a space.');
  }
  if (substr($name, -1) == ' ') {
    return t('The username cannot end with a space.');
  }
  if (strpos($name, '  ') !== FALSE) {
    return t('The username cannot contain multiple spaces in a row.');
  }
  if (preg_match('/[^\x{80}-\x{F7} a-z0-9@_.\'-]/i', $name)) {
    return t('The username contains an illegal character.');
  }
466
467
468
469
470
471
472
473
  if (preg_match('/[\x{80}-\x{A0}' .         // Non-printable ISO-8859-1 + NBSP
                   '\x{AD}' .                // Soft-hyphen
                   '\x{2000}-\x{200F}' .     // Various space characters
                   '\x{2028}-\x{202F}' .     // Bidirectional text overrides
                   '\x{205F}-\x{206F}' .     // Various text hinting characters
                   '\x{FEFF}' .              // Byte order mark
                   '\x{FF01}-\x{FF60}' .     // Full-width latin
                   '\x{FFF9}-\x{FFFD}' .     // Replacement characters
474
                   '\x{0}-\x{1F}]/u',        // NULL byte and control characters
475
476
477
                   $name)) {
    return t('The username contains an illegal character.');
  }
478
479
480
  if (drupal_strlen($name) > USERNAME_MAX_LENGTH) {
    return t('The username %name is too long: it must be %max characters or less.', array('%name' => $name, '%max' => USERNAME_MAX_LENGTH));
  }
Dries's avatar
   
Dries committed
481
482
483
}

function user_validate_mail($mail) {
484
485
486
  if (!$mail) {
    return t('You must enter an e-mail address.');
  }
487
  if (!valid_email_address($mail)) {
488
    return t('The e-mail address %mail is not valid.', array('%mail' => $mail));
Dries's avatar
   
Dries committed
489
490
491
  }
}

492
function user_validate_picture(&$form, &$form_state) {
493
  // If required, validate the uploaded picture.
494
495
496
497
498
  $validators = array(
    'file_validate_is_image' => array(),
    'file_validate_image_resolution' => array(variable_get('user_picture_dimensions', '85x85')),
    'file_validate_size' => array(variable_get('user_picture_file_size', '30') * 1024),
  );
499

500
501
502
503
504
505
506
  // Save the file as a temporary file.
  $file = file_save_upload('picture_upload', $validators);
  if ($file === FALSE) {
    form_set_error('picture_upload', t("Failed to upload the picture image; the %directory directory doesn't exist or is not writable.", array('%directory' => variable_get('user_picture_path', 'pictures'))));
  }
  elseif ($file !== NULL) {
    $form_state['values']['picture_upload'] = $file;
Dries's avatar
   
Dries committed
507
508
509
  }
}

Dries's avatar
Dries committed
510
511
512
/**
 * Generate a random alphanumeric password.
 */
Dries's avatar
   
Dries committed
513
514
function user_password($length = 10) {
  // This variable contains the list of allowable characters for the
515
516
  // password. Note that the number 0 and the letter 'O' have been
  // removed to avoid confusion between the two. The same is true
517
  // of 'I', 1, and 'l'.
518
  $allowable_characters = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';
Dries's avatar
Dries committed
519

520
521
  // Zero-based count of characters in the allowable list:
  $len = strlen($allowable_characters) - 1;
Dries's avatar
   
Dries committed
522

Dries's avatar
Dries committed
523
524
  // Declare the password as a blank string.
  $pass = '';
Dries's avatar
   
Dries committed
525

Dries's avatar
Dries committed
526
  // Loop the number of times specified by $length.
Dries's avatar
   
Dries committed
527
528
529
530
  for ($i = 0; $i < $length; $i++) {

    // Each iteration, pick a random character from the
    // allowable string and append it to the password:
531
    $pass .= $allowable_characters[mt_rand(0, $len)];
Dries's avatar
   
Dries committed
532
533
534
  }

  return $pass;
Dries's avatar
   
Dries committed
535
536
}

537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
/**
 * Determine the permissions for one or more roles.
 *
 * @param $roles
 *   An array whose keys are the role IDs of interest, such as $user->roles.
 * @param $reset
 *   Optional parameter - if TRUE data in the static variable is rebuilt.
 *
 * @return
 *   An array indexed by role ID. Each value is an array whose keys are the
 *   permission strings for the given role ID.
 */
function user_role_permissions($roles = array(), $reset = FALSE) {
  static $stored_permissions = array();

  if ($reset) {
    // Clear the data cached in the static variable.
    $stored_permissions = array();
  }

  $role_permissions = $fetch = array();

  if ($roles) {
    foreach ($roles as $rid => $name) {
      if (isset($stored_permissions[$rid])) {
        $role_permissions[$rid] = $stored_permissions[$rid];
      }
      else {
        // Add this rid to the list of those needing to be fetched.
        $fetch[] = $rid;
        // Prepare in case no permissions are returned.
        $stored_permissions[$rid] = array();
      }
    }

    if ($fetch) {
      // Get from the database permissions that were not in the static variable.
      // Only role IDs with at least one permission assigned will return rows.
575
      $result = db_query("SELECT r.rid, p.permission FROM {role} r INNER JOIN {role_permission} p ON p.rid = r.rid WHERE r.rid IN (:fetch)", array(':fetch' => $fetch));
576
577
578
579
580
581
582
583
584
585
586
587
588
589

      while ($row = db_fetch_array($result)) {
        $stored_permissions[$row['rid']][$row['permission']] = TRUE;
      }
      foreach ($fetch as $rid) {
        // For every rid, we know we at least assigned an empty array.
        $role_permissions[$rid] = $stored_permissions[$rid];
      }
    }
  }

  return $role_permissions;
}

Dries's avatar
Dries committed
590
591
592
593
594
/**
 * Determine whether the user has a given privilege.
 *
 * @param $string
 *   The permission, such as "administer nodes", being checked for.
Dries's avatar
   
Dries committed
595
596
 * @param $account
 *   (optional) The account to check, if not given use currently logged in user.
597
598
599
600
 * @param $reset
 *   (optional) Resets the user's permissions cache, which will result in a
 *   recalculation of the user's permissions. This is necessary to support
 *   dynamically added user roles.
Dries's avatar
Dries committed
601
602
 *
 * @return
603
 *   Boolean TRUE if the current user has the requested permission.
Dries's avatar
Dries committed
604
605
606
607
608
 *
 * All permission checks in Drupal should go through this function. This
 * way, we guarantee consistent behavior, and ensure that the superuser
 * can perform all actions.
 */
609
function user_access($string, $account = NULL, $reset = FALSE) {
Dries's avatar
   
Dries committed
610
  global $user;
Dries's avatar
   
Dries committed
611
  static $perm = array();
Dries's avatar
   
Dries committed
612

613
  if ($reset) {
614
    $perm = array();
615
616
  }

617
618
619
620
  if (is_null($account)) {
    $account = $user;
  }

621
  // User #1 has all privileges:
622
  if ($account->uid == 1) {
623
    return TRUE;
Dries's avatar
   
Dries committed
624
625
  }

Dries's avatar
Dries committed
626
627
  // To reduce the number of SQL queries, we cache the user's permissions
  // in a static variable.
628
  if (!isset($perm[$account->uid])) {
629
    $role_permissions = user_role_permissions($account->roles, $reset);
Dries's avatar
   
Dries committed
630

631
    $perms = array();
632
633
    foreach ($role_permissions as $one_role) {
      $perms += $one_role;
Dries's avatar
   
Dries committed
634
    }
635
    $perm[$account->uid] = $perms;
Dries's avatar
   
Dries committed
636
  }
637

638
  return isset($perm[$account->uid][$string]);
Dries's avatar
   
Dries committed
639
640
}

641
/**
642
 * Checks for usernames blocked by user administration.
643
 *
644
 * @return boolean TRUE for blocked users, FALSE for active.
645
646
 */
function user_is_blocked($name) {
647
  $deny = db_fetch_object(db_query("SELECT name FROM {users} WHERE status = 0 AND name = LOWER('%s')", $name));
648

649
  return $deny;
650
651
}

Dries's avatar
Dries committed
652
653
654
/**
 * Implementation of hook_perm().
 */
Dries's avatar
   
Dries committed
655
function user_perm() {
656
   return array(
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
     'administer permissions' =>  array(
       'title' => t('Administer permissions'),
       'description' => t('Manage the permissions assigned to user roles. %warning', array('%warning' => t('Warning: Give to trusted roles only; this permission has security implications.'))),
     ),
     'administer users' => array(
       'title' => t('Administer users'),
       'description' => t('Manage or block users, and manage their role assignments.'),
     ),
     'access user profiles' => array(
       'title' => t('Access user profiles'),
       'description' => t('View profiles of users on the site, which may contain personal information.'),
     ),
     'change own username' => array(
       'title' => t('Change own username'),
       'description' => t('Select a different username.'),
     ),
673
674
675
676
677
678
679
680
     'cancel account' => array(
       'title' => t('Cancel account'),
       'description' => t('Remove or disable own user account and unpublish, anonymize, or remove own submissions depending on the configured <a href="@user-settings-url">user settings</a>.', array('@user-settings-url' => url('admin/user/settings'))),
     ),
     'select account cancellation method' => array(
       'title' => t('Select method for cancelling own account'),
       'description' => t('Select the method for cancelling own user account. %warning', array('%warning' => t('Warning: Give to trusted roles only; this permission has security implications.'))),
     ),
681
   );
Dries's avatar
   
Dries committed
682
683
}

Dries's avatar
Dries committed
684
685
686
687
688
/**
 * Implementation of hook_file_download().
 *
 * Ensure that user pictures (avatars) are always downloadable.
 */
689
690
691
function user_file_download($filepath) {
  if (strpos($filepath, variable_get('user_picture_path', 'pictures') . '/picture-') === 0) {
    $info = image_get_info(file_create_path($filepath));
692
    return array('Content-type: ' . $info['mime_type']);
Dries's avatar
   
Dries committed
693
694
695
  }
}

696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
/**
 * Implementation of hook_file_references().
 */
function user_file_references($file) {
  // Determine if the file is used by this module.
  $count = db_query('SELECT COUNT(*) FROM {users} WHERE picture = :fid', array(':fid' => $file->fid))->fetchField();
  if ($count) {
    // Return the name of the module and how many references it has to the file.
    return array('user' => $count);
  }
}

/**
 * Implementation of hook_file_delete().
 */
function user_file_delete($file) {
  // Remove any references to the file.
  db_update('users')
    ->fields(array('picture' => 0))
    ->condition('picture', $file->fid)
    ->execute();
}

Dries's avatar
Dries committed
719
720
721
/**
 * Implementation of hook_search().
 */
722
function user_search($op = 'search', $keys = NULL, $skip_access_check = FALSE) {
723
724
  switch ($op) {
    case 'name':
725
      if ($skip_access_check || user_access('access user profiles')) {
726
        return t('Users');
727
      }
728
    case 'search':
729
730
731
732
      if (user_access('access user profiles')) {
        $find = array();
        // Replace wildcards with MySQL/PostgreSQL wildcards.
        $keys = preg_replace('!\*+!', '%', $keys);
733
734
735
736
        if (user_access('administer users')) {
          // Administrators can also search in the otherwise private email field.
          $result = pager_query("SELECT name, uid, mail FROM {users} WHERE LOWER(name) LIKE LOWER('%%%s%%') OR LOWER(mail) LIKE LOWER('%%%s%%')", 15, 0, NULL, $keys, $keys);
          while ($account = db_fetch_object($result)) {
737
            $find[] = array('title' => $account->name . ' (' . $account->mail . ')', 'link' => url('user/' . $account->uid, array('absolute' => TRUE)));
738
739
740
          }
        }
        else {
741
          $result = pager_query("SELECT name, uid FROM {users} WHERE LOWER(name) LIKE LOWER('%%%s%%')", 15, 0, NULL, $keys);
742
          while ($account = db_fetch_object($result)) {
743
            $find[] = array('title' => $account->name, 'link' => url('user/' . $account->uid, array('absolute' => TRUE)));
744
          }
745
746
        }
        return $find;
747
      }
Dries's avatar
   
Dries committed
748
749
750
  }
}

Dries's avatar
Dries committed
751
752
753
754
755
756
757
758
759
760
/**
 * Implementation of hook_elements().
 */
function user_elements() {
  return array(
    'user_profile_category' => array(),
    'user_profile_item' => array(),
  );
}

Dries's avatar
Dries committed
761
/**
762
 * Implementation of hook_user_view().
Dries's avatar
Dries committed
763
 */
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
function user_user_view(&$edit, &$account, $category = NULL) {
  $account->content['user_picture'] = array(
    '#value' => theme('user_picture', $account),
    '#weight' => -10,
  );
  if (!isset($account->content['summary'])) {
    $account->content['summary'] = array();
  }
  $account->content['summary'] += array(
    '#type' => 'user_profile_category',
    '#attributes' => array('class' => 'user-member'),
    '#weight' => 5,
    '#title' => t('History'),
  );
  $account->content['summary']['member_for'] =  array(
    '#type' => 'user_profile_item',
    '#title' => t('Member for'),
    '#markup' => format_interval(REQUEST_TIME - $account->created),
  );
}

/**
 * Implementation of hook_user_form.
 */
function user_user_form(&$edit, &$account, $category = NULL) {
  if ($category == 'account') {
790
    $form_state = array();
791
    return user_edit_form($form_state, (isset($account->uid) ? $account->uid : FALSE), $edit);
792
  }
793
}
794

795
/**
796
 * Implementation of hook_user_validate().
797
798
799
 */
function user_user_validate(&$edit, &$account, $category = NULL) {
  if ($category == 'account') {
800
801
802
803
804
805
806
807
808
809
810
    $uid = isset($account->uid) ? $account->uid : FALSE;
    // Validate the username when: new user account; or user is editing own account and can change username; or an admin user.
    if (!$uid || ($GLOBALS['user']->uid == $uid && user_access('change own username')) || user_access('administer users')) {
      if ($error = user_validate_name($edit['name'])) {
        form_set_error('name', $error);
      }
      elseif (db_result(db_query("SELECT COUNT(*) FROM {users} WHERE uid != %d AND LOWER(name) = LOWER('%s')", $uid, $edit['name'])) > 0) {
        form_set_error('name', t('The name %name is already taken.', array('%name' => $edit['name'])));
      }
    }

811
    // Validate the e-mail address, and check if it is taken by an existing user.
812
813
814
815
    if ($error = user_validate_mail($edit['mail'])) {
      form_set_error('mail', $error);
    }
    elseif (db_result(db_query("SELECT COUNT(*) FROM {users} WHERE uid != %d AND LOWER(mail) = LOWER('%s')", $uid, $edit['mail'])) > 0) {
816
817
818
819
820
821
822
      // Format error message dependent on whether the user is logged in or not.
      if ($GLOBALS['user']->uid) {
        form_set_error('mail', t('The e-mail address %email is already taken.', array('%email' => $edit['mail'])));
      }
      else {
        form_set_error('mail', t('The e-mail address %email is already registered. <a href="@password">Have you forgotten your password?</a>', array('%email' => $edit['mail'], '@password' => url('user/password'))));
      }
823
    }
824
  }
825
}
826

827
/**
828
 * Implementation of hook_user_submit().
829
830
831
 */
function user_user_submit(&$edit, &$account, $category = NULL) {
  if ($category == 'account') {
832
833
834
    if (!empty($edit['picture_upload'])) {
      $edit['picture'] = $edit['picture_upload'];
    }
835
    // Delete picture if requested, and if no replacement picture was given.
836
837
    elseif (!empty($edit['picture_delete'])) {
      $edit['picture'] = NULL;
838
    }
839
840
841
842
    // Remove these values so they don't end up serialized in the data field.
    $edit['picture_upload'] = NULL;
    $edit['picture_delete'] = NULL;

843
844
845
    if (isset($edit['roles'])) {
      $edit['roles'] = array_filter($edit['roles']);
    }
846
  }
847
}
848

849
850
851
/**
 * Implementation of hook_user_categories.
 */
852
function user_user_categories($edit, $account, $category = NULL) {
853
    return array(array('name' => 'account', 'title' => t('Account settings'), 'weight' => 1));
Dries's avatar
Dries committed
854
855
}

856
857
function user_login_block() {
  $form = array(
858
    '#action' => url($_GET['q'], array('query' => drupal_get_destination())),
859
    '#id' => 'user-login-form',
860
    '#validate' => user_login_default_validators(),
861
    '#submit' => array('user_login_submit'),
862
863
864
  );
  $form['name'] = array('#type' => 'textfield',
    '#title' => t('Username'),
865
    '#maxlength' => USERNAME_MAX_LENGTH,
866
867
868
869
870
    '#size' => 15,
    '#required' => TRUE,
  );
  $form['pass'] = array('#type' => 'password',
    '#title' => t('Password'),
871
    '#maxlength' => 60,
872
873
874
875
876
877
878
879
    '#size' => 15,
    '#required' => TRUE,
  );
  $form['submit'] = array('#type' => 'submit',
    '#value' => t('Log in'),
  );
  $items = array();
  if (variable_get('user_register', 1)) {
880
    $items[] = l(t('Create new account'), 'user/register', array('attributes' => array('title' => t('Create a new user account.'))));
881
  }
882
  $items[] = l(t('Request new password'), 'user/password', array('attributes' => array('title' => t('Request new password via e-mail.'))));
883
  $form['links'] = array('#markup' => theme('item_list', $items));
884
885
886
  return $form;
}

Dries's avatar
Dries committed
887
/**
888
 * Implementation of hook_block_list().
Dries's avatar
Dries committed
889
 */
890
function user_block_list() {
Dries's avatar
   
Dries committed
891
892
  global $user;

893
894
895
  $blocks['login']['info'] = t('User login');
  // Not worth caching.
  $blocks['login']['cache'] = BLOCK_NO_CACHE;
896

897
898
899
900
  $blocks['navigation']['info'] = t('Navigation');
  // Menu blocks can't be cached because each menu item can have
  // a custom access callback. menu.inc manages its own caching.
  $blocks['navigation']['cache'] = BLOCK_NO_CACHE;
901

902
  $blocks['new']['info'] = t('Who\'s new');
903

904
905
906
907
908
  // Too dynamic to cache.
  $blocks['online']['info'] = t('Who\'s online');
  $blocks['online']['cache'] = BLOCK_NO_CACHE;
  return $blocks;
}
909

910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
/**
 * Implementation of hook_block_configure().
 */
function user_block_configure($delta = '') {
  global $user;

  switch($delta) {
    case 'new':
      $form['user_block_whois_new_count'] = array(
        '#type' => 'select',
        '#title' => t('Number of users to display'),
        '#default_value' => variable_get('user_block_whois_new_count', 5),
        '#options' => drupal_map_assoc(array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)),
      );
      return $form;

    case 'online':
      $period = drupal_map_assoc(array(30, 60, 120, 180, 300, 600, 900, 1800, 2700, 3600, 5400, 7200, 10800, 21600, 43200, 86400), 'format_interval');
      $form['user_block_seconds_online'] = array('#type' => 'select', '#title' => t('User activity'), '#default_value' => variable_get('user_block_seconds_online', 900), '#options' => $period, '#description' => t('A user is considered online for this long after they have last viewed a page.'));
      $form['user_block_max_list_count'] = array('#type' => 'select', '#title' => t('User list length'), '#default_value' => variable_get('user_block_max_list_count', 10), '#options' => drupal_map_assoc(array(0, 5, 10, 15, 20, 25, 30, 40, 50, 75, 100)), '#description' => t('Maximum number of currently online users to display.'));
930
      $form['user_block_cache'] = array('#markup' => '<p>If page caching is disabled, the block shows the number of anonymous and authenticated users, respectively. If page caching is enabled, only the number of authenticated users is displayed.</p>');
931
      return $form;
932
  }
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
}

/**
 * Implementation of hook_block_save().
 */
function user_block_save($delta = '', $edit = array()) {
  global $user;

  switch ($delta) {
    case 'new':
      variable_set('user_block_whois_new_count', $edit['user_block_whois_new_count']);
      break;

    case 'online':
      variable_set('user_block_seconds_online', $edit['user_block_seconds_online']);
      variable_set('user_block_max_list_count', $edit['user_block_max_list_count']);
      break;
950
  }
951
}
Dries's avatar
   
Dries committed
952

953
954
955
956
957
/**
 * Implementation of hook_block_view().
 */
function user_block_view($delta = '') {
  global $user;
Dries's avatar
   
Dries committed
958

959
  $block = array();
Dries's avatar
Dries committed
960

961
962
963
964
  switch ($delta) {
    case 'login':
      // For usability's sake, avoid showing two login forms on one page.
      if (!$user->uid && !(arg(0) == 'user' && !is_numeric(arg(1)))) {
Dries's avatar
Dries committed
965

966
967
968
969
        $block['subject'] = t('User login');
        $block['content'] = drupal_get_form('user_login_block');
      }
      return $block;
Dries's avatar
   
Dries committed
970

971
972
973
974
975
976
    case 'navigation':
      if ($menu = menu_tree()) {
        $block['subject'] = $user->uid ? check_plain($user->name) : t('Navigation');
        $block['content'] = $menu;
      }
      return $block;
Dries's avatar
Dries committed
977

978
979
980
981
982
    case 'new':
      if (user_access('access content')) {
        // Retrieve a list of new users who have subsequently accessed the site successfully.
        $items = db_query_range('SELECT uid, name FROM {users} WHERE status != 0 AND access != 0 ORDER BY created DESC', array(), 0, variable_get('user_block_whois_new_count', 5))->fetchAll();
        $output = theme('user_list', $items);
983

984
985
986
987
        $block['subject'] = t('Who\'s new');
        $block['content'] = $output;
      }
      return $block;
Dries's avatar
   
Dries committed
988

989
990
991
992
    case 'online':
      if (user_access('access content')) {
        // Count users active within the defined period.
        $interval = REQUEST_TIME - variable_get('user_block_seconds_online', 900);
Dries's avatar
   
Dries committed
993

994
        // Perform database queries to gather online user lists. We use s.timestamp
995
996
        // rather than u.access because it is much faster.
        $authenticated_count = db_query("SELECT COUNT(DISTINCT s.uid) FROM {sessions} s WHERE s.timestamp >= :timestamp AND s.uid > 0", array(':timestamp' => $interval))->fetchField();
997

998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
        // When page caching is enabled, sessions are only created for
        // anonymous users when needed.
        if (variable_get('cache', CACHE_DISABLED) == CACHE_DISABLED) {
          $anonymous_count = drupal_session_count($interval);
          // Format the output with proper grammar.
          if ($anonymous_count == 1 && $authenticated_count == 1) {
            $output = t('There is currently %members and %visitors online.', array('%members' => format_plural($authenticated_count, '1 user', '@count users'), '%visitors' => format_plural($anonymous_count, '1 guest', '@count guests')));
          }
          else {
            $output = t('There are currently %members and %visitors online.', array('%members' => format_plural($authenticated_count, '1 user', '@count users'), '%visitors' => format_plural($anonymous_count, '1 guest', '@count guests')));
          }
Dries's avatar
   
Dries committed
1009
        }
1010
        else {
1011
          $output = format_plural($authenticated_count, 'There is currently 1 user online.', 'There are currently @count users online.');
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
        }

        // Display a list of currently online users.
        $max_users = variable_get('user_block_max_list_count', 10);
        if ($authenticated_count && $max_users) {
          $items = db_query_range('SELECT u.uid, u.name, MAX(s.timestamp) AS max_timestamp FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.timestamp >= :interval AND s.uid > 0 GROUP BY u.uid, u.name ORDER BY max_timestamp DESC', array(':interval' => $interval), 0, $max_users)->fetchAll();
          $output .= theme('user_list', $items, t('Online users'));
        }

        $block['subject'] = t('Who\'s online');
        $block['content'] = $output;
      }
      return $block;
Dries's avatar
   
Dries committed
1025
  }
1026
1027
}

1028
1029
1030
1031
/**
 * Process variables for user-picture.tpl.php.
 *
 * The $variables array contains the following arguments:
1032
1033
 * - $account: A user, node or comment object with 'name', 'uid' and 'picture'
 *   fields.
1034
1035
1036
1037
1038
 *
 * @see user-picture.tpl.php
 */
function template_preprocess_user_picture(&$variables) {
  $variables['picture'] = '';
Dries's avatar
   
Dries committed
1039
  if (variable_get('user_pictures', 0)) {
1040
    $account = $variables['account'];
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
    if (!empty($account->picture)) {
      // @TODO: Ideally this function would only be passed file objects, but
      // since there's a lot of legacy code that JOINs the {users} table to
      // {node} or {comments} and passes the results into this function if we
      // a numeric value in the picture field we'll assume it's a file id
      // and load it for them. Once we've got user_load_multiple() and
      // comment_load_multiple() functions the user module will be able to load
      // the picture files in mass during the object's load process.
      if (is_numeric($account->picture)) {
        $account->picture = file_load($account->picture);
      }
      if (!empty($account->picture->filepath)) {
        $filepath = $account->picture->filepath;
      }
Dries's avatar
   
Dries committed
1055
    }
1056
    elseif (variable_get('user_picture_default', '')) {
1057
      $filepath = variable_get('user_picture_default', '');
Dries's avatar
   
Dries committed
1058
    }
1059
    if (isset($filepath)) {
1060
      $alt = t("@user's picture", array('@user' => $account->name ? $account->name : variable_get('anonymous', t('Anonymous'))));
1061
      $variables['picture'] = theme('image', $filepath, $alt, $alt, '', FALSE);