/** * 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); } }
/** * Test the verify file method * * @depends testImportShort */ public function testDeleteKey(){ $gpg = new \Core\GPG\GPG(); $gpg->deleteKey($this->fingerprint); }
/** * 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; }
/** * 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]; }
/** * 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; }