. */ namespace Xibo\XTR; use Carbon\Carbon; use GuzzleHttp\Client; use GuzzleHttp\Exception\GuzzleException; use Xibo\Controller\Module; use Xibo\Event\MaintenanceDailyEvent; use Xibo\Factory\DataSetFactory; use Xibo\Factory\FontFactory; use Xibo\Factory\LayoutFactory; use Xibo\Factory\ModuleFactory; use Xibo\Factory\ModuleTemplateFactory; use Xibo\Factory\UserFactory; use Xibo\Helper\DatabaseLogHandler; use Xibo\Helper\DateFormatHelper; use Xibo\Helper\Random; use Xibo\Service\MediaService; use Xibo\Service\MediaServiceInterface; use Xibo\Support\Exception\GeneralException; use Xibo\Support\Exception\NotFoundException; /** * Class MaintenanceDailyTask * @package Xibo\XTR */ class MaintenanceDailyTask implements TaskInterface { use TaskTrait; /** @var LayoutFactory */ private $layoutFactory; /** @var UserFactory */ private $userFactory; /** @var Module */ private $moduleController; /** @var MediaServiceInterface */ private $mediaService; /** @var DataSetFactory */ private $dataSetFactory; /** @var FontFactory */ private $fontFactory; /** @var ModuleFactory */ private $moduleFactory; /** @var ModuleTemplateFactory */ private $moduleTemplateFactory; /** @var string */ private $libraryLocation; /** @inheritdoc */ public function setFactories($container) { $this->moduleController = $container->get('\Xibo\Controller\Module'); $this->layoutFactory = $container->get('layoutFactory'); $this->userFactory = $container->get('userFactory'); $this->dataSetFactory = $container->get('dataSetFactory'); $this->mediaService = $container->get('mediaService'); $this->fontFactory = $container->get('fontFactory'); $this->moduleFactory = $container->get('moduleFactory'); $this->moduleTemplateFactory = $container->get('moduleTemplateFactory'); return $this; } /** @inheritdoc */ public function run() { $this->runMessage = '# ' . __('Daily Maintenance') . PHP_EOL . PHP_EOL; // Long-running task set_time_limit(0); // Make sure our library structure is as it should be try { $this->libraryLocation = $this->getConfig()->getSetting('LIBRARY_LOCATION'); MediaService::ensureLibraryExists($this->libraryLocation); } catch (\Exception $exception) { $this->getLogger()->error('Library structure invalid, e = ' . $exception->getMessage()); $this->appendRunMessage(__('Library structure invalid')); } // Import layouts $this->importLayouts(); // Cycle the XMR Key $this->cycleXmrKey(); try { $this->appendRunMessage(__('## Build caches')); // TODO: should we remove all bundle/asset cache before we start? // Player bundle $this->cachePlayerBundle(); // Cache Assets $this->cacheAssets(); // Fonts $this->mediaService->setUser($this->userFactory->getSystemUser())->updateFontsCss(); } catch (\Exception $exception) { $this->getLogger()->error('Failure to build caches, e = ' . $exception->getMessage()); $this->appendRunMessage(__('Failure to build caches')); } // Tidy logs $this->tidyLogs(); // Tidy Cache $this->tidyCache(); // Dispatch an event so that consumers can hook into daily maintenance. $event = new MaintenanceDailyEvent(); $this->getDispatcher()->dispatch($event, MaintenanceDailyEvent::$NAME); foreach ($event->getMessages() as $message) { $this->appendRunMessage($message); } } /** * Tidy the DB logs */ private function tidyLogs() { $this->runMessage .= '## ' . __('Tidy Logs') . PHP_EOL; $maxage = $this->config->getSetting('MAINTENANCE_LOG_MAXAGE'); if ($maxage != 0) { // Run this in the log handler so that we share the same connection and don't deadlock. DatabaseLogHandler::tidyLogs( Carbon::now() ->subDays(intval($maxage)) ->format(DateFormatHelper::getSystemFormat()) ); $this->runMessage .= ' - ' . __('Done') . PHP_EOL . PHP_EOL; } else { $this->runMessage .= ' - ' . __('Disabled') . PHP_EOL . PHP_EOL; } } /** * Tidy Cache */ private function tidyCache() { $this->runMessage .= '## ' . __('Tidy Cache') . PHP_EOL; $this->pool->purge(); $this->runMessage .= ' - ' . __('Done.') . PHP_EOL . PHP_EOL; } /** * Import Layouts * @throws GeneralException|\FontLib\Exception\FontNotFoundException */ private function importLayouts() { $this->runMessage .= '## ' . __('Import Layouts and Fonts') . PHP_EOL; if ($this->config->getSetting('DEFAULTS_IMPORTED') == 0) { // Make sure the library exists $this->mediaService->initLibrary(); // Import any layouts $folder = $this->config->uri('layouts', true); foreach (array_diff(scandir($folder), array('..', '.')) as $file) { if (stripos($file, '.zip')) { try { $layout = $this->layoutFactory->createFromZip( $folder . '/' . $file, null, $this->userFactory->getSystemUser()->getId(), false, false, true, false, true, $this->dataSetFactory, null, $this->mediaService, 1 ); $layout->save([ 'audit' => false, 'import' => true ]); if (!empty($layout->getUnmatchedProperty('thumbnail'))) { rename($layout->getUnmatchedProperty('thumbnail'), $layout->getThumbnailUri()); } try { $this->layoutFactory->getById($this->config->getSetting('DEFAULT_LAYOUT')); } catch (NotFoundException $exception) { $this->config->changeSetting('DEFAULT_LAYOUT', $layout->layoutId); } } catch (\Exception $exception) { $this->log->error('Unable to import layout: ' . $file . '. E = ' . $exception->getMessage()); $this->log->debug($exception->getTraceAsString()); } } } // Fonts // ----- // install fonts from the theme folder $libraryLocation = $this->config->getSetting('LIBRARY_LOCATION'); $fontFolder = $this->config->uri('fonts', true); foreach (array_diff(scandir($fontFolder), array('..', '.')) as $file) { // check if we already have this font file if (count($this->fontFactory->getByFileName($file)) <= 0) { // if we don't add it $filePath = $fontFolder . DIRECTORY_SEPARATOR . $file; $fontLib = \FontLib\Font::load($filePath); // check embed flag, just in case $embed = intval($fontLib->getData('OS/2', 'fsType')); // if it's not embeddable, log error and skip it if ($embed != 0 && $embed != 8) { $this->log->error('Unable to install default Font: ' . $file . ' . Font file is not embeddable due to its permissions'); continue; } $font = $this->fontFactory->createEmpty(); $font->modifiedBy = $this->userFactory->getSystemUser()->userName; $font->name = $fontLib->getFontName() . ' ' . $fontLib->getFontSubfamily(); $font->familyName = strtolower(preg_replace('/\s+/', ' ', preg_replace('/\d+/u', '', $font->name))); $font->fileName = $file; $font->size = filesize($filePath); $font->md5 = md5_file($filePath); $font->save(); $copied = copy($filePath, $libraryLocation . 'fonts/' . $file); if (!$copied) { $this->getLogger()->error('importLayouts: Unable to copy fonts to ' . $libraryLocation); } } } $this->config->changeSetting('DEFAULTS_IMPORTED', 1); $this->runMessage .= ' - ' . __('Done.') . PHP_EOL . PHP_EOL; } else { $this->runMessage .= ' - ' . __('Not Required.') . PHP_EOL . PHP_EOL; } } /** * Refresh the cache of assets * @return void * @throws GeneralException */ private function cacheAssets(): void { // Assets $failedCount = 0; $assets = array_merge($this->moduleFactory->getAllAssets(), $this->moduleTemplateFactory->getAllAssets()); foreach ($assets as $asset) { try { $asset->updateAssetCache($this->libraryLocation, true); } catch (GeneralException $exception) { $failedCount++; $this->log->error('Unable to copy asset: ' . $asset->id . ', e: ' . $exception->getMessage()); } } $this->appendRunMessage(sprintf(__('Assets cached, %d failed.'), $failedCount)); } /** * Cache the player bundle. * @return void */ private function cachePlayerBundle(): void { // Output the player bundle $bundlePath = $this->getConfig()->getSetting('LIBRARY_LOCATION') . 'assets/bundle.min.js'; $bundleMd5CachePath = $bundlePath . '.md5'; copy(PROJECT_ROOT . '/modules/bundle.min.js', $bundlePath); file_put_contents($bundleMd5CachePath, md5_file($bundlePath)); $this->appendRunMessage(__('Player bundle cached')); } /** * Once per day we cycle the XMR CMS key * the old key should remain valid in XMR for up to 1 hour further to allow for cross over * @return void */ private function cycleXmrKey(): void { $this->log->debug('cycleXmrKey: adding new key'); try { $key = Random::generateString(20, 'xmr_'); $this->getConfig()->changeSetting('XMR_CMS_KEY', $key); $client = new Client($this->config->getGuzzleProxy([ 'base_uri' => $this->getConfig()->getSetting('XMR_ADDRESS'), ])); $client->post('/', [ 'json' => [ 'id' => constant('SECRET_KEY'), 'type' => 'keys', 'key' => $key, ], ]); $this->log->debug('cycleXmrKey: added new key'); } catch (GuzzleException | \Exception $e) { $this->log->error('cycleXmrKey: failed. E = ' . $e->getMessage()); } } }