Esempio n. 1
0
				CLI::PrintLine('Found ' . $target['name'] . '-' . $target['version'] . ' in repo ' . $target['source_url']);
				$compversion = $target['version'];

				$output = [];
				exec('gpg --homedir "' . GPG_HOMEDIR . '" --list-public-keys "' . $target['key'] . '"', $output, $result);
				if($result > 0){
					// Key validation failed!
					CLI::PrintError(implode("\n", $output) . "\n" . 'Is the key ' . $target['key'] . ' installed?');
					die();
				}
				/*else{
					CLI::PrintLine($output);
				}*/

				// Setup the remote file that will be used to download from.
				$file = new \Core\Filestore\Backends\FileRemote($target['location']);
				$filesize = $file->getFilesize(true);
				CLI::PrintActionStart('Downloading ' . $target['location'] . ' (' . $filesize . ')');
				$file->username = $target['source_username'];
				$file->password = $target['source_password'];
				$obj = $file->getContentsObject();
				// Getting the object simply sets it up, it doesn't download the contents yet.
				$obj->getContents();
				CLI::PrintActionStatus('ok');

				if(!($obj instanceof \Core\Filestore\Contents\ContentASC)){
					CLI::PrintError($target['location'] . ' does not appear to be a valid GPG signed archive');
					die();
				}
				// The object's key must also match what's in the repo.
				if($obj->getKey() != $target['key']){
Esempio n. 2
0
	/**
	 * Sync all features from the licensing servers and keep the values cached locally.
	 */
	public static function Sync(){
		// This feature relies on a valid server id.
		if(!defined('SERVER_ID')){
			return null;
		}
		if(strlen(SERVER_ID) != 32){
			return null;
		}
		
		$urls     = [];
		$features = self::Singleton()->_features;
		
		foreach($features as $d){
			if(!in_array($d['url'], $urls)){
				$urls[] = $d['url'];
			}
		}
		
		// URLs now is an array of all update servers to pull licensed features from.
		foreach($urls as $u){
			$r = new \Core\Filestore\Backends\FileRemote();
			$r->setRequestHeader('X-Core-Server-ID', SERVER_ID);
			$r->setFilename($u . '/licenser.json');

			$contents = $r->getContents();
			var_dump($r, $contents); die();
		}
	}
Esempio n. 3
0
 /**
  * Sync the user back to the linked Facebook account.
  *
  * <h3>Usage:</h3>
  * <pre class="code">
  * $auth->syncUser($_POST['access-token']);
  * </pre>
  *
  * @param string $access_token A valid access token for the user to sync up.
  *
  * @return bool True or false on success.
  */
 public function syncUser($access_token)
 {
     try {
         $facebook = new \Facebook(['appId' => FACEBOOK_APP_ID, 'secret' => FACEBOOK_APP_SECRET]);
         $facebook->setAccessToken($access_token);
         /** @var array $user_profile The array of user data from Facebook */
         $user_profile = $facebook->api('/me');
     } catch (\Exception $e) {
         return false;
     }
     $user = $this->_usermodel;
     if (!$user->exists()) {
         // Some config options for new accounts only.
         $profiles = $user->get('external_profiles');
         if (!is_array($profiles)) {
             $profiles = [];
         }
         $profiles[] = [['type' => 'facebook', 'url' => $user_profile['link'], 'title' => 'Facebook Profile']];
         $user->set('external_profiles', $profiles);
         // Another component from the user-social component.
         // This needs to be unique, so do a little fudging if necessary.
         try {
             $user->set('username', $user_profile['username']);
         } catch (\ModelValidationException $e) {
             $user->set('username', $user_profile['username'] . '-' . \Core\random_hex(3));
         }
         // Sync the user avatar.
         $f = new \Core\Filestore\Backends\FileRemote('http://graph.facebook.com/' . $user_profile['id'] . '/picture?type=large');
         $dest = \Core\Filestore\Factory::File('public/user/avatar/' . $f->getBaseFilename());
         $f->copyTo($dest);
         $user->set('avatar', 'public/user/avatar/' . $dest->getBaseFilename());
     }
     // Get all user configs and load in anything possible.
     $user->set('first_name', $user_profile['first_name']);
     $user->set('last_name', $user_profile['last_name']);
     $user->set('gender', ucwords($user_profile['gender']));
     $user->set('facebook_id', $user_profile['id']);
     $user->set('facebook_link', $user_profile['link']);
     $user->set('facebook_access_token', $facebook->getAccessToken());
 }
	/**
	 * @param mixed $value
	 *
	 * @return bool
	 */
	public function setValue($value) {
		if ($this->get('required') && !$value) {
			$this->_error = $this->get('label') . ' is required.';
			return false;
		}

		// _link_ allows users to paste in a URL for a given file.  This is then copied locally as normal.
		// In order to detect this, I need to look for the presence of a protocol indicator and this element needs
		// to have allowlink set.
		if($this->get('allowlink') && strpos($value, '_link_://') === 0){
			$n = $this->get('name');
			$value = substr($value, 9);

			// Source
			$f = new \Core\Filestore\Backends\FileRemote($value);

			if(!$f->exists()){
				$this->_error = 'Remote file does not seem to exist';
				return false;
			}

			// Destination
			$nf = \Core\Filestore\Factory::File($this->get('basedir') . '/' . $f->getBaseFilename());

			// do NOT copy the contents over until the accept check has been ran!

			// Now that I have a file object, (in the temp filesystem still), I should validate the filetype
			// to see if the developer wanted a strict "accept" type to be requested.
			// If present, I'll have something to run through and see if the file matches.
			// I need the destination now because I need to full filename if an extension is requested in the accept.
			if($this->get('accept')){
				$acceptcheck = \Core\check_file_mimetype($this->get('accept'), $f->getMimetype(), $nf->getExtension());

				// Now that all the mimetypes have run through, I can see if one matched.
				if($acceptcheck != ''){
					$this->_error = $acceptcheck;
					return false;
				}
			}

			// Now all the checks should be completed and I can safely copy the file away from the temporary filesystem.
			$f->copyTo($nf);

			$value = $nf->getFilename(false);
		}
		elseif(($this->get('browsable') || $this->get('browseable')) && strpos($value, '_browse_://public') === 0){
			$n = $this->get('name');
			$value = substr($value, 11);

			// Source
			$f = \Core\Filestore\Factory::File($value);

			if(!$f->exists()){
				$this->_error = 'File does not seem to exist';
				return false;
			}

			// Now that I have a file object, I still need to validate that this file was what the user was supposed to select.
			// If present, I'll have something to run through and see if the file matches.
			if($this->get('accept')){
				$acceptcheck = \Core\check_file_mimetype($this->get('accept'), $f->getMimetype(), $f->getExtension());

				// Now that all the mimetypes have run through, I can see if one matched.
				if($acceptcheck != ''){
					$this->_error = $acceptcheck;
					return false;
				}
			}
		}
		elseif ($value == '_upload_') {
			$n = $this->get('name');

			// Because PHP will have different sources depending if the name has [] in it...
			if (strpos($n, '][') !== false) {
				// This is a 2+ nested array value.

				preg_match_all('#\[([^\]]*)\]#', $n, $matches);
				$p1 = substr($n, 0, strpos($n, '['));
				$src =& $_FILES[$p1];

				$in = array(
					'name'     => $src['name'],
					'type'     => $src['type'],
					'tmp_name' => $src['tmp_name'],
					'error'    => $src['error'],
					'size'     => $src['size'],
				);

				foreach($matches[1] as $next){
					$in['name']     =& $in['name'][$next];
					$in['type']     =& $in['type'][$next];
					$in['tmp_name'] =& $in['tmp_name'][$next];
					$in['error']    =& $in['error'][$next];
					$in['size']     =& $in['size'][$next];
				}
			}
			elseif (strpos($n, '[') !== false) {
				// This is a single array value.

				$p1 = substr($n, 0, strpos($n, '['));
				$p2 = substr($n, strpos($n, '[') + 1, -1);

				if (!isset($_FILES[$p1])) {
					$this->_error = 'No file uploaded for ' . $this->get('label');
					return false;
				}

				$in = array(
					'name'     => $_FILES[$p1]['name'][$p2],
					'type'     => $_FILES[$p1]['type'][$p2],
					'tmp_name' => $_FILES[$p1]['tmp_name'][$p2],
					'error'    => $_FILES[$p1]['error'][$p2],
					'size'     => $_FILES[$p1]['size'][$p2],
				);
			}
			else {
				$in =& $_FILES[$n];
			}


			if (!isset($in)) {
				$this->_error = 'No file uploaded for ' . $this->get('label');
				return false;
			}
			else {
				$error = \Core\translate_upload_error($in['error']);
				if($error != ''){
					$this->_error = $error;
					return false;
				}

				// Source
				$f = \Core\Filestore\Factory::File($in['tmp_name']);

				// Destination
				// Make sure the filename is sanitized.
				// Also, limit the new filename to 40 characters.
				$newbasename = substr(\Core\str_to_url($in['name'], true), 0, 40);
				$nf = \Core\Filestore\Factory::File($this->get('basedir') . '/' . $newbasename);

				// do NOT copy the contents over until the accept check has been ran!

				// Now that I have a file object, (in the temp filesystem still), I should validate the filetype
				// to see if the developer wanted a strict "accept" type to be requested.
				// If present, I'll have something to run through and see if the file matches.
				// I need the destination now because I need to full filename if an extension is requested in the accept.
				if($this->get('accept')){
					$acceptcheck = \Core\check_file_mimetype($this->get('accept'), $f->getMimetype(), $nf->getExtension());

					// Now that all the mimetypes have run through, I can see if one matched.
					if($acceptcheck != ''){
						$this->_error = $acceptcheck;
						return false;
					}
				}

				// Now all the checks should be completed and I can safely copy the file away from the temporary filesystem.
				$f->copyTo($nf);

				$value = $nf->getFilename(false);
			}
		}

		$this->_attributes['value'] = $value;
		return true;
	}
Esempio n. 5
0
	public function testGetMTime() {
		$file1 = new \Core\Filestore\Backends\FileRemote($this->_testfile);

		$this->assertFalse($file1->getMTime());
	}
	public static function PerformInstall($type, $name, $version, $dryrun = false, $verbose = false){

		if($verbose){
			// These are needed to force the output to be sent immediately.
			while ( @ob_end_flush() ); // even if there is no nested output buffer
			if(function_exists('apache_setenv')){
				// This function doesn't exist in CGI mode :/
				apache_setenv('no-gzip', '1');
			}
			ini_set('output_buffering','on');
			ini_set('zlib.output_compression', 0);
			ob_implicit_flush();

			// Give some basic styles for this output.
			echo '<html>
	<head>
	<!-- Yes, the following is 1024 spaces.  This is because some browsers have a 1Kb buffer before they start rendering text -->
	' . str_repeat(" ", 1024) . '
		<style>
			body {
				background: none repeat scroll 0 0 black;
				color: #22EE33;
				font-family: monospace;
			}
		</style>
	</head>
	<body>';
		}

		$timer = microtime(true);
		// Give this script a few more seconds to run.
		set_time_limit(max(90, ini_get('max_execution_time')));

		// This will get a list of all available updates and their sources :)
		if($verbose) self::_PrintHeader('Retrieving Updates');
		$updates = UpdaterHelper::GetUpdates();
		if($verbose){
			self::_PrintInfo('Found ' . $updates['sitecount'] . ' repository site(s)!', $timer);
			self::_PrintInfo('Found ' . $updates['pkgcount'] . ' packages!', $timer);
		}

		// A list of changes that are to be applied, (mainly for the dry run).
		$changes = array();

		// Target in on the specific object we're installing.  Useful for a shortcut.
		switch($type){
			case 'core':
				$initialtarget = &$updates['core'];
				break;
			case 'components':
				$initialtarget = &$updates['components'][$name];
				break;
			case 'themes':
				$initialtarget = &$updates['themes'][$name];
				break;
			default:
				return [
					'status' => 0,
					'message' => '[' . $type . '] is not a valid installation type!',
				];
		}

		// This is a special case for testing the installer UI.
		$test = ($type == 'core' && $version == '99.1337~(test)');

		if($test && $verbose){
			self::_PrintHeader('Performing a test installation!');
		}

		if($test){
			if($verbose){
				self::_PrintInfo('Sleeping for a few seconds... because servers are always slow when you don\'t want them to be!', $timer);
			}
			sleep(4);

			// Also overwrite some of the target's information.
			$repo = UpdateSiteModel::Find(null, 1);
			$initialtarget['source'] = 'repo-' . $repo->get('id');
			$initialtarget['location'] = 'http://corepl.us/api/2_4/tests/updater-test.tgz.asc';
			$initialtarget['destdir'] = ROOT_PDIR;
			$initialtarget['key'] = 'B2BEDCCB';
			$initialtarget['status'] = 'update';

			//if($verbose){
			//	echo '[DEBUG]' . $nl;
			//	var_dump($initialtarget);
			//}
		}

		// Make sure the name and version exist in the updates list.
		// In theory, the latest version of core is the only one displayed.
		if(!$test && $initialtarget['version'] != $version){
			return [
				'status' => 0,
				'message' => $initialtarget['typetitle'] . ' does not have the requested version available.',
				'debug' => [
					'versionrequested' => $version,
					'versionfound' => $initialtarget['version'],
				],
			];
		}

		// A queue of components to check.
		$pendingqueue = array($initialtarget);
		// A queue of components that will be installed that have satisfied dependencies.
		$checkedqueue = array();

		// This will assemble the list of required installs in the correct order.
		// If a given dependency can't be met, the installation will be aborted.
		if($verbose){
			self::_PrintHeader('CHECKING DEPENDENCIES');
		}
		do{
			$lastsizeofqueue = sizeof($pendingqueue);

			foreach($pendingqueue as $k => $c){
				$good = true;

				if(isset($c['requires'])){
					if($verbose){
						self::_PrintInfo('Checking dependencies for ' . $c['typetitle'], $timer);
					}

					foreach($c['requires'] as $r){

						// Sometimes there will be blank requirements in the metafile.
						if(!$r['name']) continue;

						$result = UpdaterHelper::CheckRequirement($r, $checkedqueue, $updates);

						if($result === false){
							// Dependency not met
							return [
								'status' => 0,
								'message' => $c['typetitle'] . ' requires ' . $r['name'] . ' ' . $r['version']
							];
						}
						elseif($result === true){
							// Dependency met via either installed components or new components
							// yay
							if($verbose){
								self::_PrintInfo('Dependency [' . $r['name'] . ' ' . $r['version'] . '] met with already-installed packages.', $timer);
							}
						}
						else{
							if($verbose){
								self::_PrintInfo('Additional package [' . $result['typetitle'] . '] required to meet dependency [' . $r['name'] . ' ' . $r['version'] . '], adding to queue and retrying!', $timer);
							}
							// It's an array of requirements that are needed to satisfy this installation.
							$pendingqueue = array_merge(array($result), $pendingqueue);
							$good = false;
						}
					}
				}
				else{
					if($verbose){
						self::_PrintInfo('Skipping dependency check for ' . $c['typetitle'] . ', no requirements present', $timer);
					}

					// The require key isn't present... OK!
					// This happens with themes, as they do not have any dependency logic.
				}

				if($good === true){
					$checkedqueue[] = $c;
					$changes[] = (($c['status'] == 'update') ? 'Update' : 'Install') .
						' ' . $c['typetitle'] . ' ' . $c['version'];
					unset($pendingqueue[$k]);
				}
			}
		}
		while(sizeof($pendingqueue) && sizeof($pendingqueue) != $lastsizeofqueue);

		// Do validation checks on all these changes.  I need to make sure I have the GPG key for each one.
		// This is done here to save having to download the files from the remote server first.
		foreach($checkedqueue as $target){
			// It'll be validated prior to installation anyway.
			if(!$target['key']) continue;

			$output = array();
			exec('gpg --homedir "' . GPG_HOMEDIR . '" --list-public-keys "' . $target['key'] . '"', $output, $result);
			if($result > 0){
				// Key validation failed!
				if($verbose){
					echo implode("<br/>\n", $output);
				}
				return [
					'status' => 0,
					'message' => $c['typetitle'] . ' failed GPG verification! Is the key ' . $target['key'] . ' installed?'
				];
			}
		}


		// Check that the queued packages have not been locally modified if installed.
		if($verbose){
			self::_PrintHeader('Checking for local modifications');
		}
		foreach($checkedqueue as $target){
			if($target['status'] == 'update'){
				switch($target['type']){
					case 'core':
						$c = Core::GetComponent('core');
						break;
					case 'components':
						$c = Core::GetComponent($target['name']);
						break;
					case 'themes':
						$c = null;
						break;
				}

				if($c){
					// Are there changes?
					if(sizeof($c->getChangedAssets())){
						foreach($c->getChangedAssets() as $change){
							$changes[] = 'Overwrite locally-modified asset ' . $change;
						}
					}
					if(sizeof($c->getChangedFiles())){
						foreach($c->getChangedFiles() as $change){
							$changes[] = 'Overwrite locally-modified file ' . $change;
						}
					}
					if(sizeof($c->getChangedTemplates())){
						foreach($c->getChangedTemplates() as $change){
							$changes[] = 'Overwrite locally-modified template ' . $change;
						}
					}
				}
			}
		}


		// If dry run is enabled, stop here.
		// After this stage, dragons be let loose from thar cages.
		if($dryrun){
			return [
				'status' => 1,
				'message' => 'All dependencies are met, ok to install',
				'changes' => $changes,
			];
		}


		// Reset changes, in this case it'll be what was installed.
		$changes = array();

		// By now, $checkedqueue will contain all the pending changes, theoretically with
		// the initially requested package at the end of the list.
		foreach($checkedqueue as $target){

			if($verbose){
				self::_PrintHeader('PERFORMING INSTALL (' . strtoupper($target['typetitle']) . ')');
			}

			// This package is already installed and up to date.
			if($target['source'] == 'installed'){
				return [
					'status' => 0,
					'message' => $target['typetitle'] . ' is already installed and at the newest version.',
				];
			}
			// If this package is coming from a repo, install it from that repo.
			elseif(strpos($target['source'], 'repo-') !== false){
				/** @var $repo UpdateSiteModel */
				$repo = new UpdateSiteModel(substr($target['source'], 5));
				if($verbose){
					self::_PrintInfo('Using repository ' . $repo->get('url') . ' for installation source', $timer);
				}

				// Setup the remote file that will be used to download from.
				$file = new \Core\Filestore\Backends\FileRemote($target['location']);
				$file->username = $repo->get('username');
				$file->password = $repo->get('password');

				// The initial HEAD request pulls the metadata for the file, and sees if it exists.
				if($verbose){
					self::_PrintInfo('Performing HEAD lookup on ' . $file->getFilename(), $timer);
				}
				if(!$file->exists()){
					return [
						'status' => 0,
						'message' => $target['location'] . ' does not seem to exist!'
					];
				}
				if($verbose){
					self::_PrintInfo('Found a(n) ' . $file->getMimetype() . ' file that returned a ' . $file->getStatus() . ' status.', $timer);
				}

				// Get file contents will download the file.
				if($verbose){
					self::_PrintInfo('Downloading ' . $file->getFilename(), $timer);
				}
				$downloadtimer = microtime(true);
				$obj = $file->getContentsObject();
				// Getting the object simply sets it up, it doesn't download the contents yet.
				$obj->getContents();
				// Now it has :p
				// How long did it take?
				if($verbose){
					self::_PrintInfo('Downloaded ' . $file->getFilesize(true) . ' in ' . (round(microtime(true) - $downloadtimer, 2) . ' seconds'), $timer);
				}

				if(!($obj instanceof \Core\Filestore\Contents\ContentASC)){
					return [
						'status' => 0,
						'message' => $target['location'] . ' does not appear to be a valid GPG signed archive'
					];
				}

				if(!$obj->verify()){
					// Maybe it can at least get the key....
					if($key = $obj->getKey()){
						return [
							'status' => 0,
							'message' => 'Unable to locate public key for ' . $key . '.  Is it installed?'
						];
					}
					return [
						'status' => 0,
						'message' => 'Invalid GPG signature for ' . $target['typetitle'],
					];
				}

				// The object's key must also match what's in the repo.
				if($obj->getKey() != $target['key']){
					return [
						'status' => 0,
						'message' => '!!!WARNING!!!, Key for ' . $target['typetitle'] . ' is valid, but does not match what was expected form the repository data!  This could be a major risk!',
						'debug' => [
							'detectedkey' => $obj->getKey(),
							'expectedkey' => $target['key'],
						],
					];
				}
				if($verbose){
					self::_PrintInfo('Found key ' . $target['key'] . ' for package maintainer, appears to be valid.', $timer);
					exec('gpg --homedir "' . GPG_HOMEDIR . '" --list-public-keys "' . $target['key'] . '"', $output, $result);
					foreach($output as $line){
						if(trim($line)) self::_PrintInfo(htmlentities($line), $timer);
					}
				}

				if($verbose) self::_PrintInfo('Checking write permissions', $timer);
				$dir = \Core\directory($target['destdir']);
				if(!$dir->isWritable()){
					return [
						'status' => 0,
						'message' => $target['destdir'] . ' is not writable!'
					];
				}
				if($verbose) self::_PrintInfo('OK!', $timer);


				// Decrypt the signed file.
				if($verbose) self::_PrintInfo('Decrypting signed file', $timer);

				if(version_compare(Core::GetComponent('core')->getVersionInstalled(), '4.1.1', '<=') && $file->getBaseFilename() == 'download'){
					// HACK < 4.1.2
					// Retrieve the filename from the last part of the URL.
					// This is required because the URL may be /download?file=component/blah.tgz.asc
					$f = substr($file->getFilename(), strrpos($file->getFilename(), '/'), -4);
					/** @var $localfile \Core\Filestore\File */
					$localfile = $obj->decrypt('tmp/updater/' . $f);
				}
				else{
					/** @var $localfile \Core\Filestore\File */
					$localfile = $obj->decrypt('tmp/updater/');
				}

				/** @var $localobj \Core\Filestore\Contents\ContentTGZ */
				$localobj = $localfile->getContentsObject();
				if($verbose) self::_PrintInfo('OK!', $timer);

				// This tarball will be extracted to a temporary directory, then copied from there.
				if($verbose){
					self::_PrintInfo('Extracting tarball ' . $localfile->getFilename(), $timer);
				}
				$tmpdir = $localobj->extract('tmp/installer-' . Core::RandomHex(4));

				// Now that the data is extracted in a temporary directory, extract every file in the destination.
				/** @var $datadir \Core\Filestore\Directory */
				$datadir = $tmpdir->get('data/');
				if(!$datadir){
					return [
						'status' => 0,
						'message' => 'Invalid package, ' . $target['typetitle'] . ', does not contain a "data" directory.'
					];
				}
				if($verbose) self::_PrintInfo('OK!', $timer);


				if($verbose){
					self::_PrintInfo('Installing files into ' . $target['destdir'], $timer);
				}

				// Will give me an array of Files in the data directory.
				$files = $datadir->ls(null, true);
				// Used to get the relative path for each contained file.
				$datalen = strlen($datadir->getPath());
				foreach($files as $file){
					if(!$file instanceof \Core\Filestore\Backends\FileLocal) continue;

					// It's a file, copy it over.
					// To do so, resolve the directory path inside the temp data dir.
					$dest = \Core\Filestore\Factory::File($target['destdir'] . substr($file->getFilename(), $datalen));
					/** @var $dest \Core\Filestore\Backends\FileLocal */
					if($verbose){
						self::_PrintInfo('...' . substr($dest->getFilename(''), 0, 67), $timer);
					}
					$dest->copyFrom($file, true);
				}
				if($verbose) self::_PrintInfo('OK!', $timer);


				// Cleanup the temp directory
				if($verbose){
					self::_PrintInfo('Cleaning up temporary directory', $timer);
				}
				$tmpdir->remove();
				if($verbose) self::_PrintInfo('OK!', $timer);

				$changes[] = 'Installed ' . $target['typetitle'] . ' ' . $target['version'];
			}
		}

		// Clear the cache so the next pageload will pick up on the new components and goodies.
		\Core\Cache::Flush();
		\Core\Templates\Backends\Smarty::FlushCache();

		// Yup, that's it.
		// Just extract the files and Core will autoinstall/autoupgrade everything on the next page view.


		// yay...
		return [
			'status' => 1,
			'message' => 'Performed all operations successfully!',
			'changes' => $changes,
		];
	}
Esempio n. 7
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;
	}