public function editor() { $view = $this->getView(); $request = $this->getPageRequest(); if (!$this->setAccess('g:admin')) { return View::ERROR_ACCESSDENIED; } if ($request->getParameter('template')) { // This is the basename of the file, (unresolved) // example: "skins/basic.tpl" $file = $request->getParameter('template'); // And the fully resolved one! // example: "/home/blah/public_html/themes/awesome-one/skins/basic.tpl" $filename = \Core\Templates\Template::ResolveFile($file); // This gets resolved automatically. $mode = null; $activefile = 'template'; } elseif ($request->getParameter('file') && strpos($request->getParameter('file'), 'assets/') === 0) { $file = $request->getParameter('file'); // Trim off the base of the filename, ("assets/") $filename = substr($file, 7); // And try to look up and find this damn file... $srcdirs = array(); $srcdirs[] = ROOT_PDIR . 'themes/custom/assets/'; $srcdirs[] = ROOT_PDIR . 'themes/' . ConfigHandler::Get('/theme/selected') . '/assets/'; foreach (Core::GetComponents() as $c) { if ($c->getAssetDir()) { $srcdirs[] = $c->getAssetDir(); } } foreach ($srcdirs as $dir) { if (file_exists($dir . $filename)) { $filename = $dir . $filename; break; } } // This gets resolved automatically. $mode = null; $activefile = 'file'; } else { //no special gets... // This version of the editor doesn't support viewing without any file specified. \Core\set_message('No file requested', 'error'); \core\redirect('/theme'); } $fh = \Core\Filestore\Factory::File($filename); $customdest = \Core\directory('themes/custom'); if (!$customdest->isWritable()) { \Core\set_message('Directory themes/custom is not writable! Inline file editing disabled.', 'error'); \Core\go_back(); } // Lookup the mode. if (!$mode) { switch ($fh->getMimetype()) { case 'text/css': $mode = 'css'; break; case 'text/javascript': $mode = 'javascript'; break; case 'text/html': $mode = 'htmlmixed'; break; default: $mode = 'smarty'; break; } } // @todo Finish this. if (strpos($fh->getMimetype(), 'text/') !== 0) { \Core\set_message('Sorry, but only text files can be edited right now... Expect this to function soon though ;)', 'info'); \Core\go_back(); } // Load the last 10 revisions from the database. $revisions = ThemeTemplateChangeModel::Find(array('filename' => $file), 10, 'updated DESC'); $rev = null; if ($request->getParameter('revision')) { // Look up that revision. $rev = ThemeTemplateChangeModel::Construct($request->getParameter('revision')); if ($rev->get('filename') == $file) { $content = $rev->get('content'); $revision = $rev->get('id'); } else { \Core\set_message('Invalid revision requested!', 'error'); $rev = null; } } if (!$rev) { // No revision requested, just pull the contents from the live file $content = $fh->getContents(); if (sizeof($revisions)) { // Grab the latest one! $rev = $revisions[0]; } } if ($rev && $rev == $revisions[0]) { $islatest = true; } elseif (!$rev) { $islatest = true; } else { $islatest = false; } $basename = $fh->getBasename(); $m = new ThemeTemplateChangeModel(); $m->set('content', $content); $m->set('filename', $file); $form = Form::BuildFromModel($m); $form->set('callsmethod', 'ThemeController::_SaveEditorHandler'); // I need to add the file as a system element so core doesn't try to reuse the same forms on concurrent edits. //$form->addElement('system', array('name' => 'revision', 'value' => $revision)); $form->addElement('system', array('name' => 'file', 'value' => $file)); $form->addElement('system', array('name' => 'filetype', 'value' => $activefile)); if (!$islatest) { $form->addElement('submit', array('value' => 'Update/Revert')); } else { $form->addElement('submit', array('value' => 'Update')); } // This form needs to load the content live on every pageload! // This is because the user can jump between the different versions on-the-fly, // so if the form system has its way, it would keep the first in cache and only display that. // No cache for you! $form->clearFromSession(); $view->assign('activefile', $activefile); $view->assign('form', $form); $view->assign('content', $content); $view->assign('filename', $basename); $view->assign('revisions', $revisions); $view->assign('revision', $rev); $view->assign('file', $file); $view->assign('fh', $fh); $view->assign('islatest', $islatest); $view->assign('mode', $mode); //$view->addBreadcrumb('Theme Manager', '/theme'); $view->title = 'Editor'; }
private function testDirectoryWritable($dir){ // The configuration wouldn't be ready yet... make sure the calling methods have the appropriate constants defined. if(!defined('FTP_USERNAME')){ define('FTP_USERNAME', $_SESSION['configs']['FTP_USERNAME']); } if(!defined('FTP_PASSWORD')){ define('FTP_PASSWORD', $_SESSION['configs']['FTP_PASSWORD']); } if(!defined('FTP_PATH')){ define('FTP_PATH', $_SESSION['configs']['FTP_PATH']); } if(!defined('CDN_TYPE')){ define('CDN_TYPE', $_SESSION['configs']['CDN_TYPE']); } if(!defined('CDN_LOCAL_ASSETDIR')){ define('CDN_LOCAL_ASSETDIR', $_SESSION['configs']['CDN_LOCAL_ASSETDIR']); } if(!defined('CDN_LOCAL_PUBLICDIR')){ define('CDN_LOCAL_PUBLICDIR', $_SESSION['configs']['CDN_LOCAL_PUBLICDIR']); } /** @var $dir \Directory_Backend */ $dir = \Core\directory($dir); if(!$dir->isWritable()){ $dirname = $dir->getPath(); $whoami = trim(`whoami`); $instructions = <<<EOD <strong>GUI, FTP, or Web Management Method</strong> <p> Right click on the directory and set "group" and "other" to writable and executable. <p> <strong>CLI Lazy (insecure) Method</strong> <p> <pre>chmod -R a+wx "$dirname"</pre> </p> <strong>CLI Secure Method</strong> <p> <pre>sudo chown -R $whoami "$dirname"</pre> </p> EOD; return [ 'status' => 'failed', 'message' => $dir->getPath() . ' is not writable.', 'instructions' => $instructions, ]; } else{ return ['status' => 'passed', 'message' => $dir->getPath() . ' is writable.', 'instructions' => '']; } }
/** * Instantiate a new Directory object, ready for manipulation or access. * * @deprecated 2011.11 * @since 2011.07.09 * * @param string $directory * * @return Directory_Backend */ public static function Directory($directory) { return \Core\directory($directory); }
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, ]; }