9
9
namespace Xibo\Entity;
11
11
use Respect\Validation\Validator as v;
12
use Stash\Interfaces\PoolInterface;
13
use Xibo\Exception\ConfigurationException;
14
use Xibo\Exception\DuplicateEntityException;
15
use Xibo\Exception\InvalidArgumentException;
16
12
use Xibo\Exception\NotFoundException;
17
use Xibo\Exception\XiboException;
18
13
use Xibo\Factory\DataSetColumnFactory;
19
14
use Xibo\Factory\DataSetFactory;
20
15
use Xibo\Factory\DisplayFactory;
21
16
use Xibo\Factory\PermissionFactory;
22
use Xibo\Service\ConfigServiceInterface;
23
use Xibo\Service\DateServiceInterface;
24
use Xibo\Service\LogServiceInterface;
25
use Xibo\Service\SanitizerServiceInterface;
26
use Xibo\Storage\StorageServiceInterface;
17
use Xibo\Helper\Config;
19
use Xibo\Helper\Sanitize;
20
use Xibo\Storage\PDOConnect;
78
72
public $groupsWithPermissions;
81
* @SWG\Property(description="A code for this Data Set")
87
* @SWG\Property(description="Flag to indicate whether this DataSet is a lookup table")
93
* @SWG\Property(description="Flag to indicate whether this DataSet is Remote")
99
* @SWG\Property(description="Method to fetch the Data, can be GET or POST")
105
* @SWG\Property(description="URI to call to fetch Data from. Replacements are {{DATE}}, {{TIME}} and, in case this is a sequencial used DataSet, {{COL.NAME}} where NAME is a ColumnName from the underlying DataSet.")
111
* @SWG\Property(description="Data to send as POST-Data to the remote host with the same Replacements as in the URI.")
117
* @SWG\Property(description="Authentication method, can be none, digest, basic")
120
public $authentication;
123
* @SWG\Property(description="Username to authenticate with")
129
* @SWG\Property(description="Corresponding password")
135
* @SWG\Property(description="Time in seconds this DataSet should fetch new Datas from the remote host")
141
* @SWG\Property(description="Time in seconds when this Dataset should be cleared. If here is a lower value than in RefreshRate it will be cleared when the data is refreshed")
147
* @SWG\Property(description="DataSetID of the DataSet which should be fetched and present before the Data from this DataSet are fetched")
153
* @SWG\Property(description="Last Synchronisation Timestamp")
156
public $lastSync = 0;
159
* @SWG\Property(description="Root-Element form JSON where the data are stored in")
165
* @SWG\Property(description="Optional function to use for summarize or count unique fields in a remote request")
171
* @SWG\Property(description="JSON-Element below the Root-Element on which the consolidation should be applied on")
174
public $summarizeField;
176
/** @var array Permissions */
177
74
private $permissions = [];
180
* @var DataSetColumn[]
182
public $columns = [];
75
private $columns = [];
184
77
private $countLast = 0;
186
/** @var array Blacklist for SQL */
187
private $blackList = array(';', 'INSERT', 'UPDATE', 'SELECT', 'DELETE', 'TRUNCATE', 'TABLE', 'FROM', 'WHERE');
189
/** @var SanitizerServiceInterface */
192
/** @var ConfigServiceInterface */
195
/** @var PoolInterface */
198
/** @var DataSetFactory */
199
private $dataSetFactory;
201
/** @var DataSetColumnFactory */
202
private $dataSetColumnFactory;
204
/** @var PermissionFactory */
205
private $permissionFactory;
207
/** @var DisplayFactory */
208
private $displayFactory;
210
/** @var DateServiceInterface */
214
* Entity constructor.
215
* @param StorageServiceInterface $store
216
* @param LogServiceInterface $log
217
* @param SanitizerServiceInterface $sanitizer
218
* @param ConfigServiceInterface $config
219
* @param PoolInterface $pool
220
* @param DataSetFactory $dataSetFactory
221
* @param DataSetColumnFactory $dataSetColumnFactory
222
* @param PermissionFactory $permissionFactory
223
* @param DisplayFactory $displayFactory
224
* @param DateServiceInterface $date
226
public function __construct($store, $log, $sanitizer, $config, $pool, $dataSetFactory, $dataSetColumnFactory, $permissionFactory, $displayFactory, $date)
228
$this->setCommonDependencies($store, $log);
229
$this->sanitizer = $sanitizer;
230
$this->config = $config;
232
$this->dataSetFactory = $dataSetFactory;
233
$this->dataSetColumnFactory = $dataSetColumnFactory;
234
$this->permissionFactory = $permissionFactory;
235
$this->displayFactory = $displayFactory;
242
public function __clone()
244
$this->dataSetId = null;
246
$this->columns = array_map(function ($object) { return clone $object; }, $this->columns);
252
79
public function getId()
254
81
return $this->dataSetId;
260
84
public function getOwnerId()
262
86
return $this->userId;
301
* @param string $dataSetColumn
302
* @return DataSetColumn[]|DataSetColumn
303
* @throws NotFoundException when the heading is provided and the column cannot be found
305
public function getColumnByName($dataSetColumn)
309
foreach ($this->columns as $column) {
310
/* @var DataSetColumn $column */
311
if ($column->heading == $dataSetColumn)
315
throw new NotFoundException(sprintf(__('Column %s not found'), $dataSetColumn));
319
* @param string[] $columns Column Names to select
322
public function getUniqueColumnValues($columns)
327
foreach ($columns as $heading) {
330
foreach ($this->columns as $column) {
331
if ($column->heading == $heading) {
333
if ($column->dataSetColumnTypeId == 2) {
334
$select .= str_replace($this->blackList, '', htmlspecialchars_decode($column->formula, ENT_QUOTES)) . ' AS `' . $column->heading . '`,';
337
$select .= '`' . $column->heading . '`,';
345
throw new \InvalidArgumentException(__('Unknown Column ' . $heading));
347
$select = rtrim($select, ',');
350
return $this->getStore()->select('SELECT DISTINCT ' . $select . ' FROM `dataset_' . $this->dataSetId . '`', []);
354
124
* Get DataSet Data
355
125
* @param array $filterBy
356
* @param array $options
358
* @throws NotFoundException
360
public function getData($filterBy = [], $options = [])
128
public function getData($filterBy = [])
362
$this->touchLastAccessed();
364
$start = $this->sanitizer->getInt('start', 0, $filterBy);
365
$size = $this->sanitizer->getInt('size', 0, $filterBy);
366
$filter = $this->sanitizer->getParam('filter', $filterBy);
367
$ordering = $this->sanitizer->getString('order', $filterBy);
368
$displayId = $this->sanitizer->getInt('displayId', 0, $filterBy);
370
$options = array_merge([
371
'includeFormulaColumns' => true,
372
'requireTotal' => true
130
$start = Sanitize::getInt('start', 0, $filterBy);
131
$size = Sanitize::getInt('size', 0, $filterBy);
132
$filter = Sanitize::getString('filter', $filterBy);
133
$ordering = Sanitize::getString('order', $filterBy);
134
$displayId = Sanitize::getInt('displayId', 0, $filterBy);
378
139
// Sanitize the filter options provided
140
$blackList = array(';', 'INSERT', 'UPDATE', 'SELECT', 'DELETE', 'TRUNCATE', 'TABLE', 'FROM', 'WHERE');
379
142
// Get the Latitude and Longitude ( might be used in a formula )
380
143
if ($displayId == 0) {
381
$displayGeoLocation = "GEOMFROMTEXT('POINT(" . $this->config->GetSetting('DEFAULT_LAT') . " " . $this->config->GetSetting('DEFAULT_LONG') . ")')";
144
$displayGeoLocation = "GEOMFROMTEXT('POINT(" . Config::GetSetting('DEFAULT_LAT') . " " . Config::GetSetting('DEFAULT_LONG') . ")')";
384
147
$displayGeoLocation = '(SELECT GeoLocation FROM `display` WHERE DisplayID = :displayId)';
388
151
// Build a SQL statement, based on the columns for this dataset
391
$select = 'SELECT * FROM ( ';
154
$select = 'SELECT id';
394
156
// Keep track of the columns we are allowed to order by
395
157
$allowedOrderCols = ['id'];
397
// Are there any client side formulas
398
$clientSideFormula = [];
400
159
// Select (columns)
401
160
foreach ($this->getColumn() as $column) {
402
161
/* @var DataSetColumn $column */
403
162
$allowedOrderCols[] = $column->heading;
405
if ($column->dataSetColumnTypeId == 2 && !$options['includeFormulaColumns'])
408
164
// Formula column?
409
165
if ($column->dataSetColumnTypeId == 2) {
411
// Is this a client side column?
412
if (substr($column->formula, 0, 1) === '$') {
413
$clientSideFormula[] = $column;
417
$formula = str_replace($this->blackList, '', htmlspecialchars_decode($column->formula, ENT_QUOTES));
418
$formula = str_replace('[DisplayId]', $displayId, $formula);
420
$heading = str_replace('[DisplayGeoLocation]', $displayGeoLocation, $formula) . ' AS `' . $column->heading . '`';
166
$formula = str_replace($blackList, '', htmlspecialchars_decode($column->formula, ENT_QUOTES));
168
$heading = str_replace('[DisplayGeoLocation]', $displayGeoLocation, $formula) . ' AS \'' . $column->heading . '\'';
423
171
$heading = '`' . $column->heading . '`';
426
$body .= ', ' . $heading;
174
$select .= ', ' . $heading;
429
$body .= ' FROM `dataset_' . $this->dataSetId . '`) dataset WHERE 1 = 1 ';
177
$body = ' FROM `dataset_' . $this->dataSetId . '` WHERE 1 = 1 ';
432
180
if ($filter != '') {
433
// Support display filtering.
434
$filter = str_replace('[DisplayId]', $displayId, $filter);
435
$filter = str_replace($this->blackList, '', $filter);
437
$body .= ' AND ' . $filter;
181
$body .= ' AND ' . str_replace($blackList, '', $filter);
442
$this->sanitizer->getInt('id', $filterBy) !== null) {
186
Sanitize::getInt('id', $filterBy) !== null) {
443
187
$body .= ' AND id = :id ';
444
$params['id'] = $this->sanitizer->getInt('id', $filterBy);
188
$params['id'] = Sanitize::getInt('id', $filterBy);
489
230
$sql = $select . $body . $order . $limit;
491
$data = $this->getStore()->select($sql, $params);
232
Log::sql($sql, $params);
234
$data = PDOConnect::select($sql, $params);
493
236
// If there are limits run some SQL to work out the full payload of rows
494
if ($options['requireTotal']) {
495
$results = $this->getStore()->select('SELECT COUNT(*) AS total FROM (' . $body, $params);
496
$this->countLast = intval($results[0]['total']);
499
// Are there any client side formulas?
500
if (count($clientSideFormula) > 0) {
502
foreach ($data as $item) {
503
foreach ($clientSideFormula as $column) {
504
// Run the formula and add the resulting value to the list
507
if (substr($column->formula, 0, strlen('$dateFormat(')) === '$dateFormat(') {
508
// Pull out the column name and date format
509
$details = explode(',', str_replace(')', '', str_replace('$dateFormat(', '', $column->formula)));
511
$value = $this->date->parse($item[$details[0]])->format($details[1]);
513
} catch (\Exception $e) {
514
$this->getLog()->error('DataSet client side formula error in dataSetId ' . $this->dataSetId . ' with column formula ' . $column->formula);
517
$item[$column->heading] = $value;
520
$renderedData[] = $item;
523
$renderedData = $data;
526
return $renderedData;
237
$results = PDOConnect::select('SELECT COUNT(*) AS total ' . $body, $params);
238
$this->countLast = intval($results[0]['total']);
551
262
public function hasData()
553
return $this->getStore()->exists('SELECT id FROM `dataset_' . $this->dataSetId . '` LIMIT 1', []);
557
* Returns a Timestamp for the next Synchronisation process.
558
* @return int Seconds
560
public function getNextSyncTime()
562
return $this->lastSync + $this->refreshRate;
568
public function isTruncateEnabled()
570
return $this->clearRate === 0;
574
* Returns a Timestamp for the next Clearing process.
575
* @return int Seconds
577
public function getNextClearTime()
579
return $this->lastSync + $this->clearRate;
583
* Returns if there is a consolidation field and method present or not.
586
public function doConsolidate()
588
return ($this->summarizeField != null) && ($this->summarizeField != '')
589
&& ($this->summarize != null) && ($this->summarize != '');
593
* Returns the last Part of the Fieldname on which the consolidation should be applied on
596
public function getConsolidationField()
598
$pos = strrpos($this->summarizeField, '.');
599
if ($pos !== false) {
600
return substr($this->summarizeField, $pos + 1);
602
return $this->summarizeField;
606
* Tests if this DataSet contains parameters for getting values on the dependant DataSet
609
public function containsDependantFieldsInRequest()
611
return strpos($this->postData, '{{COL.') !== false || strpos($this->uri, '{{COL.') !== false;
264
return PDOConnect::exists('SELECT id FROM `dataset_' . $this->dataSetId . '` LIMIT 1', []);
616
* @throws InvalidArgumentException
617
* @throws DuplicateEntityException
619
270
public function validate()
621
if (!v::stringType()->notEmpty()->length(null, 50)->validate($this->dataSet))
622
throw new InvalidArgumentException(__('Name must be between 1 and 50 characters'), 'dataSet');
624
if ($this->description != null && !v::stringType()->length(null, 254)->validate($this->description))
625
throw new InvalidArgumentException(__('Description can not be longer than 254 characters'), 'description');
627
// If we are a remote dataset do some additional checks
628
if ($this->isRemote === 1) {
629
if (!v::stringType()->notEmpty()->validate($this->uri))
630
throw new InvalidArgumentException(__('A remote DataSet must have a URI.'), 'uri');
272
if (!v::string()->notEmpty()->length(null, 50)->validate($this->dataSet))
273
throw new \InvalidArgumentException(__('Name must be between 1 and 50 characters'));
275
if ($this->description != null && !v::string()->length(null, 254)->validate($this->description))
276
throw new \InvalidArgumentException(__('Description can not be longer than 254 characters'));
634
$existing = $this->dataSetFactory->getByName($this->dataSet, $this->userId);
279
$existing = DataSetFactory::getByName($this->dataSet);
636
281
if ($this->dataSetId == 0 || $this->dataSetId != $existing->dataSetId)
637
throw new DuplicateEntityException(sprintf(__('There is already dataSet called %s. Please choose another name.'), $this->dataSet));
282
throw new \InvalidArgumentException(sprintf(__('There is already dataSet called %s. Please choose another name.'), $this->dataSet));
639
284
catch (NotFoundException $e) {
688
// We've been touched
689
$this->touchLastAccessed();
691
331
// Notify Displays?
699
public function saveLastSync($time)
701
$this->lastSync = $time;
703
$this->getStore()->update('UPDATE `dataset` SET lastSync = :lastSync WHERE dataSetId = :dataSetId', [
704
'dataSetId' => $this->dataSetId,
705
'lastSync' => $this->lastSync
711
private function touchLastAccessed()
713
// Touch this dataSet
714
$dataSetCache = $this->pool->getItem('/dataset/accessed/' . $this->dataSetId);
715
$dataSetCache->set('true');
716
$dataSetCache->expiresAfter(intval($this->config->GetSetting('REQUIRED_FILES_LOOKAHEAD')) * 1.5);
717
$this->pool->saveDeferred($dataSetCache);
722
* @throws ConfigurationException
723
* @throws InvalidArgumentException
725
338
public function delete()
730
throw new ConfigurationException(__('Lookup Tables cannot be deleted'));
732
// TODO: Make sure we're not used as a dependent DataSet
734
// Make sure we're able to delete
735
if ($this->getStore()->exists('
738
WHERE `widgetoption`.type = \'attrib\'
739
AND `widgetoption`.option = \'dataSetId\'
740
AND `widgetoption`.value = :dataSetId
741
', ['dataSetId' => $this->dataSetId])) {
742
throw new InvalidArgumentException('Cannot delete because DataSet is in use on one or more Layouts.', 'dataSetId');
342
// TODO check we aren't being used
745
344
// Delete Permissions
746
345
foreach ($this->permissions as $permission) {
777
373
private function add()
779
$columns = 'DataSet, Description, UserID, `code`, `isLookup`, `isRemote`';
780
$values = ':dataSet, :description, :userId, :code, :isLookup, :isRemote';
783
'dataSet' => $this->dataSet,
784
'description' => $this->description,
785
'userId' => $this->userId,
786
'code' => ($this->code == '') ? null : $this->code,
787
'isLookup' => $this->isLookup,
788
'isRemote' => $this->isRemote
791
// Insert the extra columns we expect for a remote DataSet
792
if ($this->isRemote === 1) {
793
$columns .= ', `method`, `uri`, `postData`, `authentication`, `username`, `password`, `refreshRate`, `clearRate`, `runsAfter`, `dataRoot`, `lastSync`, `summarize`, `summarizeField`';
794
$values .= ', :method, :uri, :postData, :authentication, :username, :password, :refreshRate, :clearRate, :runsAfter, :dataRoot, :lastSync, :summarize, :summarizeField';
796
$params['method'] = $this->method;
797
$params['uri'] = $this->uri;
798
$params['postData'] = $this->postData;
799
$params['authentication'] = $this->authentication;
800
$params['username'] = $this->username;
801
$params['password'] = $this->password;
802
$params['refreshRate'] = $this->refreshRate;
803
$params['clearRate'] = $this->clearRate;
804
$params['runsAfter'] = $this->runsAfter;
805
$params['dataRoot'] = $this->dataRoot;
806
$params['summarize'] = $this->summarize;
807
$params['summarizeField'] = $this->summarizeField;
808
$params['lastSync'] = 0;
812
$this->dataSetId = $this->getStore()->insert('INSERT INTO `dataset` (' . $columns . ') VALUES (' . $values . ')', $params);
814
// Create the data table for this dataSet
815
$this->createTable();
821
private function edit()
823
$sql = 'DataSet = :dataSet, Description = :description, lastDataEdit = :lastDataEdit, `code` = :code, `isLookup` = :isLookup, `isRemote` = :isRemote ';
825
'dataSetId' => $this->dataSetId,
826
'dataSet' => $this->dataSet,
827
'description' => $this->description,
828
'lastDataEdit' => $this->lastDataEdit,
829
'code' => $this->code,
830
'isLookup' => $this->isLookup,
831
'isRemote' => $this->isRemote,
834
if ($this->isRemote) {
835
$sql .= ', method = :method, uri = :uri, postData = :postData, authentication = :authentication, `username` = :username, `password` = :password, refreshRate = :refreshRate, clearRate = :clearRate, runsAfter = :runsAfter, `dataRoot` = :dataRoot, `summarize` = :summarize, `summarizeField` = :summarizeField ';
837
$params['method'] = $this->method;
838
$params['uri'] = $this->uri;
839
$params['postData'] = $this->postData;
840
$params['authentication'] = $this->authentication;
841
$params['username'] = $this->username;
842
$params['password'] = $this->password;
843
$params['refreshRate'] = $this->refreshRate;
844
$params['clearRate'] = $this->clearRate;
845
$params['runsAfter'] = $this->runsAfter;
846
$params['dataRoot'] = $this->dataRoot;
847
$params['summarize'] = $this->summarize;
848
$params['summarizeField'] = $this->summarizeField;
851
$this->getStore()->update('UPDATE dataset SET ' . $sql . ' WHERE DataSetID = :dataSetId', $params);
855
* Create the realised table structure for this DataSet
857
private function createTable()
375
$this->dataSetId = PDOConnect::insert('
376
INSERT INTO `dataset` (DataSet, Description, UserID)
377
VALUES (:dataSet, :description, :userId)
379
'dataSet' => $this->dataSet,
380
'description' => $this->description,
381
'userId' => $this->userId
859
384
// Create the data table for this dataset
860
$this->getStore()->update('
861
386
CREATE TABLE `dataset_' . $this->dataSetId . '` (
862
387
`id` int(11) NOT NULL AUTO_INCREMENT,
863
388
PRIMARY KEY (`id`)
868
private function dropTable()
870
$this->getStore()->isolated('DROP TABLE IF EXISTS dataset_' . $this->dataSetId, []);
874
* Rebuild the dataSet table
875
* @throws XiboException
877
public function rebuild()
396
private function edit()
881
// Drop the data table
884
// Add the data table
885
$this->createTable();
887
foreach ($this->columns as $column) {
888
/* @var \Xibo\Entity\DataSetColumn $column */
889
$column->dataSetId = $this->dataSetId;
890
$column->save(['rebuilding' => true]);
399
UPDATE dataset SET DataSet = :dataSet, Description = :description, lastDataEdit = :lastDataEdit WHERE DataSetID = :dataSetId
401
'dataSetId' => $this->dataSetId,
402
'dataSet' => $this->dataSet,
403
'description' => $this->description,
404
'lastDataEdit' => $this->lastDataEdit