array( 'operations' => array( 'retrieve' => array( 'help' => 'Retrieve a user', 'callback' => '_user_resource_retrieve', 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/user_resource'), 'access callback' => '_user_resource_access', 'access arguments' => array('view'), 'access arguments append' => TRUE, 'args' => array( array( 'name' => 'uid', 'type' => 'int', 'description' => 'The uid of the user to retrieve.', 'source' => array('path' => 0), 'optional' => FALSE, ), ), ), 'create' => array( 'help' => 'Create a user', 'callback' => '_user_resource_create', 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/user_resource'), 'access callback' => '_user_resource_access', 'access arguments' => array('create'), 'access arguments append' => FALSE, 'args' => array( array( 'name' => 'account', 'type' => 'array', 'description' => 'The user object', 'source' => 'data', 'optional' => FALSE, ), ), ), 'update' => array( 'help' => 'Update a user', 'callback' => '_user_resource_update', 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/user_resource'), 'access callback' => '_user_resource_access', 'access arguments' => array('update'), 'access arguments append' => TRUE, 'args' => array( array( 'name' => 'uid', 'type' => 'int', 'description' => 'Unique identifier for this user', 'source' => array('path' => 0), 'optional' => FALSE, ), array( 'name' => 'data', 'type' => 'array', 'description' => 'The user object with updated information', 'source' => 'data', 'optional' => FALSE, ), ), ), 'delete' => array( 'help' => 'Delete a user', 'callback' => '_user_resource_delete', 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/user_resource'), 'access callback' => '_user_resource_access', 'access arguments' => array('delete'), 'access arguments append' => TRUE, 'args' => array( array( 'name' => 'uid', 'type' => 'int', 'description' => 'The id of the user to delete', 'source' => array('path' => 0), 'optional' => FALSE, ), ), ), 'index' => array( 'help' => 'List all users', 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/user_resource'), 'callback' => '_user_resource_index', 'args' => array( array( 'name' => 'page', 'optional' => TRUE, 'type' => 'int', 'description' => 'The zero-based index of the page to get, defaults to 0.', 'default value' => 0, 'source' => array('param' => 'page'), ), array( 'name' => 'fields', 'optional' => TRUE, 'type' => 'string', 'description' => 'The fields to get.', 'default value' => '*', 'source' => array('param' => 'fields'), ), array( 'name' => 'parameters', 'optional' => TRUE, 'type' => 'array', 'description' => 'Parameters', 'default value' => array(), 'source' => array('param' => 'parameters'), ), array( 'name' => 'pagesize', 'optional' => TRUE, 'type' => 'int', 'description' => 'Number of records to get per page.', 'default value' => variable_get('services_user_index_page_size', 20), 'source' => array('param' => 'pagesize'), ), ), 'access arguments' => array('access user profiles'), 'access arguments append' => FALSE, ), ), 'actions' => array( 'login' => array( 'help' => 'Login a user for a new session', 'callback' => '_user_resource_login', 'args' => array( array( 'name' => 'username', 'type' => 'string', 'description' => 'A valid username', 'source' => array('data' => 'username'), 'optional' => FALSE, ), array( 'name' => 'password', 'type' => 'string', 'description' => 'A valid password', 'source' => array('data' => 'password'), 'optional' => FALSE, ), ), 'access callback' => 'services_access_menu', 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/user_resource'), ), 'logout' => array( 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/user_resource'), 'help' => 'Logout a user session', 'callback' => '_user_resource_logout', 'access callback' => 'services_access_menu', ), 'token' => array( 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/user_resource'), 'callback' => '_user_resource_get_token', 'access callback' => 'services_access_menu', 'help' => t('Returns the CSRF token.'), ), 'request_new_password' => array( 'help' => 'Request a new password, given a user name or e-mail address', 'callback' => '_user_resource_request_new_password', 'args' => array( array( 'name' => 'name', 'type' => 'string', 'description' => 'A valid user name or e-mail address', 'source' => array('data' => 'name'), 'optional' => FALSE, ), ), 'access callback' => 'services_access_menu', 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/user_resource'), ), ), 'targeted_actions' => array( 'cancel' => array( 'help' => 'Cancel a user', 'callback' => '_user_resource_cancel', 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/user_resource'), 'access callback' => '_user_resource_access', 'access arguments' => array('cancel'), 'access arguments append' => TRUE, 'args' => array( array( 'name' => 'uid', 'optional' => FALSE, 'source' => array('path' => 0), 'type' => 'int', 'description' => 'The id of the user to cancel.', ), ), ), 'password_reset' => array( 'access callback' => '_user_resource_access', 'access arguments' => array('password_reset'), 'access arguments append' => TRUE, 'callback' => '_user_resource_password_reset', 'args' => array( array( 'name' => 'uid', 'optional' => FALSE, 'source' => array('path' => 0), 'type' => 'int', 'description' => 'The id of the user whose password to reset.', ), ), ), 'resend_welcome_email' => array( 'access callback' => '_user_resource_access', 'access arguments' => array('resend_welcome_email'), 'access arguments append' => TRUE, 'callback' => '_user_resource_resend_welcome_email', 'args' => array( array( 'name' => 'uid', 'optional' => FALSE, 'source' => array('path' => 0), 'type' => 'int', 'description' => 'The id of the user whose welcome email to resend.', ), ), ), ), ), ); $definition['user']['actions']['register'] = array_merge($definition['user']['operations']['create'], array( 'help' => 'Register a user', )); return $definition; } /** * Get user details. * * @param $uid * UID of the user to be loaded. * * @return * A user object. * * @see user_load() */ function _user_resource_retrieve($uid) { $account = user_load($uid); if (empty($account)) { return services_error(t('There is no user with ID @uid.', array('@uid' => $uid)), 404); } services_remove_user_data($account); //Lets check field_permissions $account = services_field_permissions_clean('view', 'user', $account); // Everything went right. return $account; } /** * Create a new user. * * This function uses drupal_form_submit() and as such expects all input to match * the submitting form in question. * * @param $account * A object containing account information. The $account object should * contain, at minimum, the following properties: * - name (user name) * - mail (email address) * - pass (plain text unencrypted password) * * These properties can be passed but are optional * - status (0 for blocked, otherwise will be active by default) * - notify (1 to notify user of new account, will not notify by default) * * Roles can be passed in a roles property which is an associative * array formatted with '' => '', not including * the authenticated user role, which is given by default. * * @return * The user object of the newly created user. */ function _user_resource_create($account) { // Adds backwards compatability with regression fixed in #1083242 $account = _services_arg_value($account, 'account'); // Load the required includes for saving profile information // with drupal_form_submit(). module_load_include('inc', 'user', 'user.pages'); // Register a new user. $form_state['values'] = $account; // Determine the password(s). Passwords may not be available as this callback // is used for registration as well. $pass1 = ''; $pass2 = ''; if (isset($account['pass'])) { // For legacy usage, passwords come in as a single string. To match the // actual form state value keys used by Drupal, we also can collect two // passwords via an array. if (is_array($account['pass'])) { $pass1 = $account['pass']['pass1']; $pass2 = $account['pass']['pass2']; } else { $pass1 = $account['pass']; $pass2 = $account['pass']; } } $form_state['values']['pass'] = array( 'pass1' => $pass1, 'pass2' => $pass2 ); // Set the form state op. $form_state['values']['op'] = variable_get('services_user_create_button_resource_create', t('Create new account')); // execute the register form $form_state['programmed_bypass_access_check'] = FALSE; drupal_form_submit('user_register_form', $form_state); // find and store the new user into the form_state if(isset($form_state['values']['uid'])) { $form_state['user'] = user_load($form_state['values']['uid']); } // Error if needed. if ($errors = form_get_errors()) { return services_error(implode(" ", $errors), 406, array('form_errors' => $errors)); } else { $user = array('uid' => $form_state['user']->uid); if ($uri = services_resource_uri(array('user', $user['uid']))) { $user['uri'] = $uri; } _user_resource_update_services_user($user['uid'], time()); return $user; } } /** * Update an existing user. * * This function uses drupal_form_submit() and as such expects all input to match * the submitting form in question. * * @param $uid * Unique identifier for this user * @param $account * Fields to modify for this user. * * @return * The modified user object. */ function _user_resource_update($uid, $account) { // Adds backwards compatability with regression fixed in #1083242 $account = _services_arg_value($account, 'data'); $account['uid'] = $uid; $account_loaded = user_load($uid); // Load the required includes for saving profile information // with drupal_form_submit(). module_load_include('inc', 'user', 'user.pages'); // If a profile category was passed in, use it. Otherwise default // to 'account' (for saving core user data.) $category = 'account'; if (isset($account['category'])) { $category = $account['category']; unset($account['category']); } // Drop any passed in values into the $account var. Anything // unused by the form just gets ignored. We handle roles and // password separately. foreach ($account as $key => $value) { if ($key != 'pass' && $key != 'roles') { $form_state['values'][$key] = $value; } } // Prepare values of roles. Check user's permission before allowing changes to roles. if (!isset($account['roles']) || !user_access('administer users')) { $account['roles'] = $account_loaded->roles; } foreach ($account['roles'] as $key => $value) { if (!empty($value)) { $form_state['values']['roles'][$key] = $key; } } unset($form_state['values']['roles'][2]); // Prepare values for password. if (isset($account['pass'])) { $form_state['values']['pass']['pass1'] = $account['pass']; $form_state['values']['pass']['pass2'] = $account['pass']; } // If user is changing name, make sure they have permission. if (isset($account['name']) && $account['name'] != $account_loaded->name && !(user_access('change own username') || user_access('administer users'))) { return services_error(t('You are not allowed to change your username.'), 406); } $form_state['values']['op'] = variable_get('services_user_save_button_resource_update', t('Save')); $form_state['values']['#user_category'] = $category; $form_state['values']['#account'] = $account_loaded; $form_state['programmed_bypass_access_check'] = FALSE; $ret = drupal_form_submit('user_profile_form', $form_state, $account_loaded, $category); // Error if needed. if ($errors = form_get_errors()) { return services_error(implode(" ", $errors), 406, array('form_errors' => $errors)); } else { $account = (object) $account; services_remove_user_data($account); $account = (array) $account; _user_resource_update_services_user($uid, time()); return $account; } } function _user_resource_update_services_user($uid, $time) { //Determine if a row exists, if not that means we are creating the user, //If so that means we are updating it. $result = db_select('services_user', 'su') ->fields('su') ->condition('uid', $uid,'=') ->execute() ->fetchAssoc(); //check the result if (!$result) { $id = db_insert('services_user') ->fields(array( 'uid' => $uid, 'created' => $time, 'changed' => $time, )) ->execute(); } else { db_update('services_user') ->fields(array( 'uid' => $uid, 'changed' => $time, ) ) ->execute(); } } /** * Delete a user. * * @param $uid * UID of the user to be deleted. * * @see user_delete() */ function _user_resource_delete($uid) { if ($uid == 1) { return services_error(t('The admin user cannot be deleted.'), 403); } $account = user_load($uid); if (empty($account)) { return services_error(t('There is no user with ID @uid.', array('@uid' => $uid)), 404); } user_delete($uid); // Everything went right. return TRUE; } /** * Cancel a user. * * @param $uid * UID of the user to be canceled. * * @see user_cancel() */ function _user_resource_cancel($uid) { if ($uid == 1) { return services_error(t('The admin user cannot be canceled.'), 403); } $account = user_load($uid); if (empty($account)) { return services_error(t('There is no user with ID @uid.', array('@uid' => $uid)), 404); } $edit = array( 'user_cancel_notify' => isset($account->data['user_cancel_notify']) ? $account->data['user_cancel_notify'] : variable_get('user_mail_status_canceled_notify', FALSE), ); // This defult setting is defined under "admin/config/people/accounts". $default_method = variable_get('user_cancel_method', 'user_cancel_block'); // Modules use hook_user_delete() to respond to deletion. if ($default_method != 'user_cancel_delete') { // Allow modules to add further sets to this batch. module_invoke_all('user_cancel', $edit, $account, $default_method); } _user_cancel($edit, $account, $default_method); // Everything went right. return TRUE; } /** * Login a user using the specified credentials. * * Note this will transfer a plaintext password. * * @param $username * Username to be logged in. * @param $password * Password, must be plain text and not hashed. * * @return * A valid session object. */ function _user_resource_login($username, $password) { global $user; if ($user->uid) { // user is already logged in return services_error(t('Already logged in as @user.', array('@user' => $user->name)), 406); } // Check if account is active. if (user_is_blocked($username)) { return services_error(t('The username %name has not been activated or is blocked.', array('%name' => $username)), 403); } // Emulate drupal native flood control: check for flood condition. $flood_state = array(); if (variable_get('services_flood_control_enabled', TRUE)) { $flood_state = _user_resource_flood_control_precheck($username); } // Only authenticate if a flood condition was not detected. if (empty($flood_state['flood_control_triggered'])) { $uid = user_authenticate($username, $password); } else { $uid = FALSE; } // Emulate drupal native flood control: register flood event, and throw error // if a flood condition was previously detected if (variable_get('services_flood_control_enabled', TRUE)) { $flood_state['uid'] = $uid; _user_resource_flood_control_postcheck($flood_state); } if ($uid) { $user = user_load($uid); if ($user->uid) { user_login_finalize(); $return = new stdClass(); $return->sessid = session_id(); $return->session_name = session_name(); $return->token = drupal_get_token('services'); $account = clone $user; services_remove_user_data($account); $return->user = $account; return $return; } } watchdog('user', 'Invalid login attempt for %username.', array('%username' => $username)); return services_error(t('Wrong username or password.'), 401); } /** * Logout the current user. */ function _user_resource_logout() { global $user; if (!$user->uid) { // User is not logged in return services_error(t('User is not logged in.'), 406); } watchdog('user', 'Session closed for %name.', array('%name' => $user->name)); // Destroy the current session. module_invoke_all('user_logout', $user); session_destroy(); // Load the anonymous user. $user = drupal_anonymous_user(); return TRUE; } /** * Update the current user logout callback to the new callback with a better return value. */ function _user_resource_logout_update_1_1() { $new_set = array( 'callback' => '_user_resource_logout_1_1', ); return $new_set; } /** * Logs out the currently logged in user and returns the new user object. */ function _user_resource_logout_1_1() { global $user; if (!$user->uid) { // User is not logged in return services_error(t('User is not logged in.'), 406); } watchdog('user', 'Session closed for %name.', array('%name' => $user->name)); // Destroy the current session. module_invoke_all('user_logout', $user); session_destroy(); // Load the anonymous user. $user = drupal_anonymous_user(); return $user; } /** * Request a new password given a user name or e-mail address. * * @param $name * The username or e-mail address of the requesting account. * * @see https://api.drupal.org/api/drupal/modules!user!user.pages.inc/function/user_pass_validate/7 * @see https://api.drupal.org/api/drupal/modules!user!user.pages.inc/function/user_pass_submit/7 */ function _user_resource_request_new_password($name) { $name = trim($name); // Try to load by email. $users = user_load_multiple(array(), array('mail' => $name, 'status' => '1')); $account = reset($users); if (!$account) { // No success, try to load by name. $users = user_load_multiple(array(), array('name' => $name, 'status' => '1')); $account = reset($users); } if (!isset($account->uid)) { return services_error(t('Sorry, %name is not recognized as a user name or an e-mail address.', array('%name' => $name)), 406); } // Mail one time login URL and instructions using current language. global $language; $mail = _user_mail_notify('password_reset', $account, $language); if (!empty($mail)) { watchdog('user', 'Password reset instructions mailed to %name at %email.', array('%name' => $account->name, '%email' => $account->mail)); return TRUE; } else { return FALSE; } } /** * Send a password reset email for the specified user. */ function _user_resource_password_reset($uid) { global $language; $account = user_load($uid); if (empty($account)) { return services_error(t('There is no user with ID @uid.', array('@uid' => $uid)), 404); } // Mail one time login URL and instructions using current language. $mail = _user_mail_notify('password_reset', $account, $language); if (!empty($mail)) { watchdog('user', 'Password reset instructions mailed to %name at %email.', array('%name' => $account->name, '%email' => $account->mail)); } else { watchdog('user', 'There was an error re-sending password reset instructions mailed to %name at %email', array('%name' => $account->name, '%email' => $account->mail)); } // Everything went right. return TRUE; } /** * Send a welcome email for the specified user. */ function _user_resource_resend_welcome_email($uid) { global $language; $account = user_load($uid); if (empty($account)) { return services_error(t('There is no user with ID @uid.', array('@uid' => $uid)), 404); } $user_register = variable_get('user_register', 2); switch ($user_register) { case USER_REGISTER_ADMINISTRATORS_ONLY: $op = 'register_admin_created'; break; case USER_REGISTER_VISITORS: $op = 'register_no_approval_required'; break; case USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL: $op = 'register_pending_approval'; } // Mail the welcome emaiil using current language. $mail = _user_mail_notify($op, $account, $language); if (!empty($mail)) { watchdog('user', 'Welcome message has been re-sent to %name at %email.', array('%name' => $account->name, '%email' => $account->mail)); } else { watchdog('user', 'There was an error re-sending welcome message to %name at %email', array('%name' => $account->name, '%email' => $account->mail)); } // Everything went right. return TRUE; } /** * Return an array of optionally paged uids baed on a set of criteria. * * An example request might look like * * http://domain/endpoint/user?fields=uid,name,mail¶meters[uid]=1 * * This would return an array of objects with only uid, name and mail defined, * where uid = 1. * * @param $page * Page number of results to return (in pages of 20). * @param $fields * The fields you want returned. * @param $parameters * An array containing fields and values used to build a sql WHERE clause * indicating items to retrieve. * @param $page_size * Integer number of items to be returned. * @return * An array of user objects. * * @see _node_resource_index() for more notes */ function _user_resource_index($page, $fields, $parameters, $page_size) { $user_select = db_select('users', 't') ->orderBy('created', 'DESC'); services_resource_build_index_query($user_select, $page, $fields, $parameters, $page_size, 'user'); $results = services_resource_execute_index_query($user_select); return services_resource_build_index_list($results, 'user', 'uid'); } /** * Access check callback for user resource. */ function _user_resource_access($op = 'view', $args = array()) { // Adds backwards compatability with regression fixed in #1083242 if (isset($args[0])) { $args[0] = _services_access_value($args[0], array('account', 'data')); } // Check if the user exists if appropriate. if ($op != 'create' && $op != 'register' ) { $account = user_load($args[0]); if (!$account) { return services_error(t('There is no user with ID @uid.', array('@uid' => $args[0])), 406); } } global $user; switch ($op) { case 'view': return user_view_access($account); case 'update': return ($user->uid == $account->uid || user_access('administer users')); case 'create': case 'register': if (!$user->uid && variable_get('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL) != USER_REGISTER_ADMINISTRATORS_ONLY) { return TRUE; } else { return user_access('administer users'); } case 'password_reset': return TRUE; case 'delete': case 'cancel': case 'resend_welcome_email': return user_access('administer users'); } } /** * Changes the user/login endpoint to accept the same parameters as the user/register endpoint, namely * "name" instead of "username" and "pass" instead of "password" */ function _user_resource_login_update_1_1() { $new_set = array( 'args' => array( array( 'name' => 'name', 'type' => 'string', 'description' => 'A valid username', 'source' => array('data' => 'name'), 'optional' => FALSE, ), array( 'name' => 'pass', 'type' => 'string', 'description' => 'A valid password', 'source' => array('data' => 'pass'), 'optional' => FALSE, ), ), ); return $new_set; } function _user_resource_get_token() { return array('token' => drupal_get_token('services')); } /** * Emulate native Drupal flood control, phase 1. * * This function checks for a flood condition, and determines the identifier * for user based flood checks. This is done prior to user authentication. * * @param string $username * The name of the user who is attempting to log in. * @return array * An array containing zero or more of the following keys: * - flood_control_triggered: either 'user' or 'ip' if a flood condition * was detected. * - flood_control_user_identifier: the identifier to use to register * user-based flood events. * * @see _user_resource_flood_control_postcheck(). * @see user_login_authenticate_validate(). */ function _user_resource_flood_control_precheck($username) { $flood_state = array(); // Do not allow any login from the current user's IP if the limit has been // reached. Default is 50 failed attempts allowed in one hour. This is // independent of the per-user limit to catch attempts from one IP to log // in to many different user accounts. We have a reasonably high limit // since there may be only one apparent IP for all users at an institution. if (!flood_is_allowed('failed_login_attempt_ip', variable_get('user_failed_login_ip_limit', 50), variable_get('user_failed_login_ip_window', 3600))) { $flood_state['flood_control_triggered'] = 'ip'; } else { $account = db_query("SELECT * FROM {users} WHERE name = :name AND status = 1", array(':name' => $username))->fetchObject(); if ($account) { if (variable_get('user_failed_login_identifier_uid_only', FALSE)) { // Register flood events based on the uid only, so they apply for any // IP address. This is the most secure option. $identifier = $account->uid; } else { // The default identifier is a combination of uid and IP address. This // is less secure but more resistant to denial-of-service attacks that // could lock out all users with public user names. $identifier = $account->uid . '-' . ip_address(); } $flood_state['flood_control_user_identifier'] = $identifier; // Don't allow login if the limit for this user has been reached. // Default is to allow 5 failed attempts every 6 hours. if (!flood_is_allowed('failed_login_attempt_user', variable_get('user_failed_login_user_limit', 5), variable_get('user_failed_login_user_window', 21600), $identifier)) { $flood_state['flood_control_triggered'] = 'user'; } } } return $flood_state; } /** + * Emulate native Drupal flood control, phase 2. + * + * This function records a failed login attempt, and triggers an error if a + * flood condition was previously detected. + * + * @param array $flood_state + * An array of flood information as returned by + * _user_resource_flood_control_precheck(). + * + * @throws ServicesException + * If a flood condition was previously detected. + * + * @see _user_resource_flood_control_precheck(). + * @see user_login_final_validate(). + */ function _user_resource_flood_control_postcheck($flood_state) { if (empty($flood_state['uid'])) { // Always register an IP-based failed login event. flood_register_event('failed_login_attempt_ip', variable_get('user_failed_login_ip_window', 3600)); // Register a per-user failed login event. if (isset($flood_state['flood_control_user_identifier'])) { flood_register_event('failed_login_attempt_user', variable_get('user_failed_login_user_window', 21600), $flood_state['flood_control_user_identifier']); } if (isset($flood_state['flood_control_triggered'])) { if ($flood_state['flood_control_triggered'] == 'user') { services_error(t('Account is temporarily blocked.'), 406); } else { // We did not find a uid, so the limit is IP-based. services_error(t('This IP address is temporarily blocked.'), 406); } } } elseif (isset($flood_state['flood_control_user_identifier'])) { // Clear past failures for this user so as not to block a user who might // log in and out more than once in an hour. flood_clear_event('failed_login_attempt_user', $flood_state['flood_control_user_identifier']); } }