/** * Widget to display some of the newest registrations, (if any). */ public function newestSignups(){ if(!\Core\user()->checkAccess('p:/user/users/manage')){ return ''; } // How far back do I want to search for? // 1 month sounds good! $date = new CoreDateTime(); $date->modify('-1 month'); $searches = UserModel::Find(['created > ' . $date->getFormatted('U')], 10, 'created DESC'); // No results? No problem :) if(!sizeof($searches)) return ''; $view = $this->getView(); $view->assign('enableavatar', (\ConfigHandler::Get('/user/enableavatar'))); $view->assign('users', $searches); }
/** * Form Handler for logging in. * * @static * * @param \Form $form * * @return bool|null|string */ public static function LoginHandler(\Form $form){ /** @var \FormElement $e */ $e = $form->getElement('email'); /** @var \FormElement $p */ $p = $form->getElement('pass'); /** @var \UserModel $u */ $u = \UserModel::Find(array('email' => $e->get('value')), 1); if(!$u){ // Log this as a login attempt! $logmsg = 'Failed Login. Email not registered' . "\n" . 'Email: ' . $e->get('value') . "\n"; \SystemLogModel::LogSecurityEvent('/user/login', $logmsg); $e->setError('t:MESSAGE_ERROR_USER_LOGIN_EMAIL_NOT_FOUND'); return false; } if($u->get('active') == 0){ // The model provides a quick cut-off for active/inactive users. // This is the control managed with in the admin. $logmsg = 'Failed Login. User tried to login before account activation' . "\n" . 'User: '******'email') . "\n"; \SystemLogModel::LogSecurityEvent('/user/login', $logmsg, null, $u->get('id')); $e->setError('t:MESSAGE_ERROR_USER_LOGIN_ACCOUNT_NOT_ACTIVE'); return false; } elseif($u->get('active') == -1){ // The model provides a quick cut-off for active/inactive users. // This is the control managed with in the admin. $logmsg = 'Failed Login. User tried to login after account deactivation.' . "\n" . 'User: '******'email') . "\n"; \SystemLogModel::LogSecurityEvent('/user/login', $logmsg, null, $u->get('id')); $e->setError('t:MESSAGE_ERROR_USER_LOGIN_ACCOUNT_DEACTIVATED'); return false; } try{ /** @var \Core\User\AuthDrivers\datastore $auth */ $auth = $u->getAuthDriver('datastore'); } catch(Exception $e){ $e->setError('t:MESSAGE_ERROR_USER_LOGIN_PASSWORD_AUTH_DISABLED'); return false; } // This is a special case if the password isn't set yet. // It can happen with imported users or if a password is invalidated. if($u->get('password') == ''){ // Use the Nonce system to generate a one-time key with this user's data. $nonce = \NonceModel::Generate( '20 minutes', ['type' => 'password-reset', 'user' => $u->get('id')] ); $link = '/datastoreauth/forgotpassword?e=' . urlencode($u->get('email')) . '&n=' . $nonce; $email = new \Email(); $email->setSubject('Initial Password Request'); $email->to($u->get('email')); $email->assign('link', \Core\resolve_link($link)); $email->assign('ip', REMOTE_IP); $email->templatename = 'emails/user/initialpassword.tpl'; try{ $email->send(); \SystemLogModel::LogSecurityEvent('/user/initialpassword/send', 'Initial password request sent successfully', null, $u->get('id')); \Core\set_message('t:MESSAGE_INFO_USER_LOGIN_MUST_SET_NEW_PASSWORD_INSTRUCTIONS_HAVE_BEEN_EMAILED'); return true; } catch(\Exception $e){ \Core\ErrorManagement\exception_handler($e); \Core\set_message('t:MESSAGE_ERROR_USER_LOGIN_MUST_SET_NEW_PASSWORD_UNABLE_TO_SEND_EMAIL'); return false; } } if(!$auth->checkPassword($p->get('value'))){ // Log this as a login attempt! $logmsg = 'Failed Login. Invalid password' . "\n" . 'Email: ' . $e->get('value') . "\n"; \SystemLogModel::LogSecurityEvent('/user/login/failed_password', $logmsg, null, $u->get('id')); // Also, I want to look up and see how many login attempts there have been in the past couple minutes. // If there are too many, I need to start slowing the attempts. $time = new \CoreDateTime(); $time->modify('-5 minutes'); $securityfactory = new \ModelFactory('SystemLogModel'); $securityfactory->where('code = /user/login/failed_password'); $securityfactory->where('datetime > ' . $time->getFormatted(\Time::FORMAT_EPOCH, \Time::TIMEZONE_GMT)); $securityfactory->where('ip_addr = ' . REMOTE_IP); $attempts = $securityfactory->count(); if($attempts > 4){ // Start slowing down the response. This should help deter brute force attempts. // (x+((x-7)/4)^3)-4 sleep( ($attempts+(($attempts-7)/4)^3)-4 ); // This makes a nice little curve with the following delays: // 5th attempt: 0.85 // 6th attempt: 2.05 // 7th attempt: 3.02 // 8th attempt: 4.05 // 9th attempt: 5.15 // 10th attempt: 6.52 // 11th attempt: 8.10 // 12th attempt: 10.05 } $e->setError('t:MESSAGE_ERROR_USER_LOGIN_INCORRECT_PASSWORD'); $p->set('value', ''); return false; } if($form->getElementValue('redirect')){ // The page was set via client-side javascript on the login page. // This is the most reliable option. $url = $form->getElementValue('redirect'); } elseif(REL_REQUEST_PATH == '/user/login'){ // If the user came from the registration page, get the page before that. $url = $form->referrer; } else{ // else the registration link is now on the same page as the 403 handler. $url = REL_REQUEST_PATH; } // Well, record this too! \SystemLogModel::LogSecurityEvent('/user/login', 'Login successful (via password)', null, $u->get('id')); // yay... $u->set('last_login', \CoreDateTime::Now('U', \Time::TIMEZONE_GMT)); $u->save(); \Core\Session::SetUser($u); // Allow an external script to override the redirecting URL. $overrideurl = \HookHandler::DispatchHook('/user/postlogin/getredirecturl'); if($overrideurl){ $url = $overrideurl; } return $url; }
/** * Handler to actually perform the import. * * @param \Form $form * @return bool */ public static function FormHandler2(\Form $form) { $filename = Session::Get('user-import/file'); $file = Factory::File($filename); /** @var $contents \Core\Filestore\Contents\ContentCSV */ $contents = $file->getContentsObject(); // If the user checked that it has a header... do that. $contents->_hasheader = $form->getElement('has_header')->get('checked'); // Merge $merge = $form->getElement('merge_duplicates')->get('checked'); // Handle the map-to directives. $maptos = array(); foreach($form->getElements() as $el){ if(strpos($el->get('name'), 'mapto[') === 0 && $el->get('value')){ $k = substr($el->get('name'), 6, -1); $maptos[$k] = $el->get('value'); } } // Handle the group mappings $groups = $form->getElement('groups[]')->get('value'); // And keep a log of the bad transfers and some other data. $counts = ['created' => 0, 'updated' => 0, 'failed' => 0, 'skipped' => 0]; Session::Set('user-import/fails', []); $incoming = $contents->parse(); foreach($incoming as $record){ try{ // Create a data map of this record for fields to actually map over. $dat = array(); foreach($maptos as $recordkey => $userkey){ $dat[$userkey] = $record[$recordkey]; } // No email, NO IMPORT! if(!$dat['email']){ $counts['skipped']++; continue; } // Try to find this record by email, since that's a primary key. $existing = \UserModel::Find(['email = ' . $dat['email'] ], 1); if($existing && !$merge){ // Skip existing records. $counts['skipped']++; } elseif($existing){ // Update! $existing->setFromArray($dat); $existing->setGroups($groups); if($existing->save()){ $counts['updated']++; } else{ $counts['skipped']++; } } else{ $new = new \UserModel(); $new->setFromArray($dat); $new->setGroups($groups); $new->save(); $counts['created']++; } } catch(\Exception $e){ // @todo Handle this die($e->getMessage()); } // } Session::Set('user-import/counts', $counts); return true; }
/** * Send the message * * @throws phpmailerException * @return bool */ public function send() { $m = $this->getMailer(); if(!\ConfigHandler::Get('/core/email/enable_sending')){ // Allow a config option to disable sending entirely. SystemLogModel::LogInfoEvent('/email/disabled', 'Email sending is disabled, not sending email ' . $m->Subject . '!'); return false; } if(\ConfigHandler::Get('/core/email/sandbox_to')){ $to = $m->getToAddresses(); $cc = $m->getCCAddresses(); $bcc = $m->getBCCAddresses(); $all = []; if(sizeof($to)){ foreach($to as $e){ $all[] = ['type' => 'To', 'email' => $e[0], 'name' => $e[1]]; } } if(sizeof($cc)){ foreach($cc as $e){ $all[] = ['type' => 'CC', 'email' => $e[0], 'name' => $e[1]]; } } if(sizeof($bcc)){ foreach($bcc as $e){ $all[] = ['type' => 'BCC', 'email' => $e[0], 'name' => $e[1]]; } } foreach($all as $e){ $m->AddCustomHeader('X-Original-' . $e['type'], ($e['name'] ? $e['name'] . ' <' . $e['email'] . '>' : $e['email'])); } // Allow a config option to override the "To" address, useful for testing with production data. $m->ClearAllRecipients(); $m->AddAddress(\ConfigHandler::Get('/core/email/sandbox_to')); } // Render out the body. Will be either HTML or text... $body = $this->renderBody(); // Wrap this body with the main email template if it's set. if($this->templatename && $this->_view){ // This version includes HTML tags and all that. $m->Body = $body; $m->IsHTML(true); // Use markdown for conversion. // It produces better results that phpMailer's built-in system! $converter = new \HTMLToMD\Converter(); // Manually strip out the head content. // This was throwing the converters for a loop and injecting weird characters! $body = preg_replace('#<head[^>]*?>.*</head>#ms', '', $body); $m->AltBody = $converter->convert($body); } elseif (strpos($body, '<html>') === false) { // Ensuring that the body is wrapped with <html> tags helps with spam checks with spamassassin. $m->MsgHTML('<html><body>' . $body . '</body></html>'); } else{ $m->MsgHTML($body); } if($this->_encryption){ // Encrypt this message, (both HTML and Alt), and all attachments. // I need to request the full EML from phpMailer so I can encrypt everything. // Then, the body will be recreated after Send is called. $m->PreSend(); $header = $m->CreateHeader(); $body = $m->CreateBody(); $gpg = new \Core\GPG\GPG(); if($this->_encryption === true){ // This is allowed for mutliple recipients! // This requires a little more overhead, as I need to lookup each recipient's user account // to retrieve their GPG key. $recipients = $m->getToAddresses(); foreach($recipients as $dat){ $email = $dat[0]; $user = UserModel::Find(['email = ' . $email], 1); if(!$user){ SystemLogModel::LogErrorEvent('/core/email/failed', 'Unable to locate GPG key for ' . $email . ', cannot send encrypted email to recipient!'); } else{ $key = $user->get('gpgauth_pubkey'); if(!$key){ SystemLogModel::LogErrorEvent('/core/email/failed', 'No GPG key uploaded for ' . $email . ', cannot send encrypted email to recipient!'); } else{ $enc = $gpg->encryptData($header . $body, $key); // Create a clone of the email object to send this data. /** @var PHPMailer $clone */ $clone = clone $m; $clone->ClearAddresses(); $clone->AddAddress($email); $clone->Body = $enc; $clone->AltBody = ''; $clone->Send(); } } } return true; } else{ // Single recipient! $enc = $gpg->encryptData($header . $body, $this->_encryption); $m->Body = $enc; $m->AltBody = ''; return $m->Send(); } } return $m->Send(); }
/** * Page to enable Facebook logins for user accounts. * * @return int|null|string */ public function enable() { $request = $this->getPageRequest(); $auths = \Core\User\Helper::GetEnabledAuthDrivers(); if (!isset($auths['facebook'])) { // Facebook isn't enabled, simply redirect to the home page. \Core\redirect('/'); } if (!FACEBOOK_APP_ID) { \Core\redirect('/'); } if (!FACEBOOK_APP_SECRET) { \Core\redirect('/'); } // If it was a POST, then it should be the first page. if ($request->isPost()) { $facebook = new Facebook(['appId' => FACEBOOK_APP_ID, 'secret' => FACEBOOK_APP_SECRET]); // Did the user submit the facebook login request? if (isset($_POST['login-method']) && $_POST['login-method'] == 'facebook' && $_POST['access-token']) { try { $facebook->setAccessToken($_POST['access-token']); /** @var int $fbid The user ID from facebook */ $fbid = $facebook->getUser(); /** @var array $user_profile The array of user data from Facebook */ $user_profile = $facebook->api('/me'); } catch (Exception $e) { \Core\set_message($e->getMessage(), 'error'); \Core\go_back(); return null; } // If the user is logged in, then the verification logic is slightly different. if (\Core\user()->exists()) { // Logged in users, the email must match. if (\Core\user()->get('email') != $user_profile['email']) { \Core\set_message('Your Facebook email is ' . $user_profile['email'] . ', which does not match your account email! Unable to link accounts.', 'error'); \Core\go_back(); return null; } $user = \Core\user(); } else { /** @var \UserModel|null $user */ $user = UserModel::Find(['email' => $user_profile['email']], 1); if (!$user) { \Core\set_message('No local account found with the email ' . $user_profile['email'] . ', please <a href="' . \Core\resolve_link('/user/register') . '"create an account</a> instead.', 'error'); \Core\go_back(); return null; } } // Send an email with a nonce link that will do the actual activation. // This is a security feature so just anyone can't link another user's account. $nonce = NonceModel::Generate('20 minutes', null, ['user' => $user, 'access_token' => $_POST['access-token']]); $email = new Email(); $email->to($user->get('email')); $email->setSubject('Facebook Activation Request'); $email->templatename = 'emails/facebook/enable_confirmation.tpl'; $email->assign('link', \Core\resolve_link('/facebook/enable/' . $nonce)); if ($email->send()) { \Core\set_message('An email has been sent to your account with a link enclosed. Please click on that to complete activation within twenty minutes.', 'success'); \Core\go_back(); return null; } else { \Core\set_message('Unable to send a confirmation email, please try again later.', 'error'); \Core\go_back(); return null; } } } // If there is a nonce enclosed, then it should be the second confirmation page. // This is the one that actually performs the action. if ($request->getParameter(0)) { /** @var NonceModel $nonce */ $nonce = NonceModel::Construct($request->getParameter(0)); if (!$nonce->isValid()) { \Core\set_message('Invalid key requested.', 'error'); \Core\redirect('/'); return null; } $nonce->decryptData(); $data = $nonce->get('data'); /** @var UserModel $user */ $user = $data['user']; try { $facebook = new Facebook(['appId' => FACEBOOK_APP_ID, 'secret' => FACEBOOK_APP_SECRET]); $facebook->setAccessToken($data['access_token']); $facebook->getUser(); $facebook->api('/me'); } catch (Exception $e) { \Core\set_message($e->getMessage(), 'error'); \Core\redirect('/'); return null; } $user->enableAuthDriver('facebook'); /** @var \Facebook\UserAuth $auth */ $auth = $user->getAuthDriver('facebook'); $auth->syncUser($data['access_token']); \Core\set_message('Linked Facebook successfully!', 'success'); // And log the user in! if (!\Core\user()->exists()) { $user->set('last_login', \CoreDateTime::Now('U', \Time::TIMEZONE_GMT)); $user->save(); \Core\Session::SetUser($user); } \Core\redirect('/'); return null; } }
<?php /** * Upgrade file for user data from 2.6.1 to 2.6.2. * * Namely setting the last login for users that have a password set. * It's good indication that if they have a password set, that they've logged in already. * * @author Charlie Powell <*****@*****.**> * @date 20131030.2031 * @package Core */ $timenow = Time::GetCurrentGMT(); // Find and update all user accounts that have a last login of not recorded, but have a password set (legacy data) $users = UserModel::Find(['password != ', 'last_login = 0']); foreach($users as $u){ /** @var $u UserModel */ $u->set('last_login', $timenow); $u->save(); } // Find and update all user accounts that have a last login of not recorded, but have a password set (legacy data) $users = UserModel::Find(['password != ', 'last_password = 0']); foreach($users as $u){ /** @var $u UserModel */ $u->set('last_password', $timenow); $u->save(); }
/** * Get the current user model that is logged in. * * To support legacy systems, this will also return the User object if it's available instead. * This support is for < 2.8.x Core installations and will be removed after some amount of time TBD. * * If no user systems are currently available, null is returned. * * @return \UserModel */ function user(){ static $_CurrentUserAccount = null; if(!class_exists('\\UserModel')){ return null; } if($_CurrentUserAccount !== null){ // Cache this for the page load. return $_CurrentUserAccount; } if(isset($_SERVER['HTTP_X_CORE_AUTH_KEY'])){ // Allow an auth key to be used to authentication the requested user instead! $user = \UserModel::Find(['apikey = ' . $_SERVER['HTTP_X_CORE_AUTH_KEY']], 1); if($user){ $_CurrentUserAccount = $user; } } elseif(Session::Get('user') instanceof \UserModel){ // There is a valid user account in the session! // But check if this user is forced to be resynced first. if(isset(Session::$Externals['user_forcesync'])){ // A force sync was requested by something that modified the original UserModel object. // Keep the user logged in, but reload the data from the database. $_CurrentUserAccount = \UserModel::Construct(Session::Get('user')->get('id')); // And cache this updated user model back to the session. Session::Set('user', $_CurrentUserAccount); unset(Session::$Externals['user_forcesync']); } else{ $_CurrentUserAccount = Session::Get('user'); } } if($_CurrentUserAccount === null){ // No valid user found. $_CurrentUserAccount = new \UserModel(); } // If this is in multisite mode, blank out the access string cache too! // This is because siteA may have some groups, while siteB may have another. // We don't want a user going to a site they have full access to, hopping to another and having cached permissions! if(\Core::IsComponentAvailable('multisite') && class_exists('MultiSiteHelper') && \MultiSiteHelper::IsEnabled()){ $_CurrentUserAccount->clearAccessStringCache(); } // Did this user request sudo access for another user? if(Session::Get('user_sudo') !== null){ $sudo = Session::Get('user_sudo'); if($sudo instanceof \UserModel){ // It's a valid user! if($_CurrentUserAccount->checkAccess('p:/user/users/sudo')){ // This user can SUDO! // (only if the other user is < SA or current == SA). if($sudo->checkAccess('g:admin') && !$_CurrentUserAccount->checkAccess('g:admin')){ Session::UnsetKey('user_sudo'); \SystemLogModel::LogSecurityEvent('/user/sudo', 'Authorized but non-SA user requested sudo access to a system admin!', null, $sudo->get('id')); } else{ // Ok, everything is good. // Remap the current user over to this sudo'd account! $_CurrentUserAccount = $sudo; } } else{ // This user can NOT sudo!!! Session::UnsetKey('user_sudo'); \SystemLogModel::LogSecurityEvent('/user/sudo', 'Unauthorized user requested sudo access to another user!', null, $sudo->get('id')); } } else{ Session::UnsetKey('user_sudo'); } } return $_CurrentUserAccount; }
/** * Import the given data into the destination Model. * * @param array $data Indexed array of records to import/merge from the external source. * @param array $options Any options required for the import, such as merge, key, etc. * @param boolean $output_realtime Set to true to output the log in real time as the import happens. * * @throws Exception * * @return \Core\ModelImportLogger */ public static function Import($data, $options, $output_realtime = false) { $log = new \Core\ModelImportLogger('User Importer', $output_realtime); $merge = isset($options['merge']) ? $options['merge'] : true; $pk = isset($options['key']) ? $options['key'] : null; if(!$pk) { throw new Exception( 'Import requires a "key" field on options containing the primary key to compare against locally.' ); } // Load in members from the group // Set the default group on new accounts, if a default is set. $defaultgroups = \UserGroupModel::Find(["default = 1"]); $groups = []; $gnames = []; foreach($defaultgroups as $g) { /** @var \UserGroupModel $g */ $groups[] = $g->get('id'); $gnames[] = $g->get('name'); } if(sizeof($groups)) { $log->log('Found ' . sizeof($groups) . ' default groups for new users: ' . implode(', ', $gnames)); } else { $log->log('No groups set as default, new users will not belong to any groups.'); } $log->log('Starting ' . ($merge ? '*MERGE*' : '*skipping*' ) . ' import of ' . sizeof($data) . ' users'); foreach($data as $dat) { if(isset($dat[$pk])){ // Only check the information if the primary key is set on this record. if($pk == 'email' || $pk == 'id') { // These are the only two fields on the User object itself. $user = UserModel::Find([$pk . ' = ' . $dat[ $pk ]], 1); } else { $uucm = UserUserConfigModel::Find(['key = ' . $pk, 'value = ' . $dat[ $pk ]], 1); if($uucm) { $user = $uucm->getLink('UserModel'); } else { // Try the lookup from the email address instead. // This will force accounts that exist to be synced up correctly. // The only caveat to this is that users will not be updated with the foreign key if merge is disabled. $user = UserModel::Find(['email = ' . $dat['email']], 1); } } } else{ $user = null; } $status_type = $user ? 'Updated' : 'Created'; if($user && !$merge) { $log->duplicate('Skipped user ' . $user->getLabel() . ', already exists and merge not requested'); // Skip to the next record. continue; } if(!$user) { // All incoming users must have an email address! if(!isset($dat['email'])) { $log->error('Unable to import user without an email address!'); // Skip to the next record. continue; } // Meta fields that may or may not be present, but should be for reporting purposes. if(!isset($dat['registration_ip'])) { $dat['registration_ip'] = REMOTE_IP; } if(!isset($dat['registration_source'])) { $dat['registration_source'] = \Core\user()->exists() ? 'admin' : 'self'; } if(!isset($dat['registration_invitee'])) { $dat['registration_invitee'] = \Core\user()->get('id'); } // New user! $user = new UserModel(); } // No else needed, else is there IS a valid $user object and it's setup ready to go. // Handle all the properties for this user! foreach($dat as $key => $val){ if($key == 'avatar' && strpos($val, '://') !== false){ // Sync the user avatar. $log->actionStart('Downloading ' . $dat['avatar']); $f = new \Core\Filestore\Backends\FileRemote($dat['avatar']); $dest = \Core\Filestore\Factory::File('public/user/avatar/' . $f->getBaseFilename()); if($dest->identicalTo($f)) { $log->actionSkipped(); } else { $f->copyTo($dest); $user->set('avatar', 'public/user/avatar/' . $dest->getBaseFilename()); $log->actionSuccess(); } } elseif($key == 'profiles' && is_array($val)) { $new_profiles = $val; // Pull the current profiles from the account $profiles = $user->get('external_profiles'); if($profiles && is_array($profiles)) { $current_flat = []; foreach($profiles as $current_profile) { $current_flat[] = $current_profile['url']; } // Merge in any *actual* new profile foreach($new_profiles as $new_profile) { if(!in_array($new_profile['url'], $current_flat)) { $profiles[] = $new_profile; } } unset($new_profile, $new_profiles, $current_flat, $current_profile); } else { $profiles = $new_profiles; unset($new_profiles); } $user->set('external_profiles', $profiles); } elseif($key == 'backend'){ // Was a backend requested? // This gets merged instead of replaced entirely. $user->enableAuthDriver($val); } elseif($key == 'groups'){ $user->setGroups($val); } else{ // Default Behaviour, // save the key into whatever field it was set to go to. $user->set($key, $val); } } try { // Set the default groups loaded from the system. if(!$user->exists()){ $user->setGroups($groups); } $status = $user->save(); } catch(Exception $e) { $log->error($e->getMessage()); // Skip to the next. continue; } if($status) { $log->success($status_type . ' user ' . $user->getLabel() . ' successfully!'); } else { $log->skip('Skipped user ' . $user->getLabel() . ', no changes detected.'); } } $log->finalize(); return $log; }