/** * "Upgrade" (aka) Install this theme and its assets. * * Alias of install() * * @return false | array * @throws \InstallerException */ public function upgrade() { //if(!$this->isInstalled()) return false; $changes = $this->_performInstall(); if (is_array($changes) && sizeof($changes)) { \SystemLogModel::LogInfoEvent('/updater/theme/upgrade', 'Theme ' . $this->getName() . ' installed successfully!', implode("\n", $changes)); } return $changes; }
public function finalize(){ $subject = 'Import completed in ' . $this->_profiler->getTimeFormatted(); $bits = []; if($this->success > 0){ $bits[] = 'Success: ' . $this->success; } if($this->skipped > 0){ $bits[] = 'Skipped: ' . $this->skipped; } if($this->error > 0){ $bits[] = 'Errors: ' . $this->error; } if($this->duplicate > 0){ $bits[] = 'Duplicates: ' . $this->duplicate; } $subject .= ' ' . implode(', ', $bits); \SystemLogModel::LogInfoEvent($this->_title, $subject, $this->_profiler->getEventTimesFormatted()); }
/** * Execute the actual cron for the requested type. * * @param string $cron Cron type to execute. * * @return CronLogModel * @throws Exception */ private function _performcron($cron) { switch ($cron) { case '1-minute': case '5-minute': case '15-minute': case '30-minute': case 'hourly': case '2-hour': case '3-hour': case '6-hour': case '12-hour': case 'daily': case 'weekly': case 'monthly': break; default: throw new Exception('Unsupported cron type: [' . $cron . ']'); } if (!ConfigHandler::Get('/cron/enabled')) { $msg = 'Cron execution is globally disabled via the site configuration, not executing cron!'; SystemLogModel::LogInfoEvent('/cron/' . $cron, $msg); // It needs to return something. $log = new CronLogModel(); $log->set('status', 'fail'); return $log; } // First, check and see if there's one that's still running. $runninglogs = CronLogModel::Find(array('cron' => $cron, 'status' => 'running')); if (sizeof($runninglogs)) { foreach ($runninglogs as $log) { /** @var $log CronLogModel */ $log->set('status', 'fail'); $log->set('log', $log->get('log') . "\n------------\nTIMED OUT!"); $log->save(); } } // Start recording. $log = new CronLogModel(); $log->set('cron', $cron); $log->set('status', 'running'); $log->set('memory', memory_get_usage()); $log->set('ip', REMOTE_IP); $log->save(); $start = microtime(true) * 1000; $sep = '==========================================' . "\n"; $contents = "Starting cron execution for {$cron}\n{$sep}"; // This uses the hook system, but will be slightly different than most things. $overallresult = true; $hook = HookHandler::GetHook('/cron/' . $cron); $hookcount = 0; $hooksuccesses = 0; if ($hook) { if ($hook->getBindingCount()) { $hookcount = $hook->getBindingCount(); $bindings = $hook->getBindings(); foreach ($bindings as $b) { $contents .= sprintf("\nExecuting Binding %s...\n", $b['call']); // Since these systems will just be writing to STDOUT, I'll need to capture that. try { ob_start(); $execution = $hook->callBinding($b, array()); $executiondata = ob_get_clean(); } catch (Exception $e) { $execution = false; $executiondata = 'EXCEPTION: ' . $e->getMessage() . ob_get_clean(); } if ($executiondata == '' && $execution) { $contents .= "Cron executed successfully with no output\n"; ++$hooksuccesses; } elseif ($execution) { $contents .= $executiondata . "\n"; ++$hooksuccesses; } else { $contents .= $executiondata . "\n!!FAILED\n"; $overallresult = false; } } } else { $contents = 'No bindings located for requested cron'; $overallresult = true; } } else { $contents = 'Invalid hook requested: ' . $cron; $overallresult = false; } // Just in case the contents are returning html... (they should be plain text). // Replace the most common line endings with things that make sense for plain text. // This is to ensure that all the available scenarios are met and saved/displayed without extra whitespace. // // Since some systems will provide plain text (easy!), windows/os9 line endings, // HTML (br and br/), and formatted HTML (br + \n). $contents = str_ireplace(["\r\n<br>", "\r\n<br/>", "\r\n<br />", "\n<br>", "\n<br/>", "\n<br />", "<br>", "<br/>", "<br />", "\r\n", "\r"], "\n", $contents); // Save the results. $log->set('completed', Time::GetCurrentGMT()); $log->set('duration', microtime(true) * 1000 - $start); $log->set('log', $contents); $log->set('status', $overallresult ? 'pass' : 'fail'); $log->save(); // Make a copy of this in the system log too if applicable. // This time is listed in ms $time = microtime(true) * 1000 - $start; // 0.01 = 10 ns // 1 = 1 ms // 1000 = 1 second if ($time < 1) { // TIME is less than 1, which means it executed faster than 1ms, display in nanoseconds. $time = round($time, 4) * 1000 . ' ns'; } elseif ($time < 1000) { // TIME is less than 1000, which means it executed faster than 1 second, display in milliseconds. $time = round($time, 0) . ' ms'; } else { // TIME is at least 1 second or longer... Display in minutes:seconds, (no need to display 1.453 seconds!) // First, convert the milliseconds to seconds; they are more manageable for what I need to do. // This will change time from 12345(ms) to 13(seconds) $time = ceil($time / 1000); $minutes = floor($time / 60); $seconds = $time - $minutes * 60; if ($minutes > 0) { $time = $minutes . 'm ' . str_pad($seconds, 2, '0', STR_PAD_LEFT) . 's'; } else { $time = $seconds . ' seconds'; } } if ($hookcount > 0) { $msg = 'Cron ' . $cron . ' completed in ' . $time . '. ' . $hooksuccesses . ' out of ' . $hookcount . ' hooks called successfully.'; SystemLogModel::LogInfoEvent('/cron/' . $cron, $msg, $contents); } // Just to notify the calling function. return $log; }
/** * Check permissions on the user and system and return either blank or a string containing the error. * * @param string $step * * @return array|null */ private function _checkPermissions($step) { $error = null; if (!\ConfigHandler::Get('/package_repository/base_directory')) { // Check if the config is even set, can't proceed if it's not. trigger_error('The package repository does not appear to be setup yet. Please browse to Configuration and the appropriate options.'); return ['status' => View::ERROR_SERVERERROR, 'message' => 'The package repository is not setup on this server.']; } $dir = Factory::Directory(\ConfigHandler::Get('/package_repository/base_directory')); if (!$dir->exists()) { trigger_error($dir->getPath() . ' does not appear to exist! Unable to browse repo.xml without it.'); return ['status' => View::ERROR_SERVERERROR, 'message' => $dir->getPath() . ' does not seem to exist!']; } elseif (!$dir->isReadable()) { trigger_error($dir->getPath() . ' does not appear to be readable! Unable to browse repo.xml without it.'); return ['status' => View::ERROR_SERVERERROR, 'message' => $dir->getPath() . ' does not seem to be readable!']; } if (ConfigHandler::Get('/package_repository/is_private')) { // Lookup this license key, (or request one if not present). $valid = false; $autherror = 'Access to ' . SITENAME . ' (Package Repository) requires a license key and password.'; if (isset($_SERVER['PHP_AUTH_PW']) && isset($_SERVER['PHP_AUTH_USER'])) { $user = $_SERVER['PHP_AUTH_USER']; $pw = $_SERVER['PHP_AUTH_PW']; } else { $user = $pw = null; } if ($user && $pw) { /** @var PackageRepositoryLicenseModel $license */ $license = PackageRepositoryLicenseModel::Construct($user); $licvalid = $license->isValid($pw); if ($licvalid == 0) { // Lock this license to the remote IP, if requested by the admin. if (ConfigHandler::Get('/package_repository/auto_ip_restrict') && !$license->get('ip_restriction')) { $license->set('ip_restriction', REMOTE_IP); $license->save(); } SystemLogModel::LogInfoEvent('/packagerepository/' . $step, '[' . $user . '] accessed repository successfully'); return null; } else { if (($licvalid & PackageRepositoryLicenseModel::VALID_PASSWORD) == PackageRepositoryLicenseModel::VALID_PASSWORD) { $autherror = '[' . $user . '] Invalid license password'; $status = View::ERROR_ACCESSDENIED; SystemLogModel::LogSecurityEvent('/packagerepository/password_failure', $autherror); } if (($licvalid & PackageRepositoryLicenseModel::VALID_ACCESS) == PackageRepositoryLicenseModel::VALID_ACCESS) { $autherror = '[' . $user . '] IP address not authorized'; $status = View::ERROR_ACCESSDENIED; SystemLogModel::LogSecurityEvent('/packagerepository/ip_restriction', $autherror); } if (($licvalid & PackageRepositoryLicenseModel::VALID_EXPIRED) == PackageRepositoryLicenseModel::VALID_EXPIRED) { $autherror = '[' . $user . '] License provided has expired, please request a new one.'; $status = View::ERROR_GONE; SystemLogModel::LogSecurityEvent('/packagerepository/expired_license', $autherror); } if (($licvalid & PackageRepositoryLicenseModel::VALID_INVALID) == PackageRepositoryLicenseModel::VALID_INVALID) { $autherror = '[' . $user . '] License does not exist'; $status = View::ERROR_EXPECTATIONFAILED; SystemLogModel::LogSecurityEvent('/packagerepository/invalid_license', $autherror); } return ['status' => $status, 'message' => $autherror]; } } if (!$valid) { header('WWW-Authenticate: Basic realm="' . SITENAME . ' (Package Repository)"'); header('HTTP/1.0 401 Unauthorized'); echo $autherror; exit; } } else { SystemLogModel::LogInfoEvent('/packagerepository/' . $step, '[anonymous connection] accessed repository successfully'); return null; } }
/** * Set a requested theme and template as default for the site. */ public function setdefault() { $request = $this->getPageRequest(); $view = $this->getView(); $themename = $this->getPageRequest()->getParameter(0); $template = $this->getPageRequest()->getParameter('template'); // If the browser prefers JSON data, send that. if ($request->prefersContentType(View::CTYPE_JSON)) { $view->contenttype = View::CTYPE_JSON; } // Validate the theme name if (!\Theme\validate_theme_name($themename)) { \Core\set_message('Invalid theme requested', 'error'); \Core\go_back(); } $theme = ThemeHandler::GetTheme($themename); if ($template) { // The template itself can be ignored. if (!\Theme\validate_template_name($themename, $template)) { \Core\set_message('Invalid template requested', 'error'); \Core\go_back(); } } else { // and the default one is used otherwise. $allskins = $theme->getSkins(); $template = $allskins[0]['file']; } if (Core::IsComponentAvailable('multisite') && MultiSiteHelper::GetCurrentSiteID()) { $config_default = ConfigHandler::GetConfig('/theme/default_template'); $config_selected = ConfigHandler::GetConfig('/theme/selected'); if ($config_default->get('overrideable') == 0) { // It's a child site and the admin never gave them permission to change default themes! \Core\set_message('Unable to set the default template on a child site, please ensure that the "/theme/default_template" config is set to be overrideable!', 'error'); \Core\go_back(); } if ($config_selected->get('overrideable') == 0) { // It's a child site and the admin never gave them permission to change default themes! \Core\set_message('Unable to set the selected theme on a child site, please ensure that the "/theme/selected" config is set to be overrideable!', 'error'); \Core\go_back(); } } if ($request->isPost()) { if ($themename != ConfigHandler::Get('/theme/selected')) { // The theme changed, change the admin skin too! ConfigHandler::Set('/theme/default_admin_template', $template); // And the email skin. ConfigHandler::Set('/theme/default_email_template', ''); } ConfigHandler::Set('/theme/default_template', $template); ConfigHandler::Set('/theme/selected', $themename); // reinstall theme and zee assets $t = ThemeHandler::GetTheme(); if (($change = $t->reinstall(0)) !== false) { SystemLogModel::LogInfoEvent('/updater/theme/reinstall', 'Theme ' . $t->getName() . ' reinstalled successfully', implode("\n", $change)); } \Core\set_message('Updated default theme', 'success'); \Core\redirect('/theme'); } $view->assign('theme', $themename); $view->assign('template', $template); }
/** * 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'); }
/** * Send the message * * @throws phpmailerException * @return bool */ public function send() { $m = $this->getMailer(); if(!\ConfigHandler::Get('/core/email/enable_sending')){ // Allow a config option to disable sending entirely. SystemLogModel::LogInfoEvent('/email/disabled', 'Email sending is disabled, not sending email ' . $m->Subject . '!'); return false; } if(\ConfigHandler::Get('/core/email/sandbox_to')){ $to = $m->getToAddresses(); $cc = $m->getCCAddresses(); $bcc = $m->getBCCAddresses(); $all = []; if(sizeof($to)){ foreach($to as $e){ $all[] = ['type' => 'To', 'email' => $e[0], 'name' => $e[1]]; } } if(sizeof($cc)){ foreach($cc as $e){ $all[] = ['type' => 'CC', 'email' => $e[0], 'name' => $e[1]]; } } if(sizeof($bcc)){ foreach($bcc as $e){ $all[] = ['type' => 'BCC', 'email' => $e[0], 'name' => $e[1]]; } } foreach($all as $e){ $m->AddCustomHeader('X-Original-' . $e['type'], ($e['name'] ? $e['name'] . ' <' . $e['email'] . '>' : $e['email'])); } // Allow a config option to override the "To" address, useful for testing with production data. $m->ClearAllRecipients(); $m->AddAddress(\ConfigHandler::Get('/core/email/sandbox_to')); } // Render out the body. Will be either HTML or text... $body = $this->renderBody(); // Wrap this body with the main email template if it's set. if($this->templatename && $this->_view){ // This version includes HTML tags and all that. $m->Body = $body; $m->IsHTML(true); // Use markdown for conversion. // It produces better results that phpMailer's built-in system! $converter = new \HTMLToMD\Converter(); // Manually strip out the head content. // This was throwing the converters for a loop and injecting weird characters! $body = preg_replace('#<head[^>]*?>.*</head>#ms', '', $body); $m->AltBody = $converter->convert($body); } elseif (strpos($body, '<html>') === false) { // Ensuring that the body is wrapped with <html> tags helps with spam checks with spamassassin. $m->MsgHTML('<html><body>' . $body . '</body></html>'); } else{ $m->MsgHTML($body); } if($this->_encryption){ // Encrypt this message, (both HTML and Alt), and all attachments. // I need to request the full EML from phpMailer so I can encrypt everything. // Then, the body will be recreated after Send is called. $m->PreSend(); $header = $m->CreateHeader(); $body = $m->CreateBody(); $gpg = new \Core\GPG\GPG(); if($this->_encryption === true){ // This is allowed for mutliple recipients! // This requires a little more overhead, as I need to lookup each recipient's user account // to retrieve their GPG key. $recipients = $m->getToAddresses(); foreach($recipients as $dat){ $email = $dat[0]; $user = UserModel::Find(['email = ' . $email], 1); if(!$user){ SystemLogModel::LogErrorEvent('/core/email/failed', 'Unable to locate GPG key for ' . $email . ', cannot send encrypted email to recipient!'); } else{ $key = $user->get('gpgauth_pubkey'); if(!$key){ SystemLogModel::LogErrorEvent('/core/email/failed', 'No GPG key uploaded for ' . $email . ', cannot send encrypted email to recipient!'); } else{ $enc = $gpg->encryptData($header . $body, $key); // Create a clone of the email object to send this data. /** @var PHPMailer $clone */ $clone = clone $m; $clone->ClearAddresses(); $clone->AddAddress($email); $clone->Body = $enc; $clone->AltBody = ''; $clone->Send(); } } } return true; } else{ // Single recipient! $enc = $gpg->encryptData($header . $body, $this->_encryption); $m->Body = $enc; $m->AltBody = ''; return $m->Send(); } } return $m->Send(); }
/** * Function that is fired off on page load. * This checks if a form was submitted and that form was present in the SESSION. * * @return null */ public static function CheckSavedSessionData() { // This needs to ignore the /form/savetemporary.ajax page! // This is a custom page that's meant to intercept all POST submissions. if(preg_match('#^/form/(.*)\.ajax$#', REL_REQUEST_PATH)) return; // There has to be data in the session. $forms = \Core\Session::Get('FormData/*'); $formid = (isset($_REQUEST['___formid'])) ? $_REQUEST['___formid'] : false; $form = false; foreach ($forms as $k => $v) { // If the object isn't a valid object after unserializing... if (!($el = unserialize($v))) { \Core\Session::UnsetKey('FormData/' . $k); continue; } // Check the expires time if ($el->get('expires') <= Time::GetCurrent()) { \Core\Session::UnsetKey('FormData/' . $k); continue; } if ($k == $formid) { // Remember this for after all the checks have finished. $form = $el; } } // No form found... simple enough if (!$form) return; // Otherwise /** @var $form Form */ // Ensure the submission types match up. if (strtoupper($form->get('method')) != $_SERVER['REQUEST_METHOD']) { \Core\set_message('t:MESSAGE_ERROR_FORM_SUBMISSION_TYPE_DOES_NOT_MATCH'); return; } // Ensure the REFERRER and original URL match up. if($_SERVER['HTTP_REFERER'] != $form->originalurl){ // @todo This is reported to be causing issues with production sites. // If found true, this check may need to be removed / refactored. //\Core\set_message('Form submission referrer does not match, please try your submission again.', 'error'); SystemLogModel::LogInfoEvent( 'Form Referrer Mismatch', 'Form referrer does not match! Submitted: [' . $_SERVER['HTTP_REFERER'] . '] Expected: [' . $form->originalurl . ']' ); //return; } // Run though each element submitted and try to validate it. if (strtoupper($form->get('method')) == 'POST') $src =& $_POST; else $src =& $_GET; $form->loadFrom($src); // Try to load the form from that form. That will call all of the model's validation logic // and will throw exceptions if it doesn't. try{ $form->getModel(); // Still good? if (!$form->hasError()){ $status = call_user_func($form->get('callsmethod'), $form); } else{ $status = false; } } catch(ModelValidationException $e){ \Core\set_message($e->getMessage(), 'error'); $status = false; } catch(GeneralValidationException $e){ \Core\set_message($e->getMessage(), 'error'); $status = false; } catch(Exception $e){ if(DEVELOPMENT_MODE){ // Developers get the full message \Core\set_message($e->getMessage(), 'error'); } else{ // While users of production-enabled sites get a friendlier message. \Core\set_message('t:MESSAGE_ERROR_FORM_SUBMISSION_UNHANDLED_EXCEPTION'); } Core\ErrorManagement\exception_handler($e); $status = false; } // The form was submitted. Set its persistent flag to true so that whatever may be listening for it can retrieve the user's values. $form->persistent = true; // Regardless, bundle this form back into the session so the controller can use it if needed. \Core\Session::Set('FormData/' . $formid, serialize($form)); // Fail statuses. if ($status === false) return; if ($status === null) return; // Guess it's not false and not null... must be good then. // @todo Handle an internal save procedure for "special" groups such as pageinsertables and what not. // Cleanup \Core\Session::UnsetKey('FormData/' . $formid); if ($status === 'die'){ // If it's set to die, simply exit the script without outputting anything. exit; } elseif($status === 'back'){ if($form->referrer && $form->referrer != REL_REQUEST_PATH){ // Go back to the original form's referrer. \Core\redirect($form->referrer); } else{ // Use Core to guess which page to redirect back to, (not as reliable). \Core\go_back(); } } elseif ($status === true){ // If the return code is boolean true, it's a reload. \Core\reload(); } elseif($status === REL_REQUEST_PATH || $status === CUR_CALL){ // If the page returned the same page as the current url, force a reload, (as redirect will ignore it) \Core\reload(); } else{ // Anything else gets sent to the redirect system. \core\redirect($status); } }
/** * Set this component as enabled in the database. */ public function enable(){ // If it's not installed already, it can't be disabled! if($this->isEnabled()) return false; $c = new ComponentModel($this->_name); $c->set('enabled', true); $c->save(); $this->_enabled = true; $changed = array(); $change = $this->_parseUserConfigs(); if ($change !== false) $changed = array_merge($changed, $change); $change = $this->_parsePages(); if ($change !== false) $changed = array_merge($changed, $change); // Do this when I actually have widgets to test. //$change = $this->_parseWidgets(); //if ($change !== false) $changed = array_merge($changed, $change); if(sizeof($changed)){ SystemLogModel::LogInfoEvent('/updater/component/enable', 'Component ' . $this->getName() . ' enabled successfully!', implode("\n", $changed)); } // Ensure that the core component cache is purged too! \Core\Cache::Delete('core-components'); return (sizeof($changed)) ? $changed : false; }