/**
	 * 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;
	}
Beispiel #3
0
	/**
	 * 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;
	}
Beispiel #4
0
	/**
	 * 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;
     }
 }
Beispiel #6
0
<?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;
}
Beispiel #8
0
	/**
	 * 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;
	}