~xibo-maintainers/xibo/tempel

« back to all changes in this revision

Viewing changes to lib/Widget/Ticker.php

  • Committer: Dan Garner
  • Date: 2015-03-26 14:08:33 UTC
  • Revision ID: git-v1:70d14044444f8dc5d602b99890d59dea46d9470c
Moved web servable files to web folder

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
<?php
2
 
/*
3
 
 * Xibo - Digital Signage - http://www.xibo.org.uk
4
 
 * Copyright (C) 2006-2015 Daniel Garner
5
 
 *
6
 
 * This file is part of Xibo.
7
 
 *
8
 
 * Xibo is free software: you can redistribute it and/or modify
9
 
 * it under the terms of the GNU Affero General Public License as published by
10
 
 * the Free Software Foundation, either version 3 of the License, or
11
 
 * any later version. 
12
 
 *
13
 
 * Xibo is distributed in the hope that it will be useful,
14
 
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 
 * GNU Affero General Public License for more details.
17
 
 *
18
 
 * You should have received a copy of the GNU Affero General Public License
19
 
 * along with Xibo.  If not, see <http://www.gnu.org/licenses/>.
20
 
 */
21
 
namespace Xibo\Widget;
22
 
 
23
 
use GuzzleHttp\Client;
24
 
use GuzzleHttp\Exception\RequestException;
25
 
use PicoFeed\Config\Config;
26
 
use PicoFeed\Logging\Logger;
27
 
use PicoFeed\Parser\Item;
28
 
use PicoFeed\PicoFeedException;
29
 
use PicoFeed\Reader\Reader;
30
 
use Respect\Validation\Validator as v;
31
 
use Stash\Invalidation;
32
 
use Xibo\Controller\Library;
33
 
use Xibo\Entity\DataSetColumn;
34
 
use Xibo\Exception\NotFoundException;
35
 
use Xibo\Exception\XiboException;
36
 
use Xibo\Service\LogService;
37
 
 
38
 
 
39
 
class Ticker extends ModuleWidget
40
 
{
41
 
    /**
42
 
     * Install Files
43
 
     */
44
 
    public function installFiles()
45
 
    {
46
 
        $this->mediaFactory->createModuleSystemFile(PROJECT_ROOT . '/modules/vendor/jquery-1.11.1.min.js')->save();
47
 
        $this->mediaFactory->createModuleSystemFile(PROJECT_ROOT . '/modules/vendor/moment.js')->save();
48
 
        $this->mediaFactory->createModuleSystemFile(PROJECT_ROOT . '/modules/vendor/jquery.marquee.min.js')->save();
49
 
        $this->mediaFactory->createModuleSystemFile(PROJECT_ROOT . '/modules/vendor/jquery-cycle-2.1.6.min.js')->save();
50
 
        $this->mediaFactory->createModuleSystemFile(PROJECT_ROOT . '/modules/xibo-layout-scaler.js')->save();
51
 
        $this->mediaFactory->createModuleSystemFile(PROJECT_ROOT . '/modules/xibo-text-render.js')->save();
52
 
        $this->mediaFactory->createModuleSystemFile(PROJECT_ROOT . '/modules/xibo-image-render.js')->save();
53
 
    }
54
 
 
55
 
    /**
56
 
     * @return string
57
 
     */
58
 
    public function layoutDesignerJavaScript()
59
 
    {
60
 
        // We use the same javascript as the data set view designer
61
 
        return 'datasetview-designer-javascript';
62
 
    }
63
 
 
64
 
    /**
65
 
     * DataSets
66
 
     * @return array[DataSet]
67
 
     */
68
 
    public function dataSets()
69
 
    {
70
 
        return $this->dataSetFactory->query();
71
 
    }
72
 
 
73
 
    /**
74
 
     * Get Data Set Columns
75
 
     * @return array[DataSetColumn]
76
 
     */
77
 
    public function dataSetColumns()
78
 
    {
79
 
        if ($this->getOption('dataSetId') == 0)
80
 
            throw new \InvalidArgumentException(__('DataSet not selected'));
81
 
 
82
 
       return $this->dataSetColumnFactory->getByDataSetId($this->getOption('dataSetId'));
83
 
    }
84
 
 
85
 
    /**
86
 
     * Get the Order Clause
87
 
     * @return mixed
88
 
     */
89
 
    public function getOrderClause()
90
 
    {
91
 
        return json_decode($this->getOption('orderClauses', "[]"), true);
92
 
    }
93
 
 
94
 
    /**
95
 
     * Get the Filter Clause
96
 
     * @return mixed
97
 
     */
98
 
    public function getFilterClause()
99
 
    {
100
 
        return json_decode($this->getOption('filterClauses', "[]"), true);
101
 
    }
102
 
 
103
 
    /**
104
 
     * Get Extra content for the form
105
 
     * @return array
106
 
     */
107
 
    public function getExtra()
108
 
    {
109
 
        if ($this->getOption('sourceId') == 2) {
110
 
            return [
111
 
                'templates' => $this->templatesAvailable(),
112
 
                'orderClause' => $this->getOrderClause(),
113
 
                'filterClause' => $this->getFilterClause(),
114
 
                'columns' => $this->dataSetColumns(),
115
 
                'dataSet' => ($this->getOption('dataSetId', 0) != 0) ? $this->dataSetFactory->getById($this->getOption('dataSetId')) : null
116
 
            ];
117
 
        } else {
118
 
            return [
119
 
                'templates' => $this->templatesAvailable(),
120
 
            ];
121
 
        }
122
 
    }
123
 
 
124
 
    public function validate()
125
 
    {
126
 
        // Must have a duration
127
 
        if ($this->getUseDuration() == 1 && $this->getDuration() == 0)
128
 
            throw new \InvalidArgumentException(__('Please enter a duration'));
129
 
 
130
 
        $sourceId = $this->getOption('sourceId');
131
 
 
132
 
        if ($sourceId == 1) {
133
 
            // Feed
134
 
            // Validate the URL
135
 
            if (!v::url()->notEmpty()->validate(urldecode($this->getOption('uri'))))
136
 
                throw new \InvalidArgumentException(__('Please enter a Link for this Ticker'));
137
 
 
138
 
        } else if ($sourceId == 2) {
139
 
            // DataSet
140
 
            // Validate Data Set Selected
141
 
            if ($this->getOption('dataSetId') == 0)
142
 
                throw new \InvalidArgumentException(__('Please select a DataSet'));
143
 
 
144
 
            // Check we have permission to use this DataSetId
145
 
            if (!$this->getUser()->checkViewable($this->dataSetFactory->getById($this->getOption('dataSetId'))))
146
 
                throw new \InvalidArgumentException(__('You do not have permission to use that dataset'));
147
 
 
148
 
            if ($this->widget->widgetId != 0) {
149
 
                // Some extra edit validation
150
 
                // Make sure we havent entered a silly value in the filter
151
 
                if (strstr($this->getOption('filter'), 'DESC'))
152
 
                    throw new \InvalidArgumentException(__('Cannot user ordering criteria in the Filter Clause'));
153
 
 
154
 
                if (!is_numeric($this->getOption('upperLimit')) || !is_numeric($this->getOption('lowerLimit')))
155
 
                    throw new \InvalidArgumentException(__('Limits must be numbers'));
156
 
 
157
 
                if ($this->getOption('upperLimit') < 0 || $this->getOption('lowerLimit') < 0)
158
 
                    throw new \InvalidArgumentException(__('Limits cannot be lower than 0'));
159
 
 
160
 
                // Check the bounds of the limits
161
 
                if ($this->getOption('upperLimit') < $this->getOption('lowerLimit'))
162
 
                    throw new \InvalidArgumentException(__('Upper limit must be higher than lower limit'));
163
 
            }
164
 
 
165
 
        } else {
166
 
            // Only supported two source types at the moment
167
 
            throw new \InvalidArgumentException(__('Unknown Source Type'));
168
 
        }
169
 
 
170
 
        if ($this->widget->widgetId != 0) {
171
 
            // Make sure we have a number in here
172
 
            if (!v::numeric()->validate($this->getOption('numItems', 0)))
173
 
                throw new \InvalidArgumentException(__('The value in Number of Items must be numeric.'));
174
 
 
175
 
            if (!v::intType()->min(0)->validate($this->getOption('updateInterval')))
176
 
                throw new \InvalidArgumentException(__('Update Interval must be greater than or equal to 0'));
177
 
        }
178
 
    }
179
 
 
180
 
    /**
181
 
     * Adds a Ticker Widget
182
 
     * @SWG\Post(
183
 
     *  path="/playlist/widget/ticker/{playlistId}",
184
 
     *  operationId="WidgetTickerAdd",
185
 
     *  tags={"widget"},
186
 
     *  summary="Add a ticker Widget",
187
 
     *  description="Add a new ticker Widget to the specified playlist",
188
 
     *  @SWG\Parameter(
189
 
     *      name="playlistId",
190
 
     *      in="path",
191
 
     *      description="The playlist ID to add a Widget to",
192
 
     *      type="integer",
193
 
     *      required=true
194
 
     *   ),
195
 
     *  @SWG\Parameter(
196
 
     *      name="name",
197
 
     *      in="formData",
198
 
     *      description="Optional Widget Name",
199
 
     *      type="string",
200
 
     *      required=false
201
 
     *  ),
202
 
     *  @SWG\Parameter(
203
 
     *      name="duration",
204
 
     *      in="formData",
205
 
     *      description="The Widget Duration",
206
 
     *      type="integer",
207
 
     *      required=false
208
 
     *  ),
209
 
     *  @SWG\Parameter(
210
 
     *      name="useDuration",
211
 
     *      in="formData",
212
 
     *      description="(0, 1) Select 1 only if you will provide duration parameter as well",
213
 
     *      type="integer",
214
 
     *      required=false
215
 
     *  ),
216
 
     *  @SWG\Parameter(
217
 
     *      name="sourceId",
218
 
     *      in="formData",
219
 
     *      description="Add only - 1 for rss feed, 2 for dataset",
220
 
     *      type="integer",
221
 
     *      required=true
222
 
     *  ),
223
 
     *  @SWG\Parameter(
224
 
     *      name="uri",
225
 
     *      in="formData",
226
 
     *      description="For sourceId=1, the link for the rss feed",
227
 
     *      type="string",
228
 
     *      required=true
229
 
     *  ),
230
 
     *  @SWG\Parameter(
231
 
     *      name="dataSetId",
232
 
     *      in="formData",
233
 
     *      description="For sourceId=2, Create ticker Widget using provided dataSetId of an existing dataSet",
234
 
     *      type="integer",
235
 
     *      required=true
236
 
     *  ),
237
 
     *  @SWG\Parameter(
238
 
     *      name="updateInterval",
239
 
     *      in="formData",
240
 
     *      description="EDIT Only - Update interval in minutes",
241
 
     *      type="integer",
242
 
     *      required=false
243
 
     *   ),
244
 
     *  @SWG\Parameter(
245
 
     *      name="effect",
246
 
     *      in="formData",
247
 
     *      description="Edit only - Effect that will be used to transitions between items, available options: fade, fadeout, scrollVert, scollHorz, flipVert, flipHorz, shuffle, tileSlide, tileBlind, marqueeUp, marqueeDown, marqueeRight, marqueeLeft",
248
 
     *      type="string",
249
 
     *      required=false
250
 
     *   ),
251
 
     *  @SWG\Parameter(
252
 
     *      name="speed",
253
 
     *      in="formData",
254
 
     *      description="Edit only - The transition speed of the selected effect in milliseconds (1000 = normal) or the Marquee speed in a low to high scale (normal = 1)",
255
 
     *      type="integer",
256
 
     *      required=false
257
 
     *   ),
258
 
     *  @SWG\Parameter(
259
 
     *      name="copyright",
260
 
     *      in="formData",
261
 
     *      description="EDIT Only and SourceId=1 - Copyright information to display as the last item in this feed. can be styled with the #copyright CSS selector",
262
 
     *      type="string",
263
 
     *      required=false
264
 
     *   ),
265
 
     *  @SWG\Parameter(
266
 
     *      name="numItems",
267
 
     *      in="formData",
268
 
     *      description="EDIT Only and SourceId=1 - The number of RSS items you want to display",
269
 
     *      type="integer",
270
 
     *      required=false
271
 
     *   ),
272
 
     *  @SWG\Parameter(
273
 
     *      name="takeItemsFrom",
274
 
     *      in="formData",
275
 
     *      description="EDIT Only and SourceId=1 - Take the items form the beginning or the end of the list, available options: start, end",
276
 
     *      type="string",
277
 
     *      required=false
278
 
     *   ),
279
 
     *  @SWG\Parameter(
280
 
     *      name="durationIsPerItem",
281
 
     *      in="formData",
282
 
     *      description="A flag (0, 1), The duration specified is per item, otherwise it is per feed",
283
 
     *      type="integer",
284
 
     *      required=false
285
 
     *   ),
286
 
     *  @SWG\Parameter(
287
 
     *      name="itemsSideBySide",
288
 
     *      in="formData",
289
 
     *      description="A flag (0, 1), Should items be shown side by side",
290
 
     *      type="integer",
291
 
     *      required=false
292
 
     *   ),
293
 
     *  @SWG\Parameter(
294
 
     *      name="upperLimit",
295
 
     *      in="formData",
296
 
     *      description="EDIT Only, SourceId=2 - Upper low limit for this dataSet, 0 for nor limit",
297
 
     *      type="integer",
298
 
     *      required=false
299
 
     *   ),
300
 
     *  @SWG\Parameter(
301
 
     *      name="lowerLimit",
302
 
     *      in="formData",
303
 
     *      description="EDIT Only, SourceId=2 - Lower low limit for this dataSet, 0 for nor limit",
304
 
     *      type="integer",
305
 
     *      required=false
306
 
     *   ),
307
 
     *  @SWG\Parameter(
308
 
     *      name="itemsPerPage",
309
 
     *      in="formData",
310
 
     *      description="EDIT Only - When in single mode, how many items per page should be shown",
311
 
     *      type="integer",
312
 
     *      required=false
313
 
     *   ),
314
 
     *  @SWG\Parameter(
315
 
     *      name="dateFormat",
316
 
     *      in="formData",
317
 
     *      description="EDIT Only - The date format to apply to all dates returned by the ticker",
318
 
     *      type="string",
319
 
     *      required=false
320
 
     *   ),
321
 
     *  @SWG\Parameter(
322
 
     *      name="allowedAttributes",
323
 
     *      in="formData",
324
 
     *      description="EDIT Only and SourceId=1 - A comma separated list of attributes that should not be stripped from the feed",
325
 
     *      type="string",
326
 
     *      required=false
327
 
     *   ),
328
 
     *  @SWG\Parameter(
329
 
     *      name="stripTags",
330
 
     *      in="formData",
331
 
     *      description="EDIT Only and SourceId=1 - A comma separated list of attributes that should be stripped from the feed",
332
 
     *      type="string",
333
 
     *      required=false
334
 
     *   ),
335
 
     *  @SWG\Parameter(
336
 
     *      name="backgroundColor",
337
 
     *      in="formData",
338
 
     *      description="Edit only - A HEX color to use as the background color of this widget",
339
 
     *      type="string",
340
 
     *      required=false
341
 
     *   ),
342
 
     *  @SWG\Parameter(
343
 
     *      name="disableDateSort",
344
 
     *      in="formData",
345
 
     *      description="EDIT Only, SourceId=1 - Should the date sort applied to the feed be disabled?",
346
 
     *      type="integer",
347
 
     *      required=false
348
 
     *   ),
349
 
     *  @SWG\Parameter(
350
 
     *      name="textDirection",
351
 
     *      in="formData",
352
 
     *      description="EDIT Only, SourceId=1 - Which direction does the text in the feed use? Available options: ltr, rtl",
353
 
     *      type="string",
354
 
     *      required=false
355
 
     *   ),
356
 
     *  @SWG\Parameter(
357
 
     *      name="noDataMessage",
358
 
     *      in="formData",
359
 
     *      description="EDIT Only - A message to display when no data is returned from the source",
360
 
     *      type="string",
361
 
     *      required=false
362
 
     *   ),
363
 
     *  @SWG\Parameter(
364
 
     *      name="templateId",
365
 
     *      in="formData",
366
 
     *      description="EDIT Only, SourceId=1 - Template you'd like to apply, options available: title-only, prominent-title-with-desc-and-name-separator, media-rss-with-title, media-rss-wth-left-hand-text",
367
 
     *      type="string",
368
 
     *      required=false
369
 
     *   ),
370
 
     *  @SWG\Parameter(
371
 
     *      name="overrideTemplate",
372
 
     *      in="formData",
373
 
     *      description="EDIT Only, SourceId=1 - flag (0, 1) override template checkbox",
374
 
     *      type="integer",
375
 
     *      required=false
376
 
     *   ),
377
 
     *  @SWG\Parameter(
378
 
     *      name="template",
379
 
     *      in="formData",
380
 
     *      description="Template for each item, replaces [itemsTemplate] in main template, Pass only with overrideTemplate set to 1 or with sourceId=2 ",
381
 
     *      type="string",
382
 
     *      required=false
383
 
     *   ),
384
 
     *  @SWG\Parameter(
385
 
     *      name="css",
386
 
     *      in="formData",
387
 
     *      description="Optional StyleSheet Pass only with overrideTemplate set to 1 or with sourceId=2 ",
388
 
     *      type="string",
389
 
     *      required=false
390
 
     *   ),
391
 
     *  @SWG\Parameter(
392
 
     *      name="javaScript",
393
 
     *      in="formData",
394
 
     *      description="Optional JavaScript, Pass only with overrideTemplate set to 1 ",
395
 
     *      type="string",
396
 
     *      required=false
397
 
     *   ),
398
 
     *  @SWG\Parameter(
399
 
     *      name="filter",
400
 
     *      in="formData",
401
 
     *      description="EDIT Only, SourceId=2 - SQL clause for filter this dataSet",
402
 
     *      type="string",
403
 
     *      required=false
404
 
     *   ),
405
 
     *  @SWG\Parameter(
406
 
     *      name="ordering",
407
 
     *      in="formData",
408
 
     *      description="EDIT Only, SourceId=2- SQL clause for how this dataSet should be ordered",
409
 
     *      type="string",
410
 
     *      required=false
411
 
     *   ),
412
 
     *  @SWG\Parameter(
413
 
     *      name="useOrderingClause",
414
 
     *      in="formData",
415
 
     *      description="EDIT Only, SourceId=2 - flag (0,1) Use advanced order clause - set to 1 if ordering is provided",
416
 
     *      type="integer",
417
 
     *      required=false
418
 
     *   ),
419
 
     *  @SWG\Parameter(
420
 
     *      name="useFilteringClause",
421
 
     *      in="formData",
422
 
     *      description="EDIT Only, SourceId=2 - flag (0,1) Use advanced filter clause - set to 1 if filter is provided",
423
 
     *      type="integer",
424
 
     *      required=false
425
 
     *   ),
426
 
     *  @SWG\Parameter(
427
 
     *      name="randomiseItems",
428
 
     *      in="formData",
429
 
     *      description="A flag (0, 1), whether to randomise the feed",
430
 
     *      type="integer",
431
 
     *      required=false
432
 
     *   ),
433
 
     *  @SWG\Response(
434
 
     *      response=201,
435
 
     *      description="successful operation",
436
 
     *      @SWG\Schema(ref="#/definitions/Widget"),
437
 
     *      @SWG\Header(
438
 
     *          header="Location",
439
 
     *          description="Location of the new widget",
440
 
     *          type="string"
441
 
     *      )
442
 
     *  )
443
 
     * )
444
 
     */
445
 
    public function add()
446
 
    {
447
 
        $this->setDuration($this->getSanitizer()->getInt('duration', $this->getDuration()));
448
 
        $this->setUseDuration($this->getSanitizer()->getCheckbox('useDuration'));
449
 
        $this->setOption('xmds', true);
450
 
        $this->setOption('sourceId', $this->getSanitizer()->getInt('sourceId'));
451
 
        $this->setOption('uri', urlencode($this->getSanitizer()->getString('uri')));
452
 
        $this->setOption('durationIsPerItem', 1);
453
 
        $this->setOption('updateInterval', 120);
454
 
        $this->setOption('speed', 2);
455
 
 
456
 
        if ($this->getOption('sourceId') == 2)
457
 
            $this->setOption('dataSetId', $this->getSanitizer()->getInt('dataSetId', 0));
458
 
 
459
 
        // New tickers have template override set to 0 by add.
460
 
        // the edit form can then default to 1 when the element doesn't exist (for legacy)
461
 
        $this->setOption('overrideTemplate', 0);
462
 
 
463
 
        // Save the widget
464
 
        $this->validate();
465
 
        $this->saveWidget();
466
 
    }
467
 
 
468
 
    /**
469
 
     * Edit Media
470
 
     */
471
 
    public function edit()
472
 
    {
473
 
        // Source is selected during add() and cannot be edited.
474
 
        // Other properties
475
 
        $this->setDuration($this->getSanitizer()->getInt('duration', $this->getDuration()));
476
 
        $this->setUseDuration($this->getSanitizer()->getCheckbox('useDuration'));
477
 
        $this->setOption('xmds', true);
478
 
        $this->setOption('uri', urlencode($this->getSanitizer()->getString('uri')));
479
 
        $this->setOption('updateInterval', $this->getSanitizer()->getInt('updateInterval', 120));
480
 
        $this->setOption('speed', $this->getSanitizer()->getInt('speed', 2));
481
 
        $this->setOption('name', $this->getSanitizer()->getString('name'));
482
 
        $this->setOption('effect', $this->getSanitizer()->getString('effect'));
483
 
        $this->setOption('copyright', $this->getSanitizer()->getString('copyright'));
484
 
        $this->setOption('numItems', $this->getSanitizer()->getInt('numItems'));
485
 
        $this->setOption('takeItemsFrom', $this->getSanitizer()->getString('takeItemsFrom'));
486
 
        $this->setOption('durationIsPerItem', $this->getSanitizer()->getCheckbox('durationIsPerItem'));
487
 
        $this->setOption('randomiseItems', $this->getSanitizer()->getCheckbox('randomiseItems'));
488
 
        $this->setOption('itemsSideBySide', $this->getSanitizer()->getCheckbox('itemsSideBySide'));
489
 
        $this->setOption('upperLimit', $this->getSanitizer()->getInt('upperLimit', 0));
490
 
        $this->setOption('lowerLimit', $this->getSanitizer()->getInt('lowerLimit', 0));
491
 
 
492
 
        $this->setOption('itemsPerPage', $this->getSanitizer()->getInt('itemsPerPage'));
493
 
        $this->setOption('dateFormat', $this->getSanitizer()->getString('dateFormat'));
494
 
        $this->setOption('allowedAttributes', $this->getSanitizer()->getString('allowedAttributes'));
495
 
        $this->setOption('stripTags', $this->getSanitizer()->getString('stripTags'));
496
 
        $this->setOption('backgroundColor', $this->getSanitizer()->getString('backgroundColor'));
497
 
        $this->setOption('disableDateSort', $this->getSanitizer()->getCheckbox('disableDateSort'));
498
 
        $this->setOption('textDirection', $this->getSanitizer()->getString('textDirection'));
499
 
        $this->setOption('overrideTemplate', $this->getSanitizer()->getCheckbox('overrideTemplate'));
500
 
        $this->setOption('templateId', $this->getSanitizer()->getString('templateId'));
501
 
        $this->setRawNode('noDataMessage', $this->getSanitizer()->getParam('noDataMessage', ''));
502
 
        $this->setRawNode('javaScript', $this->getSanitizer()->getParam('javaScript', ''));
503
 
 
504
 
        // DataSet
505
 
        if ($this->getOption('sourceId') == 2) {
506
 
            // We are a data set, so get the custom filter controls
507
 
            $this->setOption('filter', $this->getSanitizer()->getParam('filter', null));
508
 
            $this->setOption('ordering', $this->getSanitizer()->getString('ordering'));
509
 
            $this->setOption('useOrderingClause', $this->getSanitizer()->getCheckbox('useOrderingClause'));
510
 
            $this->setOption('useFilteringClause', $this->getSanitizer()->getCheckbox('useFilteringClause'));
511
 
 
512
 
            // Order and Filter criteria
513
 
            $orderClauses = $this->getSanitizer()->getStringArray('orderClause');
514
 
            $orderClauseDirections = $this->getSanitizer()->getStringArray('orderClauseDirection');
515
 
            $orderClauseMapping = [];
516
 
 
517
 
            $i = -1;
518
 
            foreach ($orderClauses as $orderClause) {
519
 
                $i++;
520
 
 
521
 
                if ($orderClause == '')
522
 
                    continue;
523
 
 
524
 
                // Map the stop code received to the stop ref (if there is one)
525
 
                $orderClauseMapping[] = [
526
 
                    'orderClause' => $orderClause,
527
 
                    'orderClauseDirection' => isset($orderClauseDirections[$i]) ? $orderClauseDirections[$i] : '',
528
 
                ];
529
 
            }
530
 
 
531
 
            $this->setOption('orderClauses', json_encode($orderClauseMapping));
532
 
 
533
 
            $filterClauses = $this->getSanitizer()->getStringArray('filterClause');
534
 
            $filterClauseOperator = $this->getSanitizer()->getStringArray('filterClauseOperator');
535
 
            $filterClauseCriteria = $this->getSanitizer()->getStringArray('filterClauseCriteria');
536
 
            $filterClauseValue = $this->getSanitizer()->getStringArray('filterClauseValue');
537
 
            $filterClauseMapping = [];
538
 
 
539
 
            $i = -1;
540
 
            foreach ($filterClauses as $filterClause) {
541
 
                $i++;
542
 
 
543
 
                if ($filterClause == '')
544
 
                    continue;
545
 
 
546
 
                // Map the stop code received to the stop ref (if there is one)
547
 
                $filterClauseMapping[] = [
548
 
                    'filterClause' => $filterClause,
549
 
                    'filterClauseOperator' => isset($filterClauseOperator[$i]) ? $filterClauseOperator[$i] : '',
550
 
                    'filterClauseCriteria' => isset($filterClauseCriteria[$i]) ? $filterClauseCriteria[$i] : '',
551
 
                    'filterClauseValue' => isset($filterClauseValue[$i]) ? $filterClauseValue[$i] : '',
552
 
                ];
553
 
            }
554
 
 
555
 
            $this->setOption('filterClauses', json_encode($filterClauseMapping));
556
 
 
557
 
            // DataSet Tickers always have Templates provided.
558
 
            $this->setRawNode('template', $this->getSanitizer()->getParam('ta_text', $this->getSanitizer()->getParam('template', null)));
559
 
            $this->setRawNode('css', $this->getSanitizer()->getParam('ta_css', $this->getSanitizer()->getParam('css', null)));
560
 
 
561
 
        } else if ($this->getOption('overrideTemplate') == 1) {
562
 
            // Feed tickers should only use the template if they have override set.
563
 
            $this->setRawNode('template', $this->getSanitizer()->getParam('ta_text', $this->getSanitizer()->getParam('template', null)));
564
 
            $this->setRawNode('css', $this->getSanitizer()->getParam('ta_css', $this->getSanitizer()->getParam('css', null)));
565
 
        }
566
 
        
567
 
        // Save the widget
568
 
        $this->validate();
569
 
        $this->saveWidget();
570
 
    }
571
 
 
572
 
    /**
573
 
     * @inheritdoc
574
 
     */
575
 
    public function hoverPreview()
576
 
    {
577
 
        $name = $this->getOption('name');
578
 
        $url = urldecode($this->getOption('uri'));
579
 
        $sourceId = $this->getOption('sourceId', 1);
580
 
 
581
 
        // Default Hover window contains a thumbnail, media type and duration
582
 
        $output = '<div class="thumbnail"><img alt="' . $this->module->name . ' thumbnail" src="' . $this->getConfig()->uri('img/forms/' . $this->getModuleType() . '.gif') . '"></div>';
583
 
        $output .= '<div class="info">';
584
 
        $output .= '    <ul>';
585
 
        $output .= '    <li>' . __('Type') . ': ' . $this->module->name . '</li>';
586
 
        $output .= '    <li>' . __('Name') . ': ' . $name . '</li>';
587
 
 
588
 
        if ($sourceId == 2) {
589
 
            // Get the DataSet name
590
 
            try {
591
 
                $dataSet = $this->dataSetFactory->getById($this->getOption('dataSetId'));
592
 
 
593
 
                $output .= '    <li>' . __('Source: DataSet named "%s".', $dataSet->dataSet) . '</li>';
594
 
            } catch (NotFoundException $notFoundException) {
595
 
                $this->getLog()->error('Layout Widget without a DataSet. widgetId: ' . $this->getWidgetId());
596
 
                $output .= '    <li>' . __('Warning: No DataSet found.') . '</li>';
597
 
            }
598
 
        }
599
 
        else
600
 
            $output .= '    <li>' . __('Source') . ': <a href="' . $url . '" target="_blank" title="' . __('Source') . '">' . $url . '</a></li>';
601
 
 
602
 
 
603
 
        $output .= '    <li>' . __('Duration') . ': ' . $this->getDuration() . ' ' . __('seconds') . '</li>';
604
 
        $output .= '    </ul>';
605
 
        $output .= '</div>';
606
 
 
607
 
        return $output;
608
 
    }
609
 
 
610
 
    /**
611
 
     * Get Resource
612
 
     * @param int $displayId
613
 
     * @return mixed
614
 
     * @throws XiboException
615
 
     */
616
 
    public function getResource($displayId = 0)
617
 
    {
618
 
        // Load in the template
619
 
        $data = [];
620
 
        $isPreview = ($this->getSanitizer()->getCheckbox('preview') == 1);
621
 
 
622
 
        // Replace the View Port Width?
623
 
        $data['viewPortWidth'] = ($isPreview) ? $this->region->width : '[[ViewPortWidth]]';
624
 
 
625
 
        // What is the data source for this ticker?
626
 
        $sourceId = $this->getOption('sourceId', 1);
627
 
 
628
 
        // Information from the Module
629
 
        $itemsSideBySide = $this->getOption('itemsSideBySide', 0);
630
 
        $duration = $this->getCalculatedDurationForGetResource();
631
 
        $durationIsPerItem = $this->getOption('durationIsPerItem', 1);
632
 
        $numItems = $this->getOption('numItems', 0);
633
 
        $takeItemsFrom = $this->getOption('takeItemsFrom', 'start');
634
 
        $itemsPerPage = $this->getOption('itemsPerPage', 0);
635
 
 
636
 
        // Text/CSS subsitution variables.
637
 
        $text = null;
638
 
        $css = null;
639
 
 
640
 
        // Get CSS and HTML template from the original template or from the input field
641
 
        if ($sourceId != 2 && $this->getOption('overrideTemplate') == 0) {
642
 
            // Feed tickers without override set.
643
 
            $template = $this->getTemplateById($this->getOption('templateId', 'title-only'));
644
 
            
645
 
            if (isset($template)) {
646
 
                $text = $template['template'];
647
 
                $css = $template['css'];
648
 
            } else {
649
 
                $text = $this->getRawNode('template', '');
650
 
                $css = $this->getRawNode('css', '');
651
 
            }
652
 
        } else {
653
 
            // DataSet tickers or feed tickers without overrides.
654
 
            $text = $this->getRawNode('template', '');
655
 
            $css = $this->getRawNode('css', '');
656
 
        }
657
 
        
658
 
        // Parse library references on the template
659
 
        $text = $this->parseLibraryReferences($isPreview, $text);
660
 
 
661
 
        // Parse library references on the CSS Node
662
 
        $css = $this->parseLibraryReferences($isPreview, $css);
663
 
 
664
 
        // Get the JavaScript node
665
 
        $javaScript = $this->parseLibraryReferences($isPreview, $this->getRawNode('javaScript', ''));
666
 
 
667
 
        // Handle older layouts that have a direction node but no effect node
668
 
        $oldDirection = $this->getOption('direction', 'none');
669
 
 
670
 
        if ($oldDirection == 'single')
671
 
            $oldDirection = 'noTransition';
672
 
        else if ($oldDirection != 'none')
673
 
            $oldDirection = 'marquee' . ucfirst($oldDirection);
674
 
 
675
 
        $effect = $this->getOption('effect', $oldDirection);
676
 
 
677
 
        $options = array(
678
 
            'type' => $this->getModuleType(),
679
 
            'fx' => $effect,
680
 
            'duration' => $duration,
681
 
            'durationIsPerItem' => (($durationIsPerItem == 0) ? false : true),
682
 
            'numItems' => $numItems,
683
 
            'takeItemsFrom' => $takeItemsFrom,
684
 
            'itemsPerPage' => $itemsPerPage,
685
 
            'randomiseItems' => $this->getOption('randomiseItems', 0),
686
 
            'speed' => $this->getOption('speed', 1000),
687
 
            'originalWidth' => $this->region->width,
688
 
            'originalHeight' => $this->region->height,
689
 
            'previewWidth' => $this->getSanitizer()->getDouble('width', 0),
690
 
            'previewHeight' => $this->getSanitizer()->getDouble('height', 0),
691
 
            'scaleOverride' => $this->getSanitizer()->getDouble('scale_override', 0)
692
 
        );
693
 
 
694
 
        // Generate a JSON string of substituted items.
695
 
        if ($sourceId == 2) {
696
 
            $items = $this->getDataSetItems($displayId, $isPreview, $text);
697
 
        } else {
698
 
            $items = $this->getRssItems($isPreview, $text);
699
 
        }
700
 
 
701
 
        // Return empty string if there are no items to show.
702
 
        if (count($items) == 0) {
703
 
            // Do we have a no-data message to display?
704
 
            $noDataMessage = $this->getRawNode('noDataMessage');
705
 
 
706
 
            if ($noDataMessage != '') {
707
 
                $items[] = $noDataMessage;
708
 
            } else {
709
 
                $this->getLog()->error('Request failed for dataSet id=%d. Widget=%d. Due to No Records Found', $this->getOption('dataSetId'), $this->getWidgetId());
710
 
                return '';
711
 
            }
712
 
        }
713
 
 
714
 
        // Work out how many pages we will be showing.
715
 
        $pages = $numItems;
716
 
        if ($numItems > count($items) || $numItems == 0)
717
 
            $pages = count($items);
718
 
 
719
 
        $pages = ($itemsPerPage > 0) ? ceil($pages / $itemsPerPage) : $pages;
720
 
        $totalDuration = ($durationIsPerItem == 0) ? $duration : ($duration * $pages);
721
 
 
722
 
        // Replace and Control Meta options
723
 
        $data['controlMeta'] = '<!-- NUMITEMS=' . $pages . ' -->' . PHP_EOL . '<!-- DURATION=' . $totalDuration . ' -->';   
724
 
        // Replace the head content
725
 
        $headContent = '';
726
 
        
727
 
        if ($itemsSideBySide == 1) {
728
 
            $headContent .= '<style type="text/css">';
729
 
            $headContent .= ' .item, .page { float: left; }';
730
 
            $headContent .= '</style>';
731
 
        }
732
 
 
733
 
        if ($this->getOption('textDirection') == 'rtl') {
734
 
            $headContent .= '<style type="text/css">';
735
 
            $headContent .= ' #content { direction: rtl; }';
736
 
            $headContent .= '</style>';
737
 
        }
738
 
 
739
 
        if ($this->getOption('backgroundColor') != '') {
740
 
            $headContent .= '<style type="text/css">';
741
 
            $headContent .= ' body { background-color: ' . $this->getOption('backgroundColor') . '; }';
742
 
            $headContent .= '</style>';
743
 
        }
744
 
 
745
 
        // Add the CSS if it isn't empty
746
 
        if ($css != '') {
747
 
            $headContent .= '<style type="text/css">' . $css . '</style>';
748
 
        }
749
 
 
750
 
        // Add our fonts.css file
751
 
        $headContent .= '<link href="' . (($isPreview) ? $this->getApp()->urlFor('library.font.css') : 'fonts.css') . '" rel="stylesheet" media="screen">';
752
 
        $headContent .= '<style type="text/css">' . file_get_contents($this->getConfig()->uri('css/client.css', true)) . '</style>';
753
 
 
754
 
        // Replace the Head Content with our generated javascript
755
 
        $data['head'] = $headContent;
756
 
 
757
 
        // Add some scripts to the JavaScript Content
758
 
        $javaScriptContent = '<script type="text/javascript" src="' . $this->getResourceUrl('vendor/jquery-1.11.1.min.js') . '"></script>';
759
 
 
760
 
        // Need the marquee plugin?
761
 
        if (stripos($effect, 'marquee') !== false)
762
 
            $javaScriptContent .= '<script type="text/javascript" src="' . $this->getResourceUrl('vendor/jquery.marquee.min.js') . '"></script>';
763
 
 
764
 
        // Need the cycle plugin?
765
 
        if ($effect != 'none')
766
 
            $javaScriptContent .= '<script type="text/javascript" src="' . $this->getResourceUrl('vendor/jquery-cycle-2.1.6.min.js') . '"></script>';
767
 
 
768
 
        $javaScriptContent .= '<script type="text/javascript" src="' . $this->getResourceUrl('xibo-layout-scaler.js') . '"></script>';
769
 
        $javaScriptContent .= '<script type="text/javascript" src="' . $this->getResourceUrl('xibo-text-render.js') . '"></script>';
770
 
        $javaScriptContent .= '<script type="text/javascript" src="' . $this->getResourceUrl('xibo-image-render.js') . '"></script>';
771
 
 
772
 
        $javaScriptContent .= '<script type="text/javascript">';
773
 
        $javaScriptContent .= '   var options = ' . json_encode($options) . ';';
774
 
        $javaScriptContent .= '   var items = ' . json_encode($items) . ';';
775
 
        $javaScriptContent .= '   $(document).ready(function() { ';
776
 
        $javaScriptContent .= '       $("body").xiboLayoutScaler(options); $("#content").xiboTextRender(options, items); $("#content").find("img").xiboImageRender(options); ';
777
 
        $javaScriptContent .= '   }); ';
778
 
        $javaScriptContent .= $javaScript;
779
 
        $javaScriptContent .= '</script>';
780
 
 
781
 
        // Replace the Head Content with our generated javascript
782
 
        $data['javaScript'] = $javaScriptContent;
783
 
 
784
 
        return $this->renderTemplate($data);
785
 
    }
786
 
 
787
 
    /**
788
 
     * @param $isPreview
789
 
     * @param $text
790
 
     * @return array|mixed|null
791
 
     * @throws \Xibo\Exception\ConfigurationException
792
 
     */
793
 
    private function getRssItems($isPreview, $text)
794
 
    {
795
 
        // Make sure we have the cache location configured
796
 
        Library::ensureLibraryExists($this->getConfig()->GetSetting('LIBRARY_LOCATION'));
797
 
 
798
 
        // Create a key to use as a caching key for this item.
799
 
        // the rendered feed will be cached, so it is important the key covers all options.
800
 
        $feedUrl = urldecode($this->getOption('uri'));
801
 
 
802
 
        /** @var \Stash\Item $cache */
803
 
        $cache = $this->getPool()->getItem($this->makeCacheKey(md5($feedUrl)));
804
 
        $cache->setInvalidationMethod(Invalidation::SLEEP, 5000, 15);
805
 
 
806
 
        $this->getLog()->debug('Ticker with RSS source ' . $feedUrl . '. Cache key: ' . $cache->getKey());
807
 
 
808
 
        // Get the document out of the cache
809
 
        $document = $cache->get();
810
 
 
811
 
        // Check our cache to see if the key exists
812
 
        if ($cache->isMiss()) {
813
 
            // Invalid local cache, requery using picofeed.
814
 
            $this->getLog()->debug('Cache Miss, getting RSS items');
815
 
 
816
 
            // Lock this cache item (120 seconds)
817
 
            $cache->lock(120);
818
 
 
819
 
            try {
820
 
                // Create a Guzzle Client to get the Feed XML
821
 
                $client = new Client();
822
 
                $response = $client->get($feedUrl, $this->getConfig()->getGuzzleProxy([
823
 
                    'headers' => [
824
 
                        'Accept' => 'application/rss+xml, application/rdf+xml;q=0.8, application/atom+xml;q=0.6, application/xml;q=0.4, text/xml;q=0.4, text/html;q=0.2'
825
 
                    ],
826
 
                    'timeout' => 20 // wait no more than 20 seconds: https://github.com/xibosignage/xibo/issues/1401
827
 
                ]));
828
 
 
829
 
                // Pull out the content type
830
 
                $contentType = $response->getHeaderLine('Content-Type');
831
 
 
832
 
                $this->getLog()->debug('Feed returned content-type ' . $contentType);
833
 
 
834
 
                // https://github.com/xibosignage/xibo/issues/1401
835
 
                if (stripos($contentType, 'rss') === false && stripos($contentType, 'xml') === false && stripos($contentType, 'text') === false && stripos($contentType, 'html') === false) {
836
 
                    // The content type isn't compatible
837
 
                    $this->getLog()->error('Incompatible content type: ' . $contentType);
838
 
                    return false;
839
 
                }
840
 
 
841
 
                // Get the body, etc
842
 
                $result = explode('charset=', $contentType);
843
 
                $document['encoding'] = isset($result[1]) ? $result[1] : '';
844
 
                $document['xml'] = $response->getBody()->getContents();
845
 
 
846
 
                // Add this to the cache.
847
 
                $cache->set($document);
848
 
                $cache->expiresAfter($this->getOption('updateInterval', 360) * 60);
849
 
 
850
 
                // Save
851
 
                $this->getPool()->saveDeferred($cache);
852
 
 
853
 
                $this->getLog()->debug('Processed feed and added to the cache for ' . $this->getOption('updateInterval', 360) . ' minutes');
854
 
 
855
 
            } catch (RequestException $requestException) {
856
 
                // Log and return empty?
857
 
                $this->getLog()->error('Unable to get feed: ' . $requestException->getMessage());
858
 
                $this->getLog()->debug($requestException->getTraceAsString());
859
 
 
860
 
                return false;
861
 
            }
862
 
        }
863
 
 
864
 
        //$this->getLog()->debug(var_export($document, true));
865
 
 
866
 
        // Cache HIT or we've requested
867
 
        // Load the feed XML document into a feed parser
868
 
        try {
869
 
            // Enable logging if we need to
870
 
            if (LogService::resolveLogLevel($this->getConfig()->GetSetting('audit', 'error')) == \Slim\Log::DEBUG) {
871
 
                Logger::enable();
872
 
            }
873
 
 
874
 
            // Allowable attributes
875
 
            $clientConfig = new Config();
876
 
 
877
 
            // need a sensible way to set this
878
 
            // https://github.com/fguillot/picoFeed/issues/196
879
 
            //if ($this->getOption('allowedAttributes') != null) {
880
 
                //$clientConfig->setFilterWhitelistedTags(explode(',', $this->getOption('allowedAttributes')));
881
 
            //}
882
 
 
883
 
            // Get the feed parser
884
 
            $reader = new Reader($clientConfig);
885
 
            $parser = $reader->getParser($feedUrl, $document['xml'], $document['encoding']);
886
 
 
887
 
            // Get a feed object
888
 
            $feed = $parser->execute();
889
 
 
890
 
            // Get all items
891
 
            $feedItems = $feed->getItems();
892
 
 
893
 
        } catch (PicoFeedException $picoFeedException) {
894
 
            // Log and return empty?
895
 
            $this->getLog()->error('Unable to get feed: ' . $picoFeedException->getMessage());
896
 
            $this->getLog()->debug($picoFeedException->getTraceAsString());
897
 
            return false;
898
 
        }
899
 
 
900
 
        // Output any PicoFeed logs
901
 
        if (LogService::resolveLogLevel($this->getConfig()->GetSetting('audit', 'error')) == \Slim\Log::DEBUG) {
902
 
            $this->getLog()->debug(var_export(Logger::getMessages(), true));
903
 
        }
904
 
 
905
 
        // Parse the text template
906
 
        $matches = '';
907
 
        preg_match_all('/\[.*?\]/', $text, $matches);
908
 
 
909
 
        // Disable date sorting?
910
 
        if ($this->getOption('disableDateSort') == 0 && $this->getOption('randomiseItems', 0) == 0) {
911
 
            // Sort the items array by date
912
 
            usort($feedItems, function($a, $b) {
913
 
                /* @var Item $a */
914
 
                /* @var Item $b */
915
 
 
916
 
                return ($a->getDate() < $b->getDate());
917
 
            });
918
 
        }
919
 
 
920
 
        // Date format for the feed items
921
 
        $dateFormat = $this->getOption('dateFormat', $this->getConfig()->GetSetting('DATE_FORMAT'));
922
 
 
923
 
        // Set an expiry time for the media
924
 
        $expires = $this->getDate()->parse()->addMinutes($this->getOption('updateInterval', 3600))->format('U');
925
 
 
926
 
        // Render the content now
927
 
        foreach ($feedItems as $item) {
928
 
            /* @var Item $item */
929
 
 
930
 
            // Substitute for all matches in the template
931
 
            $rowString = $text;
932
 
 
933
 
            // Run through all [] substitutes in $matches
934
 
            foreach ($matches[0] as $sub) {
935
 
                $replace = '';
936
 
 
937
 
                // Does our [] have a | - if so we need to do some special parsing
938
 
                if (strstr($sub, '|') !== false) {
939
 
                    // Use the provided name space to extract a tag
940
 
                    $attribute = NULL;
941
 
                    // Do we have more than 1 | - if we do then we are also interested in getting an attribute
942
 
                    if (substr_count($sub, '|') > 1)
943
 
                        list($tag, $namespace, $attribute) = explode('|', $sub);
944
 
                    else
945
 
                        list($tag, $namespace) = explode('|', $sub);
946
 
 
947
 
                    // Replace some things so that we know what we are looking at
948
 
                    $tag = str_replace('[', '', $tag);
949
 
                    $namespace = str_replace(']', '', $namespace);
950
 
 
951
 
                    if ($attribute !== null)
952
 
                        $attribute = str_replace(']', '', $attribute);
953
 
 
954
 
                    // What are we looking at
955
 
                    $this->getLog()->debug('Namespace: ' . $namespace . ', Tag: ' . $tag . ', Attribute: ' . $attribute);
956
 
                    //$this->getLog()->debug('Item content: %s', var_export($item, true));
957
 
 
958
 
                    // Are we an image place holder? [tag|image]
959
 
                    if ($namespace == 'image') {
960
 
                        // Try to get a link for the image
961
 
                        $link = null;
962
 
 
963
 
                        switch ($tag) {
964
 
                            case 'Link':
965
 
                                if (stripos($item->getEnclosureType(), 'image') > -1) {
966
 
                                    // Use the link to get the image
967
 
                                    $link = $item->getEnclosureUrl();
968
 
 
969
 
                                    if (empty($link)) {
970
 
                                        $this->getLog()->debug('No image found for Link|image tag using getEnclosureUrl');
971
 
                                    }
972
 
                                } else {
973
 
                                    $this->getLog()->debug('No image found for Link|image tag using getEnclosureType');
974
 
                                }
975
 
                                break;
976
 
 
977
 
                            default:
978
 
                                // Default behaviour just tries to get the content from the tag provided.
979
 
                                // it uses the attribute as a namespace if one has been provided
980
 
                                if ($attribute != null)
981
 
                                    $tags = $item->getTag($tag, $attribute);
982
 
                                else
983
 
                                    $tags = $item->getTag($tag);
984
 
 
985
 
                                if (count($tags) > 0 && !empty($tags[0]))
986
 
                                    $link = $tags[0];
987
 
                                else
988
 
                                    $this->getLog()->debug('Tag not found for [' . $tag . '] attribute [' . $attribute . ']');
989
 
                        }
990
 
 
991
 
                        $this->getLog()->debug('Resolved link: ' . $link);
992
 
 
993
 
                        // If we have managed to resolve a link, download it and replace the tag with the downloaded
994
 
                        // image url
995
 
                        if ($link != NULL) {
996
 
                            // Grab the profile image
997
 
                            $file = $this->mediaFactory->queueDownload('ticker_' . md5($this->getOption('url') . $link), $link, $expires);
998
 
 
999
 
                            $replace = '<img src="' . $this->getFileUrl($file, 'image') . '" ' . $attribute . ' />';
1000
 
                        }
1001
 
                    } else {
1002
 
                        // Our namespace is not "image". Which means we are a normal text substitution using a namespace/attribute
1003
 
                        if ($attribute != null)
1004
 
                            $tags = $item->getTag($tag, $attribute);
1005
 
                        else
1006
 
                            $tags = $item->getTag($tag);
1007
 
 
1008
 
                        // If we find some tags then do the business with them
1009
 
                        if ($tags != NULL && count($tags) > 0) {
1010
 
                            $replace = $tags[0];
1011
 
                        } else {
1012
 
                            $this->getLog()->debug('Tag not found for ' . $tag . ' attribute ' . $attribute);
1013
 
                        }
1014
 
                    }
1015
 
                } else {
1016
 
                    // Use the pool of standard tags
1017
 
                    switch ($sub) {
1018
 
                        case '[Name]':
1019
 
                            $replace = $this->getOption('name');
1020
 
                            break;
1021
 
 
1022
 
                        case '[Title]':
1023
 
                            $replace = $item->getTitle();
1024
 
                            break;
1025
 
 
1026
 
                        case '[Description]':
1027
 
                            // Try to get the description tag
1028
 
                            if (!$desc = $item->getTag('description')) {
1029
 
                                // use content with tags stripped
1030
 
                                $replace = strip_tags($item->getContent());
1031
 
                            } else {
1032
 
                                // use description
1033
 
                                $replace = $desc[0];
1034
 
                            }
1035
 
                            break;
1036
 
 
1037
 
                        case '[Content]':
1038
 
                            $replace = $item->getContent();
1039
 
                            break;
1040
 
 
1041
 
                        case '[Copyright]':
1042
 
                            $replace = $item->getAuthor();
1043
 
                            break;
1044
 
 
1045
 
                        case '[Date]':
1046
 
                            $replace = $this->getDate()->getLocalDate($item->getDate()->format('U'), $dateFormat);
1047
 
                            break;
1048
 
 
1049
 
                        case '[PermaLink]':
1050
 
                            $replace = $item->getTag('permalink');
1051
 
                            break;
1052
 
 
1053
 
                        case '[Link]':
1054
 
                            $replace = $item->getUrl();
1055
 
                            break;
1056
 
 
1057
 
                        case '[Image]':
1058
 
                            if (stripos($item->getEnclosureType(), 'image') > -1) {
1059
 
                                // Use the link to get the image
1060
 
                                $link = $item->getEnclosureUrl();
1061
 
 
1062
 
                                if (!(empty($link))) {
1063
 
                                    // Grab the image
1064
 
                                    $file = $this->mediaFactory->queueDownload('ticker_' . md5($this->getOption('url') . $link), $link, $expires);
1065
 
 
1066
 
                                    $replace = '<img src="' . $this->getFileUrl($file, 'image') . '"/>';
1067
 
                                } else {
1068
 
                                    $this->getLog()->debug('No image found for image tag using getEnclosureUrl');
1069
 
                                }
1070
 
                            } else {
1071
 
                                $this->getLog()->debug('No image found for image tag using getEnclosureType');
1072
 
                            }
1073
 
                            break;
1074
 
                    }
1075
 
                }
1076
 
 
1077
 
                if ($this->getOption('stripTags') != '') {
1078
 
                    // Handle cache path for HTML serializer
1079
 
                    $cachePath = $this->getConfig()->GetSetting('LIBRARY_LOCATION') . 'cache/HTMLPurifier';
1080
 
                    if (!is_dir($cachePath))
1081
 
                        mkdir($cachePath);
1082
 
 
1083
 
                    $config = \HTMLPurifier_Config::createDefault();
1084
 
                    $config->set('Cache.SerializerPath', $cachePath);
1085
 
                    $config->set('HTML.ForbiddenElements', explode(',', $this->getOption('stripTags')));
1086
 
                    $purifier = new \HTMLPurifier($config);
1087
 
                    $replace = $purifier->purify($replace);
1088
 
                }
1089
 
 
1090
 
                // Substitute the replacement we have found (it might be '')
1091
 
                $rowString = str_replace($sub, $replace, $rowString);
1092
 
            }
1093
 
 
1094
 
            $items[] = $rowString;
1095
 
        }
1096
 
 
1097
 
        // Process queued downloads
1098
 
        $this->mediaFactory->processDownloads(function($media) {
1099
 
            // Success
1100
 
            $this->getLog()->debug((($media->isSaveRequired) ? 'Successfully downloaded ' : 'Download not required for ') . $media->mediaId);
1101
 
 
1102
 
            // Tag this layout with this file
1103
 
            $this->assignMedia($media->mediaId);
1104
 
        });
1105
 
 
1106
 
        // Copyright information?
1107
 
        if ($this->getOption('copyright', '') != '') {
1108
 
            $items[] = '<span id="copyright">' . $this->getOption('copyright') . '</span>';
1109
 
        }
1110
 
 
1111
 
        return $items;
1112
 
    }
1113
 
 
1114
 
    /**
1115
 
     * @param $displayId
1116
 
     * @param $isPreview
1117
 
     * @param $text
1118
 
     * @return array
1119
 
     */
1120
 
    private function getDataSetItems($displayId, $isPreview, $text)
1121
 
    {
1122
 
        // Extra fields for data sets
1123
 
        $dataSetId = $this->getOption('dataSetId');
1124
 
        $upperLimit = $this->getOption('upperLimit');
1125
 
        $lowerLimit = $this->getOption('lowerLimit');
1126
 
 
1127
 
        // Ordering
1128
 
        $ordering = '';
1129
 
 
1130
 
        if ($this->getOption('useOrderingClause', 1) == 1) {
1131
 
            $ordering = $this->GetOption('ordering');
1132
 
        } else {
1133
 
            // Build an order string
1134
 
            foreach (json_decode($this->getOption('orderClauses', '[]'), true) as $clause) {
1135
 
                $ordering .= $clause['orderClause'] . ' ' . $clause['orderClauseDirection'] . ',';
1136
 
            }
1137
 
 
1138
 
            $ordering = rtrim($ordering, ',');
1139
 
        }
1140
 
 
1141
 
        // Filtering
1142
 
        $filter = '';
1143
 
 
1144
 
        if ($this->getOption('useFilteringClause', 1) == 1) {
1145
 
            $filter = $this->GetOption('filter');
1146
 
        } else {
1147
 
            // Build
1148
 
            $i = 0;
1149
 
            foreach (json_decode($this->getOption('filterClauses', '[]'), true) as $clause) {
1150
 
                $i++;
1151
 
                $criteria = '';
1152
 
 
1153
 
                switch ($clause['filterClauseCriteria']) {
1154
 
 
1155
 
                    case 'starts-with':
1156
 
                        $criteria = 'LIKE \'' . $clause['filterClauseValue'] . '%\'';
1157
 
                        break;
1158
 
 
1159
 
                    case 'ends-with':
1160
 
                        $criteria = 'LIKE \'%' . $clause['filterClauseValue'] . '\'';
1161
 
                        break;
1162
 
 
1163
 
                    case 'contains':
1164
 
                        $criteria = 'LIKE \'%' . $clause['filterClauseValue'] . '%\'';
1165
 
                        break;
1166
 
 
1167
 
                    case 'equals':
1168
 
                        $criteria = '= \'' . $clause['filterClauseValue'] . '\'';
1169
 
                        break;
1170
 
 
1171
 
                    case 'not-contains':
1172
 
                        $criteria = 'NOT LIKE \'%' . $clause['filterClauseValue'] . '%\'';
1173
 
                        break;
1174
 
 
1175
 
                    case 'not-starts-with':
1176
 
                        $criteria = 'NOT LIKE \'' . $clause['filterClauseValue'] . '%\'';
1177
 
                        break;
1178
 
 
1179
 
                    case 'not-ends-with':
1180
 
                        $criteria = 'NOT LIKE \'%' . $clause['filterClauseValue'] . '\'';
1181
 
                        break;
1182
 
 
1183
 
                    case 'not-equals':
1184
 
                        $criteria = '<> \'' . $clause['filterClauseValue'] . '\'';
1185
 
                        break;
1186
 
 
1187
 
                    case 'greater-than':
1188
 
                        $criteria = '> \'' . $clause['filterClauseValue'] . '\'';
1189
 
                        break;
1190
 
 
1191
 
                    case 'less-than':
1192
 
                        $criteria = '< \'' . $clause['filterClauseValue'] . '\'';
1193
 
                        break;
1194
 
 
1195
 
                    default:
1196
 
                        continue;
1197
 
                }
1198
 
 
1199
 
                if ($i > 1)
1200
 
                    $filter .= ' ' . $clause['filterClauseOperator'] . ' ';
1201
 
 
1202
 
                $filter .= $clause['filterClause'] . ' ' . $criteria;
1203
 
            }
1204
 
        }
1205
 
 
1206
 
        $this->getLog()->notice('Then template for each row is: ' . $text);
1207
 
 
1208
 
        // Set an expiry time for the media
1209
 
        $expires = time() + ($this->getOption('updateInterval', 3600) * 60);
1210
 
 
1211
 
        // Combine the column id's with the dataset data
1212
 
        $matches = '';
1213
 
        preg_match_all('/\[(.*?)\]/', $text, $matches);
1214
 
 
1215
 
        $columnIds = array();
1216
 
 
1217
 
        foreach ($matches[1] as $match) {
1218
 
            // Get the column id's we are interested in
1219
 
            $this->getLog()->notice('Matched column: ' . $match);
1220
 
 
1221
 
            $col = explode('|', $match);
1222
 
            $columnIds[] = $col[1];
1223
 
        }
1224
 
 
1225
 
        // Create a data set object, to get the results.
1226
 
        try {
1227
 
            $dataSet = $this->dataSetFactory->getById($dataSetId);
1228
 
 
1229
 
            // Get an array representing the id->heading mappings
1230
 
            $mappings = [];
1231
 
            foreach ($columnIds as $dataSetColumnId) {
1232
 
                // Get the column definition this represents
1233
 
                $column = $dataSet->getColumn($dataSetColumnId);
1234
 
                /* @var DataSetColumn $column */
1235
 
 
1236
 
                $mappings[$column->heading] = [
1237
 
                    'dataSetColumnId' => $dataSetColumnId,
1238
 
                    'heading' => $column->heading,
1239
 
                    'dataTypeId' => $column->dataTypeId
1240
 
                ];
1241
 
            }
1242
 
 
1243
 
            $this->getLog()->debug('Resolved column mappings: %s', json_encode($columnIds));
1244
 
 
1245
 
            $filter = [
1246
 
                'filter' => $filter,
1247
 
                'order' => $ordering,
1248
 
                'displayId' => $displayId
1249
 
            ];
1250
 
 
1251
 
            // limits?
1252
 
            if ($lowerLimit != 0 || $upperLimit != 0) {
1253
 
                // Start should be the lower limit
1254
 
                // Size should be the distance between upper and lower
1255
 
                $filter['start'] = $lowerLimit;
1256
 
                $filter['size'] = $upperLimit - $lowerLimit;
1257
 
            }
1258
 
 
1259
 
            // Set the timezone for SQL
1260
 
            $dateNow = $this->getDate()->parse();
1261
 
            if ($displayId != 0) {
1262
 
                $display = $this->displayFactory->getById($displayId);
1263
 
                $timeZone = $display->getSetting('displayTimeZone', '');
1264
 
                $timeZone = ($timeZone == '') ? $this->getConfig()->GetSetting('defaultTimezone') : $timeZone;
1265
 
                $dateNow->timezone($timeZone);
1266
 
                $this->getLog()->debug('Display Timezone Resolved: %s. Time: %s.', $timeZone, $dateNow->toDateTimeString());
1267
 
            }
1268
 
 
1269
 
            $this->getStore()->setTimeZone($this->getDate()->getLocalDate($dateNow, 'P'));
1270
 
 
1271
 
            // Get the data (complete table, filtered)
1272
 
            $dataSetResults = $dataSet->getData($filter);
1273
 
 
1274
 
            if (count($dataSetResults) <= 0)
1275
 
                throw new NotFoundException(__('Empty Result Set with filter criteria.'));
1276
 
 
1277
 
            $items = array();
1278
 
 
1279
 
            foreach ($dataSetResults as $row) {
1280
 
                // For each row, substitute into our template
1281
 
                $rowString = $text;
1282
 
 
1283
 
                foreach ($matches[1] as $sub) {
1284
 
                    // Pick the appropriate column out
1285
 
                    $subs = explode('|', $sub);
1286
 
 
1287
 
                    // The column header
1288
 
                    $header = $subs[0];
1289
 
                    $replace = $row[$header];
1290
 
 
1291
 
                    // If the value is empty, then move on
1292
 
                    if ($replace != '') {
1293
 
                        // Check in the columns array to see if this is a special one
1294
 
                        if ($mappings[$header]['dataTypeId'] == 4) {
1295
 
                            // External Image
1296
 
                            // Download the image, alter the replace to wrap in an image tag
1297
 
                            $file = $this->mediaFactory->queueDownload('ticker_dataset_' . md5($dataSetId . $mappings[$header]['dataSetColumnId'] . $replace), str_replace(' ', '%20', htmlspecialchars_decode($replace)), $expires);
1298
 
 
1299
 
                            $replace = '<img src="' . $this->getFileUrl($file, 'image') . '"/>';
1300
 
 
1301
 
                        } else if ($mappings[$header]['dataTypeId'] == 5) {
1302
 
                            // Library Image
1303
 
                            // The content is the ID of the image
1304
 
                            try {
1305
 
                                if ($replace !== 0) {
1306
 
                                    $file = $this->mediaFactory->getById($replace);
1307
 
 
1308
 
                                    // Tag this layout with this file
1309
 
                                    $this->assignMedia($file->mediaId);
1310
 
 
1311
 
                                    $replace = '<img src="' . $this->getFileUrl($file, 'image') . '" />';
1312
 
                                } else {
1313
 
                                    $replace = '';
1314
 
                                }
1315
 
                            }
1316
 
                            catch (NotFoundException $e) {
1317
 
                                $this->getLog()->error('Library Image [%s] not found in DataSetId %d.', $replace, $dataSetId);
1318
 
                                $replace = '';
1319
 
                            }
1320
 
                        }
1321
 
                    }
1322
 
 
1323
 
                    $rowString = str_replace('[' . $sub . ']', $replace, $rowString);
1324
 
                }
1325
 
 
1326
 
                $items[] = $rowString;
1327
 
            }
1328
 
 
1329
 
            // Process queued downloads
1330
 
            $this->mediaFactory->processDownloads(function($media) {
1331
 
                // Success
1332
 
                $this->getLog()->debug('Successfully downloaded ' . $media->mediaId);
1333
 
 
1334
 
                // Tag this layout with this file
1335
 
                $this->assignMedia($media->mediaId);
1336
 
            });
1337
 
 
1338
 
            return $items;
1339
 
        }
1340
 
        catch (NotFoundException $e) {
1341
 
            $this->getLog()->debug('getDataSetItems failed for id=%d. Widget=%d. Due to %s - this might be OK if we have a no-data message', $dataSetId, $this->getWidgetId(), $e->getMessage());
1342
 
            $this->getLog()->debug($e->getTraceAsString());
1343
 
            return [];
1344
 
        }
1345
 
    }
1346
 
 
1347
 
    public function isValid()
1348
 
    {
1349
 
        // Can't be sure because the client does the rendering
1350
 
        return 1;
1351
 
    }
1352
 
 
1353
 
    /** @inheritdoc */
1354
 
    public function getModifiedTimestamp($displayId)
1355
 
    {
1356
 
        $widgetModifiedDt = null;
1357
 
 
1358
 
        if ($this->getOption('sourceId', 1) == 2) {
1359
 
 
1360
 
            $dataSetId = $this->getOption('dataSetId');
1361
 
            $dataSet = $this->dataSetFactory->getById($dataSetId);
1362
 
 
1363
 
            // Set the timestamp
1364
 
            $widgetModifiedDt = $dataSet->lastDataEdit;
1365
 
 
1366
 
            // Remote dataSets are kept "active" by required files
1367
 
            if ($dataSet->isRemote) {
1368
 
                // Touch this dataSet
1369
 
                $dataSetCache = $this->getPool()->getItem('/dataset/accessed/' . $dataSet->dataSetId);
1370
 
                $dataSetCache->set('true');
1371
 
                $dataSetCache->expiresAfter($this->getSetting('REQUIRED_FILES_LOOKAHEAD') * 1.5);
1372
 
                $this->getPool()->saveDeferred($dataSetCache);
1373
 
            }
1374
 
        }
1375
 
 
1376
 
        return $widgetModifiedDt;
1377
 
    }
1378
 
 
1379
 
    /** @inheritdoc */
1380
 
    public function getCacheDuration()
1381
 
    {
1382
 
        return $this->getOption('updateInterval', 120) * 60;
1383
 
    }
1384
 
 
1385
 
    /** @inheritdoc */
1386
 
    public function getCacheKey($displayId)
1387
 
    {
1388
 
        if ($displayId === 0 || $this->getOption('sourceId', 1) == 2) {
1389
 
            // DataSets might use Display
1390
 
            return $this->getWidgetId() . '_' . $displayId;
1391
 
        } else {
1392
 
            // Tickers are non-display specific
1393
 
            return $this->getWidgetId() . (($displayId === 0) ? '_0' : '');
1394
 
        }
1395
 
    }
1396
 
 
1397
 
    /** @inheritdoc */
1398
 
    public function getLockKey()
1399
 
    {
1400
 
        if ($this->getOption('sourceId', 1) == 2) {
1401
 
            // Lock to the dataSetId, because our dataSet might have external images which are downloaded.
1402
 
            return $this->getOption('dataSetId');
1403
 
        } else {
1404
 
            // Tickers are locked to the feed
1405
 
            return md5(urldecode($this->getOption('uri')));
1406
 
        }
1407
 
    }
1408
 
}