/** * Insert or Update an user * * @param integer $userid Userid to be updated. Set to 0 if you want to insert a new user. * @param string $password Password for the user. Empty means no change. May be overriden by the $extra array * @param array $user Basic user information such as email or home page * * username * * email * * usertitle * * birthday * * usergroupid (will get no_permissions exception without administrate user permissions) * * membergroupids (will get no_permissions exception without administrate user permissions) * * list not complete * @param array $options vB options for the user * @param array $adminoptions Admin Override Options for the user * @param array $userfield User's User Profile Field data * @param array $notificationOptions * @param array $hvinput Human Verify input data. @see vB_Api_Hv::verifyToken() * @param array $extra Generic flags or data to affect processing. * * registration * * email * * newpass * * password * * acnt_settings * @return integer New or updated userid. */ public function save($userid, $password, $user, $options, $adminoptions, $userfield, $notificationOptions = array(), $hvinput = array(), $extra = array()) { $db = vB::getDbAssertor(); $vboptions = vB::getDatastore()->getValue('options'); $userContext = vB::getUserContext(); $currentUserId = $userContext->fetchUserId(); $userid = intval($userid); $coppauser = false; //set up some booleans to control behavior. This is done to simply/document the later code $newuser = !$userid; $canadminusers = $this->hasAdminPermission('canadminusers'); $adminoverride = ($canadminusers and empty($extra['acnt_settings']) and empty($extra['acnt_settings'])); $changingCurrentUser = $userid == $currentUserId; // Not sure why we do this at all. The caller should handle this appropriately. // We shouldn't set $userid = $currentUserId if $userid == 0 here // Cause we may need to allow logged-in user to register again if ($userid < 0 and $currentUserId) { $userid = $currentUserId; } //we'll need this all over the place if this isn't a new user. if (!$newuser) { $userinfo = vB_User::fetchUserInfo($userid); } //check some permissions. If we can admin users we can skip all of these checks. Some checks //only apply to some cases, such as registering a newuser. We also check various fields //in some cases and not others. if (!$canadminusers) { if ($newuser) { // Check if registration is allowed if (!$vboptions['allowregistration']) { throw new vB_Exception_Api('noregister'); } // Check Multiple Registrations Per User if ($currentUserId and !$vboptions['allowmultiregs']) { $currentUser = vB::getCurrentSession()->fetch_userinfo(); throw new vB_Exception_Api('signing_up_but_currently_logged_in_msg', array($currentUser['username'], $vboptions['frontendurl'] . '/auth/logout?logouthash=' . $currentUser['logouthash'])); } // If it's a new registration, we need to verify the HV // VBV-9386: HV is disabled when accessing through the VB_API in vb4. // Tere is also a comment saying that it should be enabled once it goes live??? if (!defined('VB_API') or defined('VB_API') and VB_API !== true) { vB_Api::instanceInternal('hv')->verifyToken($hvinput, 'register'); } // Verify Stop Forum Spam $nospam = vB_StopForumSpam::instance(); if (!$nospam->checkRegistration($user['username'], vB::getRequest()->getIpAddress(), $user['email'])) { throw new vB_Exception_Api('noregister'); } } else { //attempting to update somebody else's profile -- only admins can do this if (!$changingCurrentUser) { throw new vB_Exception_Api('no_permission'); } //we need to handle this more gracefully -- this is kindof weird. if (!$userContext->hasPermission('genericpermissions', 'canmodifyprofile')) { // User can only update email and password return $this->saveEmailPassword($extra); } if (isset($user['privacy_options']) and !$userContext->hasPermission('usercsspermissions', 'caneditprivacy')) { // User doesn't have permission to update privacy throw new vB_Exception_Api('no_permission'); } if (isset($options['invisible']) and !empty($options['invisible']) and !$userContext->hasPermission('genericpermissions', 'caninvisible')) { // User doesn't have permission to go invisible throw new vB_Exception_Api('no_permission'); } } //handle some fields that users should not be able to set (the admin can do what he wants) if (isset($user['usergroupid'])) { throw new vB_Exception_Api('no_permission'); } if (isset($user['membergroupids'])) { throw new vB_Exception_Api('no_permission'); } } /* * Some checks for all cases. */ //check the user title length. Skip for any administrator. Not sure if we should be checking for edit user permissions or not, but //it's not a major issue if admins can set their own titles to something really long so changing it at this point is not wise. if (isset($user['usertitle']) and vB_String::vbStrlen($user['usertitle']) > $vboptions['ctMaxChars'] and !$userContext->isAdministrator()) { throw new vB_Exception_Api('please_enter_user_title_with_at_least_x_characters', $vboptions['ctMaxChars']); } //don't allow changes to an unalterable user unless the user themselves requests it. We might want to lock down what the //user can edit in this case. require_once DIR . '/includes/adminfunctions.php'; if (!$changingCurrentUser and is_unalterable_user($userid)) { throw new vB_Exception_Api('user_is_protected_from_alteration_by_undeletableusers_var'); } $olduser = array(); if ($userid != 0) { // Get old user information $olduser = $db->getRow('user_fetchforupdating', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED, 'userid' => $userid)); if (!$olduser) { throw new vB_Exception_Api('invalid_user_specified'); } } // if birthday is required if ($vboptions['reqbirthday'] and empty($olduser['birthday']) and empty($user['birthday'])) { if (count($userfield)) { throw new vB_Exception_Api('birthdayfield'); } else { throw new vB_Exception_Api('birthdayfield_nonprofile_tab'); } } /* * If we are changing the password or email from the account setting we need to validate the users * existing password. */ //we allow stuff for the account profile page to be passed separately in the $extra array. //we shouldn't but cleaning that up is a larger task. if (!empty($extra['acnt_settings'])) { if (!empty($extra['email'])) { $user['email'] = $extra['email']; } //new password to set if (!empty($extra['newpass'])) { $password = $extra['newpass']; } //the user's existing password -- needed to verify to set certain sensative fields. if (!empty($extra['password'])) { $user['password'] = $extra['password']; } } //if we are setting the password or the email we may need to check the user's existing //password as an extra precaution. // * If this is an existing user // * If we are changing the password or email // * If we are not overriding as an admin if (!$newuser and (!empty($password) or !empty($user['email'])) and !$adminoverride) { $loginlib = vB_Library::instance('login'); if (!$user['password']) { throw new vB_Exception_Api('enter_current_password'); } $login = array_intersect_key($userinfo, array_flip(array('userid', 'token', 'scheme'))); $auth = $loginlib->verifyPasswordFromInfo($login, array(array('password' => $user['password'], 'encoding' => 'text'))); if (!$auth['auth']) { throw new vB_Exception_Api('badpassword', vB5_Route::buildUrl('lostpw|fullurl')); } } //this is the user's existing password which we don't need now that we've verified it. //attempting to set it to the DM, which we do below for all user fields causes problems. unset($user['password']); //if this is a newuser we need to have a password -- even if this is an admin creating the user if ($newuser and empty($password)) { throw new vB_Exception_Api('invalid_password_specified'); } /* * If we got this far, we basically have permission to update the user in the way we requested. */ $bf_misc_useroptions = vB::getDatastore()->getValue('bf_misc_useroptions'); $bf_misc_adminoptions = vB::getDatastore()->getValue('bf_misc_adminoptions'); $bf_misc_notificationoptions = vB::getDatastore()->getValue('bf_misc_usernotificationoptions'); $usergroupcache = vB::getDatastore()->getValue('usergroupcache'); $user['ipaddress'] = vB::getRequest()->getIpAddress(); $olduser = array_merge($olduser, convert_bits_to_array($olduser['options'], $bf_misc_useroptions)); $olduser = array_merge($olduser, convert_bits_to_array($olduser['adminoptions'], $bf_misc_adminoptions)); $olduser = array_merge($olduser, convert_bits_to_array($olduser['notification_options'], $bf_misc_notificationoptions)); // get threaded mode options if (isset($olduser['threadedmode']) and ($olduser['threadedmode'] == 1 or $olduser['threadedmode'] == 2)) { $threaddisplaymode = $olduser['threadedmode']; } else { if (isset($olduser['postorder']) and $olduser['postorder'] == 0) { $threaddisplaymode = 0; } else { $threaddisplaymode = 3; } } $olduser['threadedmode'] = $threaddisplaymode; // Let's handle this at API level, ignore list is causing problems in the data manager //handle ignorelist if (isset($user['ignorelist'])) { $user['ignorelist'] = $this->updateIgnorelist($userid, explode(',', $user['ignorelist'])); } else { $user['ignorelist'] = array(); } // init data manager $userdata = new vB_Datamanager_User(vB_DataManager_Constants::ERRTYPE_ARRAY_UNPROCESSED); /* * If this was called from the account settings or registration pages * (not the Admin Control Panel) then we shouldn't be setting admin override. * Should also make sure that the admin is logged in and its not just a case of someone * telling the API that we're in the ACP */ if ($adminoverride) { $userdata->adminoverride = true; } $updateUGPCache = false; // set existing info if this is an update if (!$newuser) { // birthday if (!$adminoverride and $user['birthday'] and $olduser['birthday'] and $user['birthday'] != $olduser['birthday'] and $vboptions['reqbirthday']) { throw new vB_Exception_Api('has_no_permission_change_birthday'); } // update buddy list $user['buddylist'] = array(); foreach (explode(' ', $userinfo['buddylist']) as $buddy) { if (in_array($buddy, $user['ignorelist']) === false) { $user['buddylist'][] = $buddy; } } $userinfo['posts'] = intval($user['posts']); // update usergroups cache if needed... $uInfoMUgpIds = explode(',', trim($userinfo['membergroupids'])); $uInfoUgpId = trim($userinfo['usergroupid']); $uIGpIds = explode(',', trim($userinfo['infractiongroupids'])); $mUgpIds = isset($user['membergroupids']) ? $user['membergroupids'] : false; $ugpId = isset($user['usergroupid']) ? trim($user['usergroupid']) : false; $iGpIds = isset($user['infractiongroupids']) ? explode(',', trim($user['infractiongroupids'])) : false; if ($ugpId and $uInfoUgpId != $ugpId or $mUgpIds and array_diff($uInfoMUgpIds, $mUgpIds) or $iGpIds and array_diff($iGpIds, $uIGpIds)) { $updateUGPCache = true; } $userdata->set_existing($userinfo); } else { if ($this->useCoppa()) { if (empty($user['birthday'])) { throw new vB_Exception_Api('under_thirteen_registration_denied'); } if ($this->needsCoppa($user['birthday'])) { if ($vboptions['usecoppa'] == 2) { throw new vB_Exception_Api('under_thirteen_registration_denied'); } else { if (empty($user['parentemail'])) { throw new vB_Exception_Api('coppa_rules_description'); } $userdata->set_info('coppauser', true); $userdata->set_info('coppapassword', $password); $options['coppauser'] = 1; $coppauser = true; } } else { if ($vboptions['moderatenewmembers']) { $userdata->set_info('usergroupid', 4); } else { if ($vboptions['verifyemail']) { $userdata->set_info('usergroupid', 3); } else { $userdata->set_info('usergroupid', 2); } } } } } //should not be required with the new password code. // if no username is provided then is taken from old userinfo, datamanager needs username always set to perform password checks. //$username = (empty($user['username']) ? $userinfo['username'] : $user['username']); //$userdata->set('username', $username); //unset($user['username']); // user options foreach ($bf_misc_useroptions as $key => $val) { if (isset($options["{$key}"])) { $userdata->set_bitfield('options', $key, $options["{$key}"]); } else { if (isset($olduser["{$key}"])) { $userdata->set_bitfield('options', $key, $olduser["{$key}"]); } } } foreach ($adminoptions as $key => $val) { $userdata->set_bitfield('adminoptions', $key, $val); } // notification options foreach ($notificationOptions as $key => $val) { // @TODO related to VBV-92 if ($olduser["{$key}"] != $val) { $userdata->set_bitfield('notification_options', $key, $val); } else { if ($olduser["{$key}"] == $val) { $userdata->set_bitfield('notification_options', $key, $olduser["{$key}"]); } } } $displaygroupid = (array_key_exists('displaygroupid', $user) and intval($user['displaygroupid'])) ? $user['displaygroupid'] : ''; if (isset($user['usergroupid']) and $user['usergroupid']) { $displaygroupid = $user['usergroupid']; } elseif (isset($olduser['usergroupid']) and $olduser['usergroupid']) { $displaygroupid = $olduser['usergroupid']; } // custom user title if (isset($user['usertitle']) and $user['usertitle']) { $userdata->set_usertitle($user['usertitle'], $user['customtitle'] ? false : true, $usergroupcache["{$displaygroupid}"], $userContext->hasPermission('genericpermissions', 'canusecustomtitle'), $userContext->isAdministrator()); unset($user['usertitle'], $user['customtitle']); } else { if (isset($user['usertitle']) and empty($user['usertitle']) and empty($user['customtitle'])) { $userdata->set_usertitle('', true, $usergroupcache["{$displaygroupid}"], $userContext->hasPermission('genericpermissions', 'canusecustomtitle'), $userContext->isAdministrator()); unset($user['usertitle'], $user['customtitle']); } } // privacy_options $privacyChanged = false; if (isset($user['privacy_options']) and $user['privacy_options']) { foreach ($user['privacy_options'] as $opt => $val) { if (!in_array($opt, $this->privacyOptions)) { unset($user['privacy_options'][$opt]); } } // check if we need to update cached values... if ($olduser['privacy_options']) { $check = unserialize($olduser['privacy_options']); $diff = array_diff_assoc($user['privacy_options'], $check); if (!empty($diff)) { $privacyChanged = true; } } $user['privacy_options'] = serialize($user['privacy_options']); } // Update from user fields foreach ($user as $key => $val) { if (!$userid or $olduser["{$key}"] != $val) { $userdata->set($key, $val); } } $membergroupids = false; if (isset($user['membergroupids']) and is_array($user['membergroupids'])) { $membergroupids = $user['membergroupids']; } //add facebook user group for new users being registered with FB //not entirely thrilled with putting this here, but doing it in a less //fragile way requires a greater refactoring of the registration code if ($newuser and $vboptions['facebookusergroupid']) { $fblib = vB_Library::instance('facebook'); if ($fblib->isFacebookEnabled() and $fblib->userIsLoggedIn()) { if (is_array($membergroupids)) { $membergroupids[] = $vboptions['facebookusergroupid']; } else { $membergroupids = array($vboptions['facebookusergroupid']); } } } //actually set the usergroup array if we have one if (is_array($membergroupids)) { $userdata->set('membergroupids', $membergroupids); } // custom profile fields if (!empty($userfield) and is_array($userfield)) { $userdata->set_userfields($userfield, true, 'admin'); } // handles ignorelist and buddylist correctly $userdata->set('ignorelist', $user['ignorelist']); $userdata->set('buddylist', isset($user['buddylist']) ? $user['buddylist'] : array()); // timezone if (empty($user['timezoneoffset']) and $newuser) { $userdata->set('timezoneoffset', $vboptions['timeoffset']); } //the secret really isn't related to the password, but we want to change it //periodically and for now "every time the user changes their password" //works (we previously used the password salt so that's when it got changed //prior to the refactor). if (!empty($password)) { $userdata->set('secret', vB_Library::instance('user')->generateUserSecret()); } // save data $newuserid = $userdata->save(); if ($userdata->has_errors(false)) { throw $userdata->get_exception(); } //a bit of a hack. If the DM save function runs an update of an existing user then //it returns true rather than the userid (despite what the comments say). However its //not clear how to handle that in the DM (which looks like it could be use to alter //multiple users wholesale, in which case we really don't have an ID. Better to catch it here. if ($newuserid === true) { $newuserid = $userid; } //if we have a new password, then let's set it. if (!empty($password)) { try { //lookup the history for the user we are editing, which is not necesarily the //user that we currently are. if ($changinCurrentUser) { $history = $userContext->getUsergroupLimit('passwordhistory'); } else { if ($adminoverride) { $history = 0; } else { $history = vB::getUserContext($userid)->getUsergroupLimit('passwordhistory'); } } $loginlib = vB_Library::instance('login'); $loginlib->setPassword($newuserid, $password, array('passwordhistorylength' => $history), array('passwordhistory' => $adminoverride)); } catch (Exception $e) { //if this is a new user, deleted it if we fail to set the intial password. if ($newuser) { $db->delete('user', array('userid' => $newuserid)); } throw $e; } } if ($updateUGPCache) { vB_Cache::instance(vB_Cache::CACHE_FAST)->event('perms_changed'); } if ($privacyChanged) { vB_Cache::instance()->event('userPrivacyChg_' . $userid); } // clear user info cached $this->library->clearUserInfo(array($newuserid)); // update session's languageid, VBV-11318 if (isset($user['languageid'])) { vB::getCurrentSession()->set('languageid', $user['languageid']); } if ($newuser and $vboptions['newuseremail'] != '') { // Prepare email data $customfields = ''; if (!empty($userfield) and is_array($userfield)) { $customfields = $userdata->set_userfields($userfield, true, 'register'); } $maildata = vB_Api::instanceInternal('phrase')->fetchEmailPhrases('newuser', array($user['username'], vB::getDatastore()->getOption('bbtitle'), vB5_Route::buildUrl('profile|fullurl', array('userid' => $user['userid'])), $user['email'], $user['birthday'], $user['ipaddress'], $customfields), array(vB::getDatastore()->getOption('bbtitle'))); // Send out the emails $newemails = explode(' ', $vboptions['newuseremail']); foreach ($newemails as $toemail) { if (trim($toemail)) { vB_Mail::vbmail($toemail, $maildata['subject'], $maildata['message'], false); } } } // Check if we need to send out activate email $verifyEmail = (defined('VB_AREA') and VB_AREA == 'AdminCP') ? false : true; if ($newuser and $vboptions['verifyemail'] and $verifyEmail) { $this->library->sendActivateEmail($newuserid); } // Check if we need to send out welcome email if ($newuser and $userdata->fetch_field('usergroupid') == 2 and $vboptions['welcomemail']) { // Send welcome mail $username = trim(unhtmlspecialchars($user['username'])); $maildata = vB_Api::instanceInternal('phrase')->fetchEmailPhrases('welcomemail', array($username, $vboptions['bbtitle']), array($vboptions['bbtitle']), isset($user['languageid']) ? $user['languageid'] : vB::getDatastore()->getOption('languageid')); vB_Mail::vbmail($user['email'], $maildata['subject'], $maildata['message'], true); } return $newuserid; }