user.module 96.4 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
function user_external_load($authname) {
Dries's avatar
 
Dries committed
87
  $result = db_query("SELECT uid FROM {authmap} WHERE authname = '%s'", $authname);
Dries's avatar
 
Dries committed
88

89
  if ($user = db_fetch_array($result)) {
Dries's avatar
 
Dries committed
90
    return user_load($user);
Dries's avatar
 
Dries committed
91 92
  }
  else {
93
    return FALSE;
Dries's avatar
 
Dries committed
94 95 96
  }
}

97
/**
98 99 100
 * Perform standard Drupal login operations for a user object.
 *
 * The user object must already be authenticated. This function verifies
101
 * that the user account is not blocked and then performs the login,
102
 * updates the login timestamp in the database, invokes hook_user('login'),
103
 * and regenerates the session.
104 105 106 107 108 109
 *
 * @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.
110
 *    This array is passed to hook_user op login.
111 112 113 114
 * @return boolean
 *    TRUE if the login succeeds, FALSE otherwise.
 */
function user_external_login($account, $edit = array()) {
115 116
  $form = drupal_get_form('user_login');

117 118 119 120 121
  $state['values'] = $edit;
  if (empty($state['values']['name'])) {
    $state['values']['name'] = $account->name;
  }

122
  // Check if user is blocked.
123 124
  user_login_name_validate($form, $state, (array)$account);
  if (form_get_errors()) {
125
    // Invalid login.
126 127
    return FALSE;
  }
128

129
  // Valid login.
130 131
  global $user;
  $user = $account;
132
  user_authenticate_finalize($state['values']);
133 134 135
  return TRUE;
}

Dries's avatar
Dries committed
136 137 138 139 140
/**
 * Fetch a user object.
 *
 * @param $array
 *   An associative array of attributes to search for in selecting the
141
 *   user, such as user name or e-mail address.
Dries's avatar
Dries committed
142
 *
143 144 145
 * @return
 *   A fully-loaded $user object upon successful user load or FALSE if user
 *   cannot be loaded.
Dries's avatar
Dries committed
146
 */
Dries's avatar
 
Dries committed
147
function user_load($array = array()) {
Dries's avatar
Dries committed
148
  // Dynamically compose a SQL query:
149
  $query = array();
150
  $params = array();
151

152 153 154
  if (is_numeric($array)) {
    $array = array('uid' => $array);
  }
155 156 157
  elseif (!is_array($array)) {
    return FALSE;
  }
158

Dries's avatar
 
Dries committed
159
  foreach ($array as $key => $value) {
160 161
    if ($key == 'uid' || $key == 'status') {
      $query[] = "$key = %d";
162
      $params[] = $value;
163
    }
164
    elseif ($key == 'pass') {
165
      $query[] = "pass = '%s'";
166
      $params[] = $value;
167
    }
Dries's avatar
 
Dries committed
168
    else {
169
      $query[]= "LOWER($key) = LOWER('%s')";
170
      $params[] = $value;
Dries's avatar
 
Dries committed
171 172
    }
  }
173
  $result = db_query('SELECT * FROM {users} u WHERE ' . implode(' AND ', $query), $params);
Dries's avatar
 
Dries committed
174

175
  if ($user = db_fetch_object($result)) {
176
    $user = drupal_unpack($user);
Dries's avatar
 
Dries committed
177

178
    $user->roles = array();
179 180 181 182 183 184
    if ($user->uid) {
      $user->roles[DRUPAL_AUTHENTICATED_RID] = 'authenticated user';
    }
    else {
      $user->roles[DRUPAL_ANONYMOUS_RID] = 'anonymous user';
    }
185 186 187 188
    $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;
    }
189 190 191 192 193 194 195 196

    if (!empty($user->picture) && ($file = file_load($user->picture))) {
      $user->picture = $file;
    }
    else {
      $user->picture = NULL;
    }

197
    user_module_invoke('load', $array, $user);
198 199
  }
  else {
200
    $user = FALSE;
Dries's avatar
 
Dries committed
201
  }
Dries's avatar
 
Dries committed
202 203 204 205

  return $user;
}

206
/**
207
 * Save changes to a user account or add a new user.
208 209
 *
 * @param $account
210 211
 *   The $user object for the user to modify or add. If $user->uid is
 *   omitted, a new user will be added.
212
 *
213
 * @param $edit
214
 *   An array of fields and values to save. For example array('name'
215
 *   => 'My name'). Keys that do not belong to columns in the user-related
216 217 218
 *   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.
219 220 221
 *
 * @param $category
 *   (optional) The category for storing profile information in.
222 223
 *
 * @return
224
 *   A fully-loaded $user object upon successful save or FALSE if the save failed.
225
 */
226
function user_save($account, $edit = array(), $category = 'account') {
227 228 229
  $table = drupal_get_schema('users');
  $user_fields = $table['fields'];

230
  if (!empty($edit['pass'])) {
231
    // Allow alternate password hashing schemes.
232
    require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc');
233
    $edit['pass'] = user_hash_password(trim($edit['pass']));
234
    // Abort if the hashing failed and returned FALSE.
235
    if (!$edit['pass']) {
236 237
      return FALSE;
    }
238 239 240
  }
  else {
    // Avoid overwriting an existing password with a blank password.
241
    unset($edit['pass']);
242 243
  }

244
  if (is_object($account) && $account->uid) {
245
    user_module_invoke('update', $edit, $account, $category);
Dries's avatar
Dries committed
246
    $data = unserialize(db_result(db_query('SELECT data FROM {users} WHERE uid = %d', $account->uid)));
247 248
    // Consider users edited by an administrator as logged in, if they haven't
    // already, so anonymous users can view the profile (if allowed).
249
    if (empty($edit['access']) && empty($account->access) && user_access('administer users')) {
250
      $edit['access'] = REQUEST_TIME;
251
    }
252
    foreach ($edit as $key => $value) {
253 254 255
      // Fields that don't pertain to the users or user_roles
      // automatically serialized into the users.data column.
      if ($key != 'roles' && empty($user_fields[$key])) {
256 257
        if ($value === NULL) {
          unset($data[$key]);
Dries's avatar
 
Dries committed
258
        }
259 260
        else {
          $data[$key] = $value;
Dries's avatar
 
Dries committed
261
        }
Dries's avatar
 
Dries committed
262 263 264
      }
    }

265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281

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

282 283
    $edit['data'] = $data;
    $edit['uid'] = $account->uid;
284
    // Save changes to the users table.
285 286 287 288 289
    $success = drupal_write_record('users', $edit, 'uid');
    if (!$success) {
      // The query failed - better to abort the save than risk further data loss.
      return FALSE;
    }
Dries's avatar
 
Dries committed
290

291 292 293 294 295 296 297
    // 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);
    }

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

302
      foreach (array_keys($edit['roles']) as $rid) {
303 304 305
        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);
        }
306
      }
Dries's avatar
 
Dries committed
307 308
    }

309
    // Delete a blocked user's sessions to kick them if they are online.
310
    if (isset($edit['status']) && $edit['status'] == 0) {
311
      drupal_session_destroy_uid($account->uid);
312 313
    }

314 315
    // If the password changed, delete all open sessions and recreate
    // the current one.
316
    if (!empty($edit['pass'])) {
317
      drupal_session_destroy_uid($account->uid);
318 319 320
      if ($account->uid == $GLOBALS['user']->uid) {
        drupal_session_regenerate();
      }
321 322
    }

323
    // Refresh user object.
Dries's avatar
 
Dries committed
324
    $user = user_load(array('uid' => $account->uid));
325 326

    // Send emails after we have the new user object.
327
    if (isset($edit['status']) && $edit['status'] != $account->status) {
328
      // The user's status is changing; conditionally send notification email.
329
      $op = $edit['status'] == 1 ? 'status_activated' : 'status_blocked';
330 331 332
      _user_mail_notify($op, $user);
    }

333
    user_module_invoke('after_update', $edit, $user, $category);
Dries's avatar
 
Dries committed
334 335
  }
  else {
336
    // Allow 'created' to be set by the caller.
337
    if (!isset($edit['created'])) {
338
      $edit['created'] = REQUEST_TIME;
339
    }
340 341
    // Consider users created by an administrator as already logged in, so
    // anonymous users can view the profile (if allowed).
342
    if (empty($edit['access']) && user_access('administer users')) {
343
      $edit['access'] = REQUEST_TIME;
344
    }
345

346 347 348 349 350 351
    $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;
    }
352

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

356
    user_module_invoke('insert', $edit, $user, $category);
357

358 359
    // Note, we wait with saving the data column to prevent module-handled
    // fields from being saved there.
360
    $data = array();
361
    foreach ($edit as $key => $value) {
362
      if (($key != 'roles') && (empty($user_fields[$key])) && ($value !== NULL)) {
363 364 365
        $data[$key] = $value;
      }
    }
366 367 368 369
    if (!empty($data)) {
      $data_array = array('uid' => $user->uid, 'data' => $data);
      drupal_write_record('users', $data_array, 'uid');
    }
370

371
    // Save user roles (delete just to be safe).
372 373 374
    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) {
375
        if (!in_array($rid, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) {
376
          db_query('INSERT INTO {users_roles} (uid, rid) VALUES (%d, %d)', $edit['uid'], $rid);
377
        }
378 379 380
      }
    }

381
    // Build the finished user object.
382
    $user = user_load(array('uid' => $edit['uid']));
Dries's avatar
 
Dries committed
383 384 385 386 387
  }

  return $user;
}

Dries's avatar
Dries committed
388 389 390
/**
 * Verify the syntax of the given name.
 */
Dries's avatar
 
Dries committed
391
function user_validate_name($name) {
392 393 394 395 396 397 398 399 400 401 402 403 404 405 406
  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.');
  }
407 408 409 410 411 412 413 414
  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
415
                   '\x{0}-\x{1F}]/u',        // NULL byte and control characters
416 417 418
                   $name)) {
    return t('The username contains an illegal character.');
  }
419 420 421
  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
422 423 424
}

function user_validate_mail($mail) {
425 426 427
  if (!$mail) {
    return t('You must enter an e-mail address.');
  }
428
  if (!valid_email_address($mail)) {
429
    return t('The e-mail address %mail is not valid.', array('%mail' => $mail));
Dries's avatar
 
Dries committed
430 431 432
  }
}

433
function user_validate_picture(&$form, &$form_state) {
434
  // If required, validate the uploaded picture.
435 436 437 438 439
  $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),
  );
440

441 442 443 444 445 446 447
  // 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
448 449 450
  }
}

Dries's avatar
Dries committed
451 452 453
/**
 * Generate a random alphanumeric password.
 */
Dries's avatar
 
Dries committed
454 455
function user_password($length = 10) {
  // This variable contains the list of allowable characters for the
456 457
  // password. Note that the number 0 and the letter 'O' have been
  // removed to avoid confusion between the two. The same is true
458
  // of 'I', 1, and 'l'.
459
  $allowable_characters = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';
Dries's avatar
Dries committed
460

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

Dries's avatar
Dries committed
464 465
  // Declare the password as a blank string.
  $pass = '';
Dries's avatar
 
Dries committed
466

Dries's avatar
Dries committed
467
  // Loop the number of times specified by $length.
Dries's avatar
 
Dries committed
468 469 470 471
  for ($i = 0; $i < $length; $i++) {

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

  return $pass;
Dries's avatar
 
Dries committed
476 477
}

478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515
/**
 * 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.
516
      $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));
517 518 519 520 521 522 523 524 525 526 527 528 529 530

      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
531 532 533 534 535
/**
 * Determine whether the user has a given privilege.
 *
 * @param $string
 *   The permission, such as "administer nodes", being checked for.
Dries's avatar
 
Dries committed
536 537
 * @param $account
 *   (optional) The account to check, if not given use currently logged in user.
538 539 540 541
 * @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
542 543
 *
 * @return
544
 *   Boolean TRUE if the current user has the requested permission.
Dries's avatar
Dries committed
545 546 547 548 549
 *
 * 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.
 */
550
function user_access($string, $account = NULL, $reset = FALSE) {
Dries's avatar
 
Dries committed
551
  global $user;
Dries's avatar
 
Dries committed
552
  static $perm = array();
Dries's avatar
 
Dries committed
553

554
  if ($reset) {
555
    $perm = array();
556 557
  }

558 559 560 561
  if (is_null($account)) {
    $account = $user;
  }

562
  // User #1 has all privileges:
563
  if ($account->uid == 1) {
564
    return TRUE;
Dries's avatar
 
Dries committed
565 566
  }

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

572
    $perms = array();
573 574
    foreach ($role_permissions as $one_role) {
      $perms += $one_role;
Dries's avatar
 
Dries committed
575
    }
576
    $perm[$account->uid] = $perms;
Dries's avatar
 
Dries committed
577
  }
578

579
  return isset($perm[$account->uid][$string]);
Dries's avatar
 
Dries committed
580 581
}

582
/**
583
 * Checks for usernames blocked by user administration.
584
 *
585
 * @return boolean TRUE for blocked users, FALSE for active.
586 587
 */
function user_is_blocked($name) {
588
  $deny = db_fetch_object(db_query("SELECT name FROM {users} WHERE status = 0 AND name = LOWER('%s')", $name));
589

590
  return $deny;
591 592
}

Dries's avatar
Dries committed
593 594 595
/**
 * Implementation of hook_perm().
 */
Dries's avatar
 
Dries committed
596
function user_perm() {
597
   return array(
598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613
     '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.'),
     ),
614 615 616 617 618 619 620 621
     '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.'))),
     ),
622
   );
Dries's avatar
 
Dries committed
623 624
}

Dries's avatar
Dries committed
625 626 627 628 629
/**
 * Implementation of hook_file_download().
 *
 * Ensure that user pictures (avatars) are always downloadable.
 */
630 631 632
function user_file_download($filepath) {
  if (strpos($filepath, variable_get('user_picture_path', 'pictures') . '/picture-') === 0) {
    $info = image_get_info(file_create_path($filepath));
633
    return array('Content-type: ' . $info['mime_type']);
Dries's avatar
 
Dries committed
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
/**
 * 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
660 661 662
/**
 * Implementation of hook_search().
 */
663
function user_search($op = 'search', $keys = NULL, $skip_access_check = FALSE) {
664 665
  switch ($op) {
    case 'name':
666
      if ($skip_access_check || user_access('access user profiles')) {
667
        return t('Users');
668
      }
669
    case 'search':
670 671 672 673
      if (user_access('access user profiles')) {
        $find = array();
        // Replace wildcards with MySQL/PostgreSQL wildcards.
        $keys = preg_replace('!\*+!', '%', $keys);
674 675 676 677
        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)) {
678
            $find[] = array('title' => $account->name . ' (' . $account->mail . ')', 'link' => url('user/' . $account->uid, array('absolute' => TRUE)));
679 680 681
          }
        }
        else {
682
          $result = pager_query("SELECT name, uid FROM {users} WHERE LOWER(name) LIKE LOWER('%%%s%%')", 15, 0, NULL, $keys);
683
          while ($account = db_fetch_object($result)) {
684
            $find[] = array('title' => $account->name, 'link' => url('user/' . $account->uid, array('absolute' => TRUE)));
685
          }
686 687
        }
        return $find;
688
      }
Dries's avatar
 
Dries committed
689 690 691
  }
}

Dries's avatar
Dries committed
692 693 694 695 696 697 698 699 700 701
/**
 * Implementation of hook_elements().
 */
function user_elements() {
  return array(
    'user_profile_category' => array(),
    'user_profile_item' => array(),
  );
}

Dries's avatar
Dries committed
702
/**
703
 * Implementation of hook_user_view().
Dries's avatar
Dries committed
704
 */
705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730
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') {
731
    $form_state = array();
732
    return user_edit_form($form_state, (isset($account->uid) ? $account->uid : FALSE), $edit);
733
  }
734
}
735

736
/**
737
 * Implementation of hook_user_validate().
738 739 740
 */
function user_user_validate(&$edit, &$account, $category = NULL) {
  if ($category == 'account') {
741 742 743 744 745 746 747 748 749 750 751
    $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'])));
      }
    }

752
    // Validate the e-mail address, and check if it is taken by an existing user.
753 754 755 756
    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) {
757 758 759 760 761 762 763
      // 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'))));
      }
764
    }
765
  }
766
}
767

768
/**
769
 * Implementation of hook_user_submit().
770 771 772
 */
function user_user_submit(&$edit, &$account, $category = NULL) {
  if ($category == 'account') {
773 774 775
    if (!empty($edit['picture_upload'])) {
      $edit['picture'] = $edit['picture_upload'];
    }
776
    // Delete picture if requested, and if no replacement picture was given.
777 778
    elseif (!empty($edit['picture_delete'])) {
      $edit['picture'] = NULL;
779
    }
780 781 782 783
    // Remove these values so they don't end up serialized in the data field.
    $edit['picture_upload'] = NULL;
    $edit['picture_delete'] = NULL;

784 785 786
    if (isset($edit['roles'])) {
      $edit['roles'] = array_filter($edit['roles']);
    }
787
  }
788
}
789

790 791 792
/**
 * Implementation of hook_user_categories.
 */
793
function user_user_categories($edit, $account, $category = NULL) {
794
    return array(array('name' => 'account', 'title' => t('Account settings'), 'weight' => 1));
Dries's avatar
Dries committed
795 796
}

797 798
function user_login_block() {
  $form = array(
799
    '#action' => url($_GET['q'], array('query' => drupal_get_destination())),
800
    '#id' => 'user-login-form',
801
    '#validate' => user_login_default_validators(),
802
    '#submit' => array('user_login_submit'),
803 804 805
  );
  $form['name'] = array('#type' => 'textfield',
    '#title' => t('Username'),
806
    '#maxlength' => USERNAME_MAX_LENGTH,
807 808 809 810 811
    '#size' => 15,
    '#required' => TRUE,
  );
  $form['pass'] = array('#type' => 'password',
    '#title' => t('Password'),
812
    '#maxlength' => 60,
813 814 815 816 817 818 819 820
    '#size' => 15,
    '#required' => TRUE,
  );
  $form['submit'] = array('#type' => 'submit',
    '#value' => t('Log in'),
  );
  $items = array();
  if (variable_get('user_register', 1)) {
821
    $items[] = l(t('Create new account'), 'user/register', array('attributes' => array('title' => t('Create a new user account.'))));
822
  }
823
  $items[] = l(t('Request new password'), 'user/password', array('attributes' => array('title' => t('Request new password via e-mail.'))));
824
  $form['links'] = array('#markup' => theme('item_list', $items));
825 826 827
  return $form;
}

Dries's avatar
Dries committed
828
/**
829
 * Implementation of hook_block_list().
Dries's avatar
Dries committed
830
 */
831
function user_block_list() {
Dries's avatar
 
Dries committed
832 833
  global $user;

834 835 836
  $blocks['login']['info'] = t('User login');
  // Not worth caching.
  $blocks['login']['cache'] = BLOCK_NO_CACHE;
837

838 839 840 841
  $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;
842

843
  $blocks['new']['info'] = t('Who\'s new');
844

845 846 847 848 849
  // Too dynamic to cache.
  $blocks['online']['info'] = t('Who\'s online');
  $blocks['online']['cache'] = BLOCK_NO_CACHE;
  return $blocks;
}
850

851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870
/**
 * 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.'));
871
      $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>');
872
      return $form;
873
  }
874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890
}

/**
 * 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;
891
  }
892
}
Dries's avatar
 
Dries committed
893

894 895 896 897 898
/**
 * Implementation of hook_block_view().
 */
function user_block_view($delta = '') {
  global $user;
Dries's avatar
 
Dries committed
899

900
  $block = array();
Dries's avatar
Dries committed
901

902 903 904 905
  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
906

907 908 909 910
        $block['subject'] = t('User login');
        $block['content'] = drupal_get_form('user_login_block');
      }
      return $block;
Dries's avatar
 
Dries committed
911

912 913 914 915 916 917
    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
918

919 920 921 922 923
    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);
924

925 926 927 928
        $block['subject'] = t('Who\'s new');
        $block['content'] = $output;
      }
      return $block;
Dries's avatar
 
Dries committed
929

930 931 932 933
    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
934

935
        // Perform database queries to gather online user lists. We use s.timestamp
936 937
        // 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();
938

939 940 941 942 943 944 945 946 947 948 949
        // 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
950
        }