/**
  * Get the repository XML as a string that can be returned to the browser or cached for future use.
  *
  * @return string
  */
 private function _getRepoXML()
 {
     $repo = new RepoXML();
     $repo->setDescription(ConfigHandler::Get('/package_repository/description'));
     $dir = Factory::Directory(\ConfigHandler::Get('/package_repository/base_directory'));
     $coredir = $dir->getPath() . 'core/';
     $componentdir = $dir->getPath() . 'components/';
     $themedir = $dir->getPath() . 'themes/';
     $tmpdir = Factory::Directory('tmp/exports/');
     $gpg = new Core\GPG\GPG();
     $keysfound = [];
     $private = ConfigHandler::Get('/package_repository/is_private') || strpos($dir->getPath(), ROOT_PDIR) !== 0;
     $addedpackages = 0;
     $failedpackages = 0;
     $iterator = new \Core\Filestore\DirectoryIterator($dir);
     // Only find signed packages.
     $iterator->findExtensions = ['asc'];
     // Recurse into sub directories
     $iterator->recursive = true;
     // No directories
     $iterator->findDirectories = false;
     // Just files
     $iterator->findFiles = true;
     // And sort them by their filename to make things easy.
     $iterator->sortBy('filename');
     // Ensure that the necessary temp directory exists.
     $tmpdir->mkdir();
     foreach ($iterator as $file) {
         /** @var \Core\Filestore\File $file */
         $fullpath = $file->getFilename();
         // Used in the XML file.
         if ($private) {
             $relpath = \Core\resolve_link('/packagerepository/download?file=' . substr($file->getFilename(), strlen($dir->getPath())));
         } else {
             $relpath = $file->getFilename(ROOT_PDIR);
         }
         // Drop the .asc extension.
         $basename = $file->getBasename(true);
         // Tarball of the temporary package
         $tgz = Factory::File($tmpdir->getPath() . $basename);
         $output = [];
         // I need to 1) retrieve and 2) verify the key for this package.
         try {
             $signature = $gpg->verifyFileSignature($fullpath);
             if (!in_array($signature->keyID, $keysfound)) {
                 $repo->addKey($signature->keyID, null, null);
                 $keysfound[] = $signature->keyID;
             }
         } catch (\Exception $e) {
             trigger_error($fullpath . ' was not able to be verified as authentic, (probably because the GPG public key was not available)');
             $failedpackages++;
             continue;
         }
         // decode and untar it in a temp directory to get the package.xml file.
         exec('gpg --homedir "' . GPG_HOMEDIR . '" -q -d "' . $fullpath . '" > "' . $tgz->getFilename() . '" 2>/dev/null', $output, $ret);
         if ($ret) {
             trigger_error('Decryption of file ' . $fullpath . ' failed!');
             $failedpackages++;
             continue;
         }
         exec('tar -xzf "' . $tgz->getFilename() . '" -C "' . $tmpdir->getPath() . '" ./package.xml', $output, $ret);
         if ($ret) {
             trigger_error('Unable to extract package.xml from' . $tgz->getFilename());
             unlink($tmpdir->getPath() . $basename);
             $failedpackages++;
             continue;
         }
         // Read in that package file and append it to the repo xml.
         $package = new PackageXML($tmpdir->getPath() . 'package.xml');
         $package->getRootDOM()->setAttribute('key', $signature->keyID);
         $package->setFileLocation($relpath);
         $repo->addPackage($package);
         $addedpackages++;
         // But I can still cleanup!
         unlink($tmpdir->getPath() . 'package.xml');
         $tgz->delete();
     }
     return $repo->asPrettyXML();
 }
	/**
	 * Add a repository to the site.
	 * This will also handle the embedded keys, (as of 2.4.5).
	 *
	 * This contains the first step and second steps.
	 */
	public function repos_add() {
		$request = $this->getPageRequest();
		$view    = $this->getView();

		$site = new UpdateSiteModel();

		$form = Form::BuildFromModel($site);
		$form->set('action', \Core\resolve_link('/updater/repos/add'));
		$form->addElement('submit', array('value' => 'Next'));

		$view->title = 'Add Repo';
		// Needed because dynamic pages do not record navigation.
		$view->addBreadcrumb('Repositories', 'updater/repos');

		$view->assign('form', $form);

		if(!is_dir(GPG_HOMEDIR)){
			// Try to create it?
			if(is_writable(dirname(GPG_HOMEDIR))){
				// w00t
				mkdir(GPG_HOMEDIR);
			}
			else{
				\Core\set_message(GPG_HOMEDIR . ' does not exist and could not be created!  Please fix this before proceeding!', 'error');
				$form = null;
			}
		}
		elseif(!is_writable(GPG_HOMEDIR)){
			\Core\set_message(GPG_HOMEDIR . ' is not writable!  Please fix this before proceeding!', 'error');
			$form = null;
		}


		// This is the logic for step 2 (confirmation).
		// This is after all the template logic from step 1 because it will fallback to that form if necessary.
		if($request->isPost()){
			$url      = $request->getPost('model[url]');
			$username = $request->getPost('model[username]');
			$password = $request->getPost('model[password]');

			// Validate and standardize this repo url.
			// This is because most people will simply type repo.corepl.us.
			if(strpos($url, '://') === false){
				$url = 'http://' . $url;
			}

			// Lookup that URL first!
			if(UpdateSiteModel::Count(array('url' => $url)) > 0){
				\Core\set_message($url . ' is already used!', 'error');
				return;
			}

			// Load up a new Model, that's the easiest way to pull the repo data.
			$model = new UpdateSiteModel();
			$model->setFromArray([
				'url' => $url,
				'username' => $username,
				'password' => $password,
			]);

			// From here on out, populate the previous form with this new model.
			$form = Form::BuildFromModel($model);
			$form->set('action', \Core\resolve_link('/updater/repos/add'));
			$form->addElement('submit', array('value' => 'Next'));
			$view->assign('form', $form);

			/** @var \Core\Filestore\Backends\FileRemote $remote */
			$remote = $model->getFile();

			if($remote->requiresAuthentication()){
				if(!$username){
					\Core\set_message($url . ' requires authentication!', 'error');
					return;
				}
				else{
					\Core\set_message('Invalid credentials for ' . $url, 'error');
					return;
				}
			}

			if(!$model->isValid()){
				\Core\set_message($url . ' does not appear to be a valid repository!', 'error');
				return;
			}

			$repo = new RepoXML();
			$repo->loadFromFile($remote);

			// Make sure the keys are good
			if(!$repo->validateKeys()){
				\Core\set_message('There were invalid/unpublished keys in the repo!  Refusing to import.', 'error');
				return;
			}

			// The very final bit of this logic is to look and see if there's a "confirm" present.
			// If there is, the user clicked accept on the second page and I need to go ahead and import the data.
			if($request->getPost('confirm')){
				$model->set('description', $repo->getDescription());
				$model->save();
				$keysimported = 0;
				$keycount     = sizeof($repo->getKeys());
				$gpg          = new \Core\GPG\GPG();

				foreach($repo->getKeys() as $keyData){
					try{
						$gpg->importKey($keyData['key']);
						++$keysimported;
					}
					catch(Exception $e){
						\Core\set_message('Unable to import key [' . $keyData['key'] . '] from keyserver!', 'error');
					}
				}

				if(!$keycount){
					\Core\set_message('Added repository site successfully!', 'success');
				}
				elseif($keycount != $keysimported){
					\Core\set_message('Added repository site, but unable to import ' . ($keycount-$keysimported) . ' key(s).', 'info');
				}
				else{
					\Core\set_message('Added repository site and imported ' . $keysimported . ' key(s) successfully!', 'success');
				}

				\core\redirect('/updater/repos');
			}

			$view->templatename = 'pages/updater/repos_add2.tpl';
			$view->assign('description', $repo->getDescription());
			$view->assign('keys', $repo->getKeys());
			$view->assign('url', $url);
			$view->assign('username', $username);
			$view->assign('password', $password);
		}
	}
Example #3
0
	/**
	 * Test the verify file method
	 *
	 * @depends testImportShort
	 */
	public function testDeleteKey(){
		$gpg = new \Core\GPG\GPG();
		$gpg->deleteKey($this->fingerprint);
	}
Example #4
0
	/**
	 * Get an array of keys to install automatically with this repo.
	 *
	 * @return array
	 */
	public function getKeys(){
		if($this->_keys !== null){
			// Cache!
			return $this->_keys;
		}

		$gpg         = new \Core\GPG\GPG();
		$this->_keys = [];

		foreach($this->getElements('keys/key') as $k){
			$id    = $k->getAttribute('id');
			$key   = null;
			$local = true;

			// Try to find more info about this key!
			// First step is to assign the key from local data.
			// If that fails, gracefully search remote servers for it.
			if(($key = $gpg->getKey($id)) === null){
				$remoteKeys = $gpg->searchRemoteKeys($id);
				foreach($remoteKeys as $k){
					/** @var \Core\GPG\PublicKey $k */
					if($k->id == $id || $k->id_short == $id){
						$key = $k;
						$local = false;
						break;
					}
				}
			}

			if($key !== null){
				$dat = [
					'key'        => $id,
					'available'  => true,
					'installed'  => $local,
					'fingerprint' => \Core\GPG\GPG::FormatFingerprint($key->fingerprint, false, true),
					'uids'        => [],
				];

				foreach($key->uids as $uid){
					/** @var \Core\GPG\UID $uid */
					if($uid->isValid()){
						$dat['uids'][] = ['name' => $uid->fullname, 'email' => $uid->email];
					}
				}
			}
			else{
				$dat = [
					'key'        => $id,
					'available'  => false,
					'installed'  => false,
					'fingerprint' => '',
					'uids'        => [],
				];
			}

			$this->_keys[] = $dat;
		}
		return $this->_keys;
	}
Example #5
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();
	}
 public static function RebuildPackages()
 {
     $dir = \Core\Filestore\Factory::Directory(\ConfigHandler::Get('/package_repository/base_directory'));
     $coredir = $dir->getPath() . 'core/';
     $componentdir = $dir->getPath() . 'components/';
     $themedir = $dir->getPath() . 'themes/';
     $tmpdir = \Core\Filestore\Factory::Directory('tmp/exports/');
     $gpg = new Core\GPG\GPG();
     $keysfound = [];
     $addedpackages = 0;
     $failedpackages = 0;
     $skippedpackages = 0;
     $ls = $dir->ls('asc', true);
     \Core\CLI\CLI::PrintProgressBar(0);
     $totalPackages = sizeof($ls);
     $percentEach = 100 / $totalPackages;
     $currentPercent = 0;
     // Ensure that the necessary temp directory exists.
     $tmpdir->mkdir();
     foreach ($ls as $file) {
         /** @var \Core\Filestore\File $file */
         $fullpath = $file->getFilename();
         $relpath = substr($file->getFilename(), strlen($dir->getPath()));
         $tmpdirpath = $tmpdir->getPath();
         // Drop the .asc extension.
         $basename = $file->getBasename(true);
         // Tarball of the temporary package
         $tgz = \Core\Filestore\Factory::File($tmpdirpath . $basename);
         $output = [];
         // I need to 1) retrieve and 2) verify the key for this package.
         try {
             $signature = $gpg->verifyFileSignature($fullpath);
         } catch (\Exception $e) {
             trigger_error($fullpath . ' was not able to be verified as authentic, (probably because the GPG public key was not available)');
             $failedpackages++;
             continue;
         }
         // decode and untar it in a temp directory to get the package.xml file.
         exec('gpg --homedir "' . GPG_HOMEDIR . '" -q -d "' . $fullpath . '" > "' . $tgz->getFilename() . '" 2>/dev/null', $output, $ret);
         if ($ret) {
             trigger_error('Decryption of file ' . $fullpath . ' failed!');
             $failedpackages++;
             continue;
         }
         // Extract the package.xml metafile, this is critical!
         exec('tar -xzf "' . $tgz->getFilename() . '" -C "' . $tmpdirpath . '" ./package.xml', $output, $ret);
         if ($ret) {
             trigger_error('Unable to extract package.xml from' . $tgz->getFilename());
             unlink($tmpdirpath . $basename);
             $failedpackages++;
             continue;
         }
         // Read in that package file and append it to the repo xml.
         $package = new PackageXML($tmpdirpath . 'package.xml');
         $package->getRootDOM()->setAttribute('key', $signature->keyID);
         $package->setFileLocation($relpath);
         // Core has a few differences than most components.
         if ($package->getKeyName() == 'core') {
             $pkgName = 'Core Plus';
             $chngName = 'Core Plus';
             $type = 'core';
             $chngDepth = 3;
             $chngFile = './data/core/CHANGELOG';
             $xmlFile = './data/core/component.xml';
         } else {
             $pkgName = $package->getName();
             $chngName = ($package->getType() == 'theme' ? 'Theme/' : '') . $package->getName();
             $type = $package->getType();
             $chngDepth = 2;
             $chngFile = './data/CHANGELOG';
             $xmlFile = './data/' . ($package->getType() == 'theme' ? 'theme.xml' : 'component.xml');
         }
         // Lookup this package in the database or create if it doesn't exist.
         $model = PackageRepositoryPackageModel::Find(['type = ' . $package->getType(), 'key = ' . $package->getKeyName(), 'version = ' . $package->getVersion()], 1);
         if (!$model) {
             $model = new PackageRepositoryPackageModel();
             $model->set('type', $type);
             $model->set('key', $package->getKeyName());
             $model->set('version', $package->getVersion());
         }
         // Set the data provided by the package.xml file.
         $model->set('name', $pkgName);
         $model->set('gpg_key', $package->getKey());
         $model->set('packager', $package->getPackager());
         $model->set('file', $relpath);
         $model->set('description', $package->getDescription());
         $model->set('requires', $package->getRequires());
         $model->set('provides', $package->getProvides());
         $model->set('upgrades', $package->getUpgrades());
         unlink($tmpdirpath . 'package.xml');
         // Extract out the CHANGELOG file, this is not so critical.
         // I need strip-components=2 to drop off the "." and "data" prefixes.
         exec('tar -xzf "' . $tgz->getFilename() . '" -C "' . $tmpdirpath . '" --strip-components=' . $chngDepth . ' ' . $chngFile, $output, $ret);
         // If there is a CHANGELOG, parse that too!
         if (file_exists($tmpdirpath . 'CHANGELOG')) {
             try {
                 $ch = new Core\Utilities\Changelog\Parser($chngName, $tmpdirpath . 'CHANGELOG');
                 $ch->parse();
                 // Get the version for this iteration.
                 $chsec = $ch->getSection($model->get('version'));
                 $model->set('packager_name', $chsec->getPackagerName());
                 $model->set('packager_email', $chsec->getPackagerEmail());
                 $model->set('datetime_released', $chsec->getReleasedDateUTC());
                 $model->set('changelog', $chsec->fetchAsHTML(null));
             } catch (Exception $e) {
                 // meh, we just won't have a changelog.
             } finally {
                 if (file_exists($tmpdirpath . 'CHANGELOG')) {
                     // Cleanup
                     unlink($tmpdirpath . 'CHANGELOG');
                 }
             }
         }
         // Retrieve out the screenshots from this component.
         exec('tar -xzf "' . $tgz->getFilename() . '" -O ' . $xmlFile . ' > "' . $tmpdirpath . 'comp.xml"', $output, $ret);
         if (file_exists($tmpdirpath . 'comp.xml')) {
             try {
                 $images = [];
                 $c = new Component_2_1($tmpdirpath . 'comp.xml');
                 $screens = $c->getScreenshots();
                 if (sizeof($screens)) {
                     foreach ($screens as $s) {
                         // Extract out this screen and save it to the filesystem.
                         $archivedFile = dirname($xmlFile) . '/' . $s;
                         $localFile = \Core\Filestore\Factory::File('public/packagerepo-screens/' . $model->get('type') . '-' . $model->get('key') . '-' . $model->get('version') . '/' . basename($s));
                         // Write something into the file so that it exists on the filesystem.
                         $localFile->putContents('');
                         // And now tar can extract directly to that destination!
                         exec('tar -xzf "' . $tgz->getFilename() . '" -O ' . $archivedFile . ' > "' . $localFile->getFilename() . '"', $output, $ret);
                         if (!$ret) {
                             // Return code should be 0 on a successful write.
                             $images[] = $localFile->getFilename(false);
                         }
                     }
                 }
                 $model->set('screenshots', $images);
             } catch (Exception $e) {
                 // meh, we just won't have images..
             } finally {
                 if (file_exists($tmpdirpath . 'comp.xml')) {
                     // Cleanup
                     unlink($tmpdirpath . 'comp.xml');
                 }
             }
         }
         if ($model->changed()) {
             $model->save(true);
             $addedpackages++;
         } else {
             $skippedpackages++;
         }
         // But I can still cleanup!
         $tgz->delete();
         $currentPercent += $percentEach;
         \Core\CLI\CLI::PrintProgressBar($currentPercent);
     }
     // Commit everything!
     PackageRepositoryPackageModel::CommitSaves();
     return ['updated' => $addedpackages, 'skipped' => $skippedpackages, 'failed' => $failedpackages];
 }
Example #7
0
	/**
	 * Validate the verification email, part 2 of confirmation.
	 *
	 * @param string $nonce
	 * @param string $signature
	 *
	 * @return bool|string
	 */
	public static function ValidateVerificationResponse($nonce, $signature) {
		/** @var \NonceModel $nonce */
		$nonce = \NonceModel::Construct($nonce);

		if(!$nonce->isValid()){
			\SystemLogModel::LogSecurityEvent('/user/gpg/verified', 'FAILED to verify key (Invalid NONCE)', null);
			return 'Invalid nonce provided!';
		}

		// Now is where the real fun begins.

		$nonce->decryptData();

		$data = $nonce->get('data');

		/** @var \UserModel $user */
		$user   = \UserModel::Construct($data['user']);
		$gpg    = new \Core\GPG\GPG();
		$key    = $data['key'];
		$pubKey = $gpg->getKey($key);

		try{
			$sig = $gpg->verifyDataSignature($signature, $data['sentence']);
		}
		catch(\Exception $e){
			\SystemLogModel::LogSecurityEvent('/user/gpg/verified', 'FAILED to verify key ' . $key, null, $user->get('id'));
			return 'Invalid signature';
		}

		$fpr = str_replace(' ', '', $sig->fingerprint); // Trim spaces.
		if($key != $fpr && $key != $sig->keyID){
			// They must match!
			\SystemLogModel::LogSecurityEvent('/user/gpg/verified', 'FAILED to verify key ' . $key, null, $user->get('id'));
			return 'Invalid signature';
		}

		// Otherwise?
		$user->enableAuthDriver('gpg');
		$user->set('gpgauth_pubkey', $fpr);

		// Was there a photo attached to this public key?
		if(sizeof($pubKey->getPhotos()) > 0){
			$p = $pubKey->getPhotos();
			// I just want the first.
			/** @var \Core\Filestore\File $p */
			$p = $p[0];

			$localFile = \Core\Filestore\Factory::File('public/user/avatar/' . $pubKey->fingerprint . '.' . $p->getExtension());
			$p->copyTo($localFile);
			$user->set('avatar', $localFile->getFilename(false));
		}
		
		$user->save();

		$nonce->markUsed();

		\SystemLogModel::LogSecurityEvent('/user/gpg/verified', 'Verified key ' . $fpr, null, $user->get('id'));

		return true;
	}