diff --git a/.github/workflows/test-suite.yaml b/.github/workflows/test-suite.yaml index 47d9ae3af8..643cd3fa88 100644 --- a/.github/workflows/test-suite.yaml +++ b/.github/workflows/test-suite.yaml @@ -33,7 +33,7 @@ jobs: docker pull ghcr.io/xibosignage/xibo-xmr:latest - name: Run run: | - docker run --name cms-db -e MYSQL_RANDOM_ROOT_PASSWORD=yes -e MYSQL_DATABASE=cms -e MYSQL_USER=cms -e MYSQL_PASSWORD=jenkins -d mysql:5.7 + docker run --name cms-db -e MYSQL_RANDOM_ROOT_PASSWORD=yes -e MYSQL_DATABASE=cms -e MYSQL_USER=cms -e MYSQL_PASSWORD=jenkins -d mysql:8 docker run --name cms-xmr -d ghcr.io/xibosignage/xibo-xmr:latest docker run --name cms-web -e MYSQL_USER=cms -e MYSQL_PASSWORD=jenkins -e XIBO_DEV_MODE=true -e XMR_HOST=cms-xmr --link cms-db:db --link cms-xmr:50001 -d cms-web - name: Wait for CMS @@ -47,7 +47,7 @@ jobs: run: | docker exec cms-db mysql -ucms -pjenkins cms -e "UPDATE setting SET value=\"6v4RduQhaw5Q\" WHERE setting = \"SERVER_KEY\" " docker exec cms-db mysql -ucms -pjenkins cms -e "INSERT INTO task (name, class, status, isActive, configFile, options, schedule) VALUES ('Seed Database', '\\\\Xibo\\\\XTR\\\\SeedDatabaseTask', 2, 1, '/tasks/seed-database.task', '{}', '* * * * * *')" - docker exec --user www-data -t cms-web /bin/bash -c "cd /var/www/cms; /usr/bin/php bin/run.php 20" + docker exec --user www-data -t cms-web /bin/bash -c "cd /var/www/cms; /usr/bin/php bin/run.php \"Seed Database\"" sleep 5 - name: Run PHP Unit run: | diff --git a/db/migrations/20220512130000_add_twitter_connector_migration.php b/db/migrations/20220512130000_add_twitter_connector_migration.php index dc8cc0de8e..2e735cc4d1 100644 --- a/db/migrations/20220512130000_add_twitter_connector_migration.php +++ b/db/migrations/20220512130000_add_twitter_connector_migration.php @@ -1,8 +1,8 @@ table('connectors') + /*$this->table('connectors') ->insert([ 'className' => '\\Xibo\\Connector\\TwitterConnector', 'isEnabled' => 0, 'isVisible' => 1 ]) - ->save(); + ->save();*/ } } diff --git a/db/migrations/20220915100902_add_fonts_table_migration.php b/db/migrations/20220915100902_add_fonts_table_migration.php index 4bf3656471..6cf41ea464 100644 --- a/db/migrations/20220915100902_add_fonts_table_migration.php +++ b/db/migrations/20220915100902_add_fonts_table_migration.php @@ -101,22 +101,5 @@ public function change() if (file_exists($libraryLocation . 'fonts.css')) { @unlink($libraryLocation . 'fonts.css'); } - - // add a task that will re-generate fonts.css for the player - $this->table('task') - ->insert([ - [ - 'name' => 'Generate Player font css', - 'class' => '\Xibo\XTR\GeneratePlayerCssTask', - 'options' => '[]', - 'schedule' => '*/5 * * * * *', - 'isActive' => '1', - 'configFile' => '/tasks/player-css.task', - 'pid' => 0, - 'lastRunDt' => 0, - 'lastRunDuration' => 0, - 'lastRunExitCode' => 0 - ], - ])->save(); } } diff --git a/db/migrations/20230731194700_lkdgdg_primary_key_migration.php b/db/migrations/20230731194700_lkdgdg_primary_key_migration.php new file mode 100644 index 0000000000..3bb1377453 --- /dev/null +++ b/db/migrations/20230731194700_lkdgdg_primary_key_migration.php @@ -0,0 +1,38 @@ +. + */ + +use Phinx\Migration\AbstractMigration; + +/** + * lkdgdg must have a primary key for MySQL8 clustering + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace + */ +class LkdgdgPrimaryKeyMigration extends AbstractMigration +{ + public function change(): void + { + $pk = $this->fetchAll('SHOW KEYS FROM `lkdgdg` WHERE `Key_name` = \'PRIMARY\''); + if (count($pk) <= 0) { + $this->execute('ALTER TABLE `lkdgdg` ADD COLUMN `id` INT(11) PRIMARY KEY AUTO_INCREMENT'); + } + } +} diff --git a/lib/Connector/TwitterConnector.php b/lib/Connector/TwitterConnector.php deleted file mode 100644 index fc0b9d323b..0000000000 --- a/lib/Connector/TwitterConnector.php +++ /dev/null @@ -1,382 +0,0 @@ -. - */ - -namespace Xibo\Connector; - -use Carbon\Carbon; -use GuzzleHttp\Exception\GuzzleException; -use GuzzleHttp\Exception\RequestException; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Xibo\Event\WidgetDataRequestEvent; -use Xibo\Event\WidgetEditOptionRequestEvent; -use Xibo\Support\Exception\AccessDeniedException; -use Xibo\Support\Exception\NotFoundException; -use Xibo\Support\Sanitizer\SanitizerInterface; -use Xibo\Widget\DataType\SocialMedia; -use Xibo\Widget\Provider\DataProviderInterface; - -/** - * A connector to get data from the Twitter API for use by the Twitter Widget - */ -class TwitterConnector implements ConnectorInterface -{ - use ConnectorTrait; - - public function registerWithDispatcher(EventDispatcherInterface $dispatcher): ConnectorInterface - { - $dispatcher->addListener(WidgetDataRequestEvent::$NAME, [$this, 'onDataRequest']); - $dispatcher->addListener(WidgetEditOptionRequestEvent::$NAME, [$this, 'onWidgetEditOption']); - return $this; - } - - public function getSourceName(): string - { - return 'twitter'; - } - - public function getTitle(): string - { - return 'Twitter'; - } - - public function getDescription(): string - { - return 'Use the Twitter Standard 1.1 API to search for and display tweets.'; - } - - public function getThumbnail(): string - { - return 'theme/default/img/connectors/twitter.png'; - } - - public function getSettingsFormTwig(): string - { - return 'twitter-form-settings'; - } - - public function processSettingsForm(SanitizerInterface $params, array $settings): array - { - if (!$this->isProviderSetting('apiKey')) { - $settings['delegated'] = $params->getCheckbox('delegated'); - $settings['apiKey'] = $params->getString('apiKey'); - $settings['apiSecret'] = $params->getString('apiSecret'); - $settings['cachePeriod'] = $params->getInt('cachePeriod'); - $settings['cachePeriodImages'] = $params->getInt('cachePeriodImages'); - } - return $settings; - } - - public function onWidgetEditOption(WidgetEditOptionRequestEvent $event) - { - $this->getLogger()->debug('onWidgetEditOption'); - - // Pull the widget we're working with. - $widget = $event->getWidget(); - if ($widget === null) { - throw new NotFoundException(); - } - - // We handle the twitter widget and the property with id="type" - if ($widget->type === 'twitter' && $event->getPropertyId() === 'language') { - if (empty($this->getSetting('apiKey')) || empty($this->getSetting('apiSecret'))) { - $this->getLogger()->debug('onWidgetEditOption: twitter not configured.'); - return; - } - - $delegated = $this->getSetting('delegated'); - if ($delegated && empty($this->getSetting('oAuthToken'))) { - $this->getLogger()->debug('onWidgetEditOption: twitter not configured.'); - return; - } - - try { - $twitterLanguages = $this->getLanguages(); - - if (count($twitterLanguages) === 0) { - $twitterLanguages[] = ['name' => 'English', 'type' => 'en', 'id' => 'language']; - } - - $event->setOptions($twitterLanguages); - - } catch (\Exception $exception) { - $this->getLogger()->error('onWidgetEditOption: Failed to get twitter languages. e = ' . $exception->getMessage()); - } - } - } - - public function getLanguages(): array - { - try { - $request = $this->getClient()->request('GET', 'https://api.twitter.com/1.1/help/languages.json', [ - 'headers' => [ - 'Authorization' => 'Bearer ' . $this->getToken( - $this->getSetting('apiKey'), - $this->getSetting('apiSecret') - ) - ], - ]); - - $body = $request->getBody()->getContents(); - $this->getLogger()->debug($body); - $body = json_decode($body, true); - - if (is_array($body)) { - return $body; - } - } catch (RequestException $requestException) { - $this->getLogger()->error('Unable to reach twitter api. ' . $requestException->getMessage()); - } catch (GuzzleException $exception) { - $this->getLogger()->error('Unable to reach twitter api. ' . $exception->getMessage()); - } - - return []; - } - - public function onDataRequest(WidgetDataRequestEvent $event) - { - if ($event->getDataProvider()->getDataSource() === 'twitter') { - if (empty($this->getSetting('apiKey')) || empty($this->getSetting('apiSecret'))) { - $this->getLogger()->debug('onDataRequest: twitter not configured.'); - $event->getDataProvider()->addError(__('Twitter not configured')); - return; - } - - $delegated = $this->getSetting('delegated'); - if ($delegated && empty($this->getSetting('oAuthToken'))) { - $this->getLogger()->debug('onDataRequest: twitter not configured.'); - $event->getDataProvider()->addError(__('Twitter not configured')); - return; - } - - // Handle this event. - $event->stopPropagation(); - - // Expiry time for any media that is downloaded - $expires = Carbon::now()->addHours($this->getSetting('cachePeriodImages', 24))->format('U'); - - try { - $dataProvider = $event->getDataProvider(); - foreach ($this->getFeed($dataProvider) as $item) { - // Parse the tweet - $tweet = new SocialMedia(); - - // Get the tweet text to operate on - // if it is a retweet we need to take the full_text in a different way - if (isset($item['retweeted_status'])) { - $tweet->text = 'RT @' . $item['retweeted_status']['user']['screen_name'] - . ': ' . $item['retweeted_status']['full_text']; - } else { - $tweet->text = $item['full_text']; - } - - // Replace URLs with their display_url before removal - if (isset($item['entities']['urls'])) { - foreach ($item['entities']['urls'] as $url) { - $tweet->text = str_replace($url['url'], $url['display_url'], $tweet->text); - } - } - - $tweet->user = $item['user']['name']; - $tweet->screenName = $item['user']['screen_name'] != '' ? '@' . $item['user']['screen_name'] : ''; - $tweet->date = $item['created_at']; - $tweet->location = $item['user']['location']; - - // Profile image - if (!empty($item['user']['profile_image_url'])) { - $id = 'twitter_' . ($item['user']['id_str'] ?: $item['user']['id']); - - // Original Default Image - $tweet->userProfileImage = $dataProvider->addImage( - $id, - $item['user']['profile_image_url'], - $expires - ); - - // Mini image - $url = str_replace('_normal', '_mini', $item['user']['profile_image_url']); - $tweet->userProfileImageMini = $dataProvider->addImage($id . '_mini', $url, $expires); - - // Bigger image - $url = str_replace('_normal', '_bigger', $item['user']['profile_image_url']); - $tweet->userProfileImageBigger = $dataProvider->addImage($id . '_bigger', $url, $expires); - } - - // Photo - // See if there are any photos associated with this tweet. - if ((isset($item['entities']['media']) && count($item['entities']['media']) > 0) - || (isset($item['retweeted_status']['entities']['media']) - && count($item['retweeted_status']['entities']['media']) > 0) - ) { - // See if it's an image from a tweet or RT, and only take the first one - $mediaObject = (isset($item['entities']['media'])) - ? $item['entities']['media'][0] - : $item['retweeted_status']['entities']['media'][0]; - - $photoUrl = $mediaObject['media_url']; - if (!empty($photoUrl)) { - $tweet->photo = $dataProvider->addImage( - 'twitter_' . ($mediaObject['id_str'] ?? $mediaObject['id']), - $photoUrl, - $expires - ); - } - } - - $event->getDataProvider()->addItem($tweet); - } - - // If we've got data, then set our cache period. - $event->getDataProvider()->setCacheTtl($this->getSetting('cachePeriod', 3600)); - $event->getDataProvider()->setIsHandled(); - } catch (\Exception $exception) { - $this->getLogger()->error('onDataRequest: Failed to get feed. e = ' . $exception->getMessage()); - $event->getDataProvider()->addError(__('Unable to get Tweets')); - } - } - } - - /** - * @param \Xibo\Widget\Provider\DataProviderInterface $dataProvider - * @return array - * @throws \Xibo\Support\Exception\AccessDeniedException - */ - private function getFeed(DataProviderInterface $dataProvider): array - { - // TODO: delegated access - user needs to have authenticated. - - // Append filters to the search term. - $searchTerm = $dataProvider->getProperty('searchTerm', ''); - if ($searchTerm == 1) { - $searchTerm .= ' -filter:media'; - } else if ($searchTerm == 2) { - $searchTerm .= ' filter:twimg'; - } - - $query = [ - 'q' => trim($searchTerm), - 'result_type' => $dataProvider->getProperty('resultType', 'mixed'), - 'count' => $dataProvider->getProperty('numItems', 15), - 'include_entities' => true, - 'tweet_mode' => 'extended' - ]; - - if (!empty($dataProvider->getProperty('language'))) { - $query['lang'] = $dataProvider->getProperty('language'); - } - - // Do we need to do geo? - $distance = $dataProvider->getProperty('tweetDistance', 0); - if ($distance > 0) { - $query['geocode'] = implode( - ',', - [ - $dataProvider->getDisplayLatitude(), - $dataProvider->getDisplayLongitude(), - $distance - ] - ) . 'mi'; - } - - // Search - try { - $request = $this->getClient()->request('GET', 'https://api.twitter.com/1.1/search/tweets.json', [ - 'headers' => [ - 'Authorization' => 'Bearer ' . $this->getToken( - $this->getSetting('apiKey'), - $this->getSetting('apiSecret') - ) - ], - 'query' => $query - ]); - - $body = $request->getBody()->getContents(); - $this->getLogger()->debug($body); - $body = json_decode($body, true); - if (isset($body['statuses'])) { - return $body['statuses']; - } - } catch (RequestException $requestException) { - $this->getLogger()->error('Unable to reach twitter api. ' . $requestException->getMessage()); - } catch (GuzzleException $exception) { - $this->getLogger()->error('Unable to reach twitter api. ' . $exception->getMessage()); - } - return []; - } - - /** - * Get an auth token - * @param string $apiKey - * @param string $apiSecret - * @return string - * @throws \Xibo\Support\Exception\AccessDeniedException - */ - private function getToken(string $apiKey, string $apiSecret): string - { - // Prepare the consumer key and secret - $key = base64_encode(urlencode($apiKey) . ':' . urlencode($apiSecret)); - - // Check to see if we have the bearer token already cached - $cache = $this->getPool()->getItem('connector/bearer_' . md5($key)); - $token = $cache->get(); - - if ($cache->isHit()) { - $this->getLogger()->debug('Bearer Token served from cache'); - return $token; - } - - // We can take up to 30 seconds to request a new token - $cache->lock(30); - - try { - $response = $this->getClient()->request('POST', 'https://api.twitter.com/oauth2/token', [ - 'form_params' => [ - 'grant_type' => 'client_credentials' - ], - 'headers' => [ - 'Authorization' => 'Basic ' . $key - ] - ]); - - $result = json_decode($response->getBody()->getContents()); - - if ($result->token_type !== 'bearer') { - $this->getLogger()->error('Twitter API returned OK, but without a bearer token. ' - . var_export($result, true)); - throw new AccessDeniedException(__('Twitter is not authenticated')); - } - - // It is, so lets cache it - // long times... - $cache->set($result->access_token); - $cache->expiresAfter(100000); - $this->getPool()->saveDeferred($cache); - - return $result->access_token; - } catch (RequestException $requestException) { - $this->getLogger()->error('Twitter API returned ' . $requestException->getMessage() - . ' status. Unable to proceed.'); - throw new AccessDeniedException(__('Twitter is not authenticated')); - } catch (GuzzleException $exception) { - throw new AccessDeniedException(__('Twitter is not authenticated')); - } - } -} diff --git a/lib/Controller/Connector.php b/lib/Controller/Connector.php index afcf32f006..19713a3486 100644 --- a/lib/Controller/Connector.php +++ b/lib/Controller/Connector.php @@ -1,8 +1,8 @@ decorate($this->connectorFactory->create($connector)); + } catch (NotFoundException $ignored) { + $this->getLog()->info('Connector installed which is not found in this CMS. ' . $connector->className); } catch (\Exception $e) { $this->getLog()->error('Incorrectly configured connector ' . $connector->className . '. e=' . $e->getMessage()); diff --git a/lib/Controller/Folder.php b/lib/Controller/Folder.php index 64a77f4d45..83d57d4a8d 100644 --- a/lib/Controller/Folder.php +++ b/lib/Controller/Folder.php @@ -445,12 +445,7 @@ public function move(Request $request, Response $response, $folderId) } if ($folder->parentId === $newParentFolder->getId() && $params->getCheckbox('merge') !== 1) { - throw new InvalidArgumentException( - __( - 'This Folder is already a sub-folder of the selected Folder, - if you wish to move its content to the parent Folder, please check the merge checkbox.' - ) - ); + throw new InvalidArgumentException(__('This Folder is already a sub-folder of the selected Folder, if you wish to move its content to the parent Folder, please check the merge checkbox.'));//phpcs:ignore } // if we need to merge contents of the folder, dispatch an event that will move every object inside the folder diff --git a/lib/Controller/Module.php b/lib/Controller/Module.php index 4ae108a14b..a78a5055f9 100644 --- a/lib/Controller/Module.php +++ b/lib/Controller/Module.php @@ -288,6 +288,7 @@ public function templateGrid(Request $request, Response $response, string $dataT * @return \Psr\Http\Message\ResponseInterface|Response * @throws \Xibo\Support\Exception\InvalidArgumentException * @throws \Xibo\Support\Exception\NotFoundException + * @throws \Xibo\Support\Exception\GeneralException */ public function assetDownload(Request $request, Response $response, string $assetId): Response { @@ -297,10 +298,11 @@ public function assetDownload(Request $request, Response $response, string $asse // Get this asset from somewhere $asset = $this->moduleFactory->getAssetsFromAnywhereById($assetId, $this->moduleTemplateFactory); + $asset->updateAssetCache($this->getConfig()->getSetting('LIBRARY_LOCATION')); $this->getLog()->debug('assetDownload: found appropriate asset for assetId ' . $assetId); // The asset can serve itself. - return $asset->psrResponse($request, $response); + return $asset->psrResponse($request, $response, $this->getConfig()->getSetting('SENDFILE_MODE')); } } diff --git a/lib/Controller/Task.php b/lib/Controller/Task.php index 4a29104fd7..56c3b87fbe 100644 --- a/lib/Controller/Task.php +++ b/lib/Controller/Task.php @@ -395,7 +395,11 @@ public function runNow(Request $request, Response $response, $id) public function run(Request $request, Response $response, $id) { // Get this task - $task = $this->taskFactory->getById($id); + if (is_numeric($id)) { + $task = $this->taskFactory->getById($id); + } else { + $task = $this->taskFactory->getByName($id); + } // Set to running $this->getLog()->debug('run: Running Task ' . $task->name diff --git a/lib/Entity/DataSet.php b/lib/Entity/DataSet.php index a189b3f364..3314431ab1 100644 --- a/lib/Entity/DataSet.php +++ b/lib/Entity/DataSet.php @@ -696,7 +696,7 @@ public function assignColumn($column) */ public function hasData() { - return $this->getStore()->exists('SELECT id FROM `dataset_' . $this->dataSetId . '` LIMIT 1', []); + return $this->getStore()->exists('SELECT id FROM `dataset_' . $this->dataSetId . '` LIMIT 1', [], 'isolated'); } /** @@ -956,8 +956,7 @@ public function delete() // Delete Columns foreach ($this->columns as $column) { - /* @var \Xibo\Entity\DataSetColumn $column */ - $column->delete(); + $column->delete(true); } // Delete any dataSet rss diff --git a/lib/Entity/DataSetColumn.php b/lib/Entity/DataSetColumn.php index 2ab422e513..5ea26b1bdc 100644 --- a/lib/Entity/DataSetColumn.php +++ b/lib/Entity/DataSetColumn.php @@ -1,8 +1,8 @@ getStore()->update('DELETE FROM `datasetcolumn` WHERE DataSetColumnID = :dataSetColumnId', ['dataSetColumnId' => $this->dataSetColumnId]); - // Delete column - if (($this->dataSetColumnTypeId == 1) || ($this->dataSetColumnTypeId == 3)) { + // Delete column (unless remote, or dropping the whole dataset) + if (!$isDeletingDataset && $this->dataSetColumnTypeId !== 2) { $this->getStore()->update('ALTER TABLE `dataset_' . $this->dataSetId . '` DROP `' . $this->heading . '`', []); } } diff --git a/lib/Entity/Schedule.php b/lib/Entity/Schedule.php index df7fddc6f3..27cdde3166 100644 --- a/lib/Entity/Schedule.php +++ b/lib/Entity/Schedule.php @@ -1844,7 +1844,11 @@ public function getChangedProperties($jsonEncodeArrays = false) return $changedProperties; } - public static function getEventTypesForm() + /** + * Get an array of event types for the add/edit form + * @return array + */ + public static function getEventTypesForm(): array { return [ ['eventTypeId' => self::$LAYOUT_EVENT, 'eventTypeName' => __('Layout')], @@ -1853,12 +1857,16 @@ public static function getEventTypesForm() ['eventTypeId' => self::$INTERRUPT_EVENT, 'eventTypeName' => __('Interrupt Layout')], ['eventTypeId' => self::$CAMPAIGN_EVENT, 'eventTypeName' => __('Campaign')], ['eventTypeId' => self::$ACTION_EVENT, 'eventTypeName' => __('Action')], - ['eventTypeId' => self::$MEDIA_EVENT, 'eventTypeName' => __('Full Screen Video/Image')], - ['eventTypeId' => self::$PLAYLIST_EVENT, 'eventTypeName' => __('Full Screen Playlist')], + ['eventTypeId' => self::$MEDIA_EVENT, 'eventTypeName' => __('Video/Image')], + ['eventTypeId' => self::$PLAYLIST_EVENT, 'eventTypeName' => __('Playlist')], ]; } - public static function getEventTypesGrid() + /** + * Get an array of event types for the grid + * @return array + */ + public static function getEventTypesGrid(): array { $events = self::getEventTypesForm(); $events[] = ['eventTypeId' => self::$SYNC_EVENT, 'eventTypeName' => __('Synchronised Event')]; @@ -1867,7 +1875,6 @@ public static function getEventTypesGrid() } /** - * @param $eventId * @return string */ public function getSyncTypeForEvent(): string diff --git a/lib/Event/XmdsDependencyRequestEvent.php b/lib/Event/XmdsDependencyRequestEvent.php index bfdcdb7211..30f2756585 100644 --- a/lib/Event/XmdsDependencyRequestEvent.php +++ b/lib/Event/XmdsDependencyRequestEvent.php @@ -34,7 +34,6 @@ class XmdsDependencyRequestEvent extends Event private $fileType; private $id; private $path; - private $fullPath; /** * @var string|null */ @@ -61,27 +60,11 @@ public function setRelativePathToLibrary(string $path): XmdsDependencyRequestEve return $this; } - /** - * Set the full path to this dependency, including the library folder if applicable. - * @param string $fullPath - * @return $this - */ - public function setFullPath(string $fullPath): XmdsDependencyRequestEvent - { - $this->fullPath = $fullPath; - return $this; - } - public function getRelativePath(): ?string { return $this->path; } - public function getFullPath(): ?string - { - return $this->fullPath; - } - public function getFileType(): string { return $this->fileType; diff --git a/lib/Factory/LayoutFactory.php b/lib/Factory/LayoutFactory.php index 3ab84797e9..e385da6141 100644 --- a/lib/Factory/LayoutFactory.php +++ b/lib/Factory/LayoutFactory.php @@ -1303,10 +1303,14 @@ public function createFromZip( // Construct the Layout if ($playlistDetails !== false) { $playlistDetails = json_decode(($playlistDetails), true); + } else { + $playlistDetails = []; } if ($nestedPlaylistDetails !== false) { $nestedPlaylistDetails = json_decode($nestedPlaylistDetails, true); + } else { + $nestedPlaylistDetails = []; } $jsonResults = $this->loadByJson( diff --git a/lib/Factory/ModuleFactory.php b/lib/Factory/ModuleFactory.php index 562669ec10..f6284a89e0 100644 --- a/lib/Factory/ModuleFactory.php +++ b/lib/Factory/ModuleFactory.php @@ -26,6 +26,7 @@ use Slim\Views\Twig; use Stash\Interfaces\PoolInterface; use Xibo\Entity\Module; +use Xibo\Entity\ModuleTemplate; use Xibo\Entity\Widget; use Xibo\Service\ConfigServiceInterface; use Xibo\Support\Exception\NotFoundException; @@ -432,8 +433,8 @@ public function getAssetById(string $assetId): Asset } /** - * @param \Xibo\Entity\ModuleTemplate[] $templates - * @return void + * @param ModuleTemplate[] $templates + * @return Asset[] */ public function getAssetsFromTemplates(array $templates): array { @@ -453,6 +454,21 @@ public function getAssetsFromTemplates(array $templates): array return $assets; } + /** + * Get all assets + * @return Asset[] + */ + public function getAllAssets(): array + { + $assets = []; + foreach ($this->getEnabled() as $module) { + foreach ($module->getAssets() as $asset) { + $assets[$asset->id] = $asset; + } + } + return $assets; + } + /** * Get an asset from anywhere by its ID * @param string $assetId diff --git a/lib/Factory/ModuleTemplateFactory.php b/lib/Factory/ModuleTemplateFactory.php index df555f35dc..b3185205d0 100644 --- a/lib/Factory/ModuleTemplateFactory.php +++ b/lib/Factory/ModuleTemplateFactory.php @@ -129,6 +129,21 @@ public function getAll(): array return $this->load(); } + /** + * Get an array of all modules + * @return Asset[] + */ + public function getAllAssets(): array + { + $assets = []; + foreach ($this->load() as $template) { + foreach ($template->getAssets() as $asset) { + $assets[$asset->id] = $asset; + } + } + return $assets; + } + /** * Load templates * @return \Xibo\Entity\ModuleTemplate[] diff --git a/lib/Factory/RequiredFileFactory.php b/lib/Factory/RequiredFileFactory.php index 2a389c8a19..e11c0286ed 100644 --- a/lib/Factory/RequiredFileFactory.php +++ b/lib/Factory/RequiredFileFactory.php @@ -36,6 +36,11 @@ class RequiredFileFactory extends BaseFactory { private $statement = null; + private $hydrate = [ + 'intProperties' => ['bytesRequested', 'complete'], + 'stringProperties' => ['realId'], + ]; + /** * @return RequiredFile */ @@ -65,7 +70,7 @@ private function query($params) $this->statement->execute($params); foreach ($this->statement->fetchAll(\PDO::FETCH_ASSOC) as $item) { - $files[] = $this->createEmpty()->hydrate($item, ['stringProperties' => ['realId']]); + $files[] = $this->createEmpty()->hydrate($item, $this->hydrate); } return $files; @@ -125,10 +130,11 @@ public function getByDisplayAndWidget($displayId, $widgetId, $type = 'W') * @param int $displayId * @param string $fileType The file type of this dependency * @param int $id The ID of this dependency + * @param bool $isUseRealId Should we use the realId as a lookup? * @return RequiredFile * @throws NotFoundException */ - public function getByDisplayAndDependency($displayId, $fileType, $id, bool $isUseRealId = true) + public function getByDisplayAndDependency($displayId, $fileType, $id, bool $isUseRealId = true): RequiredFile { if (!$isUseRealId && $id < 0) { $fileType = self::getLegacyFileType($id); @@ -152,7 +158,7 @@ public function getByDisplayAndDependency($displayId, $fileType, $id, bool $isUs throw new NotFoundException(__('Required file not found for Display and Dependency')); } - return $this->createEmpty()->hydrate($result[0], ['stringProperties' => ['realId']]); + return $this->createEmpty()->hydrate($result[0], $this->hydrate); } /** @@ -165,8 +171,10 @@ private static function getLegacyFileType($id): string return match (true) { $id < 0 && $id > Dependency::LEGACY_ID_OFFSET_FONT * -1 => 'bundle', $id === Dependency::LEGACY_ID_OFFSET_FONT * -1 => 'fontCss', - $id < Dependency::LEGACY_ID_OFFSET_FONT * -1 && $id > Dependency::LEGACY_ID_OFFSET_PLAYER_SOFTWARE * -1 => 'font', - $id < Dependency::LEGACY_ID_OFFSET_PLAYER_SOFTWARE * -1 && $id > Dependency::LEGACY_ID_OFFSET_ASSET * -1 => 'playersoftware', + $id < Dependency::LEGACY_ID_OFFSET_FONT * -1 + && $id > Dependency::LEGACY_ID_OFFSET_PLAYER_SOFTWARE * -1 => 'font', + $id < Dependency::LEGACY_ID_OFFSET_PLAYER_SOFTWARE * -1 + && $id > Dependency::LEGACY_ID_OFFSET_ASSET * -1 => 'playersoftware', $id < Dependency::LEGACY_ID_OFFSET_PLAYER_SOFTWARE * -1 => 'asset', }; } @@ -195,7 +203,7 @@ public function getByDisplayAndDependencyPath($displayId, $path) throw new NotFoundException(__('Required file not found for Display and Path')); } - return $this->createEmpty()->hydrate($result[0], ['stringProperties' => ['realId']]); + return $this->createEmpty()->hydrate($result[0], $this->hydrate); } /** @@ -222,7 +230,7 @@ public function getByDisplayAndDependencyId($displayId, $id) throw new NotFoundException(__('Required file not found for Display and Dependency ID')); } - return $this->createEmpty()->hydrate($result[0], ['stringProperties' => ['realId']]); + return $this->createEmpty()->hydrate($result[0], $this->hydrate); } /** @@ -298,6 +306,7 @@ public function createForGetData($displayId, $widgetId): RequiredFile * @param $id * @param string|int $realId * @param $path + * @param int $size * @param bool $isUseRealId * @return RequiredFile */ @@ -307,6 +316,7 @@ public function createForGetDependency( $id, $realId, $path, + int $size, bool $isUseRealId = true ): RequiredFile { try { @@ -321,6 +331,7 @@ public function createForGetDependency( $requiredFile->fileType = $fileType; $requiredFile->realId = $realId; $requiredFile->path = $path; + $requiredFile->size = $size; return $requiredFile; } @@ -357,30 +368,38 @@ public function resolveRequiredFileFromRequest($request): RequiredFile { $params = $this->getSanitizer($request); $displayId = $params->getInt('displayId'); - $itemId = $params->getInt('itemId'); switch ($params->getString('type')) { case 'L': + $itemId = $params->getInt('itemId'); $file = $this->getByDisplayAndLayout($displayId, $itemId); break; case 'M': + $itemId = $params->getInt('itemId'); $file = $this->getByDisplayAndMedia($displayId, $itemId); break; case 'P': + $itemId = $params->getString('itemId'); $fileType = $params->getString('fileType'); if (empty($fileType)) { throw new NotFoundException(__('Missing fileType')); } + + // File type media means that we will use a special negative itemId to get out the actual file. + if ($fileType === 'media') { + $itemId = intval($itemId); + } + $file = $this->getByDisplayAndDependency( $displayId, $fileType, $itemId, - !($fileType == 'media' && $itemId < 0) + !($fileType === 'media' && $itemId < 0) ); - // Update $file->path with the path on disk (likely /dependencies/$fileType/$itemId) + // Update $file->path with the path on disk (likely /assets/$itemId) $event = new XmdsDependencyRequestEvent($file); $this->getDispatcher()->dispatch($event, XmdsDependencyRequestEvent::$NAME); diff --git a/lib/Listener/ListenerConfigTrait.php b/lib/Listener/ListenerConfigTrait.php new file mode 100644 index 0000000000..e733dbc397 --- /dev/null +++ b/lib/Listener/ListenerConfigTrait.php @@ -0,0 +1,49 @@ +. + */ + +namespace Xibo\Listener; + +use Xibo\Service\ConfigServiceInterface; + +trait ListenerConfigTrait +{ + /** @var ConfigServiceInterface */ + private $config; + + /** + * @param ConfigServiceInterface $config + * @return $this + */ + public function useConfig(ConfigServiceInterface $config) + { + $this->config = $config; + return $this; + } + + /** + * @return ConfigServiceInterface + */ + protected function getConfig() + { + return $this->config; + } +} diff --git a/lib/Middleware/Handlers.php b/lib/Middleware/Handlers.php index 1704c8a7c8..82b3e0309a 100644 --- a/lib/Middleware/Handlers.php +++ b/lib/Middleware/Handlers.php @@ -1,6 +1,6 @@ render($response, $template . '.twig', array_merge($viewParams, $exceptionData)) - ->withStatus($statusCode); + try { + return $twig->render($response, $template . '.twig', array_merge($viewParams, $exceptionData)) + ->withStatus($statusCode); + } catch (\Exception $exception) { + $response->getBody()->write('Fatal error'); + return $response->withStatus(500); + } } } }; diff --git a/lib/Middleware/ListenersMiddleware.php b/lib/Middleware/ListenersMiddleware.php index 36ff46f327..03569a55f6 100644 --- a/lib/Middleware/ListenersMiddleware.php +++ b/lib/Middleware/ListenersMiddleware.php @@ -374,7 +374,9 @@ public static function setXmdsListeners(App $app) $dispatcher = $c->get('dispatcher'); $playerBundleListener = new XmdsPlayerBundleListener(); - $playerBundleListener->useLogger($c->get('logger')); + $playerBundleListener + ->useLogger($c->get('logger')) + ->useConfig($c->get('configService')); $fontsListener = new XmdsFontsListener($c->get('fontFactory')); $fontsListener->useLogger($c->get('logger')); @@ -386,6 +388,9 @@ public static function setXmdsListeners(App $app) $c->get('moduleFactory'), $c->get('moduleTemplateFactory') ); + $assetsListener + ->useLogger($c->get('logger')) + ->useConfig($c->get('configService')); $dispatcher->addListener('xmds.dependency.list', [$playerBundleListener, 'onDependencyList']); $dispatcher->addListener('xmds.dependency.request', [$playerBundleListener, 'onDependencyRequest']); diff --git a/lib/Service/MediaService.php b/lib/Service/MediaService.php index 646a4708c6..e373d252cd 100644 --- a/lib/Service/MediaService.php +++ b/lib/Service/MediaService.php @@ -300,6 +300,10 @@ public static function ensureLibraryExists($libraryFolder) mkdir($libraryFolder . '/savedreport', 0777, true); } + if (!file_exists($libraryFolder . '/assets')) { + mkdir($libraryFolder . '/assets', 0777, true); + } + // Check that we are now writable - if not then error if (!is_writable($libraryFolder)) { throw new ConfigurationException(__('Library not writable')); diff --git a/lib/Widget/Definition/Asset.php b/lib/Widget/Definition/Asset.php index 0b64f95426..c6251b4dd9 100644 --- a/lib/Widget/Definition/Asset.php +++ b/lib/Widget/Definition/Asset.php @@ -28,6 +28,7 @@ use Psr\Http\Message\ResponseInterface; use Slim\Http\Response; use Slim\Http\ServerRequest; +use Xibo\Support\Exception\GeneralException; use Xibo\Support\Exception\NotFoundException; use Xibo\Xmds\Entity\Dependency; @@ -46,6 +47,9 @@ class Asset implements \JsonSerializable public $assetNo; + private $fileSize; + private $md5; + /** @inheritDoc */ public function jsonSerialize(): array { @@ -67,6 +71,40 @@ public function isSendToPlayer(): bool return !($this->cmsOnly ?? false); } + /** + * @param string $libraryLocation + * @param bool $forceUpdate + * @return $this + * @throws GeneralException + */ + public function updateAssetCache(string $libraryLocation, bool $forceUpdate = false): Asset + { + // Verify the asset is cached and update its path. + $assetPath = $libraryLocation . 'assets/' . $this->getFilename(); + if (!file_exists($assetPath) || $forceUpdate) { + $result = @copy(PROJECT_ROOT . $this->path, $assetPath); + if (!$result) { + throw new GeneralException('Unable to copy asset'); + } + $forceUpdate = true; + } + + // Get the bundle MD5 + $assetMd5CachePath = $assetPath . '.md5'; + if (!file_exists($assetMd5CachePath) || $forceUpdate) { + $assetMd5 = md5_file($assetPath); + file_put_contents($assetMd5CachePath, $assetMd5); + } else { + $assetMd5 = file_get_contents($assetPath . '.md5'); + } + + $this->path = $assetPath; + $this->md5 = $assetMd5; + $this->fileSize = filesize($assetPath); + + return $this; + } + /** * Get this asset as a dependency. * @return \Xibo\Xmds\Entity\Dependency @@ -75,27 +113,26 @@ public function isSendToPlayer(): bool public function getDependency(): Dependency { // Check that this asset is valid. - if (!file_exists(PROJECT_ROOT . $this->path)) { - throw new NotFoundException(__('Asset not found')); + if (!file_exists($this->path)) { + throw new NotFoundException(sprintf(__('Asset %s not found'), $this->path)); } - // Get the file size and md5 of the asset. - // TODO: cache this? - $md5 = md5_file(PROJECT_ROOT . $this->path); - $size = filesize(PROJECT_ROOT . $this->path); - // Return a dependency return new Dependency( 'asset', $this->id, $this->getLegacyId(), $this->path, - $size, - $md5, - false + $this->fileSize, + $this->md5, + true ); } + /** + * Get the file name for this asset + * @return string + */ public function getFilename(): string { return basename($this->path); @@ -103,23 +140,32 @@ public function getFilename(): string /** * Generate a PSR response for this asset. - * We cannot use sendfile because the asset isn't in the library folder. * @throws \Xibo\Support\Exception\NotFoundException */ - public function psrResponse(ServerRequest $request, Response $response): ResponseInterface + public function psrResponse(ServerRequest $request, Response $response, string $sendFileMode): ResponseInterface { // Make sure this asset exists - if (!file_exists(PROJECT_ROOT . $this->path)) { + if (!file_exists($this->path)) { throw new NotFoundException(__('Asset file does not exist')); } - if (Str::startsWith('image', $this->mimeType)) { - return Img::make(PROJECT_ROOT . '/' . $this->path)->psrResponse(); + $response = $response->withHeader('Content-Length', $this->fileSize); + $response = $response->withHeader('Content-Type', $this->mimeType); + + // Output the file + if ($sendFileMode === 'Apache') { + // Send via Apache X-Sendfile header? + $response = $response->withHeader('X-Sendfile', $this->path); + } else if ($sendFileMode === 'Nginx') { + // Send via Nginx X-Accel-Redirect? + $response = $response->withHeader('X-Accel-Redirect', '/download/assets/' . $this->getFilename()); + } else if (Str::startsWith('image', $this->mimeType)) { + $response = Img::make('/' . $this->path)->psrResponse(); } else { // Set the right content type. - $response = $response->withHeader('Content-Type', $this->mimeType); - return $response->withBody(new Stream(fopen(PROJECT_ROOT . $this->path, 'r'))); + $response = $response->withBody(new Stream(fopen($this->path, 'r'))); } + return $response; } /** diff --git a/lib/XTR/GeneratePlayerCssTask.php b/lib/XTR/GeneratePlayerCssTask.php deleted file mode 100644 index 434463428c..0000000000 --- a/lib/XTR/GeneratePlayerCssTask.php +++ /dev/null @@ -1,39 +0,0 @@ -mediaService = $container->get('mediaService'); - return $this; - } - - public function run() - { - $this->runMessage = '# ' . __('Generate Player font css') . PHP_EOL . PHP_EOL; - - $this->mediaService->updateFontsCss(); - - $this->runMessage = '# Generated Player Font css file ' . PHP_EOL . PHP_EOL; - - // Disable the task - $this->appendRunMessage('# Disabling task.'); - - $this->getTask()->isActive = 0; - $this->getTask()->save(); - - $this->appendRunMessage(__('Done.'. PHP_EOL)); - } -} diff --git a/lib/XTR/MaintenanceDailyTask.php b/lib/XTR/MaintenanceDailyTask.php index ed9b14c62a..3ddee40fa6 100644 --- a/lib/XTR/MaintenanceDailyTask.php +++ b/lib/XTR/MaintenanceDailyTask.php @@ -24,14 +24,16 @@ use Carbon\Carbon; use Xibo\Controller\Module; -use Xibo\Entity\Font; 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\Service\MediaService; use Xibo\Service\MediaServiceInterface; use Xibo\Support\Exception\GeneralException; use Xibo\Support\Exception\NotFoundException; @@ -58,11 +60,19 @@ class MaintenanceDailyTask implements TaskInterface /** @var DataSetFactory */ private $dataSetFactory; - /** - * @var FontFactory - */ + + /** @var FontFactory */ private $fontFactory; + /** @var ModuleFactory */ + private $moduleFactory; + + /** @var ModuleTemplateFactory */ + private $moduleTemplateFactory; + + /** @var string */ + private $libraryLocation; + /** @inheritdoc */ public function setFactories($container) { @@ -72,6 +82,8 @@ public function setFactories($container) $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; } @@ -80,12 +92,38 @@ public function run() { $this->runMessage = '# ' . __('Daily Maintenance') . PHP_EOL . PHP_EOL; - // Long running task + // 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(); + 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(); @@ -134,7 +172,7 @@ private function tidyCache() /** * Import Layouts - * @throws GeneralException + * @throws GeneralException|\FontLib\Exception\FontNotFoundException */ private function importLayouts() { @@ -186,10 +224,11 @@ private function importLayouts() } } + // Fonts + // ----- // install fonts from the theme folder $libraryLocation = $this->config->getSetting('LIBRARY_LOCATION'); $fontFolder = $this->config->uri('fonts', true); - $fontsAdded = false; foreach (array_diff(scandir($fontFolder), array('..', '.')) as $file) { // check if we already have this font file if (count($this->fontFactory->getByFileName($file)) <= 0) { @@ -206,7 +245,6 @@ private function importLayouts() continue; } - /** @var Font $font */ $font = $this->fontFactory->createEmpty(); $font->modifiedBy = $this->userFactory->getSystemUser()->userName; $font->name = $fontLib->getFontName() . ' ' . $fontLib->getFontSubfamily(); @@ -216,7 +254,6 @@ private function importLayouts() $font->md5 = md5_file($filePath); $font->save(); - $fontsAdded = true; $copied = copy($filePath, $libraryLocation . 'fonts/' . $file); if (!$copied) { $this->getLogger()->error('importLayouts: Unable to copy fonts to ' . $libraryLocation); @@ -224,11 +261,6 @@ private function importLayouts() } } - if ($fontsAdded) { - // if we added any fonts here fonts.css file - $this->mediaService->setUser($this->userFactory->getSystemUser())->updateFontsCss(); - } - $this->config->changeSetting('DEFAULTS_IMPORTED', 1); $this->runMessage .= ' - ' . __('Done.') . PHP_EOL . PHP_EOL; @@ -236,4 +268,42 @@ private function importLayouts() $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')); + } } diff --git a/lib/Xmds/Listeners/XmdsAssetsListener.php b/lib/Xmds/Listeners/XmdsAssetsListener.php index 6243157d16..076d8b0929 100644 --- a/lib/Xmds/Listeners/XmdsAssetsListener.php +++ b/lib/Xmds/Listeners/XmdsAssetsListener.php @@ -25,6 +25,7 @@ use Xibo\Event\XmdsDependencyRequestEvent; use Xibo\Factory\ModuleFactory; use Xibo\Factory\ModuleTemplateFactory; +use Xibo\Listener\ListenerConfigTrait; use Xibo\Listener\ListenerLoggerTrait; use Xibo\Support\Exception\NotFoundException; @@ -34,6 +35,7 @@ class XmdsAssetsListener { use ListenerLoggerTrait; + use ListenerConfigTrait; /** @var \Xibo\Factory\ModuleFactory */ private $moduleFactory; @@ -62,8 +64,11 @@ public function onDependencyRequest(XmdsDependencyRequestEvent $event): void ->getAssetsFromAnywhereById($event->getRealId(), $this->moduleTemplateFactory); if ($asset->isSendToPlayer()) { + // Make sure the asset cache is there + $asset->updateAssetCache($this->getConfig()->getSetting('LIBRARY_LOCATION')); + // Return the full path to this asset - $event->setFullPath(PROJECT_ROOT . $asset->path); + $event->setRelativePathToLibrary('assets/' . $asset->getFilename()); $event->stopPropagation(); } else { $this->getLogger()->debug('onDependencyRequest: asset found but is cms only'); diff --git a/lib/Xmds/Listeners/XmdsPlayerBundleListener.php b/lib/Xmds/Listeners/XmdsPlayerBundleListener.php index afb7be7c44..10734e0311 100644 --- a/lib/Xmds/Listeners/XmdsPlayerBundleListener.php +++ b/lib/Xmds/Listeners/XmdsPlayerBundleListener.php @@ -24,7 +24,10 @@ use Xibo\Event\XmdsDependencyListEvent; use Xibo\Event\XmdsDependencyRequestEvent; +use Xibo\Listener\ListenerCacheTrait; +use Xibo\Listener\ListenerConfigTrait; use Xibo\Listener\ListenerLoggerTrait; +use Xibo\Support\Exception\GeneralException; /** * XMDS player bundle listener @@ -34,22 +37,39 @@ class XmdsPlayerBundleListener { use ListenerLoggerTrait; + use ListenerConfigTrait; public function onDependencyList(XmdsDependencyListEvent $event) { $this->getLogger()->debug('onDependencyList: XmdsPlayerBundleListener'); // Output the player bundle - $bundlePath = PROJECT_ROOT . '/modules/bundle.min.js'; - $bundleSize = filesize($bundlePath); + $forceUpdate = false; + $bundlePath = $this->getConfig()->getSetting('LIBRARY_LOCATION') . 'assets/bundle.min.js'; + if (!file_exists($bundlePath)) { + $result = @copy(PROJECT_ROOT . '/modules/bundle.min.js', $bundlePath); + if (!$result) { + throw new GeneralException('Unable to copy asset'); + } + $forceUpdate = true; + } + + // Get the bundle MD5 + $bundleMd5CachePath = $bundlePath . '.md5'; + if (!file_exists($bundleMd5CachePath) || $forceUpdate) { + $bundleMd5 = md5_file($bundlePath); + file_put_contents($bundleMd5CachePath, $bundleMd5); + } else { + $bundleMd5 = file_get_contents($bundlePath . '.md5'); + } $event->addDependency( 'bundle', 1, - PROJECT_ROOT . '/modules/bundle.min.js', - $bundleSize, - md5_file($bundlePath), - false, + 'assets/bundle.min.js', + filesize($bundlePath), + $bundleMd5, + true, -1 ); } @@ -58,10 +78,8 @@ public function onDependencyRequest(XmdsDependencyRequestEvent $event) { // Can we return this type of file? if ($event->getFileType() === 'bundle' && $event->getRealId() == 1) { - // Yes! - // we only set a full path as this file not available over HTTP (it can't be because it isn't stored - // under the library folder). - $event->setFullPath(PROJECT_ROOT . '/modules/bundle.min.js'); + // Set the path + $event->setRelativePathToLibrary('assets/bundle.min.js'); // No need to carry on, we've found it. $event->stopPropagation(); diff --git a/lib/Xmds/Listeners/XmdsPlayerVersionListener.php b/lib/Xmds/Listeners/XmdsPlayerVersionListener.php index 95b91088bf..d745d4eaa5 100644 --- a/lib/Xmds/Listeners/XmdsPlayerVersionListener.php +++ b/lib/Xmds/Listeners/XmdsPlayerVersionListener.php @@ -89,7 +89,7 @@ public function onDependencyRequest(XmdsDependencyRequestEvent $event) if ($event->getFileType() === 'playersoftware') { $version = $this->playerVersionFactory->getById($event->getRealId()); - $event->setRelativePathToLibrary('/playersoftware/' . $version->fileName); + $event->setRelativePathToLibrary('playersoftware/' . $version->fileName); } } } diff --git a/lib/Xmds/Soap.php b/lib/Xmds/Soap.php index c6a11707fd..c1bf6286fb 100644 --- a/lib/Xmds/Soap.php +++ b/lib/Xmds/Soap.php @@ -910,6 +910,8 @@ protected function doRequiredFiles( continue; } + $asset->updateAssetCache($libraryLocation); + // Add a new required file for this. try { $this->addDependency( @@ -945,6 +947,8 @@ protected function doRequiredFiles( continue; } + $asset->updateAssetCache($libraryLocation); + // Add a new required file for this. try { $this->addDependency( @@ -2738,9 +2742,10 @@ private function addDependency( ->createForGetDependency( $this->display->displayId, $dependency->fileType, - $isSupportsDependency ? $dependency->id : $dependency->legacyId, + $dependency->legacyId, $dependency->id, $dependencyBasePath, + $dependency->size, $isSupportsDependency ) ->save()->rfId; diff --git a/lib/Xmds/Soap3.php b/lib/Xmds/Soap3.php index 6c23c15934..f71c6fc320 100644 --- a/lib/Xmds/Soap3.php +++ b/lib/Xmds/Soap3.php @@ -1,6 +1,6 @@ . - * */ namespace Xibo\Xmds; @@ -231,11 +230,14 @@ public function GetFile($serverKey, $hardwareKey, $filePath, $fileType, $chunkOf $event = new XmdsDependencyRequestEvent($requiredFile); $this->getDispatcher()->dispatch($event, 'xmds.dependency.request'); - $path = $event->getFullPath(); + // Get the path + $path = $event->getRelativePath(); if (empty($path)) { throw new NotFoundException(__('File not found')); } + $path = $libraryLocation . $path; + $f = fopen($path, 'r'); } diff --git a/lib/Xmds/Soap4.php b/lib/Xmds/Soap4.php index cd6b27262e..60d77dde7d 100644 --- a/lib/Xmds/Soap4.php +++ b/lib/Xmds/Soap4.php @@ -378,21 +378,14 @@ function GetFile($serverKey, $hardwareKey, $fileId, $fileType, $chunkOffset, $ch $event = new XmdsDependencyRequestEvent($requiredFile); $this->getDispatcher()->dispatch($event, 'xmds.dependency.request'); - // Return either using the full path (for items not cached to the library) - // or the relative path for those that are. - $path = $event->getFullPath(); + // Get the path + $path = $event->getRelativePath(); if (empty($path)) { - // Try the relative path - $path = $event->getRelativePath(); - - if (empty($path)) { - // We've tried both options, so we fail here. - throw new NotFoundException(__('File not found')); - } - - $path = $libraryLocation . $path; + throw new NotFoundException(__('File not found')); } + $path = $libraryLocation . $path; + $f = fopen($path, 'r'); if (!$f) { throw new NotFoundException(__('Unable to get file pointer')); diff --git a/modules/README.md b/modules/README.md index 897403de63..e584a50b73 100644 --- a/modules/README.md +++ b/modules/README.md @@ -1,7 +1,2 @@ -# /modules -This folder contains resources used by core modules. - -The root folder contains `twig` templates for rendering the add/edit/settings and layout page javascript. It also -contains a JSON file for any module which will be installed optionally. - -Sub folders are used here for templates. \ No newline at end of file +# Core modules, templates and data types +https://xibosignage.com/docs/developer/widgets/creating-a-module diff --git a/modules/mastodon.xml b/modules/mastodon.xml index 29e5d70827..608dca7e2e 100644 --- a/modules/mastodon.xml +++ b/modules/mastodon.xml @@ -189,9 +189,9 @@ $(target).xiboLayoutAnimate(properties); - - - - + + + + diff --git a/modules/templates/social-media-static.xml b/modules/templates/social-media-static.xml index 048f56f877..013dedc8e4 100644 --- a/modules/templates/social-media-static.xml +++ b/modules/templates/social-media-static.xml @@ -2654,12 +2654,12 @@ $metroRenderContainer.xiboMetroRender(properties, itemsHTML, colors); $metroRenderContainer.find('.cell').xiboImageRender(properties); ]]> - - - - - - + + + + + + diff --git a/modules/twitter.xml b/modules/twitter.xml deleted file mode 100644 index 41d796bf0c..0000000000 --- a/modules/twitter.xml +++ /dev/null @@ -1,187 +0,0 @@ - - - core-twitter - Twitter - Core - Twitter - - \Xibo\Widget\Compatibility\SocialMediaWidgetCompatibility - twitter - social-media - %searchTerm%_%language%_%resultType%_%numItems% - 2 - 1 - 1 - html - 60 - - - - Search Term - Search term. You can test your search term in the twitter.com search box first. - - - - - - - - - Language - Language in which tweets should be returned - - - Count - The number of Tweets to return (default = 15). - 15 - - - Type - Recent shows only the most recent tweets, Popular the most popular and Mixed includes both popular and recent results. - 1 - - - - - - Fit supported from: - - - Date Format - The format to apply to all dates returned by the Widget. - #DATE_FORMAT# - - - Distance - Distance in miles that the tweets should be returned from. Set to 0 for no restrictions. - - - - Duration is per item - The duration specified is per item otherwise it is per feed. - 0 - - - Remove Mentions? - Should mentions (@someone) be removed from the Tweet Text? - 0 - - - Remove Hashtags? - Should Hashtags (#something) be removed from the Tweet Text? - 0 - - - Remove URLs? - Should URLs be removed from the Tweet Text? Most URLs do not compliment digital signage. - 1 - - - - - - - - - - - - - - - diff --git a/package-lock.json b/package-lock.json index 30c1fac95d..781c629714 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8029,9 +8029,9 @@ "dev": true }, "hls.js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.2.1.tgz", - "integrity": "sha512-+m/5+ikSpmQQvb6FmVWZUZfzvTJMn/QVfiCGP1Oq9WW4RKrAvxlExkhhbcVGgGqLNPFk1kdFkVQur//wKu3JVw==" + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.4.10.tgz", + "integrity": "sha512-wAVSj4Fm2MqOHy5+BlYnlKxXvJlv5IuZHjlzHu18QmjRzSDFQiUDWdHs5+NsFMQrgKEBwuWDcyvaMC9dUzJ5Uw==" }, "home-or-tmp": { "version": "2.0.0", diff --git a/package.json b/package.json index 18394d9cd6..57e6fa0949 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "font-awesome": "~4.7.0", "form-serializer": "~2.5.0", "handlebars": "^4.7.7", - "hls.js": "^1.1.5", + "hls.js": "^1.4.10", "html-to-image": "^1.7.0", "imagesloaded": "^4.1.4", "jquery": "^3.5.1", diff --git a/tasks/player-css.task b/tasks/player-css.task deleted file mode 100644 index 7a48d504e7..0000000000 --- a/tasks/player-css.task +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Generate Player font css", - "class": "\\Xibo\\XTR\\GeneratePlayerCssTask", - "options": [] -} \ No newline at end of file diff --git a/tests/http-client.env.json b/tests/http-client.env.json index 9ebc50e278..8a5fb49553 100644 --- a/tests/http-client.env.json +++ b/tests/http-client.env.json @@ -1,7 +1,7 @@ { "dev": { "url": "http://localhost", - "serverKey": "test", + "serverKey": "6v4RduQhaw5Q", "hardwareKey": "phpstorm", "displayName": "PHPStorm", "clientType": "windows", diff --git a/views/layout-page.twig b/views/layout-page.twig index 6edde8007d..cf506596eb 100644 --- a/views/layout-page.twig +++ b/views/layout-page.twig @@ -435,284 +435,6 @@ }); }); - function layoutAddFormOpen(dialog) { - // Form - var $form = $('#layoutAddForm'); - - // Popovers - $(dialog).find('[data-toggle="popover"]').popover(); - - // Stepper - var navListItems = $(dialog).find('div.stepper-nav div a'), - allWells = $(dialog).find('.stepper-panel'), - stepWizard = $(dialog).find('.stepwizard'); - - navListItems.click(function (e) { - e.preventDefault(); - var $target = $($(this).attr('href')), - $item = $(this); - - if (!$item.attr('disabled')) { - // Set all step links to inactive - navListItems - .removeClass('btn-success') - .addClass('btn-default'); - - // Activate this specific one - $item.addClass('btn-success'); - - // Hide all the panels and show this specific one - allWells.hide(); - $target.show(); - $target.find('input:eq(0)').focus(); - - // Set the active panel on the links - stepWizard.data('active', $target.prop('id')) - - // Is the next action to finish? - if ($target.data('next') === 'finished') { - $(dialog).find('#layout-create-stepper-next-button').html("{{ "Save"|trans }}"); - } else { - $(dialog).find('#layout-create-stepper-next-button').html("{{ "Next"|trans }}") - } - } - }); - - // Add some buttons. - $(dialog). - find('.modal-footer'). - append( - $('').html("{{ "Close"|trans }}").click(function(e) { - e.preventDefault(); - XiboDialogClose(); - })). - append($(''). - html("{{ "Next"|trans }}"). - click(function(e) { - e.preventDefault(); - var steps = $(dialog).find('.stepwizard'), - curStep = $(dialog).find('#' + steps.data('active')), - curInputs = curStep.find('input[type=\'text\'],input[type=\'url\']'), - isValid = true; - - // What is the next step? - if (curStep.data('next') === 'finished') { - // add a progress spinning to the button - var $button = $(this); - $button.append(' '); - $button.addClass("disabled"); - - // Submit the form thereby creating the layout - $('#layoutAddForm').trigger('submit'); - } else { - var nextStepWizard = steps.find('a[href=\'#' + curStep.data('next') + '\']'); - - $(dialog).find('.form-group').removeClass('has-error'); - for (var i = 0; i < curInputs.length; i++) { - if (!curInputs[i].validity.valid) { - isValid = false; - $(curInputs[i]).closest('.form-group').addClass('has-error'); - } - } - - if (isValid) { - nextStepWizard.removeAttr('disabled').trigger('click'); - } - } - })); - - // Card handling - // Get our template ready to roll. - var cardsTemplate = Handlebars.compile($('#template-layout-add-template-cards').html()); - var cardColumn = $(dialog).find('#layout-add-templates'); - - // Initialise masonary - we do this after we've added our first card. - var masonry; - masonry = new Masonry('#layout-add-templates', { - percentPosition: true - }); - - // filter form - var $filter = $('#layout-add-templates-filter'); - var filter = { - start: 0, - length: 15, - }; - filter = $.extend(filter, $filter.serializeObject()); - - // track start/length - $filter.on('change', function() { - // Clear everything. - masonry.remove(cardColumn.find('.template-card')); - - // Run a new query. - filter = $.extend(filter, $filter.serializeObject()); - - if (!filter.template && (filter.provider && (filter.provider === 'both' || filter.provider === 'local'))) { - loadPredefined(cardsTemplate, cardColumn, masonry, filter); - } - - loadTemplates(cardsTemplate, cardColumn, masonry, filter, 'both'); - }); - - if (filter.provider && (filter.provider === 'both' || filter.provider === 'local')) { - loadPredefined(cardsTemplate, cardColumn, masonry, filter); - } - - cardColumn.imagesLoaded(function() { - // All images loaded - // Make a request to get our templates. - loadTemplates(cardsTemplate, cardColumn, masonry, filter, 'both'); - }).progress(function() { - // Layout the image card we've loaded - masonry.layout(); - }); - - // Add the more button - $(dialog).find('#layout-add-templates-more').on('click', function(e) { - e.preventDefault(); - filter.start = filter.start + filter.length; - loadTemplates(cardsTemplate, cardColumn, masonry, filter, 'both'); - }); - - // Folder selector. - if ($('#folder-tree-form-modal').length === 0) { - // compile tree folder modal and append it to Form - var folderTreeModal = Handlebars.compile($('#folder-tree-template').html()); - var treeConfig = {"container": "container-folder-form-tree", "modal": "folder-tree-form-modal"}; - $("body").append(folderTreeModal(treeConfig)); - - $("#folder-tree-form-modal").on('hidden.bs.modal', function() { - // Fix for 2nd/overlay modal - $('.modal:visible').length && $(document.body).addClass('modal-open'); - - $(this).data('bs.modal', null); - }); - } - - // select current working folder if one is selected in the grid - if ($('#container-folder-tree').jstree("get_selected", true)[0] !== undefined) { - $('#layoutAddForm' + ' #folderId').val($('#container-folder-tree').jstree("get_selected", true)[0].id); - } - - initJsTreeAjax($('#folder-tree-form-modal').find('#container-folder-form-tree'), 'layoutAddForm', true, 600); - - // Add a submit handler - $('#layoutAddForm').submit(function(e) { - e.preventDefault(); - var $form = $(this); - var url = $(this).data().redirect; - XiboFormSubmit($form, null, function(xhr, form) { - // Remove the cogs/disabled. - var $saveButton = $(this).find('.saving'); - $saveButton.parent().removeClass('disabled'); - $saveButton.find('.saving').remove(); - - if (xhr.success) { - // Reload the designer - XiboRedirect(url.replace(":id", xhr.id)); - } - }); - }); - } - - function loadPredefined(cardsTemplate, cardColumn, masonry, filter) { - // Add full screen, l-bar-right and l-bar-left - masonry.addItems(addTemplateCard(cardsTemplate, cardColumn, { - title: '{{ "Full screen"|trans }}', - description: '{{ "Full screen content"|trans }}', - thumbnail: '{{ theme.rootUri() }}theme/default/img/layout_grids/full-screen.png', - source: 'local', - id: '0|full-screen' - })); - masonry.addItems(addTemplateCard(cardsTemplate, cardColumn, { - title: '{{ "L shape left"|trans }}', - description: '{{ "3 regions in an L shape, on the left"|trans }}', - thumbnail: '{{ theme.rootUri() }}theme/default/img/layout_grids/l-bar-left.png', - source: 'local', - id: '0|l-bar-left' - })); - masonry.addItems(addTemplateCard(cardsTemplate, cardColumn, { - title: '{{ "L shape right"|trans }}', - description: '{{ "3 regions in an L shape, on the right"|trans }}', - thumbnail: '{{ theme.rootUri() }}theme/default/img/layout_grids/l-bar-right.png', - source: 'local', - id: '0|l-bar-right' - })); - - // Add our blank resolution - masonry.addItems(addTemplateCard(cardsTemplate, cardColumn, { - title: '{{ "Blank"|trans }}', - description: '{{ "Add your own regions using the editor"|trans }}', - thumbnail: null, - source: 'local', - id: '0|blank' - })); - } - - function loadTemplates(cardsTemplate, cardColumn, masonry, filter) { - var spinner = cardColumn.closest('.modal').find('.panel-footer .spinner-grow'); - var moreButton = cardColumn.closest('.modal').find('#layout-add-templates-more'); - spinner.removeClass('d-none'); - moreButton.prop('disabled', true); - $.ajax({ - method: 'GET', - url: '{{ url_for("template.search.all") }}', - data: filter, - success: function(response) { - if (response && response.data && response.data.length > 0) { - $.each(response.data, function(index, el) { - masonry.addItems(addTemplateCard(cardsTemplate, cardColumn, el)); - }); - - moreButton.prop('disabled', false); - } else { - toastr.info('{{ "There are no more templates to show"|trans }}'); - } - - cardColumn.imagesLoaded().progress(function () { - masonry.layout(); - }); - - spinner.addClass('d-none'); - } - }); - } - - function addTemplateCard(cardsTemplate, cardColumn, el) { - el.showFooter = el.orientation || (el.provider && el.provider.logoUrl) || (el.tags && el.tags.length > 0); - el.thumbnail = el.thumbnail || '{{ theme.rootUri() }}theme/default/img/thumbs/placeholder.png'; - var $element = $(cardsTemplate(el)); - $element.find('.card').on('click', function(e) { - e.preventDefault(); - // Remove all selections. - cardColumn.find('.border-success').removeClass('border-success'); - - // Select this one. - var $that = $(this); - $that.addClass('border-success'); - $('#layout-create-stepper-next-button').removeClass('disabled'); - - // If source is local and layoutId is 0, then show the resolution filter otherwise don't - var layoutId = $(this).data('layout-id') + ""; - var source = $(this).data('source'); - var download = $(this).data('download'); - - var $form = $('#layoutAddForm'); - $form.find('input[name="layoutId"]').val(layoutId); - $form.find('input[name="source"]').val(source); - $form.find('input[name="download"]').val(download); - - if (layoutId.startsWith("0|")) { - $form.find('.resolution-group').removeClass('d-none'); - } else { - $form.find('.resolution-group').addClass('d-none'); - } - }); - cardColumn.append($element); - return $element; - } - function layoutExportFormSubmit() { var $form = $("#layoutExportForm"); window.location = $form.attr("action") + "?" + $form.serialize(); diff --git a/web/xmds.php b/web/xmds.php index babb9ccaae..ffd9e363e2 100755 --- a/web/xmds.php +++ b/web/xmds.php @@ -173,6 +173,11 @@ throw new \Xibo\Support\Exception\InstanceSuspendedException('Bandwidth Exceeded'); } + // Bandwidth + // Add the size to the bytes we have already requested. + $file->bytesRequested = $file->bytesRequested + $file->size; + $file->save(); + // Issue magic packet $libraryLocation = $container->get('configService')->getSetting('LIBRARY_LOCATION'); $logger->info('HTTP GetFile request redirecting to ' . $libraryLocation . $file->path); @@ -187,11 +192,6 @@ header('HTTP/1.0 404 Not Found'); } - // Bandwidth - // Add the size to the bytes we have already requested. - $file->bytesRequested = $file->bytesRequested + $file->size; - $file->save(); - // Also add to the overall bandwidth used by get file $container->get('bandwidthFactory')->createAndSave( \Xibo\Entity\Bandwidth::$GETFILE,