diff --git a/bin/locale.php b/bin/locale.php index c35dadda4b..fb49be7732 100644 --- a/bin/locale.php +++ b/bin/locale.php @@ -36,7 +36,6 @@ $twig->addExtension(new \Slim\Views\TwigExtension()); $twig->addExtension(new \Xibo\Twig\TransExtension()); $twig->addExtension(new \Xibo\Twig\ByteFormatterTwigExtension()); -$twig->addExtension(new \Xibo\Twig\UrlDecodeTwigExtension()); $twig->addExtension(new \Xibo\Twig\DateFormatTwigExtension()); diff --git a/lib/Controller/Campaign.php b/lib/Controller/Campaign.php index 01032d246e..f68293e590 100644 --- a/lib/Controller/Campaign.php +++ b/lib/Controller/Campaign.php @@ -888,18 +888,20 @@ public function preview(Request $request, Response $response, $id) $duration = 0 ; $extendedLayouts = []; - foreach($layouts as $layout) + foreach ($layouts as $layout) { $duration += $layout->duration; - $extendedLayouts[] = ['layout' => $layout, - 'duration' => $layout->duration, - 'previewOptions' => [ - 'getXlfUrl' => $this->urlFor($request,'layout.getXlf', ['id' => $layout->layoutId]), - 'getResourceUrl' => $this->urlFor($request,'module.getResource', ['regionId' => ':regionId', 'id' => ':id']), - 'libraryDownloadUrl' => $this->urlFor($request,'library.download'), - 'layoutBackgroundDownloadUrl' => $this->urlFor($request,'layout.download.background', ['id' => ':id']), - 'loaderUrl' => $this->getConfig()->uri('img/loader.gif')] - ]; + $extendedLayouts[] = [ + 'layout' => $layout, + 'duration' => $layout->duration, + 'previewOptions' => [ + 'getXlfUrl' => $this->urlFor($request,'layout.getXlf', ['id' => $layout->layoutId]), + 'getResourceUrl' => $this->urlFor($request,'module.getResource', ['regionId' => ':regionId', 'id' => ':id']), + 'libraryDownloadUrl' => $this->urlFor($request,'library.download'), + 'layoutBackgroundDownloadUrl' => $this->urlFor($request,'layout.download.background', ['id' => ':id']), + 'loaderUrl' => $this->getConfig()->uri('img/loader.gif') + ] + ]; } $this->getState()->template = 'campaign-preview'; $this->getState()->setData([ diff --git a/lib/Controller/Display.php b/lib/Controller/Display.php index 9b4b3de90b..368cb1d48d 100644 --- a/lib/Controller/Display.php +++ b/lib/Controller/Display.php @@ -2225,4 +2225,47 @@ public function checkLicence(Request $request, Response $response, $id) return $this->render($request, $response); } + + /** + * @SWG\Get( + * path="/display/status/{id}", + * operationId="displayStatus", + * tags={"display"}, + * summary="Display Status", + * description="Get the display status window for this Display.", + * @SWG\Parameter( + * name="id", + * in="path", + * description="Display Id", + * type="integer", + * required=true + * ), + * @SWG\Response( + * response=200, + * description="successful operation", + * @SWG\Schema( + * type="array", + * @SWG\Items(type="string") + * ) + * ) + * ) + * + * @param Request $request + * @param Response $response + * @param int $id displayId + * @return \Psr\Http\Message\ResponseInterface|Response + * @throws \Xibo\Support\Exception\AccessDeniedException + * @throws \Xibo\Support\Exception\InvalidArgumentException + * @throws \Xibo\Support\Exception\NotFoundException + */ + public function statusWindow(Request $request, Response $response, $id) + { + $display = $this->displayFactory->getById($id); + + if (!$this->getUser()->checkViewable($display)) { + throw new AccessDeniedException(); + } + + return $response->withJson($display->getStatusWindow($this->pool)); + } } diff --git a/lib/Controller/Module.php b/lib/Controller/Module.php index 6168dbb46c..7a000b7a10 100644 --- a/lib/Controller/Module.php +++ b/lib/Controller/Module.php @@ -1327,18 +1327,18 @@ public function getResource(Request $request, Response $response, $regionId, $id if ($module->getModule()->regionSpecific == 0) { // Non region specific module - no caching required as this is only ever called via preview. - $resource = $module->download($request, $response); + $response = $module->download($request, $response); } else { // Region-specific module, need to handle caching and locking. $resource = $module->getResourceOrCache(); + + if (!empty($resource)) { + $response->getBody()->write($resource); + } } $this->setNoOutput(true); - if (!empty($resource)) { - $response->getBody()->write($resource); - } - return $this->render($request, $response); } diff --git a/lib/Entity/Display.php b/lib/Entity/Display.php index 84a6d6dffe..de73d75c4d 100644 --- a/lib/Entity/Display.php +++ b/lib/Entity/Display.php @@ -1109,4 +1109,38 @@ public function setCurrentScreenShotTime($pool, $date) return $this; } + + /** + * @param PoolInterface $pool + * @return array + */ + public function getStatusWindow($pool) + { + $item = $pool->getItem('/statusWindow/' . $this->displayId); + + if ($item->isMiss()) { + return []; + } else { + return $item->get(); + } + } + + /** + * @param PoolInterface $pool + * @param array $status + * @return $this + */ + public function setStatusWindow($pool, $status) + { + // Cache it + $this->getLog()->debug('Caching statusWindow with Pool'); + + $item = $pool->getItem('/statusWindow/' . $this->displayId); + $item->set($status); + $item->expiresAfter(new \DateInterval('P1D')); + + $pool->saveDeferred($item); + + return $this; + } } diff --git a/lib/Entity/Module.php b/lib/Entity/Module.php index 7d75e39b82..e10759da8f 100644 --- a/lib/Entity/Module.php +++ b/lib/Entity/Module.php @@ -105,7 +105,7 @@ class Module implements \JsonSerializable public $defaultDuration; /** - * @SWG\Property(description="An array of additional module specific settings") + * @SWG\Property(description="An array of additional module specific settings", type="array", @SWG\Items(type="string")) * @var array */ public $settings = []; diff --git a/lib/Entity/Schedule.php b/lib/Entity/Schedule.php index 89fde35b62..cf35c0fd9c 100644 --- a/lib/Entity/Schedule.php +++ b/lib/Entity/Schedule.php @@ -808,6 +808,9 @@ public function getEvents($fromDt, $toDt) $this->getLog()->debug('The main event has a start and end date within the month, no need to pull it in from the prior month. [eventId:' . $this->eventId . ']'); } + // Keep a cache of schedule exclusions, so we look them up by eventId only one time per event + $scheduleExclusions = $this->scheduleExclusionFactory->query(null, ['eventId' => $this->eventId]); + // Request month cache while ($fromDt < $toDt) { @@ -823,8 +826,6 @@ public function getEvents($fromDt, $toDt) foreach ($this->scheduleEvents as $scheduleEvent) { // Find the excluded recurring events - $scheduleExclusions = $this->scheduleExclusionFactory->query(null, ['eventId' => $this->eventId]); - $exclude = false; foreach ($scheduleExclusions as $exclusion) { if ($scheduleEvent->fromDt == $exclusion->fromDt && @@ -857,6 +858,9 @@ public function getEvents($fromDt, $toDt) $fromDt->addMonth(); } + // Clear our cache of schedule exclusions + $scheduleExclusions = null; + $this->getLog()->debug('Filtered ' . count($this->scheduleEvents) . ' to ' . count($events) . ', events: ' . json_encode($events, JSON_PRETTY_PRINT)); return $events; diff --git a/lib/Entity/User.php b/lib/Entity/User.php index 2e306af7e1..f0ab153cc7 100644 --- a/lib/Entity/User.php +++ b/lib/Entity/User.php @@ -1202,9 +1202,10 @@ public function routeViewable($route) if ($this->isSuperAdmin()) return true; - // All users have access to the logout page - if ($route === '/logout') + // All users have access to the logout page and welcome page + if ($route === '/logout' || $route === '/welcome') { return true; + } try { if ($this->pagePermissionCache == null) { diff --git a/lib/Factory/ContainerFactory.php b/lib/Factory/ContainerFactory.php index 7d29a4fcbe..105343c695 100644 --- a/lib/Factory/ContainerFactory.php +++ b/lib/Factory/ContainerFactory.php @@ -109,7 +109,6 @@ public static function create() ]); $view->addExtension(new TransExtension()); $view->addExtension(new ByteFormatterTwigExtension()); - $view->addExtension(new UrlDecodeTwigExtension()); $view->addExtension(new DateFormatTwigExtension()); return $view; @@ -122,16 +121,14 @@ public static function create() }, 'timeSeriesStore' => function(ContainerInterface $c) { if ($c->get('configService')->timeSeriesStore == null) { - return (new MySqlTimeSeriesStore()) - ->setDependencies($c->get('logService'), - $c->get('layoutFactory'), - $c->get('campaignFactory')) - ->setStore($c->get('store')); + $timeSeriesStore = new MySqlTimeSeriesStore(); } else { $timeSeriesStore = $c->get('configService')->timeSeriesStore; $timeSeriesStore = $timeSeriesStore(); + } - return $timeSeriesStore->setDependencies( + return $timeSeriesStore + ->setDependencies( $c->get('logService'), $c->get('layoutFactory'), $c->get('campaignFactory'), @@ -139,8 +136,8 @@ public static function create() $c->get('widgetFactory'), $c->get('displayFactory'), $c->get('displayGroupFactory') - ); - } + ) + ->setStore($c->get('store')); }, 'state' => function() { return new ApplicationState(); diff --git a/lib/Storage/MongoDbTimeSeriesStore.php b/lib/Storage/MongoDbTimeSeriesStore.php index 58f252f520..c9666473d3 100644 --- a/lib/Storage/MongoDbTimeSeriesStore.php +++ b/lib/Storage/MongoDbTimeSeriesStore.php @@ -88,40 +88,60 @@ class MongoDbTimeSeriesStore implements TimeSeriesStoreInterface */ public function __construct($config = null) { - $this->config = $config; } /** * @inheritdoc */ - public function setDependencies($log, $layoutFactory = null, $campaignFactory = null, $mediaFactory = null, $widgetFactory = null, $displayFactory = null, $displayGroupFactory = null) + public function setDependencies($log, $layoutFactory, $campaignFactory, $mediaFactory, $widgetFactory, $displayFactory, $displayGroupFactory) { $this->log = $log; + $this->layoutFactory = $layoutFactory; + $this->campaignFactory = $campaignFactory; $this->mediaFactory = $mediaFactory; $this->widgetFactory = $widgetFactory; - $this->layoutFactory = $layoutFactory; $this->displayFactory = $displayFactory; $this->displayGroupFactory = $displayGroupFactory; - $this->campaignFactory = $campaignFactory; + return $this; + } - try { - $uri = isset($this->config['uri']) ? $this->config['uri'] : 'mongodb://' . $this->config['host'] . ':' . $this->config['port']; - $this->client = new Client($uri, [ - 'username' => $this->config['username'], - 'password' => $this->config['password'] - ], (array_key_exists('driverOptions', $this->config) ? $this->config['driverOptions'] : [])); - } catch (\MongoDB\Exception\RuntimeException $e) { - $this->log->critical($e->getMessage()); + /** + * Set Client in the event you want to completely replace the configuration options and roll your own client. + * @param \MongoDB\Client $client + */ + public function setClient($client) + { + $this->client = $client; + } + + /** + * Get a MongoDB client to use. + * @return \MongoDB\Client + * @throws \Xibo\Support\Exception\GeneralException + */ + private function getClient() + { + if ($this->client === null) { + try { + $uri = isset($this->config['uri']) ? $this->config['uri'] : 'mongodb://' . $this->config['host'] . ':' . $this->config['port']; + $this->client = new Client($uri, [ + 'username' => $this->config['username'], + 'password' => $this->config['password'] + ], (array_key_exists('driverOptions', $this->config) ? $this->config['driverOptions'] : [])); + } catch (\MongoDB\Exception\RuntimeException $e) { + $this->log->error('Unable to connect to MongoDB: ' . $e->getMessage()); + $this->log->debug($e->getTraceAsString()); + throw new GeneralException('Connection to Time Series Database failed, please try again.'); + } } - return $this; + return $this->client; } /** @inheritdoc */ public function addStat($statData) { - // We need to transform string date to UTC date $statData['statDate'] = new UTCDateTime($statData['statDate']->format('U') * 1000); @@ -358,12 +378,14 @@ public function addStat($statData) } - /** @inheritdoc */ + /** @inheritdoc + * @throws \Xibo\Support\Exception\GeneralException + */ public function addStatFinalize() { // Insert statistics if (count($this->stats) > 0) { - $collection = $this->client->selectCollection($this->config['database'], $this->table); + $collection = $this->getClient()->selectCollection($this->config['database'], $this->table); try { $collection->insertMany($this->stats); @@ -375,7 +397,7 @@ public function addStatFinalize() } // Create a period collection if it doesnot exist - $collectionPeriod = $this->client->selectCollection($this->config['database'], $this->periodTable); + $collectionPeriod = $this->getClient()->selectCollection($this->config['database'], $this->periodTable); try { $cursor = $collectionPeriod->findOne(['name' => 'period']); @@ -397,7 +419,7 @@ public function addStatFinalize() /** @inheritdoc */ public function getEarliestDate() { - $collection = $this->client->selectCollection($this->config['database'], $this->table); + $collection = $this->getClient()->selectCollection($this->config['database'], $this->table); try { $earliestDate = $collection->aggregate([ [ @@ -521,7 +543,7 @@ public function getStats($filterBy = []) $campaignIds[] = $this->layoutFactory->getCampaignIdFromLayoutHistory($layoutId); } catch (NotFoundException $notFoundException) { // Ignore the missing one - $this->getLog()->debug('Filter for Layout without Layout History Record, layoutId is ' . $layoutId); + $this->log->debug('Filter for Layout without Layout History Record, layoutId is ' . $layoutId); } } $match['$match']['campaignId'] = [ '$in' => $campaignIds ]; @@ -532,7 +554,7 @@ public function getStats($filterBy = []) $match['$match']['mediaId'] = [ '$in' => $mediaIds ]; } - $collection = $this->client->selectCollection($this->config['database'], $this->table); + $collection = $this->getClient()->selectCollection($this->config['database'], $this->table); $group = [ '$group' => [ @@ -559,9 +581,6 @@ public function getStats($filterBy = []) $totalCount = $totalCursor->toArray(); $total = (count($totalCount) > 0) ? $totalCount[0]['count'] : 0; - } catch (\MongoDB\Exception\RuntimeException $e) { - $this->log->error('Error: Total Count. '. $e->getMessage()); - throw new GeneralException(__('Sorry we encountered an error getting Proof of Play data, please consult your administrator')); } catch (\Exception $e) { $this->log->error('Error: Total Count. '. $e->getMessage()); throw new GeneralException(__('Sorry we encountered an error getting Proof of Play data, please consult your administrator')); @@ -619,9 +638,6 @@ public function getStats($filterBy = []) // Total $result->totalCount = $total; - } catch (\MongoDB\Exception\RuntimeException $e) { - $this->log->error('Error: Get total. '. $e->getMessage()); - throw new GeneralException(__('Sorry we encountered an error getting Proof of Play data, please consult your administrator')); } catch (\Exception $e) { $this->log->error('Error: Get total. '. $e->getMessage()); throw new GeneralException(__('Sorry we encountered an error getting Proof of Play data, please consult your administrator')); @@ -655,7 +671,7 @@ public function getExportStatsCount($filterBy = []) $match['$match']['displayId'] = [ '$in' => $displayIds ]; } - $collection = $this->client->selectCollection($this->config['database'], $this->table); + $collection = $this->getClient()->selectCollection($this->config['database'], $this->table); // Get total try { @@ -673,9 +689,6 @@ public function getExportStatsCount($filterBy = []) $totalCount = $totalCursor->toArray(); $total = (count($totalCount) > 0) ? $totalCount[0]['count'] : 0; - } catch (\MongoDB\Exception\RuntimeException $e) { - $this->log->error($e->getMessage()); - throw new GeneralException(__('Sorry we encountered an error getting total number of Proof of Play data, please consult your administrator')); } catch (\Exception $e) { $this->log->error($e->getMessage()); throw new GeneralException(__('Sorry we encountered an error getting total number of Proof of Play data, please consult your administrator')); @@ -699,7 +712,7 @@ public function deleteStats($maxage, $fromDt = null, $options = []) $toDt = new UTCDateTime($maxage->format('U')*1000); - $collection = $this->client->selectCollection($this->config['database'], $this->table); + $collection = $this->getClient()->selectCollection($this->config['database'], $this->table); $rows = 1; $count = 0; @@ -764,7 +777,7 @@ public function executeQuery($options = []) $aggregateConfig['maxTimeMS']= $options['maxTimeMS']; } - $collection = $this->client->selectCollection($this->config['database'], $options['collection']); + $collection = $this->getClient()->selectCollection($this->config['database'], $options['collection']); try { $cursor = $collection->aggregate($options['query'], $aggregateConfig); @@ -775,10 +788,10 @@ public function executeQuery($options = []) } catch (\MongoDB\Driver\Exception\RuntimeException $e) { $this->log->error($e->getMessage()); + $this->log->debug($e->getTraceAsString()); throw new GeneralException($e->getMessage()); } return $results; - } } \ No newline at end of file diff --git a/lib/Storage/MySqlTimeSeriesStore.php b/lib/Storage/MySqlTimeSeriesStore.php index 93b827fbc9..720c51aea2 100644 --- a/lib/Storage/MySqlTimeSeriesStore.php +++ b/lib/Storage/MySqlTimeSeriesStore.php @@ -62,7 +62,7 @@ public function __construct($config = null) /** * @inheritdoc */ - public function setDependencies($log, $layoutFactory = null, $campaignFactory = null, $mediaFactory = null, $widgetFactory = null, $displayFactory = null) + public function setDependencies($log, $layoutFactory, $campaignFactory, $mediaFactory, $widgetFactory, $displayFactory, $displayGroupFactory) { $this->log = $log; $this->layoutFactory = $layoutFactory; @@ -424,9 +424,7 @@ public function getExportStatsCount($filterBy = []) $resTotal = $this->store->select($sql, $params); // Total - $totalCount = isset($resTotal[0]['total']) ? $resTotal[0]['total'] : 0; - - return $totalCount; + return isset($resTotal[0]['total']) ? $resTotal[0]['total'] : 0; } /** @inheritdoc */ diff --git a/lib/Storage/TimeSeriesStoreInterface.php b/lib/Storage/TimeSeriesStoreInterface.php index 79e171b92e..997ca1303b 100644 --- a/lib/Storage/TimeSeriesStoreInterface.php +++ b/lib/Storage/TimeSeriesStoreInterface.php @@ -46,13 +46,22 @@ public function __construct($config = null); /** * Set Time series Dependencies * @param LogServiceInterface $logger + * @param LayoutFactory $layoutFactory + * @param CampaignFactory $campaignFactory * @param MediaFactory $mediaFactory * @param WidgetFactory $widgetFactory - * @param LayoutFactory $layoutFactory * @param DisplayFactory $displayFactory - * @param CampaignFactory $campaignFactory + * @param \Xibo\Entity\DisplayGroup $displayGroupFactory */ - public function setDependencies($logger, $layoutFactory = null, $campaignFactory = null, $mediaFactory = null, $widgetFactory = null, $displayFactory = null); + public function setDependencies( + $logger, + $layoutFactory, + $campaignFactory, + $mediaFactory, + $widgetFactory, + $displayFactory, + $displayGroupFactory + ); /** * Process and add a single statdata to array diff --git a/lib/Twig/ByteFormatterTwigExtension.php b/lib/Twig/ByteFormatterTwigExtension.php index 04b8dc5d16..4d828c42cb 100644 --- a/lib/Twig/ByteFormatterTwigExtension.php +++ b/lib/Twig/ByteFormatterTwigExtension.php @@ -1,16 +1,33 @@ . */ - - namespace Xibo\Twig; - -use Xibo\Helper\ByteFormatter; use Twig\Extension\AbstractExtension; +use Xibo\Helper\ByteFormatter; + +/** + * Class ByteFormatterTwigExtension + * @package Xibo\Twig + */ class ByteFormatterTwigExtension extends AbstractExtension { public function getName() diff --git a/lib/Twig/DateFormatTwigExtension.php b/lib/Twig/DateFormatTwigExtension.php index fbf1e15799..a9faeb3eeb 100644 --- a/lib/Twig/DateFormatTwigExtension.php +++ b/lib/Twig/DateFormatTwigExtension.php @@ -1,16 +1,33 @@ . */ - namespace Xibo\Twig; - use Twig\Extension\AbstractExtension; +/** + * Class DateFormatTwigExtension + * @package Xibo\Twig + */ class DateFormatTwigExtension extends AbstractExtension { /** @@ -19,7 +36,7 @@ class DateFormatTwigExtension extends AbstractExtension public function getFilters() { return array( - 'datehms' => new \Twig\TwigFilter('dateFormat', $this) + new \Twig\TwigFilter('datehms', [$this, 'dateFormat']) ); } @@ -30,7 +47,7 @@ public function getFilters() * * @return string formated as HH:mm:ss */ - public function dateFormat( $date ) + public function dateFormat($date) { return gmdate('H:i:s', $date); } diff --git a/lib/Twig/UrlDecodeTwigExtension.php b/lib/Twig/UrlDecodeTwigExtension.php deleted file mode 100644 index a003eb937b..0000000000 --- a/lib/Twig/UrlDecodeTwigExtension.php +++ /dev/null @@ -1,47 +0,0 @@ - new \Twig\TwigFilter('urlDecode', $this) - ); - } - - /** - * URL Decode a string - * - * @param string $url - * - * @return string The decoded URL - */ - public function urlDecode( $url ) - { - return urldecode( $url ); - } - - /** - * Returns the name of the extension. - * - * @return string The extension name - */ - public function getName() - { - return 'url_decode'; - } -} \ No newline at end of file diff --git a/lib/Widget/Weather.php b/lib/Widget/Weather.php index 6f4d52e4c5..7966aca3d7 100644 --- a/lib/Widget/Weather.php +++ b/lib/Widget/Weather.php @@ -244,11 +244,11 @@ public function settings(Request $request, Response $response): Response * Edit Widget * * @SWG\Put( - * path="/playlist/widget/{widgetId}?weather", - * operationId="WidgetWeatherEdit", + * path="/playlist/widget/{widgetId}?weatherTiles", + * operationId="WidgetWeatherTilesEdit", * tags={"widget"}, - * summary="Edit Weather Widget", - * description="Edit Weather Widget. This call will replace existing Widget object, all not supplied parameters will be set to default.", + * summary="Edit Weather Tiles Widget", + * description="Edit Weather Tiles Widget. This call will replace existing Widget object, all not supplied parameters will be set to default.", * @SWG\Parameter( * name="widgetId", * in="path", diff --git a/lib/Xmds/Soap4.php b/lib/Xmds/Soap4.php index 56a8cf9727..9d7f5d2a8a 100644 --- a/lib/Xmds/Soap4.php +++ b/lib/Xmds/Soap4.php @@ -591,7 +591,15 @@ public function NotifyStatus($serverKey, $hardwareKey, $status) $statusDialog = $sanitizedStatus->getString('statusDialog', ['default' => null]); if ($statusDialog !== null) { + // Log in as an alert $this->getLog()->alert($statusDialog); + + // Cache on the display as transient data + try { + $this->display->setStatusWindow($this->getPool(), json_decode($statusDialog, true)); + } catch (\Exception $exception) { + $this->getLog()->error('Unable to cache display status. e = ' . $exception->getMessage()); + } } // Resolution diff --git a/lib/Xmds/Soap5.php b/lib/Xmds/Soap5.php index 11870a619e..295534ae2a 100644 --- a/lib/Xmds/Soap5.php +++ b/lib/Xmds/Soap5.php @@ -34,6 +34,10 @@ use Xibo\Support\Exception\GeneralException; use Xibo\Support\Exception\NotFoundException; +/** + * Class Soap5 + * @package Xibo\Xmds + */ class Soap5 extends Soap4 { /** diff --git a/lib/routes.php b/lib/routes.php index f5de753b7f..b0ef4de81c 100644 --- a/lib/routes.php +++ b/lib/routes.php @@ -268,6 +268,7 @@ $app->put('/display/requestscreenshot/{id}', ['\Xibo\Controller\Display','requestScreenShot'])->setName('display.requestscreenshot'); $app->put('/display/licenceCheck/{id}', ['\Xibo\Controller\Display','checkLicence'])->setName('display.licencecheck'); $app->get('/display/screenshot/{id}', ['\Xibo\Controller\Display','screenShot'])->setName('display.screenShot'); +$app->get('/display/status/{id}', ['\Xibo\Controller\Display','statusWindow'])->setName('display.statusWindow'); $app->post('/display/{id}/displaygroup/assign', ['\Xibo\Controller\Display','assignDisplayGroup'])->setName('display.assign.displayGroup'); $app->put('/display/{id}/moveCms', ['\Xibo\Controller\Display','moveCms'])->setName('display.moveCms'); $app->post('/display/addViaCode', ['\Xibo\Controller\Display','addViaCode'])->setName('display.addViaCode'); diff --git a/views/campaign-preview.twig b/views/campaign-preview.twig index 5690b69738..e555cc7ba2 100644 --- a/views/campaign-preview.twig +++ b/views/campaign-preview.twig @@ -78,7 +78,7 @@ {% autoescape "js" %} previewTranslations.actionControllerTitle = "{{ "Action Controller"|trans }}"; previewTranslations.navigateToLayout = "{{ "Navigate to layout with code [layoutTag]?"|trans }}"; - previewTranslations.emptyRegionMessage = "{{% "Empty Region"|trans }}"; + previewTranslations.emptyRegionMessage = "{{ "Empty Region"|trans }}"; {% endautoescape %} (function($){ diff --git a/web/swagger.json b/web/swagger.json index 15b27647c1..c8193a1ba8 100644 --- a/web/swagger.json +++ b/web/swagger.json @@ -21,6 +21,205 @@ "application/json" ], "paths": { + "/action": { + "get": { + "tags": [ + "action" + ], + "summary": "Search Actions", + "description": "Search all Actions this user has access to", + "operationId": "actionSearch", + "parameters": [ + { + "name": "actionId", + "in": "query", + "description": "Filter by Action Id", + "required": false, + "type": "integer" + }, + { + "name": "ownerId", + "in": "query", + "description": "Filter by Owner Id", + "required": false, + "type": "integer" + }, + { + "name": "triggerType", + "in": "query", + "description": "Filter by Action trigger type", + "required": false, + "type": "string" + }, + { + "name": "triggerCode", + "in": "query", + "description": "Filter by Action trigger code", + "required": false, + "type": "string" + }, + { + "name": "actionType", + "in": "query", + "description": "Filter by Action type", + "required": false, + "type": "string" + }, + { + "name": "source", + "in": "query", + "description": "Filter by Action source", + "required": false, + "type": "string" + }, + { + "name": "sourceId", + "in": "query", + "description": "Filter by Action source Id", + "required": false, + "type": "integer" + }, + { + "name": "target", + "in": "query", + "description": "Filter by Action target", + "required": false, + "type": "string" + }, + { + "name": "targetId", + "in": "query", + "description": "Filter by Action target Id", + "required": false, + "type": "integer" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Action" + } + } + } + } + } + }, + "/action/{source}/{sourceId}": { + "post": { + "tags": [ + "action" + ], + "summary": "Add Action", + "description": "Add a new Action", + "operationId": "actionAdd", + "parameters": [ + { + "name": "source", + "in": "path", + "description": "Source for this action layout, region or widget", + "required": true, + "type": "string" + }, + { + "name": "sourceId", + "in": "path", + "description": "The id of the source object, layoutId, regionId or widgetId", + "required": true, + "type": "integer" + }, + { + "name": "triggerType", + "in": "formData", + "description": "Action trigger type, touch or webhook", + "required": true, + "type": "string" + }, + { + "name": "triggerCode", + "in": "formData", + "description": "Action trigger code", + "required": true, + "type": "string" + }, + { + "name": "actionType", + "in": "formData", + "description": "Action type, next, previous, navLayout, navWidget", + "required": true, + "type": "string" + }, + { + "name": "target", + "in": "formData", + "description": "Target for this action, screen or region", + "required": true, + "type": "string" + }, + { + "name": "targetId", + "in": "formData", + "description": "The id of the target for this action - regionId if the target is set to region", + "required": false, + "type": "string" + }, + { + "name": "widgetId", + "in": "formData", + "description": "For navWidget actionType, the WidgetId to navigate to", + "required": false, + "type": "integer" + }, + { + "name": "layoutCode", + "in": "formData", + "description": "For navLayout, the Layout Code identifier to navigate to", + "required": false, + "type": "string" + } + ], + "responses": { + "201": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Action" + }, + "headers": { + "Location": { + "description": "Location of the new record", + "type": "string" + } + } + } + } + } + }, + "/action/{actionId}": { + "delete": { + "tags": [ + "action" + ], + "summary": "Delete Action", + "description": "Delete an existing Action", + "operationId": "actionDelete", + "parameters": [ + { + "name": "actionId", + "in": "path", + "description": "The Action ID to Delete", + "required": true, + "type": "integer" + } + ], + "responses": { + "204": { + "description": "successful operation" + } + } + } + }, "/campaign": { "get": { "tags": [ @@ -324,6 +523,27 @@ "description": "A unique code for this command", "required": true, "type": "string" + }, + { + "name": "commandString", + "in": "formData", + "description": "The Command String for this Command. Can be overridden on Display Settings.", + "required": false, + "type": "string" + }, + { + "name": "validationString", + "in": "formData", + "description": "The Validation String for this Command. Can be overridden on Display Settings.", + "required": false, + "type": "string" + }, + { + "name": "availableOn", + "in": "formData", + "description": "An array of Player types this Command is available on, empty for all.", + "required": false, + "type": "string" } ], "responses": { @@ -371,6 +591,27 @@ "description": "A description for the command", "required": false, "type": "string" + }, + { + "name": "commandString", + "in": "formData", + "description": "The Command String for this Command. Can be overridden on Display Settings.", + "required": false, + "type": "string" + }, + { + "name": "validationString", + "in": "formData", + "description": "The Validation String for this Command. Can be overridden on Display Settings.", + "required": false, + "type": "string" + }, + { + "name": "availableOn", + "in": "formData", + "description": "An array of Player types this Command is available on, empty for all.", + "required": false, + "type": "string" } ], "responses": { @@ -581,6 +822,34 @@ "description": "Which field should be used to summarize", "required": false, "type": "string" + }, + { + "name": "sourceId", + "in": "formData", + "description": "For remote DataSet, what type data is it? 1 - json, 2 - csv", + "required": false, + "type": "integer" + }, + { + "name": "ignoreFirstRow", + "in": "formData", + "description": "For remote DataSet with sourceId 2 (CSV), should we ignore first row?", + "required": false, + "type": "integer" + }, + { + "name": "rowLimit", + "in": "formData", + "description": "For remote DataSet, maximum number of rows this DataSet can hold, if left empty the CMS Setting for DataSet row limit will be used.", + "required": false, + "type": "integer" + }, + { + "name": "limitPolicy", + "in": "formData", + "description": "For remote DataSet, what should happen when the DataSet row limit is reached? stop, fifo or truncate", + "required": false, + "type": "string" } ], "responses": { @@ -733,6 +1002,34 @@ "description": "Which field should be used to summarize", "required": false, "type": "string" + }, + { + "name": "sourceId", + "in": "formData", + "description": "For remote DataSet, what type data is it? 1 - json, 2 - csv", + "required": false, + "type": "integer" + }, + { + "name": "ignoreFirstRow", + "in": "formData", + "description": "For remote DataSet with sourceId 2 (CSV), should we ignore first row?", + "required": false, + "type": "integer" + }, + { + "name": "rowLimit", + "in": "formData", + "description": "For remote DataSet, maximum number of rows this DataSet can hold, if left empty the CMS Setting for DataSet row limit will be used.", + "required": false, + "type": "integer" + }, + { + "name": "limitPolicy", + "in": "formData", + "description": "For remote DataSet, what should happen when the DataSet row limit is reached? stop, fifo or truncate", + "required": false, + "type": "string" } ], "responses": { @@ -2011,6 +2308,20 @@ "description": "Clear the cached XMR configuration and send a rekey", "required": false, "type": "integer" + }, + { + "name": "teamViewerSerial", + "in": "formData", + "description": "The TeamViewer serial number for this Display, if applicable", + "required": false, + "type": "string" + }, + { + "name": "webkeySerial", + "in": "formData", + "description": "The Webkey serial number for this Display, if applicable", + "required": false, + "type": "string" } ], "responses": { @@ -2175,6 +2486,36 @@ } } }, + "/display/status/{id}": { + "get": { + "tags": [ + "display" + ], + "summary": "Display Status", + "description": "Get the display status window for this Display.", + "operationId": "displayStatus", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Display Id", + "required": true, + "type": "integer" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, "/displaygroup": { "get": { "tags": [ @@ -3333,6 +3674,20 @@ "description": "If a Template is not provided, provide the resolutionId for this Layout.", "required": false, "type": "integer" + }, + { + "name": "returnDraft", + "in": "formData", + "description": "Should we return the Draft Layout or the Published Layout on Success?", + "required": false, + "type": "boolean" + }, + { + "name": "code", + "in": "formData", + "description": "Code identifier for this Layout", + "required": false, + "type": "string" } ], "responses": { @@ -3400,6 +3755,13 @@ "description": "Flag indicating whether the Layout stat is enabled", "required": false, "type": "integer" + }, + { + "name": "code", + "in": "formData", + "description": "Code identifier for this Layout", + "required": false, + "type": "string" } ], "responses": { @@ -5671,6 +6033,39 @@ } } }, + "/region/drawer/{id}": { + "post": { + "tags": [ + "layout" + ], + "summary": "Add drawer Region", + "description": "Add a drawer Region to a Layout", + "operationId": "regionDrawerAdd", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The Layout ID to add the Region to", + "required": true, + "type": "integer" + } + ], + "responses": { + "201": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Region" + }, + "headers": { + "Location": { + "description": "Location of the new record", + "type": "string" + } + } + } + } + } + }, "/resolution": { "get": { "tags": [ @@ -6302,7 +6697,7 @@ { "name": "type", "in": "query", - "description": "The type of stat to return. Layout|Media|Widget or All", + "description": "The type of stat to return. Layout|Media|Widget", "required": false, "type": "string" }, @@ -6341,43 +6736,111 @@ "required": false, "type": "integer" }, + { + "name": "displayIds", + "in": "query", + "description": "An optional array of display Id to filter", + "required": false, + "type": "array", + "items": { + "type": "integer" + } + }, { "name": "layoutId", "in": "query", "description": "An optional array of layout Id to filter", "required": false, - "type": "array", - "items": { - "type": "integer" - } + "type": "array", + "items": { + "type": "integer" + } + }, + { + "name": "mediaId", + "in": "query", + "description": "An optional array of media Id to filter", + "required": false, + "type": "array", + "items": { + "type": "integer" + } + }, + { + "name": "campaignId", + "in": "query", + "description": "An optional Campaign Id to filter", + "required": false, + "type": "integer" + }, + { + "name": "returnDisplayLocalTime", + "in": "query", + "description": "true/1/On if the results should be in display local time, otherwise CMS time", + "required": false, + "type": "boolean" + }, + { + "name": "returnDateFormat", + "in": "query", + "description": "A PHP formatted date format for how the dates in this call should be returned.", + "required": false, + "type": "string" + }, + { + "name": "embed", + "in": "query", + "description": "Should the return embed additional data, options are layoutTags,displayTags and mediaTags", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/StatisticsData" + } + } + } + } + } + }, + "/stats/getExportStatsCount": { + "get": { + "tags": [ + "statistics" + ], + "summary": "Total count of stats", + "operationId": "getExportStatsCount", + "parameters": [ + { + "name": "fromDt", + "in": "query", + "description": "The start date for the filter. Default = 24 hours ago", + "required": false, + "type": "string" }, { - "name": "mediaId", + "name": "toDt", "in": "query", - "description": "An optional array of media Id to filter", + "description": "The end date for the filter. Default = now.", "required": false, - "type": "array", - "items": { - "type": "integer" - } + "type": "string" }, { - "name": "campaignId", + "name": "displayId", "in": "query", - "description": "An optional Campaign Id to filter", + "description": "An optional display Id to filter", "required": false, "type": "integer" } ], "responses": { "200": { - "description": "successful operation", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/StatisticsData" - } - } + "description": "successful operation" } } } @@ -9354,7 +9817,7 @@ { "name": "widgetId", "in": "path", - "description": "The Widget ID", + "description": "The WidgetId ID", "required": true, "type": "integer" }, @@ -9694,6 +10157,20 @@ "required": false, "type": "string" }, + { + "name": "globalCommand", + "in": "formData", + "description": "Enter a global command line compatible with multiple devices", + "required": false, + "type": "string" + }, + { + "name": "androidCommand", + "in": "formData", + "description": "Enter a Android command line compatible command", + "required": false, + "type": "string" + }, { "name": "windowsCommand", "in": "formData", @@ -9704,7 +10181,7 @@ { "name": "linuxCommand", "in": "formData", - "description": "Enter a Android / Linux command line compatible command", + "description": "Enter a Linux command line compatible command", "required": false, "type": "string" }, @@ -10926,6 +11403,159 @@ } } }, + "/playlist/widget/{widgetId}?weatherTiles": { + "put": { + "tags": [ + "widget" + ], + "summary": "Edit Weather Tiles Widget", + "description": "Edit Weather Tiles Widget. This call will replace existing Widget object, all not supplied parameters will be set to default.", + "operationId": "WidgetWeatherTilesEdit", + "parameters": [ + { + "name": "widgetId", + "in": "path", + "description": "The WidgetId to Edit", + "required": true, + "type": "integer" + }, + { + "name": "name", + "in": "formData", + "description": "Optional Widget Name", + "required": false, + "type": "string" + }, + { + "name": "duration", + "in": "formData", + "description": "Widget Duration", + "required": false, + "type": "integer" + }, + { + "name": "useDuration", + "in": "formData", + "description": "(0, 1) Select 1 only if you will provide duration parameter as well", + "required": false, + "type": "integer" + }, + { + "name": "enableStat", + "in": "formData", + "description": "The option (On, Off, Inherit) to enable the collection of Widget Proof of Play statistics", + "required": false, + "type": "string" + }, + { + "name": "useDisplayLocation", + "in": "formData", + "description": "Flag (0, 1) Use the location configured on display", + "required": true, + "type": "integer" + }, + { + "name": "longitude", + "in": "formData", + "description": "The longitude for this weather widget, only pass if useDisplayLocation set to 0", + "required": false, + "type": "number" + }, + { + "name": "latitude", + "in": "formData", + "description": "The latitude for this weather widget, only pass if useDisplayLocation set to 0", + "required": false, + "type": "number" + }, + { + "name": "templateId", + "in": "formData", + "description": "Use pre-configured templates, available options: weather-module0-5day, weather-module0-singleday, weather-module0-singleday2, weather-module1l, weather-module1p, weather-module2l, weather-module2p, weather-module3l, weather-module3p, weather-module4l, weather-module4p, weather-module5l, weather-module6v, weather-module6h", + "required": false, + "type": "string" + }, + { + "name": "units", + "in": "formData", + "description": "Units you would like to use, available options: auto, ca, si, uk2, us", + "required": false, + "type": "string" + }, + { + "name": "updateInterval", + "in": "formData", + "description": "Update interval in minutes, should be kept as high as possible, if data change once per hour, this should be set to 60", + "required": false, + "type": "integer" + }, + { + "name": "lang", + "in": "formData", + "description": "Language you'd like to use, supported languages ar, az, be, bs, cs, de, en, el, es, fr, hr, hu, id, it, is, kw, nb, nl, pl, pt, ru, sk, sr, sv, tet, tr, uk, x-pig-latin, zh, zh-tw", + "required": false, + "type": "string" + }, + { + "name": "dayConditionsOnly", + "in": "formData", + "description": "Flag (0, 1) Would you like to only show the Daytime weather conditions", + "required": false, + "type": "integer" + }, + { + "name": "overrideTemplate", + "in": "formData", + "description": "flag (0, 1) set to 0 and use templateId or set to 1 and provide whole template in the next parameters", + "required": false, + "type": "integer" + }, + { + "name": "widgetOriginalWidth", + "in": "formData", + "description": "This is the intended Width of the template and is used to scale the Widget within it's region when the template is applied, Pass only with overrideTemplate set to 1", + "required": false, + "type": "integer" + }, + { + "name": "widgetOriginalHeight", + "in": "formData", + "description": "This is the intended Height of the template and is used to scale the Widget within it's region when the template is applied, Pass only with overrideTemplate set to 1", + "required": false, + "type": "integer" + }, + { + "name": "template", + "in": "formData", + "description": "Current template, Pass only with overrideTemplate set to 1 ", + "required": false, + "type": "string" + }, + { + "name": "styleSheet", + "in": "formData", + "description": "Optional StyleSheet, Pass only with overrideTemplate set to 1 ", + "required": false, + "type": "string" + }, + { + "name": "styleSheet", + "in": "formData", + "description": "Optional JavaScript, Pass only with overrideTemplate set to 1 ", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Widget" + } + } + } + } + }, "/playlist/widget/{widgetId}?webpage": { "put": { "tags": [ @@ -11172,18 +11802,15 @@ "widgetId": { "type": "integer" }, + "scheduleId": { + "type": "integer" + }, "numberPlays": { "type": "integer" }, "duration": { "type": "integer" }, - "minStart": { - "type": "string" - }, - "maxEnd": { - "type": "string" - }, "start": { "type": "string" }, @@ -11220,6 +11847,54 @@ } } }, + "Action": { + "properties": { + "actionId": { + "description": "The Action Id", + "type": "integer" + }, + "ownerId": { + "description": "The Owner Id", + "type": "integer" + }, + "triggerType": { + "description": "The Action trigger type", + "type": "string" + }, + "triggerCode": { + "description": "The Action trigger code", + "type": "string" + }, + "actionType": { + "description": "The Action type", + "type": "string" + }, + "source": { + "description": "The Action source (layout, region or widget)", + "type": "string" + }, + "sourceId": { + "description": "The Action source Id (layoutId, regionId or widgetId)", + "type": "integer" + }, + "target": { + "description": "The Action target (region)", + "type": "string" + }, + "targetId": { + "description": "The Action target Id (regionId)", + "type": "integer" + }, + "widgetId": { + "description": "Widget ID that will be loaded as a result of navigate to Widget Action type", + "type": "integer" + }, + "layoutCode": { + "description": "Layout Code identifier", + "type": "string" + } + } + }, "Application": { "properties": { "key": { @@ -11253,6 +11928,10 @@ "clientCredentials": { "description": "Flag indicating whether to allow the clientCredentials Grant Type", "type": "integer" + }, + "isConfidential": { + "description": "Flag indicating whether this Application will be confidential or not (can it keep a secret?)", + "type": "integer" } } }, @@ -11343,11 +12022,27 @@ "type": "integer" }, "commandString": { - "description": "Command String - when child of a Display Profile", + "description": "Command String", "type": "string" }, "validationString": { - "description": "Validation String - when child of a Display Profile", + "description": "Validation String", + "type": "string" + }, + "displayProfileId": { + "description": "DisplayProfileId if specific to a Display Profile", + "type": "integer" + }, + "commandStringDisplayProfile": { + "description": "Command String specific to the provided DisplayProfile", + "type": "string" + }, + "validationStringDisplayProfile": { + "description": "Validation String specific to the provided DisplayProfile", + "type": "string" + }, + "availableOn": { + "description": "A comma separated list of player types this command is available on", "type": "string" }, "groupsWithPermissions": { @@ -11465,6 +12160,14 @@ "ignoreFirstRow": { "description": "A flag whether to ignore the first row, for CSV source remote dataSet", "type": "integer" + }, + "rowLimit": { + "description": "Soft limit on number of rows per DataSet, if left empty the global DataSet row limit will be used.", + "type": "integer" + }, + "limitPolicy": { + "description": "Type of action that should be taken on next remote DataSet sync - stop, fifo or truncate", + "type": "string" } } }, @@ -11773,6 +12476,14 @@ "commercialLicence": { "description": "Status of the commercial licence for this Display. 0 - Not licensed, 1 - licensed, 2 - trial licence, 3 - not applicable", "type": "integer" + }, + "teamViewerSerial": { + "description": "The TeamViewer serial number for this Display", + "type": "string" + }, + "webkeySerial": { + "description": "The Webkey serial number for this Display", + "type": "string" } } }, @@ -11981,6 +12692,10 @@ "description": "Flag indicating whether the default transitions should be applied to this Layout", "type": "integer" }, + "code": { + "description": "Code identifier for this Layout", + "type": "string" + }, "regions": { "description": "An array of Regions belonging to this Layout", "type": "array", @@ -12522,6 +13237,10 @@ "description": "A read-only estimate of this Regions's total duration in seconds. This is valid when the parent layout status is 1 or 2.", "type": "integer" }, + "isDrawer": { + "description": "Flag, whether this region is used as an interactive drawer attached to a layout.", + "type": "integer" + }, "regionPlaylist": { "description": "This Regions Playlist - null if getPlaylist() has not been called.", "$ref": "#/definitions/Playlist" @@ -12645,6 +13364,10 @@ "storedAs": { "description": "Stored As", "type": "string" + }, + "schemaVersion": { + "description": "Schema Version", + "type": "integer" } } }, @@ -12760,7 +13483,7 @@ "type": "integer" }, "shareOfVoice": { - "description": "Percentage (0-100) of each full hour that is scheduled that this Layout should occupy", + "description": "Seconds (0-3600) of each full hour that is scheduled that this Layout should occupy", "type": "integer" }, "isGeoAware": { @@ -13333,7 +14056,77 @@ } } }, + "parameters": { + "actionId": { + "name": "actionId", + "in": "path", + "description": "Action ID to edit", + "required": true, + "type": "integer" + }, + "triggerType": { + "name": "triggerType", + "in": "formData", + "description": "Action trigger type, touch, webhook", + "required": true, + "type": "string" + }, + "triggerCode": { + "name": "triggerCode", + "in": "formData", + "description": "Action trigger code", + "required": false, + "type": "string" + }, + "actionType": { + "name": "actionType", + "in": "formData", + "description": "Action type, next, previous, navLayout, navWidget", + "required": true, + "type": "string" + }, + "target": { + "name": "target", + "in": "formData", + "description": "Target for this action, screen or region", + "required": true, + "type": "string" + }, + "targetId": { + "name": "targetId", + "in": "formData", + "description": "The id of the target for this action, regionId if target set to region", + "required": false, + "type": "integer" + }, + "widgetId": { + "name": "widgetId", + "in": "formData", + "description": "For navWidget actionType, the WidgetId to navigate to", + "required": false, + "type": "integer" + }, + "layoutCode": { + "name": "layoutCode", + "in": "formData", + "description": "For navLayout, the Layout Code identifier to navigate to", + "required": false, + "type": "string" + } + }, "responses": { + "201": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Action" + }, + "headers": { + "Location": { + "description": "Location of the new record", + "type": "string" + } + } + }, "200": { "description": "successful operation" } @@ -13457,6 +14250,10 @@ { "name": "tags", "description": "Tags" + }, + { + "name": "actions", + "description": "Actions" } ], "externalDocs": {