/** * Test that a message can be written to a log file. * * @return \Core\Utilities\Logger\LogFile The log file created. */ public function testWrite(){ $type = 'testphpunit'; $msg = \BaconIpsumGenerator::Make_a_Sentence(); $code = '/test/' . Core::RandomHex(6); // First, I'll test the functional method. \Core\Utilities\Logger\append_to($type, $msg, $code); // Now a file should exist called testphpunit.log that contains the line above. $this->assertTrue(file_exists(ROOT_PDIR . 'logs/' . $type . '.log')); $contents = file_get_contents(ROOT_PDIR . 'logs/' . $type . '.log'); $this->assertContains($msg, $contents); $this->assertContains($code, $contents); // And now the class method, (should be identical). $type = 'testphpunit'; $msg = \BaconIpsumGenerator::Make_a_Sentence(); $code = '/test/' . Core::RandomHex(6); // First, I'll test the functional method. $log = new \Core\Utilities\Logger\LogFile($type); $log->write($msg, $code); // Now a file should exist called testphpunit.log that contains the line above. $this->assertTrue($log->exists()); $contents = $log->getContents(); $this->assertContains($msg, $contents); $this->assertContains($code, $contents); return $log; }
/** * Test the creation and validation of a good nonce. * * This will use the shortcut methods to perform the operations. */ public function testQuickCreateValidNonce() { $randomdata = Core::RandomHex(90); // Make sure core::randomhex gives me the correct number of characters. $this->assertEquals(90, strlen($randomdata)); $key = NonceModel::Generate('10 minutes', $randomdata); $this->assertTrue($key != '' && strlen($key) >= 40); // Do something blah // ..... and check it! $this->assertTrue(NonceModel::ValidateAndUse($key, $randomdata)); }
public function render() { if (!$this->get('basedir')) { throw new Exception('MultiFileInput cannot be rendered without a basedir attribute!'); } // Make sure it ends with a trailing slash. if (substr($this->get('basedir'), -1) != '/') { $this->_attributes['basedir'] .= '/'; } //var_dump($_SESSION['multifileinputobjects'], serialize($this->_attributes)); die(); // This is a slightly different element than the traditional form system, as it must be able to be called without // the rest of the form system on submit. // This is because this system will do an ajax submit to do the actual upload. if (!is_array(\Core\Session::Get('multifileinputobjects'))) { \Core\Session::Set('multifileinputobjects', []); } // I don't need this key to be cryptographically secure, just generally unique. $key = md5(serialize($this->_attributes)); foreach (\Core\Session::Get('multifileinputobjects') as $obj) { if (!isset($obj['key'])) { continue; } if ($obj['key'] == $key) { $this->set('id', $obj['id']); } } if (!isset($this->_attributes['id'])) { // This system requires a valid id. $this->set('id', 'multifileinput-' . Core::RandomHex('2')); } $this->set('key', $key); $this->set('uploadkey', $key); // Convert the string representation of a filesize to the raw bytes. $size = strtoupper(str_replace(' ', '', ini_get('upload_max_filesize'))); if (strpos($size, 'G') !== false) { $size = preg_replace('/[^0-9]/', '', $size); $size = $size * (1024 * 1024 * 1024); } elseif (strpos($size, 'M') !== false) { $size = preg_replace('/[^0-9]/', '', $size); $size = $size * (1024 * 1024); } elseif (strpos($size, 'K') !== false) { $size = preg_replace('/[^0-9]/', '', $size); $size = $size * 1024; } $this->set('maxsize', $size); // Now that the session variable has been initialized, the traditional session variable is reliable. $_SESSION['multifileinputobjects'][$key] = array('obj' => serialize($this), 'key' => $key, 'id' => $this->_attributes['id']); return parent::render(); }
protected function setUp() { // Setup some variables that will be used throughout this method. $title = 'Blog Bug 409 [' . Core::RandomHex(6) . ']'; $this->blog = new BlogModel(); $this->blog->set('title', $title); // Make sure the page model has been loaded into this model too. $page = $this->blog->getLink('Page'); $page->set('title', $title); $this->blog->save(); $bacon = new BaconIpsumGenerator(); // Create an article with an invalid title. $this->article = new BlogArticleModel(); $this->article->setFromArray(['blogid' => $this->blog->get('id'), 'title' => 'Sömé "ḮnvÁlid" & \'Bad\' T¹tle!¡', 'body' => $bacon->getParagraphsAsMarkup(), 'status' => 'published']); $this->article->save(); }
/** * Write the raw contents of this file * * Essentially file_put_contents() * * @param mixed $data * * @return boolean */ public function putContents($data) { // Resolve it from its default. // This is provided from a config define, (probably). $mode = (defined('DEFAULT_FILE_PERMS') ? DEFAULT_FILE_PERMS : 0644); // FTP requires a filename, not data... $tmpfile = Filestore\get_tmp_path() . 'ftpupload-' . \Core::RandomHex(4); file_put_contents($tmpfile, $data); if (!ftp_put($this->_ftp->getConn(), $this->_filename, $tmpfile, FTP_BINARY)) { // Well, delete the temp file anyway... unlink($tmpfile); return false; } if (!ftp_chmod($this->_ftp->getConn(), $mode, $this->_filename)) return false; // woot... but cleanup the trash first. unlink($tmpfile); $this->_tmplocal = null; return true; }
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'; }
/** * Verify that some given data has a valid signature. * * Calls verifyFileSignature internally! * * @param string $signature * @param string $content * * @throws \Exception * * @return Signature */ public function verifyDataSignature($signature, $content){ // First, write a temporary file to contain the signature. $sig = \Core\Filestore\Factory::File('tmp/gpg-verify-' . \Core::RandomHex(6) . '.asc'); $sig->putContents($signature); // And the content $con = \Core\Filestore\Factory::File('tmp/gpg-verify-' . \Core::RandomHex(6) . '.dat'); $con->putContents($content); try{ $result = $this->verifyFileSignature($sig, $con); } catch(\Exception $e){ // Cleanup. $sig->delete(); $con->delete(); throw $e; } $sig->delete(); $con->delete(); return $result; }
/** * Use /dev/urandom to generate a pseudo-random key for this nonce. */ private function _generateAndSetKey() { // This will guarantee that a key is unique. // A UUID is based on the current server, (in the group), date, and a small amount of entropy. $key = Core::GenerateUUID(); // But since this is designed to be somewhat secure... I want to be a little more cryptographically secure. $fp = fopen('/dev/random', 'rb'); if ($fp !== FALSE) { $bits = fread($fp, 16); fclose($fp); } // Convert that to ASCII $bits_b64 = base64_encode($bits); // Damn "==" of base64 :/ $bits_b64 = substr($bits_b64, 0, -2); // And append. $key .= $bits_b64; // And convert spaces and other invalid characters to a random digit. $randombit = Core::RandomHex(2); $key = str_replace(['+', ' ', '\\', '/'], $randombit, $key); // make sure it's all lowercase... URLs don't like capital letters! $key = strtolower($key); $this->set('key', $key); }
/** * Display ALL the system configuration options. * * @return int */ public function config() { // Admin-only page. if(!\Core\user()->checkAccess('g:admin')){ return View::ERROR_ACCESSDENIED; } $view = $this->getView(); $where = array(); // If the enterprise component is installed and multisite is enabled, configurations have another layer of complexity. if(Core::IsComponentAvailable('multisite') && MultiSiteHelper::GetCurrentSiteID()){ $where['overrideable'] = '1'; } $configs = ConfigModel::Find($where, null, 'key'); $groups = array(); foreach ($configs as $c) { /** @var ConfigModel $c */ // Export out the group for this config option. $el = $c->getAsFormElement(); $gname = $el->get('group'); if (!isset($groups[$gname])){ $groups[$gname] = new FormGroup( [ 'title' => $gname, 'name' => $gname, //'class' => 'collapsible collapsed' 'class' => 'system-config-group' ] ); } $groups[$gname]->addElement($el); } $form = new Form(); $form->set('callsmethod', 'AdminController::_ConfigSubmit'); // This form gives me more trouble with its persistence.... // @todo Implement a better option than this. // This hack is designed to prevent this form from using stale values from a previous page load instead of // pulling them from the database. $form->set('uniqueid', 'admin-config-' . Core::RandomHex(6)); foreach ($groups as $g) { $form->addElement($g); } $form->addElement('submit', array('value' => t('STRING_SAVE'))); $this->setTemplate('/pages/admin/config.tpl'); $view->assign('form', $form); $view->assign('config_count', sizeof($configs)); }
/** * Write a string to a file * * @link http://php.net/manual/en/function.file-put-contents.php * * @param string $filename <p> * Path to the file where to write the data. * </p> * @param mixed $data <p> * The data to write. Can be either a string, an * array or a stream resource. * </p> * <p> * If data is a stream resource, the * remaining buffer of that stream will be copied to the specified file. * This is similar with using stream_copy_to_stream. * </p> * <p> * You can also specify the data parameter as a single * dimension array. This is equivalent to * file_put_contents($filename, implode('', $array)). * </p> * * @return bool Returns true on success or false on failure. */ public static function _PutContents($filename, $data) { $ftp = \Core\ftp(); $tmpdir = TMP_DIR; if ($tmpdir{0} != '/') $tmpdir = ROOT_PDIR . $tmpdir; // Needs to be fully resolved // Resolve it from its default. // This is provided from a config define, (probably). $mode = (defined('DEFAULT_FILE_PERMS') ? DEFAULT_FILE_PERMS : 0644); if (!$ftp) { $ret = file_put_contents($filename, $data); if ($ret === false) return $ret; chmod($filename, $mode); return $ret; } elseif (strpos($filename, $tmpdir) === 0) { // Tmp files should be written directly. $ret = file_put_contents($filename, $data); if ($ret === false) return $ret; chmod($filename, $mode); return $ret; } else { // Trim off the ROOT_PDIR since it'll be relative to the ftp root set in the config. if (strpos($filename, ROOT_PDIR) === 0) $filename = substr($filename, strlen(ROOT_PDIR)); //$filename = ConfigHandler::Get('/core/ftp/path') . $filename; // FTP requires a filename, not data... $tmpfile = $tmpdir . 'ftpupload-' . Core::RandomHex(4); file_put_contents($tmpfile, $data); if (!ftp_put($ftp, $filename, $tmpfile, FTP_BINARY)) { // Well, delete the temp file anyway... unlink($tmpfile); return false; } if (!ftp_chmod($ftp, $mode, $filename)) return false; // woot... but cleanup the trash first. unlink($tmpfile); return true; } }
/** * Test the creation of a blog article based off the newly created blog * * @depends testCreateBlog */ public function testCreateBlogArticle() { // Update the current user so it has admin access. \Core\user()->set('admin', true); // Setup some variables that will be used throughout this method. $title = 'New Test Blog Article'; $randomsnippet = 'Random-Snippet-' . Core::RandomHex(10); $lorem = new BaconIpsumGenerator(); $body = $lorem->getParagraph(1); // Tack on the random snipped I'll be looking for. $body .= $lorem->getParagraphsAsMarkup(8, $randomsnippet); $blog = new BlogModel(self::$TestBlogID); $request = new PageRequest('/blog/article/create/' . self::$TestBlogID); $request->execute(); $view = $request->getView(); $this->assertEquals(200, $view->error, 'Checking that article creation returns a valid page'); // The returned data should have a "form" available. This is the actual creation form. /** @var $form Form */ $form = $view->getVariable('form'); $this->assertInstanceOf('Form', $form, 'Checking that the form is set from the blog article create controller'); // Set some variables on the form $form->getElement('page[title]')->set('value', $title); $form->getElement('page[rewriteurl]')->set('value', $blog->get('rewriteurl') . '/' . \Core\str_to_url($title)); $form->getElement('model[image]')->set('value', 'public/blog/blog-test-image.png'); $form->getElement('model[body]')->set('value', $body); // Copy in the image $src = \Core\Filestore\Factory::File(ROOT_PDIR . 'components/blog/tests/blog-test-image.png'); /** @var $dest \Core\Filestore\File */ $dest = \Core\Filestore\Factory::File('public/blog/blog-test-image.png'); $src->copyTo($dest, true); // make sure that it exists $this->assertTrue($dest->exists(), 'Checking that files can be copied into the public filestore'); // And submit this form to the handler. // On a successful submission, it should be simply the URL of the blog. $formsubmission = call_user_func_array($form->get('callsmethod'), array($form)); if ($formsubmission === false) { throw new Exception(implode("\n", $form->getErrors())); } // Go to the parent listing page and find this entry. $request = new PageRequest($blog->get('rewriteurl')); $request->execute(); $view = $request->getView(); $this->assertEquals(200, $view->error); $html = $view->fetch(); $this->assertContains($title, $html); $this->assertContains('itemtype="http://schema.org/BlogPosting"', $html); preg_match_all('#<div[^>]*itemtype="http://schema.org/BlogPosting"[^>]*>.*<a[^>]*href="(.*)"[^>]*>(.*)</a>#msU', $html, $matches); // Title should now have three keys, with at least one value each. $this->assertNotEmpty($matches[1]); $this->assertNotEmpty($matches[2]); // This node contains the URL. $foundurl = $matches[1][0]; $foundtitle = trim($matches[2][0]); // Make sure the url contains the site url. $this->assertStringStartsWith(ROOT_URL, $foundurl); // And trim it off. This is because PageRequest expects that the url is already trimmed. $foundurl = '/' . substr($foundurl, strlen(ROOT_URL)); $this->assertEquals($title, $foundtitle); //$this->assertStringStartsWith('/blog/article/view/', $formsubmission, 'Checking that blog article creation was successful'); // Go to the page and make sure that it loads up! $request = new PageRequest($foundurl); $request->execute(); $view = $request->getView(); $this->assertEquals(200, $view->error, 'Checking that public blog article exists'); $html = $view->fetch(); $this->assertContains($title, $html, 'Checking that the public blog article page contains the correct title'); $this->assertContains($randomsnippet, $html, 'Checking that the public blog article page contains the correct body'); $this->assertContains('blog-test-image', $html, 'Checking that the public blog article page contains the correct image'); }
/** * Prompt the user a question and return the result. * * @param string $question The question to prompt to the user. * @param array|string $answers What answers to provide to the user. * array - Will prompt the user with the value of each pair, returning the key. * "boolean" - Will ask for a yes/no response and return true/false. * "text" - Open-ended text input, user can type in anything and that input is returned. * "text-required" - Open-ended text input, user can type in anything (non-blank), and that value is returned. * @param string|bool $default string The default answer if the user simply presses "enter". [optional] * * @throws \Exception * @return bool|string */ public static function PromptUser($question, $answers, $default = false) { $isanswered = false; while (!$isanswered) { echo NL . $question . NL; $extras = []; if (is_array($answers)) { $answerhash = array(); $x = 0; foreach ($answers as $a => $q) { if (($a === 'exit')) { // && ($x+1 == sizeof($answers))){ // This is a 'special' action, so it gets a special key. $answerhash['x'] = $a; echo TAB . " x - $q" . NL; $extras[] = 'x'; } elseif($a === 'quit'){ $answerhash['q'] = $a; echo TAB . " q - $q" . NL; $extras[] = 'q'; } elseif($a === 'menu'){ $answerhash['m'] = $a; echo TAB . " m - $q" . NL; $extras[] = 'm'; } elseif ($a === 'save') { $answerhash['s'] = $a; echo TAB . " s - $q" . NL; $extras[] = 's'; } else { $x++; $answerhash[$x] = $a; $indent = ($x < 10) ? ' ' : ''; if ($default !== false && $default == $a) { echo TAB . $indent . "$x*- $q (default)" . NL; } else { echo TAB . $indent . "$x - $q" . NL; } } } // Print the "enter a number 1-10..." text. if ($x == 1 && !sizeof($extras)){ echo NL . '(Enter 1 to continue) '; } elseif($x > 1 && !sizeof($extras)){ echo NL . "(Enter a number, 1-$x) "; } else{ $extras = array_merge(['1-' . $x], $extras); echo NL . '(Enter '; $last = null; while(($e = array_shift($extras))){ if($last){ echo $last . ', '; } $last = $e; } echo 'or ' . $last . ') '; } // Read the response. $line = strtolower(trim(fgets(STDIN))); echo NL; // Maybe there's a default. if ($line == '' && $default !== false) { return $default; } if (!isset($answerhash[$line])) { echo "Invalid Response!" . NL . NL; sleep(1.5); continue; } return $answerhash[$line]; } else { switch (strtolower($answers)) { case 'boolean': case 'bool': echo "(enter y for yes, n for no.) "; $line = strtolower(trim(fgets(STDIN))); echo NL; // Maybe there's a default. if ($line == '' && $default !== false) { return $default; } elseif ($line == 'y' || $line == 'yes') { return true; } elseif ($line == 'n' || $line == 'no') { return false; } else { echo "Invalid Response!" . NL . NL; sleep(1.5); continue; } break; case 'text': if ($default !== false) { echo "Press enter for [" . $default . "]" . NL; } $line = trim(fgets(STDIN)); echo NL; if ($line == '' && $default !== false) { return $default; } else { return $line; } break; case 'text-required': if ($default !== false) { echo "Press enter for [" . $default . "]" . NL; } $line = trim(fgets(STDIN)); echo NL; if ($line == '' && $default !== false) { return $default; } elseif ($line != '') { return $line; } else { echo "Invalid Response!" . NL . NL; sleep(1.5); continue; } break; case 'textarea': // This requires a bit of app-trickery. // I need to pass any "default" data into a text file in /tmp, then edit that and read the file afterwards. echo '(Press enter to open with ' . basename($_SERVER['EDITOR']) . '. Save and close when done.)'; fgets(STDIN); $file = "/tmp/cae2-cli-textarea-" . \Core::RandomHex(4) . '.tmp'; file_put_contents($file, $default); system($_SERVER['EDITOR'] . " " . $file . " > `tty`"); // And read back in that file. $data = file_get_contents($file); // Remove the file from the filesystem, no need for clutter. unlink($file); return $data; default: throw new \Exception('Unsupported answer choice [' . strtolower($answers) . '], please ensure it is either an array of options, "boolean", "text", "text-required", or "textarea"!'); } } } }
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, ]; }
/** * Generate a new secure API key for this user. * * This is a built-in function that can be used for automated access to * secured resources on the application/site. * * Will only set the config, save() still needs to be called externally. * * @since 2011.08 */ public function generateNewApiKey() { $this->set('apikey', Core::RandomHex(64, true)); }