21
21
namespace Xibo\Widget;
23
use Intervention\Image\ImageManagerStatic as Img;
26
use Stash\Interfaces\PoolInterface;
27
use Stash\Invalidation;
29
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
30
use Xibo\Entity\Media;
31
24
use Xibo\Entity\User;
32
25
use Xibo\Exception\ControllerNotImplemented;
33
use Xibo\Exception\InvalidArgumentException;
34
26
use Xibo\Exception\NotFoundException;
35
use Xibo\Exception\XiboException;
36
use Xibo\Factory\CommandFactory;
37
use Xibo\Factory\DataSetColumnFactory;
38
use Xibo\Factory\DataSetFactory;
39
use Xibo\Factory\DisplayFactory;
40
use Xibo\Factory\DisplayGroupFactory;
41
use Xibo\Factory\LayoutFactory;
42
27
use Xibo\Factory\MediaFactory;
43
use Xibo\Factory\ModuleFactory;
44
use Xibo\Factory\PermissionFactory;
45
use Xibo\Factory\ScheduleFactory;
46
28
use Xibo\Factory\TransitionFactory;
47
use Xibo\Factory\UserGroupFactory;
48
use Xibo\Factory\WidgetFactory;
49
use Xibo\Service\ConfigServiceInterface;
50
use Xibo\Service\DateServiceInterface;
51
use Xibo\Service\LogServiceInterface;
52
use Xibo\Service\SanitizerServiceInterface;
53
use Xibo\Storage\StorageServiceInterface;
29
use Xibo\Helper\Config;
31
use Xibo\Helper\Sanitize;
32
use Xibo\Helper\Theme;
56
35
* Class ModuleWidget
92
71
protected $codeSchemaVersion = -1;
95
* @var string A module populated status message set during isValid.
97
protected $statusMessage;
99
/** @var string|null Cache Key Prefix */
100
private $cacheKeyPrefix = null;
103
// Injected Factory Classes and Services Follow
107
* @var StorageServiceInterface
117
* @var LogServiceInterface
122
* @var ConfigServiceInterface
124
private $configService;
127
* @var DateServiceInterface
129
private $dateService;
132
* @var SanitizerServiceInterface
134
private $sanitizerService;
136
/** @var EventDispatcherInterface */
139
/** @var ModuleFactory */
140
protected $moduleFactory;
145
protected $mediaFactory;
148
* @var DataSetFactory
150
protected $dataSetFactory;
153
* @var DataSetColumnFactory
155
protected $dataSetColumnFactory;
158
* @var TransitionFactory
160
protected $transitionFactory;
163
* @var DisplayFactory
165
protected $displayFactory;
168
* @var CommandFactory
170
protected $commandFactory;
172
/** @var LayoutFactory */
173
protected $layoutFactory;
175
/** @var WidgetFactory */
176
protected $widgetFactory;
178
/** @var DisplayGroupFactory */
179
protected $displayGroupFactory;
181
/** @var ScheduleFactory */
182
protected $scheduleFactory;
184
/** @var PermissionFactory */
185
protected $permissionFactory;
187
/** @var UserGroupFactory */
188
protected $userGroupFactory;
191
* ModuleWidget constructor.
193
* @param StorageServiceInterface $store
194
* @param PoolInterface $pool
195
* @param LogServiceInterface $log
196
* @param ConfigServiceInterface $config
197
* @param DateServiceInterface $date
198
* @param SanitizerServiceInterface $sanitizer
199
* @param EventDispatcherInterface $dispatcher
200
* @param ModuleFactory $moduleFactory
201
* @param MediaFactory $mediaFactory
202
* @param DataSetFactory $dataSetFactory
203
* @param DataSetColumnFactory $dataSetColumnFactory
204
* @param TransitionFactory $transitionFactory
205
* @param DisplayFactory $displayFactory
206
* @param CommandFactory $commandFactory
207
* @param ScheduleFactory $scheduleFactory
208
* @param PermissionFactory $permissionFactory
209
* @param UserGroupFactory $userGroupFactory
211
public function __construct($app, $store, $pool, $log, $config, $date, $sanitizer, $dispatcher, $moduleFactory, $mediaFactory, $dataSetFactory, $dataSetColumnFactory, $transitionFactory, $displayFactory, $commandFactory, $scheduleFactory, $permissionFactory, $userGroupFactory)
214
$this->store = $store;
216
$this->logService = $log;
217
$this->configService = $config;
218
$this->dateService = $date;
219
$this->sanitizerService = $sanitizer;
220
$this->dispatcher = $dispatcher;
222
$this->moduleFactory = $moduleFactory;
223
$this->mediaFactory = $mediaFactory;
224
$this->dataSetFactory = $dataSetFactory;
225
$this->dataSetColumnFactory = $dataSetColumnFactory;
226
$this->transitionFactory = $transitionFactory;
227
$this->displayFactory = $displayFactory;
228
$this->commandFactory = $commandFactory;
229
$this->scheduleFactory = $scheduleFactory;
230
$this->permissionFactory = $permissionFactory;
231
$this->userGroupFactory = $userGroupFactory;
237
* Set Child Object Dependencies
238
* @param LayoutFactory $layoutFactory
239
* @param WidgetFactory $widgetFactory
240
* @param DisplayGroupFactory $displayGroupFactory
242
public function setChildObjectDependencies($layoutFactory, $widgetFactory, $displayGroupFactory)
244
$this->layoutFactory = $layoutFactory;
245
$this->widgetFactory = $widgetFactory;
246
$this->displayGroupFactory = $displayGroupFactory;
74
* Create the controller
76
public function __construct()
78
$this->app = Slim::getInstance();
253
85
protected function getApp()
255
if ($this->app == null)
256
throw new \RuntimeException(__('Module Widget Application not set'));
258
87
return $this->app;
263
* @return \Stash\Interfaces\PoolInterface
265
protected function getPool()
272
* @return StorageServiceInterface
274
protected function getStore()
281
* @return LogServiceInterface
283
protected function getLog()
285
return $this->logService;
290
* @return ConfigServiceInterface
292
public function getConfig()
294
return $this->configService;
299
* @return DateServiceInterface
301
protected function getDate()
303
return $this->dateService;
308
* @return SanitizerServiceInterface
310
protected function getSanitizer()
312
return $this->sanitizerService;
318
protected function getDispatcher()
320
return $this->dispatcher;
324
// End of Injected Factories and Services
328
* Any initialisation code
330
public function init()
340
public function makeCacheKey($id)
342
return $this->getCacheKeyPrefix() . '/' . $id;
346
* Get the cache prefix, including the leading /
347
* @return null|string
349
private function getCacheKeyPrefix()
351
if ($this->cacheKeyPrefix == null) {
352
$className = get_class($this);
353
$this->cacheKeyPrefix = '/widget/' . substr($className, strrpos($className, '\\') + 1);
356
return $this->cacheKeyPrefix;
360
* Dump the cache for this module
362
public function dumpCacheForModule()
364
$this->getPool()->deleteItem($this->getCacheKeyPrefix());
367
// <editor-fold desc="request locking">
373
* Hold a lock on concurrent requests
374
* blocks if the request is locked
375
* @param string|null $key
376
* @param int $ttl seconds
377
* @param int $wait seconds
379
* @throws XiboException
381
public function concurrentRequestLock($key = null, $ttl = 900, $wait = 5, $tries = 100)
383
// If the key is null default to the widgetId
385
$key = $this->widget->widgetId;
387
$this->lock = $this->getPool()->getItem('locks/widget/' . $key);
389
// Set the invalidation method to simply return the value (not that we use it, but it gets us a miss on expiry)
390
$this->lock->setInvalidationMethod(Invalidation::VALUE);
393
// other requests will wait here until we're done, or we've timed out
396
// Did we get a lock?
397
// if we're a miss, then we're not already locked
398
if ($this->lock->isMiss()) {
400
$this->lock->set(true);
401
$this->lock->expiresAfter($ttl);
406
// We are a hit - we must be locked
407
$this->getLog()->debug('LOCK hit for ' . $key);
413
// We've waited long enough
414
throw new XiboException('Concurrent record locked, time out.');
416
$this->getLog()->debug('Unable to get a lock, trying again. Remaining retries: ' . $tries);
418
// Hang about waiting for the lock to be released.
421
// Recursive request (we've decremented the number of tries)
422
$this->concurrentRequestLock($key, $ttl, $wait, $tries);
428
* Release a lock on concurrent requests
430
public function concurrentRequestRelease()
432
if ($this->lock !== null) {
434
$this->lock->set(false);
435
$this->lock->expiresAfter(1); // Expire straight away
437
$this->getPool()->saveDeferred($this->lock);
445
92
* @param \Xibo\Entity\Widget $widget
837
* @param string|null $type
840
protected function getFileUrl($file, $type = null)
842
$isPreview = ($this->getSanitizer()->getCheckbox('preview') == 1);
843
$params = ['id' => $file->mediaId];
845
if ($type !== null) {
846
$params['type'] = $type;
850
return $this->getApp()->urlFor('library.download', $params) . '?preview=1"';
852
$url = $file->storedAs;
859
465
* Get Resource Url
860
* @param string $uri The file name
861
* @param string|null $type
864
protected function getResourceUrl($uri, $type = null)
469
protected function getResourceUrl($uri)
866
$isPreview = ($this->getSanitizer()->getCheckbox('preview') == 1);
868
// Local clients store all files in the root of the library
869
$uri = basename($uri);
872
// Use the URI to get this media record
874
$media = $this->mediaFactory->getByName($uri);
875
$params = ['id' => $media->mediaId];
877
if ($type !== null) {
878
$params['type'] = $type;
881
return $this->getApp()->urlFor('library.download', $params) . '?preview=1';
883
} catch (NotFoundException $notFoundException) {
884
$this->getLog()->info('Widget referencing a resource that doesnt exist: ' . $this->getModuleType() . ' for ' . $uri);
886
// Return a URL which will 404
471
$isPreview = (Sanitize::getCheckbox('preview') == 1);
474
$uri = $this->getApp()->rootUri . 'modules/' . $uri;
476
$uri = basename($uri);
1096
657
protected function download()
1098
$media = $this->mediaFactory->getById($this->getMediaId());
1100
$this->getLog()->debug('Download for mediaId ' . $media->mediaId);
1102
// Are we a preview or not?
1103
$isPreview = ($this->getSanitizer()->getCheckbox('preview') == 1);
1106
$libraryPath = $this->getConfig()->GetSetting('LIBRARY_LOCATION') . $media->storedAs;
1108
// Set the content length
1109
$headers = $this->getApp()->response()->headers();
1110
$headers->set('Content-Length', filesize($libraryPath));
1112
// Different behaviour depending on whether we are a preview or not.
1114
// correctly grab the MIME type of the file we want to serve
1115
$mimeTypes = new MimeTypes();
1116
$ext = explode('.', $media->storedAs);
1117
$headers->set('Content-Type', $mimeTypes->getMimeType($ext[count($ext) - 1]));
1119
// This widget is expected to output a file - usually this is for file based media
1120
// Get the name with library
1121
$attachmentName = $this->getSanitizer()->getString('attachment', $media->storedAs);
1123
// Issue some headers
1124
$this->getApp()->etag($media->md5);
1125
$this->getApp()->expires('+1 week');
1127
$headers->set('Content-Type', 'application/octet-stream');
1128
$headers->set('Content-Transfer-Encoding', 'Binary');
1129
$headers->set('Content-disposition', 'attachment; filename="' . $attachmentName . '"');
1133
if ($this->getConfig()->GetSetting('SENDFILE_MODE') == 'Apache') {
1134
// Send via Apache X-Sendfile header?
1135
$headers->set('X-Sendfile', $libraryPath);
1137
else if ($this->getConfig()->GetSetting('SENDFILE_MODE') == 'Nginx') {
1138
// Send via Nginx X-Accel-Redirect?
1139
$headers->set('X-Accel-Redirect', '/download/' . $media->storedAs);
659
$media = MediaFactory::getById($this->getMediaId());
661
// This widget is expected to output a file - usually this is for file based media
662
// Get the name with library
663
$libraryLocation = Config::GetSetting('LIBRARY_LOCATION');
664
$libraryPath = $libraryLocation . $media->storedAs;
665
$attachmentName = Sanitize::getString('attachment', $media->storedAs);
667
$size = filesize($libraryPath);
669
// Issue some headers
670
$this->getApp()->etag($media->md5);
671
$this->getApp()->expires('+1 week');
672
header('Content-Type: application/octet-stream');
673
header('Content-Transfer-Encoding: Binary');
674
header('Content-disposition: attachment; filename="' . $attachmentName . '"');
675
header('Content-Length: ' . $size);
677
// Send via Apache X-Sendfile header?
678
if (Config::GetSetting('SENDFILE_MODE') == 'Apache') {
679
header("X-Sendfile: $libraryPath");
681
// Send via Nginx X-Accel-Redirect?
682
else if (Config::GetSetting('SENDFILE_MODE') == 'Nginx') {
683
header("X-Accel-Redirect: /download/" . $attachmentName);
1142
686
// Return the file with PHP
1190
* Get templatesAvailable
1191
* @param bool $loadImage Should the image URL be loaded?
1194
public function templatesAvailable($loadImage = true)
1196
if (!isset($this->module->settings['templates'])) {
1198
$this->module->settings['templates'] = [];
1200
// Scan the folder for template files
1201
foreach (glob(PROJECT_ROOT . '/modules/' . $this->module->type . '/*.template.json') as $template) {
1202
// Read the contents, json_decode and add to the array
1203
$template = json_decode(file_get_contents($template), true);
1205
if (isset($template['image'])) {
1206
$template['fileName'] = $template['image'];
1209
// We ltrim this because the control is expecting a relative URL
1210
$template['image'] = ltrim($this->getApp()->urlFor('module.getTemplateImage', ['type' => $this->module->type, 'templateId' => $template['id']]), '/');
1213
$template['fileName'] = '';
1214
$template['image'] = '';
1217
$this->module->settings['templates'][] = $template;
1221
return $this->module->settings['templates'];
1225
* Get by Template Id
1226
* @param int $templateId
1227
* @return array|null
1229
public function getTemplateById($templateId)
1231
$templates = $this->templatesAvailable(false);
1234
if (count($templates) <= 0)
1237
foreach ($templates as $item) {
1238
if ($item['id'] == $templateId) {
1248
734
* Set template data
1249
735
* @param array $data
1258
* Download an image for this template
1259
* @param int $templateId
1260
* @throws NotFoundException
1262
public function getTemplateImage($templateId)
1264
$template = $this->getTemplateById($templateId);
1266
if ($template === null || !isset($template['fileName']) || $template['fileName'] == '')
1267
throw new NotFoundException();
1269
// Output the image associated with this template
1270
echo Img::make(PROJECT_ROOT . '/' . $template['fileName'])->response();
1274
744
* Determine duration
1275
745
* @param string|null $fileName
1278
748
public function determineDuration($fileName = null)
1280
return $this->getModule()->defaultDuration;
1284
754
* Pre-processing
1285
* this is run before the media item is created.
1286
755
* @param string|null $fileName
1288
public function preProcessFile($fileName = null)
1290
$this->getLog()->debug('No pre-processing rules for this module type');
1295
* this is run before the media item is saved
1296
* @param Media $media
1297
* @param string $filePath
1299
public function preProcess($media, $filePath) {
1305
* this is run after the media item has been created and before it is saved.
1306
* @param Media $media
1308
public function postProcess($media)
757
public function preProcess($fileName = null)
759
Log::debug('No pre-processing rules for this module type');
1316
765
public function setDefaultWidgetOptions()
1318
$this->getLog()->debug('Default Widget Options: Setting use duration to 0');
1319
$this->setUseDuration(0);
1323
* Get Status Message
1326
public function getStatusMessage()
1328
return $this->statusMessage;
1332
* Get the modified date of this Widget
1333
* @param int $displayId
1336
public function getModifiedTimestamp($displayId)
1341
//<editor-fold desc="GetResource Helpers">
1346
* Initialise getResource
1349
protected function initialiseGetResource()
1351
$this->data['isPreview'] = ($this->getSanitizer()->getCheckbox('preview') == 1);
1352
$this->data['javaScript'] = '';
1353
$this->data['styleSheet'] = '';
1354
$this->data['head'] = '';
1355
$this->data['body'] = '';
1356
$this->data['controlMeta'] = '';
1357
$this->data['options'] = '{}';
1358
$this->data['items'] = '{}';
1363
* @return bool Is Preview
1365
protected function isPreview()
1367
return $this->data['isPreview'];
1371
* Finalise getResource
1372
* @param string $templateName an optional template name
1373
* @return string the rendered template
1375
protected function finaliseGetResource($templateName = 'get-resource')
1377
$this->data['javaScript'] = '<script type="text/javascript">var options = ' . $this->data['options'] . '; var items = ' . $this->data['items'] . ';</script>' . PHP_EOL . $this->data['javaScript'];
1378
return $this->renderTemplate($this->data, $templateName);
1382
* Append the view port width - usually the region width
1386
protected function appendViewPortWidth($width)
1388
$this->data['viewPortWidth'] = ($this->data['isPreview']) ? $width : '[[ViewPortWidth]]';
1396
protected function appendFontCss()
1398
$this->data['styleSheet'] .= '<link href="' . (($this->isPreview()) ? $this->getApp()->urlFor('library.font.css') : 'fonts.css') . '" rel="stylesheet" media="screen" />' . PHP_EOL;
1404
* @param string $uri The URI, according to whether this is a CMS preview or not
1407
protected function appendCssFile($uri)
1409
$this->data['styleSheet'] .= '<link href="' . $this->getResourceUrl($uri) . '" rel="stylesheet" media="screen" />' . PHP_EOL;
1414
* Append CSS content
1415
* @param string $css
1418
protected function appendCss($css)
1421
if (stripos($css, '<style') !== false)
1422
$this->data['styleSheet'] .= $css . PHP_EOL;
1424
$this->data['styleSheet'] .= '<style type="text/css">' . $css . '</style>' . PHP_EOL;
1431
* Append JavaScript file
1432
* @param string $uri
1435
protected function appendJavaScriptFile($uri)
1437
$this->data['javaScript'] .= '<script type="text/javascript" src="' . $this->getResourceUrl($uri) . '"></script>' . PHP_EOL;
1443
* @param string $javasScript
1446
protected function appendJavaScript($javasScript)
1448
if (!empty($javasScript))
1449
$this->data['javaScript'] .= '<script type="text/javascript">' . $javasScript . '</script>' . PHP_EOL;
1456
* @param string $body
1459
protected function appendBody($body)
1462
$this->data['body'] .= $body . PHP_EOL;
1469
* @param array $options
1472
protected function appendOptions($options)
1474
$this->data['options'] = json_encode($options);
1480
* @param array $items
1483
protected function appendItems($items)
1485
$this->data['items'] = json_encode($items);
1491
* @param string $key
1492
* @param string $item
1495
protected function appendRaw($key, $item)
1497
$this->data[$key] .= $item . PHP_EOL;
767
Log::debug('No default options for this module type');