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#
-
-
-
- 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,