user.module 102 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
 * Implement 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('elements' => 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
    ),
    'user_list' => array(
      'arguments' => array('users' => NULL, 'title' => NULL),
    ),
60
    'user_admin_permissions' => array(
61
      '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
 * Implement hook_fieldable_info().
Dries's avatar
   
Dries committed
88
89
90
91
92
93
94
95
96
97
98
99
 */
function user_fieldable_info() {
  $return = array(
    'user' => array(
      'name' => t('User'),
      'id key' => 'uid',
    ),
  );
  return $return;
}

/**
100
 * Implement hook_field_build_modes().
Dries's avatar
   
Dries committed
101
102
103
104
105
106
107
108
109
110
111
 */
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) {
113
  $uid = db_query("SELECT uid FROM {authmap} WHERE authname = :authname", array(':authname' => $authname))->fetchField();
Dries's avatar
   
Dries committed
114

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

Dries's avatar
Dries committed
123
/**
124
 * Load multiple users based on certain conditions.
Dries's avatar
Dries committed
125
 *
126
127
128
 * This function should be used whenever you need to load more than one user
 * from the database. Users are loaded into memory and will not require
 * database access if loaded again during the same page request.
Dries's avatar
Dries committed
129
 *
130
131
132
133
134
135
136
137
 * @param $uids
 *   An array of user IDs.
 * @param $conditions
 *   An array of conditions to match against the {users} table. These
 *   should be supplied in the form array('field_name' => 'field_value').
 * @param $reset
 *   A boolean indicating that the internal cache should be reset. Use this if
 *   loading a user object which has been altered during the page request.
138
 * @return
139
140
141
142
143
 *   An array of user objects, indexed by uid.
 *
 * @see user_load()
 * @see user_load_by_mail()
 * @see user_load_by_name()
Dries's avatar
Dries committed
144
 */
145
146
147
148
function user_load_multiple($uids = array(), $conditions = array(), $reset = FALSE) {
  static $user_cache = array();
  if ($reset) {
    $user_cache = array();
149
  }
150

151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
  $users = array();

  // Create a new variable which is either a prepared version of the $uids
  // array for later comparison with the user cache, or FALSE if no $uids were
  // passed. The $uids array is reduced as items are loaded from cache, and we
  // need to know if it's empty for this reason to avoid querying the database
  // when all requested users are loaded from cache.
  $passed_uids = !empty($uids) ? array_flip($uids) : FALSE;

  // Load any available users from the internal cache.
  if ($user_cache) {
    if ($uids && !$conditions) {
      $users += array_intersect_key($user_cache, $passed_uids);
      // If any users were loaded, remove them from the $uids still to load.
      $uids = array_keys(array_diff_key($passed_uids, $users));
Dries's avatar
   
Dries committed
166
167
168
    }
  }

169
170
171
172
  // Load any remaining users from the database, this is necessary if we have
  // $uids still to load, or if $conditions was passed without $uids.
  if ($uids || ($conditions && !$passed_uids)) {
    $query = db_select('users', 'u')->fields('u');
Dries's avatar
   
Dries committed
173

174
175
176
    // If the $uids array is populated, add those to the query.
    if ($uids) {
      $query->condition('u.uid', $uids, 'IN');
177
    }
178
179
    // If the conditions array is populated, add those to the query.
    if ($conditions) {
180
181
      // TODO D7: Using LIKE() to get a case insensitive comparison because Crell
      // and chx promise that dbtng will map it to ILIKE in postgres.
182
      if (isset($conditions['name'])) {
183
        $query->condition('u.name', $conditions['name'], 'LIKE');
184
185
186
        unset($conditions['name']);
      }
      if (isset($conditions['mail'])) {
187
        $query->condition('u.mail', $conditions['mail'], 'LIKE');
188
189
190
191
192
        unset($conditions['mail']);
      }
      foreach ($conditions as $field => $value) {
        $query->condition('u.' . $field, $value);
      }
193
    }
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
    $result = $query->execute();

    $queried_users = array();
    // Build an array of user picture IDs so that these can be fetched later.
    $picture_fids = array();
    foreach ($result as $record) {
      $picture_fids[] = $record->picture;
      $queried_users[$record->uid] = drupal_unpack($record);
      $queried_users[$record->uid]->roles = array();
      if ($record->uid) {
        $queried_users[$record->uid]->roles[DRUPAL_AUTHENTICATED_RID] = 'authenticated user';
      }
      else {
        $queried_users[$record->uid]->roles[DRUPAL_ANONYMOUS_RID] = 'anonymous user';
      }
209
    }
210

211
212
213
214
215
216
    if (!empty($queried_users)) {
      // Add any additional roles from the database.
      $result = db_query('SELECT r.rid, r.name, ur.uid FROM {role} r INNER JOIN {users_roles} ur ON ur.rid = r.rid WHERE ur.uid IN (:uids)', array(':uids' => array_keys($queried_users)));
      foreach ($result as $record) {
        $queried_users[$record->uid]->roles[$record->rid] = $record->name;
      }
Dries's avatar
   
Dries committed
217

218
219
220
221
222
223
224
225
226
227
228
229
230
      // Add the full file objects for user pictures if enabled.
      if (!empty($picture_fids) && variable_get('user_pictures', 1) == 1) {
        $pictures = file_load_multiple($picture_fids);
        foreach ($queried_users as $account) {
          if (!empty($account->picture) && isset($pictures[$account->picture])) {
            $account->picture = $pictures[$account->picture];
          }
          else {
            $account->picture = NULL;
          }
        }
      }

231
232
      field_attach_load('user', $queried_users);

233
234
235
236
237
238
      // Invoke hook_user_load() on the users loaded from the database
      // and add them to the static cache.
      foreach (module_implements('user_load') as $module) {
        $function = $module . '_user_load';
        $function($queried_users);
      }
239

240

241
242
243
244

      $users = $users + $queried_users;
      $user_cache = $user_cache + $queried_users;
    }
245
  }
246
247
248
249
250
251
252
253
254
255

  // Ensure that the returned array is ordered the same as the original $uids
  // array if this was passed in and remove any invalid uids.
  if ($passed_uids) {
    // Remove any invalid uids from the array.
    $passed_uids = array_intersect_key($passed_uids, $users);
    foreach ($users as $user) {
      $passed_uids[$user->uid] = $user;
    }
    $users = $passed_uids;
Dries's avatar
   
Dries committed
256
  }
Dries's avatar
   
Dries committed
257

258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
  return $users;
}


/**
 * Fetch a user object.
 *
 * @param $uid
 *   Integer specifying the user id.
 * @param $reset
 *   A boolean indicating that the internal cache should be reset.
 * @return
 *   A fully-loaded $user object upon successful user load or FALSE if user
 *   cannot be loaded.
 *
 * @see user_load_multiple()
 */
function user_load($uid, $reset = FALSE) {
  $users = user_load_multiple(array($uid), array(), $reset);
  return reset($users);
}

/**
 * Fetch a user object by email address.
 *
 * @param $mail
 *   String with the account's e-mail address.
 * @return
 *   A fully-loaded $user object upon successful user load or FALSE if user
 *   cannot be loaded.
 *
 * @see user_load_multiple()
 */
function user_load_by_mail($mail) {
  $users = user_load_multiple(array(), array('mail' => $mail));
  return reset($users);
}

/**
 * Fetch a user object by account name.
 *
 * @param $name
 *   String with the account's user name.
 * @return
 *   A fully-loaded $user object upon successful user load or FALSE if user
 *   cannot be loaded.
 *
 * @see user_load_multiple()
 */
function user_load_by_name($name) {
  $users = user_load_multiple(array(), array('name' => $name));
  return reset($users);
Dries's avatar
   
Dries committed
310
311
}

312
/**
313
 * Save changes to a user account or add a new user.
314
315
 *
 * @param $account
316
317
318
319
320
321
 *   (optional) The user object to modify or add. If you want to modify
 *   an existing user account, you will need to ensure that (a) $account 
 *   is an object, and (b) you have set $account->uid to the numeric 
 *   user ID of the user account you wish to modify. If you
 *   want to create a new user account, you can set $account->is_new to
 *   TRUE or omit the $account->uid field. 
322
 * @param $edit
323
 *   An array of fields and values to save. For example array('name'
324
 *   => 'My name'). Keys that do not belong to columns in the user-related
325
326
 *   tables are added to the a serialized array in the 'data' column
 *   and will be loaded in the $user->data array by user_load().
327
328
 *   Setting a field to NULL deletes it from the data column, if you are
 *   modifying an existing user account.
329
330
 * @param $category
 *   (optional) The category for storing profile information in.
331
332
 *
 * @return
333
 *   A fully-loaded $user object upon successful save or FALSE if the save failed.
334
 */
335
function user_save($account, $edit = array(), $category = 'account') {
336
  $table = drupal_get_schema('users');
337
338
  $user_fields = $table['fields'];

339
  if (!empty($edit['pass'])) {
340
    // Allow alternate password hashing schemes.
341
    require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc');
342
    $edit['pass'] = user_hash_password(trim($edit['pass']));
343
    // Abort if the hashing failed and returned FALSE.
344
    if (!$edit['pass']) {
345
346
      return FALSE;
    }
347
348
349
  }
  else {
    // Avoid overwriting an existing password with a blank password.
350
    unset($edit['pass']);
351
352
  }

Dries's avatar
   
Dries committed
353
354
355
356
357
358
359
360
361
362
363
364
  // 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;

365
  if (!isset($account->is_new)) {
366
    $account->is_new = empty($account->uid);
367
368
  }
  if (is_object($account) && !$account->is_new) {
369
    user_module_invoke('update', $edit, $account, $category);
370
    $data = unserialize(db_query('SELECT data FROM {users} WHERE uid = :uid', array(':uid' => $account->uid))->fetchField());
371
372
    // Consider users edited by an administrator as logged in, if they haven't
    // already, so anonymous users can view the profile (if allowed).
373
    if (empty($edit['access']) && empty($account->access) && user_access('administer users')) {
374
      $edit['access'] = REQUEST_TIME;
375
    }
376
    foreach ($edit as $key => $value) {
Dries's avatar
   
Dries committed
377
      // Form fields that don't pertain to the users, user_roles, or
378
      // Field API are automatically serialized into the users.data
Dries's avatar
   
Dries committed
379
      // column.
380
      if (!in_array($key, array('roles', 'is_new')) && empty($user_fields[$key]) && empty($field_form[$key])) {
381
382
        if ($value === NULL) {
          unset($data[$key]);
Dries's avatar
   
Dries committed
383
        }
384
385
        else {
          $data[$key] = $value;
Dries's avatar
   
Dries committed
386
        }
Dries's avatar
   
Dries committed
387
388
389
      }
    }

390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406

    // 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;

407
408
    $edit['data'] = $data;
    $edit['uid'] = $account->uid;
409
    // Save changes to the user table.
410
    $success = drupal_write_record('users', $edit, 'uid');
411
    if (!$success) {
Dries's avatar
   
Dries committed
412
413
414
415
      // 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
416
      // the user table are changed, drupal_write_record returns
Dries's avatar
   
Dries committed
417
      // FALSE because rowCount() (rows changed) is 0.  However,
418
      // non-users data may have been changed, e.g. fields.
Dries's avatar
   
Dries committed
419
      // return FALSE;
420
    }
Dries's avatar
   
Dries committed
421

422
    // If the picture changed or was unset, remove the old one. This step needs
423
    // to occur after updating the {users} record so that user_file_references()
424
425
426
427
428
    // doesn't report it in use and block the deletion.
    if (!empty($account->picture->fid) && ($edit['picture'] != $account->picture->fid)) {
      file_delete($account->picture);
    }

429
    // Reload user roles if provided.
430
    if (isset($edit['roles']) && is_array($edit['roles'])) {
431
432
433
      db_delete('users_roles')
        ->condition('uid', $account->uid)
        ->execute();
Dries's avatar
   
Dries committed
434

435
      $query = db_insert('users_roles')->fields(array('uid', 'rid'));
436
      foreach (array_keys($edit['roles']) as $rid) {
437
        if (!in_array($rid, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) {
438
439
440
441
          $query->values(array(
            'uid' => $account->uid,
            'rid' => $rid,
          ));
442
        }
443
      }
444
      $query->execute();
Dries's avatar
   
Dries committed
445
446
    }

447
    // Delete a blocked user's sessions to kick them if they are online.
448
    if (isset($edit['status']) && $edit['status'] == 0) {
449
      drupal_session_destroy_uid($account->uid);
450
451
    }

452
453
    // If the password changed, delete all open sessions and recreate
    // the current one.
454
    if (!empty($edit['pass'])) {
455
      drupal_session_destroy_uid($account->uid);
456
457
458
      if ($account->uid == $GLOBALS['user']->uid) {
        drupal_session_regenerate();
      }
459
460
    }

Dries's avatar
   
Dries committed
461
    // Save Field data.
462
463
    $object = (object) $edit;
    field_attach_update('user', $object);
Dries's avatar
   
Dries committed
464

465
    // Refresh user object.
466
    $user = user_load($account->uid, TRUE);
467
468

    // Send emails after we have the new user object.
469
    if (isset($edit['status']) && $edit['status'] != $account->status) {
470
      // The user's status is changing; conditionally send notification email.
471
      $op = $edit['status'] == 1 ? 'status_activated' : 'status_blocked';
472
473
474
      _user_mail_notify($op, $user);
    }

475
    user_module_invoke('after_update', $edit, $user, $category);
Dries's avatar
   
Dries committed
476
477
  }
  else {
478
    // Allow 'created' to be set by the caller.
479
    if (!isset($edit['created'])) {
480
      $edit['created'] = REQUEST_TIME;
481
    }
482
483
    // Consider users created by an administrator as already logged in, so
    // anonymous users can view the profile (if allowed).
484
    if (empty($edit['access']) && user_access('administer users')) {
485
      $edit['access'] = REQUEST_TIME;
486
    }
487

488
    $edit['mail'] = trim($edit['mail']);
489
    $success = drupal_write_record('users', $edit);
490
491
492
493
494
    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;
    }
495

496
    // Build the initial user object.
497
    $user = user_load($edit['uid'], TRUE);
Dries's avatar
   
Dries committed
498

499
500
    $object = (object) $edit;
    field_attach_insert('user', $object);
Dries's avatar
   
Dries committed
501

502
    user_module_invoke('insert', $edit, $user, $category);
503

504
505
    // Note, we wait with saving the data column to prevent module-handled
    // fields from being saved there.
506
    $data = array();
507
    foreach ($edit as $key => $value) {
508
      // Form fields that don't pertain to the users, user_roles, or
509
      // Field API are automatically serialized into the user.data
Dries's avatar
   
Dries committed
510
      // column.
511
      if ((!in_array($key, array('roles', 'is_new'))) && (empty($user_fields[$key]) && empty($field_form[$key])) && ($value !== NULL)) {
512
513
514
        $data[$key] = $value;
      }
    }
515
516
    if (!empty($data)) {
      $data_array = array('uid' => $user->uid, 'data' => $data);
517
      drupal_write_record('users', $data_array, 'uid');
518
    }
519

520
    // Save user roles (delete just to be safe).
521
    if (isset($edit['roles']) && is_array($edit['roles'])) {
522
523
524
525
      db_delete('users_roles')
        ->condition('uid', $edit['uid'])
        ->execute();
      $query = db_insert('users_roles')->fields(array('uid', 'rid'));
526
      foreach (array_keys($edit['roles']) as $rid) {
527
        if (!in_array($rid, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) {
528
529
530
531
          $query->values(array(
            'uid' => $edit['uid'],
            'rid' => $rid,
          ));
532
        }
533
      }
534
      $query->execute();
535
536
    }

537
    // Build the finished user object.
538
    $user = user_load($edit['uid'], TRUE);
Dries's avatar
   
Dries committed
539
540
541
542
543
  }

  return $user;
}

Dries's avatar
Dries committed
544
545
546
/**
 * Verify the syntax of the given name.
 */
Dries's avatar
   
Dries committed
547
function user_validate_name($name) {
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
  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.');
  }
563
  if (preg_match('/[\x{80}-\x{A0}' .         // Non-printable ISO-8859-1 + NBSP
564
565
566
567
568
569
570
571
572
                  '\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
                  '\x{0}-\x{1F}]/u',        // NULL byte and control characters
                  $name)) {
573
574
    return t('The username contains an illegal character.');
  }
575
576
577
  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
578
579
580
}

function user_validate_mail($mail) {
581
  $mail = trim($mail);
582
583
584
  if (!$mail) {
    return t('You must enter an e-mail address.');
  }
585
  if (!valid_email_address($mail)) {
586
    return t('The e-mail address %mail is not valid.', array('%mail' => $mail));
Dries's avatar
   
Dries committed
587
588
589
  }
}

590
function user_validate_picture(&$form, &$form_state) {
591
  // If required, validate the uploaded picture.
592
593
594
595
596
  $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),
  );
597

598
599
600
601
602
603
604
  // 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
605
606
607
  }
}

Dries's avatar
Dries committed
608
609
610
/**
 * Generate a random alphanumeric password.
 */
Dries's avatar
   
Dries committed
611
612
function user_password($length = 10) {
  // This variable contains the list of allowable characters for the
613
614
  // password. Note that the number 0 and the letter 'O' have been
  // removed to avoid confusion between the two. The same is true
615
  // of 'I', 1, and 'l'.
616
  $allowable_characters = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';
Dries's avatar
Dries committed
617

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

Dries's avatar
Dries committed
621
622
  // Declare the password as a blank string.
  $pass = '';
Dries's avatar
   
Dries committed
623

Dries's avatar
Dries committed
624
  // Loop the number of times specified by $length.
Dries's avatar
   
Dries committed
625
626
627
628
  for ($i = 0; $i < $length; $i++) {

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

  return $pass;
Dries's avatar
   
Dries committed
633
634
}

635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
/**
 * 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.
673
      $result = db_query("SELECT rid, permission FROM {role_permission} WHERE rid IN (:fetch)", array(':fetch' => $fetch));
674

675
676
      foreach ($result as $row) {
        $stored_permissions[$row->rid][$row->permission] = TRUE;
677
678
679
680
681
682
683
684
685
686
687
      }
      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
688
689
690
691
692
/**
 * Determine whether the user has a given privilege.
 *
 * @param $string
 *   The permission, such as "administer nodes", being checked for.
Dries's avatar
   
Dries committed
693
694
 * @param $account
 *   (optional) The account to check, if not given use currently logged in user.
695
696
697
698
 * @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
699
700
 *
 * @return
701
 *   Boolean TRUE if the current user has the requested permission.
Dries's avatar
Dries committed
702
703
704
705
706
 *
 * 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.
 */
707
function user_access($string, $account = NULL, $reset = FALSE) {
Dries's avatar
   
Dries committed
708
  global $user;
Dries's avatar
   
Dries committed
709
  static $perm = array();
Dries's avatar
   
Dries committed
710

711
  if ($reset) {
712
    $perm = array();
713
714
  }

715
716
717
718
  if (is_null($account)) {
    $account = $user;
  }

719
  // User #1 has all privileges:
720
  if ($account->uid == 1) {
721
    return TRUE;
Dries's avatar
   
Dries committed
722
723
  }

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

729
    $perms = array();
730
731
    foreach ($role_permissions as $one_role) {
      $perms += $one_role;
Dries's avatar
   
Dries committed
732
    }
733
    $perm[$account->uid] = $perms;
Dries's avatar
   
Dries committed
734
  }
735

736
  return isset($perm[$account->uid][$string]);
Dries's avatar
   
Dries committed
737
738
}

739
/**
740
 * Checks for usernames blocked by user administration.
741
 *
742
 * @return boolean TRUE for blocked users, FALSE for active.
743
744
 */
function user_is_blocked($name) {
745
  $deny = db_query("SELECT name FROM {users} WHERE status = 0 AND name = LOWER(:name)", array(':name' => $name))->fetchObject();
746

747
  return $deny;
748
749
}

Dries's avatar
Dries committed
750
/**
751
 * Implement hook_permission().
Dries's avatar
Dries committed
752
 */
753
function user_permission() {
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
  return array(
    '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.'),
    ),
    'cancel account' => array(
      'title' => t('Cancel account'),
773
      '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/settings/user'))),
774
775
776
777
778
779
    ),
    '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.'))),
    ),
  );
Dries's avatar
   
Dries committed
780
781
}

Dries's avatar
Dries committed
782
/**
783
 * Implement hook_file_download().
Dries's avatar
Dries committed
784
785
786
 *
 * Ensure that user pictures (avatars) are always downloadable.
 */
787
788
789
function user_file_download($filepath) {
  if (strpos($filepath, variable_get('user_picture_path', 'pictures') . '/picture-') === 0) {
    $info = image_get_info(file_create_path($filepath));
790
    return array('Content-Type' => $info['mime_type']);
Dries's avatar
   
Dries committed
791
792
793
  }
}

794
/**
795
 * Implement hook_file_references().
796
797
798
 */
function user_file_references($file) {
  // Determine if the file is used by this module.
799
800
  $file_used = (bool) db_query_range('SELECT 1 FROM {users} WHERE picture = :fid', array(':fid' => $file->fid), 0, 1)->fetchField();
  if ($file_used) {
801
802
803
804
805
806
    // Return the name of the module and how many references it has to the file.
    return array('user' => $count);
  }
}

/**
807
 * Implement hook_file_delete().
808
809
810
 */
function user_file_delete($file) {
  // Remove any references to the file.
811
  db_update('users')
812
813
814
815
816
    ->fields(array('picture' => 0))
    ->condition('picture', $file->fid)
    ->execute();
}

Dries's avatar
Dries committed
817
/**
818
 * Implement hook_search().
Dries's avatar
Dries committed
819
 */
820
function user_search($op = 'search', $keys = NULL, $skip_access_check = FALSE) {
821
822
  switch ($op) {
    case 'name':
823
      if ($skip_access_check || user_access('access user profiles')) {
824
        return t('Users');
825
      }
826
    case 'search':
827
828
829
830
      if (user_access('access user profiles')) {
        $find = array();
        // Replace wildcards with MySQL/PostgreSQL wildcards.
        $keys = preg_replace('!\*+!', '%', $keys);
831
        $query = db_select('users')->extend('PagerDefault');
832
        $query->fields('users', array('name', 'uid', 'mail'));
833
834
        if (user_access('administer users')) {
          // Administrators can also search in the otherwise private email field.
835
          $query->condition(db_or()->
836
837
            where('LOWER(name) LIKE LOWER(:name)', array(':name' => "%$keys%"))->
            where('LOWER(mail) LIKE LOWER(:mail)', array(':mail' => "%$keys%")));
838
839
        }
        else {
840
841
          $query->where('LOWER(name) LIKE LOWER(:name)', array(':name' => "%$keys%"));
        }
842
843
844
        $result = $query
          ->limit(15)
          ->execute();
845
846
        foreach ($result as $account) {
          $find[] = array('title' => $account->name . ' (' . $account->mail . ')', 'link' => url('user/' . $account->uid, array('absolute' => TRUE)));
847
848
        }
        return $find;
849
      }
Dries's avatar
   
Dries committed
850
851
852
  }
}

Dries's avatar
Dries committed
853
/**
854
 * Implement hook_elements().
Dries's avatar
Dries committed
855
856
857
 */
function user_elements() {
  return array(
858
859
860
861
862
863
    'user_profile_category' => array(
      '#theme_wrapper' => 'user_profile_category'
    ),
    'user_profile_item' => array(
      '#theme' => 'user_profile_item'
    ),
Dries's avatar
Dries committed
864
865
866
  );
}

Dries's avatar
Dries committed
867
/**
868
 * Implement hook_user_view().
Dries's avatar
Dries committed
869
 */
870
871
function user_user_view(&$edit, &$account, $category = NULL) {
  $account->content['user_picture'] = array(
872
    '#markup' => theme('user_picture', $account),
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
    '#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),
  );
}

/**
892
 * Implement hook_user_form.
893
894
895
 */
function user_user_form(&$edit, &$account, $category = NULL) {
  if ($category == 'account') {
896
    $form_state = array();
897
    return user_edit_form($form_state, (isset($account->uid) ? $account->uid : FALSE), $edit);
898
  }
899
}
900

901
/**
902
 * Implement hook_user_validate().
903
904
905
 */
function user_user_validate(&$edit, &$account, $category = NULL) {
  if ($category == 'account') {
906
907
908
909
910
911
    $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);
      }
912
      elseif ((bool) db_query_range("SELECT 1 FROM {users} WHERE uid <> :uid AND LOWER(name) = LOWER(:name)", array(':uid' => $uid, ':name' => $edit['name']), 0, 1)->fetchField()) {
913
914
915
916
        form_set_error('name', t('The name %name is already taken.', array('%name' => $edit['name'])));
      }
    }

917
    // Validate the e-mail address, and check if it is taken by an existing user.
918
919
920
    if ($error = user_validate_mail($edit['mail'])) {
      form_set_error('mail', $error);
    }
921
    elseif ((bool) db_query_range("SELECT 1 FROM {users} WHERE uid <> :uid AND LOWER(mail) = LOWER(:mail)", array(':uid' => $uid, ':mail' => $edit['mail']), 0, 1)->fetchField()) {
922
923
924
925
926
927
928
      // 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'))));
      }
929
    }
930
931
932
933
934
935
936
937
938

    // Make sure the signature isn't longer than the size of the database field.
    // Signatures are disabled by default, so make sure it exists first.
    if (isset($edit['signature'])) {
      $user_schema = drupal_get_schema('users');
      if (strlen($edit['signature']) > $user_schema['fields']['signature']['length']) {
        form_set_error('signature', t('The signature is too long: it must be %max characters or less.', array('%max' => $user_schema['fields']['signature']['length'])));
      }
    }
939
  }
940
}
941

942
/**
943
 * Implement hook_user_submit().
944
945
946
 */
function user_user_submit(&$edit, &$account, $category = NULL) {
  if ($category == 'account') {
947
948
949
    if (!empty($edit['picture_upload'])) {
      $edit['picture'] = $edit['picture_upload'];
    }
950
    // Delete picture if requested, and if no replacement picture was given.
951
952
    elseif (!empty($edit['picture_delete'])) {
      $edit['picture'] = NULL;
953
    }
954
955
956
957
    // Remove these values so they don't end up serialized in the data field.
    $edit['picture_upload'] = NULL;
    $edit['picture_delete'] = NULL;

958
959
960
    if (isset($edit['roles'])) {
      $edit['roles'] = array_filter($edit['roles']);
    }
961
  }
962
}
963

964
/**
965
 * Implement hook_user_categories().
966
 */
967
968
969
970
971
972
function user_user_categories() {
  return array(array(
    'name' => 'account',
    'title' => t('Account settings'),
    'weight' => 1,
  ));
Dries's avatar
Dries committed
973
974
}

975
976
function user_login_block() {
  $form = array(
977
    '#action' => url($_GET['q'], array('query' => drupal_get_destination())),
978
    '#id' => 'user-login-form',
979
    '#validate' => user_login_default_validators(),
980
    '#submit' => array('user_login_submit'),
981
982
983
  );
  $form['name'] = array('#type' => 'textfield',
    '#title' => t('Username'),
984
    '#maxlength' => USERNAME_MAX_LENGTH,
985
986
987
988
989
    '#size' => 15,
    '#required' => TRUE,
  );
  $form['pass'] = array('#type' => 'password',
    '#title' => t('Password'),
990
    '#maxlength' => 60,
991
992
993
994
995
996
997
998
    '#size' => 15,
    '#required' => TRUE,
  );
  $form['submit'] = array('#type' => 'submit',
    '#value' => t('Log in'),
  );
  $items = array();
  if (variable_get('user_register', 1)) {
999
    $items[] = l(t('Create new account'), 'user/register', array('attributes' => array('title' => t('Create a new user account.'))));
1000
  }
1001
  $items[] = l(t('Request new password'), 'user/password', array('attributes' => array('title' => t('Request new password via e-mail.'))));
1002
  $form['links'] = array('#markup' => theme('item_list', $items));
1003
1004
1005
  return $form;
}

Dries's avatar
Dries committed
1006
/**
1007
 * Implement hook_block_list().
Dries's avatar
Dries committed
1008
 */
1009
function user_block_list() {
Dries's avatar
   
Dries committed
1010
1011
  global $user;

1012
1013
1014
  $blocks['login']['info'] = t('User login');
  // Not worth caching.
  $blocks['login']['cache'] = BLOCK_NO_CACHE;
1015

1016
  $blocks['new']['info'] = t('Who\'s new');
1017

1018
1019
1020
1021
1022
  // Too dynamic to cache.
  $blocks['online']['info'] = t('Who\'s online');
  $blocks['online']['cache'] = BLOCK_NO_CACHE;
  return $blocks;
}
1023

1024
/**
1025
 * Implement hook_block_configure().
1026
1027
1028
1029
 */
function user_block_configure($delta = '') {
  global $user;

1030
  switch ($delta) {
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
    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' =&