/** * Get the repository XML as a string that can be returned to the browser or cached for future use. * * @param string|null $serverid The server ID making the request, or null for anonymous. * @param string|null $limitPackager Limit the packager returned to at least version X.Y. * * @return RepoXML */ public static function GetAsRepoXML($serverid, $limitPackager) { $repo = new RepoXML(); $repo->setDescription(ConfigHandler::Get('/package_repository/description')); $gpg = new Core\GPG\GPG(); $keysfound = []; $where = []; if ($limitPackager) { $where[] = 'packager LIKE ' . $limitPackager . '%'; } $packages = PackageRepositoryPackageModel::Find($where, null, 'type DESC, key ASC, version'); foreach ($packages as $pkg) { /** @var PackageRepositoryPackageModel $pkg */ $package = new PackageXML(null); $package->setType($pkg->get('type')); $package->setName($pkg->get('name')); $package->setVersion($pkg->get('version')); $package->setPackager($pkg->get('packager')); $package->setDescription($pkg->get('description')); $package->setKey($pkg->get('gpg_key')); if (!in_array($pkg->get('gpg_key'), $keysfound)) { $keysfound[] = $pkg->get('gpg_key'); } $package->setFileLocation(\Core\resolve_link('/packagerepository/download?file=' . $pkg->get('file'))); $upgrades = $pkg->get('requires'); foreach ($upgrades as $dat) { $package->setRequire($dat['type'], $dat['name'], $dat['version'], $dat['operation']); } $upgrades = $pkg->get('provides'); foreach ($upgrades as $dat) { $package->setProvide($dat['type'], $dat['name'], $dat['version']); } $upgrades = $pkg->get('upgrades'); foreach ($upgrades as $dat) { $package->setUpgrade($dat['from'], $dat['to']); } $screens = $pkg->get('screenshots'); foreach ($screens as $dat) { $f = \Core\Filestore\Factory::File($dat); $package->setScreenshot($f->getURL()); } $package->setChangelog($pkg->get('changelog')); $repo->addPackage($package); } return $repo; }
/** * 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); } }
/** * 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(); }
if(!sizeof($updatesites)){ CLI::PrintActionStart('No repositories installed'); CLI::PrintActionStatus('skip'); } foreach($updatesites as $site){ CLI::PrintActionStart('Scanning repository ' . $site->get('url')); /** @var UpdateSiteModel $site */ if(!$site->isValid()){ CLI::PrintActionStatus('failed'); continue; } ++$sitecount; $file = $site->getFile(); $repoxml = new RepoXML(); $repoxml->loadFromFile($file); $rootpath = dirname($file->getFilename()) . '/'; CLI::PrintActionStatus('ok'); foreach($repoxml->getPackages() as $pkg){ /** @var PackageXML $pkg */ $n = str_replace(' ', '-', strtolower($pkg->getName())); $type = $pkg->getType(); $location = $pkg->getFileLocation(); if(strpos($location, '://') === false){ // The remote location may or may not be fully resolved. $location = $rootpath . $location; }
$destdir = BASE_DIR . 'exports/'; $tmpdir = BASE_DIR . 'exports/_tmp/'; // Ensure the export directory exists. if(!is_dir($destdir)) exec('mkdir -p "' . $destdir . '"'); if(!is_dir($tmpdir)) exec('mkdir -p "' . $tmpdir . '"'); if(file_exists($destdir . 'repo.xml')){ // w00t, load it up and import the info! $repo = new RepoXML($destdir . 'repo.xml'); // Don't forget to remove any previous components! $repo->clearPackages(); } else{ // Just a new one works... $repo = new RepoXML(); } // Prompt the user if there's not information already set on the necessary ones. if(!$repo->getDescription()){ $desc = \Core\CLI\CLI::PromptUser('Please enter a short description for this repo.', 'textarea'); $repo->setDescription($desc); } // Load in the keys if not set. if(!sizeof($repo->getKeys())){ // Find and use the package maintainer's key. $out = array(); exec('gpg --homedir "' . GPG_HOMEDIR . '" --no-permission-warning --list-secret-keys', $out); $key = null;
/** * Perform a lookup on any repository sites installed and get a list of provided pacakges. * * @return array */ public static function GetUpdates(){ // Allow this to be cached for x amount of time. This will save the number of remote requests. //if(false && isset($_SESSION['updaterhelper_getupdates']) && $_SESSION['updaterhelper_getupdates']['expire'] <= time()){ // return $_SESSION['updaterhelper_getupdates']['data']; //} // Build a list of components currently installed, this will act as a base. $components = array(); $core = array(); $themes = array(); $sitecount = 0; $pkgcount = 0; $current = Core::GetComponents(); $froze = \ConfigHandler::Get('/core/updater/versionfreeze'); // If Core isn't installed yet, GetComponents will yield null. if($current === null) $current = array(); /** @var string $backportVersion Add support for "~bpoXYZ" version strings for backported packages. */ $coreVersion = Core::GetVersion(); $coreVersionParts = Core::VersionSplit($coreVersion); $backportVersion = '~bpo' . $coreVersionParts['major'] . $coreVersionParts['minor'] . $coreVersionParts['point']; foreach($current as $c){ /** @var $c Component_2_1 */ $n = $c->getKeyName(); $parts = Core::VersionSplit($c->getVersion()); if($n == 'core'){ $core = array( 'name' => $n, 'title' => $c->getName(), 'version' => $c->getVersion(), 'feature' => $parts['major'] . '.' . $parts['minor'], 'source' => 'installed', 'description' => $c->getDescription(), 'provides' => $c->getProvides(), 'requires' => $c->getRequires(), 'location' => null, 'status' => 'installed', 'type' => 'core', 'typetitle' => 'Core', 'key' => null, 'destdir' => $c->getBaseDir(), ); } else{ $components[$n] = array( 'name' => $n, 'title' => $c->getName(), 'version' => $c->getVersion(), 'feature' => $parts['major'] . '.' . $parts['minor'], 'source' => 'installed', 'description' => $c->getDescription(), 'provides' => $c->getProvides(), 'requires' => $c->getRequires(), 'location' => null, 'status' => 'installed', 'type' => 'components', 'typetitle' => 'Component ' . $c->getName(), 'key' => null, 'destdir' => $c->getBaseDir(), ); } } foreach(Core::GetDisabledComponents() as $c){ /** @var $c Component_2_1 */ $n = $c->getKeyName(); $parts = Core::VersionSplit($c->getVersion()); $components[$n] = array( 'name' => $n, 'title' => $c->getName(), 'version' => $c->getVersion(), 'feature' => $parts['major'] . '.' . $parts['minor'], 'source' => 'installed', 'description' => $c->getDescription(), 'provides' => $c->getProvides(), 'requires' => $c->getRequires(), 'location' => null, 'status' => 'disabled', 'type' => 'components', 'typetitle' => 'Component ' . $c->getName(), 'key' => null, 'destdir' => $c->getBaseDir(), ); } // And repeat for the themes. // I need to do a check if they exist because if called from the installer, it may not. if(class_exists('ThemeHandler')){ $currentthemes = ThemeHandler::GetAllThemes(); if($currentthemes === null) $currentthemes = array(); } else{ $currentthemes = array(); } foreach($currentthemes as $t){ /** @var $t Theme */ $n = $t->getKeyName(); $parts = Core::VersionSplit($t->getVersion()); $themes[$n] = array( 'name' => $n, 'title' => $t->getName(), 'version' => $t->getVersion(), 'feature' => $parts['major'] . '.' . $parts['minor'], 'source' => 'installed', 'description' => $t->getDescription(), 'location' => null, 'status' => 'installed', 'type' => 'themes', 'typetitle' => 'Theme ' . $t->getName(), 'key' => null, 'destdir' => $t->getBaseDir(), ); } // Now, look up components from all the updates sites. // If the system isn't installed yet, then this will not be found. Just use a blank array. if(class_exists('UpdateSiteModel')){ $updatesites = UpdateSiteModel::Find(); } else{ $updatesites = array(); } foreach($updatesites as $site){ if(!$site->isValid()) continue; ++$sitecount; $file = $site->getFile(); $repoxml = new RepoXML(); $repoxml->loadFromFile($file); $rootpath = dirname($file->getFilename()) . '/'; foreach($repoxml->getPackages() as $pkg){ /** @var $pkg PackageXML */ // Already installed and is up to date, don't do anything. //if($pkg->isCurrent()) continue; $n = str_replace(' ', '-', strtolower($pkg->getName())); $type = $pkg->getType(); if($n == 'core') $type = 'core'; // Override the core, even though it is a component... ++$pkgcount; switch($type){ case 'core': $vers = $pkg->getVersion(); // Only display the newest version available. if(!Core::VersionCompare($vers, $core['version'], 'gt')) continue; // Only display new feature versions if it's not frozen. $parts = Core::VersionSplit($pkg->getVersion()); if($froze && $core['feature'] != $parts['major'] . '.' . $parts['minor']) continue; $core = array( 'name' => $n, 'title' => $pkg->getName(), 'version' => $vers, 'feature' => $parts['major'] . '.' . $parts['minor'], 'source' => 'repo-' . $site->get('id'), 'sourceurl' => $site->get('url'), 'description' => $pkg->getDescription(), 'provides' => $pkg->getProvides(), 'requires' => $pkg->getRequires(), 'location' => $pkg->getFileLocation(), 'status' =>'update', 'type' => 'core', 'typetitle' => 'Core ', 'key' => $pkg->getKey(), 'destdir' => ROOT_PDIR, ); break; case 'component': $vers = $pkg->getVersion(); $parts = Core::VersionSplit($pkg->getVersion()); $packagedWithVersion = $pkg->getRootDOM()->getAttribute('packager'); if($packagedWithVersion && Core::VersionCompare($packagedWithVersion, $coreVersion, '>')){ // Skip any package created with a newer version of Core than what is currently installed! continue; } // Is it already loaded in the list? if(isset($components[$n])){ if(strpos($vers, '~bpo') !== false && strpos($vers, $backportVersion) === false){ // Skip back ported versions not specifically for this version of Core. // This will check and see if the string ~bpo is present, // and when it is, enforce that it matches exactly this version of Core. continue; } // I only want the newest version. if(!Core::VersionCompare($vers, $components[$n]['version'], 'gt')){ continue; } // Only display new feature versions if it's not frozen. if( $froze && $components[$n]['status'] == 'installed' && $components[$n]['feature'] != $parts['major'] . '.' . $parts['minor'] ){ continue; } } // If it's available in the core, it's an update... otherwise it's new. $status = Core::GetComponent($n) ? 'update' : 'new'; $components[$n] = array( 'name' => $n, 'title' => $pkg->getName(), 'version' => $vers, 'feature' => $parts['major'] . '.' . $parts['minor'], 'source' => 'repo-' . $site->get('id'), 'sourceurl' => $site->get('url'), 'description' => $pkg->getDescription(), 'provides' => $pkg->getProvides(), 'requires' => $pkg->getRequires(), 'location' => $pkg->getFileLocation(), 'status' => $status, 'type' => 'components', 'typetitle' => 'Component ' . $pkg->getName(), 'key' => $pkg->getKey(), 'destdir' => ROOT_PDIR . 'components/' . $n . '/', ); break; case 'theme': $vers = $pkg->getVersion(); $parts = Core::VersionSplit($pkg->getVersion()); // Is it already loaded in the list? if(isset($themes[$n])){ // I only want the newest version. if(!Core::VersionCompare($vers, $themes[$n]['version'], 'gt')) continue; // Only display new feature versions if it's not frozen. if( $froze && $themes[$n]['status'] == 'installed' && $themes[$n]['feature'] != $parts['major'] . '.' . $parts['minor'] ){ continue; } } $status = ThemeHandler::GetTheme($n) ? 'update' : 'new'; $themes[$n] = array( 'name' => $n, 'title' => $pkg->getName(), 'version' => $vers, 'feature' => $parts['major'] . '.' . $parts['minor'], 'source' => 'repo-' . $site->get('id'), 'sourceurl' => $site->get('url'), 'description' => $pkg->getDescription(), 'location' => $pkg->getFileLocation(), 'status' => $status, 'type' => 'themes', 'typetitle' => 'Theme ' . $pkg->getName(), 'key' => $pkg->getKey(), 'destdir' => ROOT_PDIR . 'themes/' . $n . '/', ); } //var_dump($pkg->asPrettyXML()); die(); } } // Give me the components in alphabetical order. ksort($components); ksort($themes); // Cache this for next pass. //$_SESSION['updaterhelper_getupdates'] = array(); //$_SESSION['updaterhelper_getupdates']['data'] = array('core' => $core, 'components' => $components, 'themes' => $themes); //$_SESSION['updaterhelper_getupdates']['expire'] = time() + 60; return [ 'core' => $core, 'components' => $components, 'themes' => $themes, 'sitecount' => $sitecount, 'pkgcount' => $pkgcount, ]; }