public static function _UploadHandler(Form $form) { $localfile = \Core\Filestore\Factory::File($form->getElement('upload')->get('value')); $localobj = $localfile->getContentsObject(); if(!$localobj instanceof Core\Filestore\Contents\ContentTGZ){ $localfile->delete(); \Core\set_message('Invalid file uploaded', 'error'); return false; } $tmpdir = $localobj->extract('tmp/installer-' . Core::RandomHex(4)); // There should now be a package.xml metafile inside that temporary directory. // Parse it to get the necessary information for this package. $metafile = \Core\Filestore\Factory::File($tmpdir->getPath() . 'package.xml'); if(!$metafile->exists()){ $localfile->delete(); $tmpdir->delete(); \Core\set_message('Invalid package, package does not contain a "package.xml" file.'); return false; } $pkg = new PackageXML($metafile->getFilename()); $key = str_replace(' ', '-', strtolower($pkg->getName())); $name = $pkg->getName(); $type = $pkg->getType(); $version = $pkg->getVersion(); // Validate the contents of the package. if(!( $type == 'component' || $type == 'theme' || $type == 'core' )){ $localfile->delete(); $tmpdir->delete(); \Core\set_message('Invalid package, package does not appear to be a valid Core package.'); return false; } // 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){ \Core\set_message('Invalid package, package does not contain a "data" directory.'); return false; } if($type == 'component'){ $destdir = ROOT_PDIR . 'components/' . $key . '/'; } elseif($type == 'theme'){ $destdir = ROOT_PDIR . 'themes/' . $key . '/'; } else{ $destdir = ROOT_PDIR . '/'; } try{ // 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($destdir . substr($file->getFilename(), $datalen)); /** @var $dest \Core\Filestore\Backends\FileLocal */ $dest->copyFrom($file, true); } } catch(Exception $e){ // OH NOES! $localfile->delete(); $tmpdir->delete(); \Core\set_message($e->getMessage(), 'error'); return false; } // Cleanup everything $localfile->delete(); $tmpdir->delete(); // Clear the cache so the next pageload will pick up on the new components and goodies. \Core\Cache::Flush(); \Core\Templates\Backends\Smarty::FlushCache(); // Print a nice message to the user that it completed. \Core\set_message('Successfully installed ' . $name . ' ' . $version, 'success'); return '/updater'; }
// All Models MUST reside in the global namespace in order to be valid. continue; } $ref = new ReflectionClass($class); if(!$ref->getProperty('HasSearch')->getValue()){ // This model doesn't have the searchable flag, skip it. continue; } CLI::PrintActionStart("Syncing searchable model $class"); $fac = new ModelFactory($class); foreach($fac->get() as $m){ /** @var Model $m */ $m->set('search_index_pri', '!'); $m->save(); } CLI::PrintActionStatus('ok'); $changes[] = "Synced searchable model " . $class; } } // Flush the system cache, just in case \Core\Cache::Flush(); \Core\Templates\Backends\Smarty::FlushCache(); CLI::PrintHeader('DONE!'); foreach($changes as $line){ CLI::PrintLine($line); }
/** * Run through and reinstall all components and themes. * * @return int */ public function reinstallAll() { // Admin-only page. if(!\Core\user()->checkAccess('g:admin')){ return View::ERROR_ACCESSDENIED; } // Just run through every component currently installed and reinstall it. // This will just ensure that the component is up to date and correct as per the component.xml metafile. $view = $this->getView(); $request = $this->getPageRequest(); if($request->isPost()){ $view->mode = View::MODE_NOOUTPUT; $view->contenttype = View::CTYPE_HTML; $view->record = false; $view->templatename = null; $view->render(); // Try to perform the reinstall. $changes = array(); $errors = array(); $allpages = []; $t = ThemeHandler::GetTheme(); CLI::PrintHeader('Reinstalling Theme ' . $t->getName()); if (($change = $t->reinstall(1)) !== false) { SystemLogModel::LogInfoEvent('/updater/theme/reinstall', 'Theme ' . $t->getName() . ' reinstalled successfully', implode("\n", $change)); $changes[] = '<b>Changes to theme [' . $t->getName() . ']:</b><br/>' . "\n" . implode("<br/>\n", $change) . "<br/>\n<br/>\n"; } foreach (Core::GetComponents() as $c) { /** @var $c Component_2_1 */ try{ if(!$c->isInstalled()) continue; if(!$c->isEnabled()) continue; CLI::PrintHeader('Reinstalling Component ' . $c->getName()); // Request the reinstallation $change = $c->reinstall(1); // 1.0 version components don't support verbose changes :( if ($change === true) { $changes[] = '<b>Changes to component [' . $c->getName() . ']:</b><br/>' . "\n(list of changes not supported with this component!)<br/>\n<br/>\n"; } // 2.1 components support an array of changes, yay! elseif ($change !== false) { $changes[] = '<b>Changes to component [' . $c->getName() . ']:</b><br/>' . "\n" . implode("<br/>\n", $change) . "<br/>\n<br/>\n"; } // I don't care about "else", nothing changed if it was false. // Get the pages, (for the cleanup operation) $allpages = array_merge($allpages, $c->getPagesDefined()); } catch(DMI_Query_Exception $e){ $changes[] = 'Attempted database changes to component [' . $c->getName() . '], but failed!<br/>'; //var_dump($e); die(); $errors[] = array( 'type' => 'component', 'name' => $c->getName(), 'message' => $e->getMessage() . '<br/>' . $e->query, ); } catch(Exception $e){ $changes[] = 'Attempted changes to component [' . $c->getName() . '], but failed!<br/>'; //var_dump($e); die(); $errors[] = array( 'type' => 'component', 'name' => $c->getName(), 'message' => $e->getMessage(), ); } } // Flush any non-existent admin page. // These can be created from developers changing their page URLs after the page is already registered. // Purging admin-only pages is relatively safe because these are defined in component metafiles anyway. CLI::PrintHeader('Cleaning up non-existent pages'); $pageremovecount = 0; foreach( \Core\Datamodel\Dataset::Init() ->select('baseurl') ->table('page') ->where('admin = 1') ->execute() as $row ){ $baseurl = $row['baseurl']; // This page existed already, no need to do anything :) if(isset($allpages[$baseurl])) continue; ++$pageremovecount; // Otherwise, this page was deleted or for some reason doesn't exist in the component list..... // BUH BAI \Core\Datamodel\Dataset::Init()->delete()->table('page')->where('baseurl = ' . $baseurl)->execute(); \Core\Datamodel\Dataset::Init()->delete()->table('page_meta')->where('baseurl = ' . $baseurl)->execute(); CLI::PrintLine("Flushed non-existent admin page: " . $baseurl); $changes[] = "<b>Flushed non-existent admin page:</b> " . $baseurl; } if($pageremovecount == 0){ CLI::PrintLine('No pages flushed'); } if(sizeof($errors) > 0){ CLI::PrintHeader('Done, but with errors'); foreach($errors as $e){ CLI::PrintError('Error while processing ' . $e['type'] . ' ' . $e['name'] . ': ' . $e['message']); } } else{ CLI::PrintHeader('DONE!'); } foreach($changes as $str){ echo $str; } // Flush the system cache, just in case \Core\Cache::Flush(); \Core\Templates\Backends\Smarty::FlushCache(); // Increment the version counter. $version = ConfigHandler::Get('/core/filestore/assetversion'); ConfigHandler::Set('/core/filestore/assetversion', ++$version); } // End if is post. //$page->title = 'Reinstall All Components'; $this->setTemplate('/pages/admin/reinstallall.tpl'); }
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, ]; }