Files

1413 lines
45 KiB
PHP
Raw Permalink Normal View History

2025-12-02 10:32:59 -05:00
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Entity;
use Carbon\Carbon;
use Carbon\Factory;
use Respect\Validation\Validator as v;
use Stash\Interfaces\PoolInterface;
use Xibo\Factory\DataSetColumnFactory;
use Xibo\Factory\DataSetFactory;
use Xibo\Factory\PermissionFactory;
use Xibo\Helper\SanitizerService;
use Xibo\Service\ConfigServiceInterface;
use Xibo\Service\DisplayNotifyServiceInterface;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\ConfigurationException;
use Xibo\Support\Exception\DuplicateEntityException;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
use Xibo\Widget\Definition\Sql;
/**
* Class DataSet
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class DataSet implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The dataSetId")
* @var int
*/
public $dataSetId;
/**
* @SWG\Property(description="The dataSet Name")
* @var string
*/
public $dataSet;
/**
* @SWG\Property(description="The dataSet description")
* @var string
*/
public $description;
/**
* @SWG\Property(description="The userId of the User that owns this DataSet")
* @var int
*/
public $userId;
/**
* @SWG\Property(description="Timestamp indicating the date/time this DataSet was edited last")
* @var int
*/
public $lastDataEdit;
/**
* @SWG\Property(description="The user name of the User that owns this DataSet")
* @var string
*/
public $owner;
/**
* @SWG\Property(description="A comma separated list of Groups/Users that have permission to this DataSet")
* @var string
*/
public $groupsWithPermissions;
/**
* @SWG\Property(description="A code for this Data Set")
* @var string
*/
public $code;
/**
* @SWG\Property(description="Flag to indicate whether this DataSet is a lookup table")
* @var int
*/
public $isLookup = 0;
/**
* @SWG\Property(description="Flag to indicate whether this DataSet is Remote")
* @var int
*/
public $isRemote = 0;
/**
* @SWG\Property(description="Flag to indicate whether this DataSet is Real time")
* @var int
*/
public $isRealTime = 0;
/**
* @SWG\Property(description="Indicates the source of the data connector. Requires the Real time flag. Can be null,
* user-defined, or a connector.")
* @var string
*/
public $dataConnectorSource;
/**
* @SWG\Property(description="Method to fetch the Data, can be GET or POST")
* @var string
*/
public $method;
/**
* @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.")
* @var string
*/
public $uri;
/**
* @SWG\Property(description="Data to send as POST-Data to the remote host with the same Replacements as in the URI.")
* @var string
*/
public $postData;
/**
* @SWG\Property(description="Authentication method, can be none, digest, basic")
* @var string
*/
public $authentication;
/**
* @SWG\Property(description="Username to authenticate with")
* @var string
*/
public $username;
/**
* @SWG\Property(description="Corresponding password")
* @var string
*/
public $password;
/**
* @SWG\Property(description="Comma separated string of custom HTTP headers")
* @var string
*/
public $customHeaders;
/**
* @SWG\Property(description="Custom User agent")
* @var string
*/
public $userAgent;
/**
* @SWG\Property(description="Time in seconds this DataSet should fetch new Datas from the remote host")
* @var int
*/
public $refreshRate;
/**
* @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")
* @var int
*/
public $clearRate;
/**
* @SWG\Property(description="Flag whether to truncate DataSet data if no new data is pulled from remote source")
* @var int
*/
public $truncateOnEmpty;
/**
* @SWG\Property(description="DataSetID of the DataSet which should be fetched and present before the Data from this DataSet are fetched")
* @var int
*/
public $runsAfter;
/**
* @SWG\Property(description="Last Synchronisation Timestamp")
* @var int
*/
public $lastSync = 0;
/**
* @SWG\Property(description="Last Clear Timestamp")
* @var int
*/
public $lastClear = 0;
/**
* @SWG\Property(description="Root-Element form JSON where the data are stored in")
* @var String
*/
public $dataRoot;
/**
* @SWG\Property(description="Optional function to use for summarize or count unique fields in a remote request")
* @var String
*/
public $summarize;
/**
* @SWG\Property(description="JSON-Element below the Root-Element on which the consolidation should be applied on")
* @var String
*/
public $summarizeField;
/**
* @SWG\Property(description="The source id for remote dataSet, 1 - JSON, 2 - CSV")
* @var integer
*/
public $sourceId;
/**
* @SWG\Property(description="A flag whether to ignore the first row, for CSV source remote dataSet")
* @var integer
*/
public $ignoreFirstRow;
/**
* @SWG\Property(description="Soft limit on number of rows per DataSet, if left empty the global DataSet row limit will be used.")
* @var integer
*/
public $rowLimit = null;
/**
* @SWG\Property(description="Type of action that should be taken on next remote DataSet sync - stop, fifo or truncate")
* @var string
*/
public $limitPolicy;
/**
* @SWG\Property(description="Custom separator for CSV source, comma will be used by default")
* @var string
*/
public $csvSeparator;
/**
* @SWG\Property(description="The id of the Folder this DataSet belongs to")
* @var int
*/
public $folderId;
/**
* @SWG\Property(description="The id of the Folder responsible for providing permissions for this DataSet")
* @var int
*/
public $permissionsFolderId;
/** @var array Permissions */
private $permissions = [];
/**
* @var DataSetColumn[]
*/
public $columns = [];
private $countLast = 0;
/** @var \Xibo\Helper\SanitizerService */
private $sanitizerService;
/** @var ConfigServiceInterface */
private $config;
/** @var PoolInterface */
private $pool;
/** @var DataSetFactory */
private $dataSetFactory;
/** @var DataSetColumnFactory */
private $dataSetColumnFactory;
/** @var PermissionFactory */
private $permissionFactory;
/** @var DisplayNotifyServiceInterface */
private $displayNotifyService;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
* @param SanitizerService $sanitizerService
* @param ConfigServiceInterface $config
* @param PoolInterface $pool
* @param DataSetFactory $dataSetFactory
* @param DataSetColumnFactory $dataSetColumnFactory
* @param PermissionFactory $permissionFactory
* @param DisplayNotifyServiceInterface $displayNotifyService
*/
public function __construct($store, $log, $dispatcher, $sanitizerService, $config, $pool, $dataSetFactory, $dataSetColumnFactory, $permissionFactory, $displayNotifyService)
{
$this->setCommonDependencies($store, $log, $dispatcher);
$this->sanitizerService = $sanitizerService;
$this->config = $config;
$this->pool = $pool;
$this->dataSetFactory = $dataSetFactory;
$this->dataSetColumnFactory = $dataSetColumnFactory;
$this->permissionFactory = $permissionFactory;
$this->displayNotifyService = $displayNotifyService;
}
/**
* @param $array
* @return \Xibo\Support\Sanitizer\SanitizerInterface
*/
protected function getSanitizer($array)
{
return $this->sanitizerService->getSanitizer($array);
}
/**
* Clone
*/
public function __clone()
{
$this->dataSetId = null;
$this->columns = array_map(function ($object) { return clone $object; }, $this->columns);
}
/**
* @return int
*/
public function getId()
{
return $this->dataSetId;
}
public function getPermissionFolderId()
{
return $this->permissionsFolderId;
}
/**
* @return int
*/
public function getOwnerId()
{
return $this->userId;
}
/**
* Set the owner of this DataSet
* @param $userId
*/
public function setOwner($userId)
{
$this->userId = $userId;
}
/**
* Get the Count of Records in the last getData()
* @return int
*/
public function countLast()
{
return $this->countLast;
}
/**
* Get the Display Notify Service
* @return DisplayNotifyServiceInterface
*/
public function getDisplayNotifyService(): DisplayNotifyServiceInterface
{
return $this->displayNotifyService->init();
}
/**
* Get Column
* @param int[Optional] $dataSetColumnId
* @return DataSetColumn[]|DataSetColumn
* @throws NotFoundException when the heading is provided and the column cannot be found
*/
public function getColumn($dataSetColumnId = 0)
{
$this->load();
if ($dataSetColumnId != 0) {
foreach ($this->columns as $column) {
/* @var DataSetColumn $column */
if ($column->dataSetColumnId == $dataSetColumnId)
return $column;
}
throw new NotFoundException(sprintf(__('Column %s not found'), $dataSetColumnId));
} else {
return $this->columns;
}
}
/**
* Get Column
* @param string $dataSetColumn
* @return DataSetColumn[]|DataSetColumn
* @throws NotFoundException when the heading is provided and the column cannot be found
*/
public function getColumnByName($dataSetColumn)
{
$this->load();
foreach ($this->columns as $column) {
/* @var DataSetColumn $column */
if ($column->heading == $dataSetColumn)
return $column;
}
throw new NotFoundException(sprintf(__('Column %s not found'), $dataSetColumn));
}
/**
* @param string[] $columns Column Names to select
* @return array
* @throws InvalidArgumentException
*/
public function getUniqueColumnValues($columns)
{
$this->load();
$select = '';
foreach ($columns as $heading) {
// Check this exists
$found = false;
foreach ($this->columns as $column) {
if ($column->heading == $heading) {
// Formula column?
if ($column->dataSetColumnTypeId == 2) {
$select .= str_replace(
Sql::DISALLOWED_KEYWORDS,
'',
htmlspecialchars_decode($column->formula, ENT_QUOTES)
) . ' AS `' . $column->heading . '`,';
} else {
$select .= '`' . $column->heading . '`,';
}
$found = true;
break;
}
}
if (!$found) {
throw new InvalidArgumentException(__('Unknown Column ' . $heading));
}
}
$select = rtrim($select, ',');
// $select is safe
return $this->getStore()->select('SELECT DISTINCT ' . $select . ' FROM `dataset_' . $this->dataSetId . '`', []);
}
/**
* Get DataSet Data
* @param array $filterBy
* @param array $options
* @param array $extraParams Extra params to apply to the final query
* @return array
* @throws NotFoundException
*/
public function getData($filterBy = [], $options = [], $extraParams = [])
{
$sanitizer = $this->getSanitizer($filterBy);
$start = $sanitizer->getInt('start', ['default' => 0]);
$size = $sanitizer->getInt('size', ['default' => 0]);
$filter = $filterBy['filter'] ?? '';
$ordering = $sanitizer->getString('order');
$displayId = $sanitizer->getInt('displayId', ['default' => 0]);
$options = array_merge([
'includeFormulaColumns' => true,
'requireTotal' => true,
'connection' => 'default'
], $options);
// Params (start from extraParams supplied)
$params = $extraParams;
// Fetch display tag value/s
if ($filter != '' && $displayId != 0) {
// Define the regular expression to match [Tag:...]
$pattern = '/\[Tag:[^]]+\]/';
// Find all instances of [Tag:...]
preg_match_all($pattern, $filter, $matches);
// Check if matches were found
if (!empty($matches[0])) {
$displayTags = [];
// Iterate through the matches and process each tag
foreach ($matches[0] as $tagString) {
// Remove the enclosing [Tag:] brackets
$tagContent = substr($tagString, 5, -1);
// Explode the tag content by ":" to separate tagName and defaultValue (if present)
$parts = explode(':', $tagContent);
$tagName = $parts[0];
$defaultTagValue = $parts[1] ?? '';
$displayTags[] = [
'tagString' => $tagString,
'tagName' => $tagName,
'defaultValue' => $defaultTagValue
];
}
$tagCount = 1;
// Loop through each tag and get the actual tag value from the database
foreach ($displayTags as $tag) {
$tagSanitizer = $this->getSanitizer($tag);
$tagName = $tagSanitizer->getString('tagName');
$defaultTagValue = $tagSanitizer->getString('defaultValue');
$tagString = $tag['tagString'];
$query = 'SELECT `lktagdisplaygroup`.`value` AS tagValue
FROM `lkdisplaydg`
INNER JOIN `displaygroup`
ON `displaygroup`.displayGroupId = `lkdisplaydg`.displayGroupId
AND `displaygroup`.isDisplaySpecific = 1
INNER JOIN `lktagdisplaygroup`
ON `lktagdisplaygroup`.displayGroupId = `lkdisplaydg`.displayGroupId
INNER JOIN `tag` ON `lktagdisplaygroup`.tagId = `tag`.tagId
WHERE `lkdisplaydg`.displayId = :displayId
AND `tag`.`tag` = :tagName
LIMIT 1';
$tagParams = [
'displayId' => $displayId,
'tagName' => $tagName
];
// Execute the query
$results = $this->getStore()->select($query, $tagParams);
// Determine the tag value
if (!empty($results)) {
$tagValue = !empty($results[0]['tagValue']) ? $results[0]['tagValue'] : '';
} else {
// Use default tag value if no tag is found
$tagValue = $defaultTagValue;
}
// Replace the tag string in the filter with the actual tag value or default value
$filter = str_replace($tagString, ':tagValue_'.$tagCount, $filter);
$params['tagValue_'.$tagCount] = $tagValue;
$tagCount++;
}
}
}
// Sanitize the filter options provided
// Get the Latitude and Longitude ( might be used in a formula )
if ($displayId == 0) {
$displayGeoLocation =
"ST_GEOMFROMTEXT('POINT(" . $this->config->getSetting('DEFAULT_LAT') .
' ' . $this->config->getSetting('DEFAULT_LONG') . ")')";
} else {
$displayGeoLocation = '(SELECT GeoLocation FROM `display` WHERE DisplayID =' . $displayId. ')';
}
// Build a SQL statement, based on the columns for this dataset
$this->load();
$select = 'SELECT * FROM ( ';
$body = 'SELECT id';
// Keep track of the columns we are allowed to order by
$allowedOrderCols = ['id'];
// Are there any client side formulas
$clientSideFormula = [];
// Select (columns)
foreach ($this->getColumn() as $column) {
/* @var DataSetColumn $column */
if ($column->dataSetColumnTypeId == 2 && !$options['includeFormulaColumns']) {
continue;
}
// Formula column?
if ($column->dataSetColumnTypeId == 2) {
// Is this a client side column?
if (str_starts_with($column->formula, '$')) {
$clientSideFormula[] = $column;
continue;
}
$count = 0;
$formula = str_ireplace(
Sql::DISALLOWED_KEYWORDS,
'',
htmlspecialchars_decode($column->formula, ENT_QUOTES),
$count
);
if ($count > 0) {
$this->getLog()->error(
'Formula contains disallowed keywords on DataSet ID ' . $this->dataSetId
);
continue;
}
$formula = str_replace('[DisplayId]', $displayId, $formula);
$heading = str_replace('[DisplayGeoLocation]', $displayGeoLocation, $formula)
. ' AS `' . $column->heading . '`';
} else {
$heading = '`' . $column->heading . '`';
}
$allowedOrderCols[] = $column->heading;
$body .= ', ' . $heading;
}
$body .= ' FROM `dataset_' . $this->dataSetId . '`) dataset WHERE 1 = 1 ';
// Filtering
if ($filter != '') {
// Support display filtering.
$filter = str_replace('[DisplayId]', $displayId, $filter);
$filter = str_ireplace(Sql::DISALLOWED_KEYWORDS, '', $filter);
$body .= ' AND ' . $filter;
}
// Filter by ID
if ($sanitizer->getInt('id') !== null) {
$body .= ' AND id = :id ';
$params['id'] = $sanitizer->getInt('id');
}
// Ordering
$order = '';
if ($ordering != '') {
$order = ' ORDER BY ';
$ordering = explode(',', $ordering);
foreach ($ordering as $orderPair) {
// Sanitize the clause
$sanitized = str_replace('`', '', str_replace(' ASC', '', str_replace(' DESC', '', $orderPair)));
// Check allowable
if (!in_array($sanitized, $allowedOrderCols)) {
$found = false;
$this->getLog()->info('Potentially disallowed column: ' . $sanitized);
// the gridRenderSort will strip spaces on column names go through allowed order columns
// and see if we can find a match by stripping spaces from the heading
foreach ($allowedOrderCols as $allowedOrderCol) {
$this->getLog()->info('Checking spaces in original name : ' . $sanitized);
if (str_replace(' ', '', $allowedOrderCol) === $sanitized) {
$found = true;
// put the column heading with the space as sanitized to make sql happy.
$sanitized = $allowedOrderCol;
}
}
// we tried, but it was not found, omit this pair
if (!$found) {
continue;
}
}
// Substitute
if (strripos($orderPair, ' DESC')) {
$order .= sprintf(' `%s` DESC,', $sanitized);
} else if (strripos($orderPair, ' ASC')) {
$order .= sprintf(' `%s` ASC,', $sanitized);
} else {
$order .= sprintf(' `%s`,', $sanitized);
}
}
$order = trim($order, ',');
// if after all that we still do not have any column name to order by, default to order by id
if (trim($order) === 'ORDER BY') {
$order = ' ORDER BY id ';
}
} else {
$order = ' ORDER BY id ';
}
// Limit
$limit = '';
if ($start != 0 || $size != 0) {
// Substitute in
// handle case where lower limit is set to > 0 and upper limit to 0 https://github.com/xibosignage/xibo/issues/2187
// it is with <= 0 because in some Widgets we calculate the size as upper - lower, https://github.com/xibosignage/xibo/issues/2263.
if ($start != 0 && $size <= 0) {
$size = PHP_INT_MAX;
}
$limit = sprintf(' LIMIT %d, %d ', $start, $size);
}
$sql = $select . $body . $order . $limit;
$data = $this->getStore()->select($sql, $params, $options['connection']);
// If there are limits run some SQL to work out the full payload of rows
if ($options['requireTotal']) {
$results = $this->getStore()->select(
'SELECT COUNT(*) AS total FROM (' . $body,
$params,
$options['connection']
);
$this->countLast = intval($results[0]['total']);
}
// Are there any client side formulas?
if (count($clientSideFormula) > 0) {
$renderedData = [];
foreach ($data as $item) {
foreach ($clientSideFormula as $column) {
// Run the formula and add the resulting value to the list
$value = null;
try {
if (substr($column->formula, 0, strlen('$dateFormat(')) === '$dateFormat(') {
// Pull out the column name and date format
$details = explode(',', str_replace(')', '', str_replace('$dateFormat(', '', $column->formula)));
if (isset($details[2])) {
$language = str_replace(' ', '', $details[2]);
} else {
$language = $this->config->getSetting('DEFAULT_LANGUAGE', 'en_GB');
}
$carbonFactory = new Factory(['locale' => $language], Carbon::class);
$value = $carbonFactory->parse($item[$details[0]])->translatedFormat($details[1]);
}
} catch (\Exception $e) {
$this->getLog()->error('DataSet client side formula error in dataSetId ' . $this->dataSetId . ' with column formula ' . $column->formula);
}
$item[$column->heading] = $value;
}
$renderedData[] = $item;
}
} else {
$renderedData = $data;
}
return $renderedData;
}
/**
* Assign a column
* @param DataSetColumn $column
*/
public function assignColumn($column)
{
$this->load();
// Set the dataSetId
$column->dataSetId = $this->dataSetId;
// Set the column order if we need to
if ($column->columnOrder == 0)
$column->columnOrder = count($this->columns) + 1;
$this->columns[] = $column;
}
/**
* Has Data?
* @return bool
*/
public function hasData()
{
return $this->getStore()->exists('SELECT id FROM `dataset_' . $this->dataSetId . '` LIMIT 1', [], 'isolated');
}
/**
* Returns a Timestamp for the next Synchronisation process.
* @return int Seconds
*/
public function getNextSyncTime()
{
return $this->lastSync + $this->refreshRate;
}
/**
* @return bool
*/
public function isTruncateEnabled()
{
return $this->clearRate !== 0;
}
/**
* Returns a Timestamp for the next Clearing process.
* @return int Seconds
*/
public function getNextClearTime()
{
return $this->lastClear + $this->clearRate;
}
/**
* Returns if there is a consolidation field and method present or not.
* @return boolean
*/
public function doConsolidate()
{
return ($this->summarizeField != null) && ($this->summarizeField != '')
&& ($this->summarize != null) && ($this->summarize != '');
}
/**
* Returns the last Part of the Fieldname on which the consolidation should be applied on
* @return String
*/
public function getConsolidationField()
{
$pos = strrpos($this->summarizeField, '.');
if ($pos !== false) {
return substr($this->summarizeField, $pos + 1);
}
return $this->summarizeField;
}
/**
* Tests if this DataSet contains parameters for getting values on the dependant DataSet
* @return boolean
*/
public function containsDependantFieldsInRequest()
{
return strpos($this->postData, '{{COL.') !== false || strpos($this->uri, '{{COL.') !== false;
}
/**
* Validate
* @throws InvalidArgumentException
* @throws DuplicateEntityException
*/
public function validate()
{
if (!v::stringType()->notEmpty()->length(null, 50)->validate($this->dataSet)) {
throw new InvalidArgumentException(__('Name must be between 1 and 50 characters'), 'dataSet');
}
if ($this->description != null && !v::stringType()->length(null, 254)->validate($this->description)) {
throw new InvalidArgumentException(__('Description can not be longer than 254 characters'), 'description');
}
// If we are a remote dataset do some additional checks
if ($this->isRemote === 1) {
if (!v::stringType()->notEmpty()->validate($this->uri)) {
throw new InvalidArgumentException(__('A remote DataSet must have a URI.'), 'uri');
}
if ($this->rowLimit > $this->config->getSetting('DATASET_HARD_ROW_LIMIT')) {
throw new InvalidArgumentException(__('DataSet row limit cannot be larger than the CMS dataSet row limit'));
}
// Check if the length is within the current URI character limit
if (!v::stringType()->length(null, 250)->validate($this->uri)) {
throw new InvalidArgumentException(__('URI can not be longer than 250 characters'), 'uri');
}
}
// Does this dataset have existing columns?
// Additional pre-checking for column headings when copying existing datasets
if (!empty($this->columns)) {
foreach ($this->columns as $column) {
if (!v::stringType()->notEmpty()->noWhitespace()->validate($column->heading)) {
throw new InvalidArgumentException(__(
'Cannot copy this Dataset due to invalid column headings. Please remove any spaces.'),
'dataSet'
);
}
}
}
try {
$existing = $this->dataSetFactory->getByName($this->dataSet, $this->userId);
if ($this->dataSetId == 0 || $this->dataSetId != $existing->dataSetId) {
throw new DuplicateEntityException(sprintf(__('There is already dataSet called %s. Please choose another name.'), $this->dataSet));
}
}
catch (NotFoundException $e) {
// This is good
}
}
/**
* Load all known information
*/
public function load()
{
if ($this->loaded || $this->dataSetId == 0)
return;
// Load Columns
$this->columns = $this->dataSetColumnFactory->getByDataSetId($this->dataSetId);
// Load Permissions
$this->permissions = $this->permissionFactory->getByObjectId(get_class($this), $this->getId());
$this->loaded = true;
}
/**
* Save this DataSet
* @param array $options
* @throws InvalidArgumentException
* @throws DuplicateEntityException
*/
public function save($options = [])
{
$options = array_merge([
'validate' => true,
'saveColumns' => true,
'activate' => true,
'notify' => true,
], $options);
if ($options['validate']) {
$this->validate();
}
if ($this->dataSetId == 0) {
$this->add();
} else {
$this->edit();
}
// Columns
if ($options['saveColumns']) {
foreach ($this->columns as $column) {
$column->dataSetId = $this->dataSetId;
$column->save($options);
}
}
// We've been touched
if ($options['activate']) {
$this->setActive();
}
// Notify Displays?
if ($options['notify']) {
$this->notify();
}
}
/**
* @param int $time
* @return $this
*/
public function saveLastSync($time)
{
$this->lastSync = $time;
$this->getStore()->update('UPDATE `dataset` SET lastSync = :lastSync WHERE dataSetId = :dataSetId', [
'dataSetId' => $this->dataSetId,
'lastSync' => $this->lastSync
]);
return $this;
}
/**
* @param int $time
* @return $this
*/
public function saveLastClear($time)
{
$this->lastSync = $time;
$this->getStore()->update('UPDATE `dataset` SET lastClear = :lastClear WHERE dataSetId = :dataSetId', [
'dataSetId' => $this->dataSetId,
'lastClear' => $this->lastClear
]);
return $this;
}
/**
* Is this DataSet active currently
* @return bool
*/
public function isActive()
{
$cache = $this->pool->getItem('/dataset/accessed/' . $this->dataSetId);
return $cache->isHit();
}
/**
* Indicate that this DataSet has been accessed recently
* @return $this
*/
public function setActive()
{
$this->getLog()->debug('Setting ' . $this->dataSetId . ' as active');
$cache = $this->pool->getItem('/dataset/accessed/' . $this->dataSetId);
$cache->set('true');
$cache->expiresAfter(intval($this->config->getSetting('REQUIRED_FILES_LOOKAHEAD')) * 1.5);
$this->pool->saveDeferred($cache);
return $this;
}
/**
* Delete DataSet
* @throws ConfigurationException
* @throws InvalidArgumentException
*/
public function delete()
{
$this->load();
if ($this->isLookup) {
throw new ConfigurationException(__('Lookup Tables cannot be deleted'));
}
// check if any other DataSet depends on this DataSet
if ($this->getStore()->exists(
'SELECT dataSetId FROM dataset WHERE runsAfter = :runsAfter AND dataSetId <> :dataSetId',
[
'runsAfter' => $this->dataSetId,
'dataSetId' => $this->dataSetId
])) {
throw new InvalidArgumentException(__('Cannot delete because this DataSet is set as dependent DataSet for another DataSet'), 'dataSetId');
}
// Make sure we're able to delete
if ($this->getStore()->exists('
SELECT widgetId
FROM `widgetoption`
WHERE `widgetoption`.type = \'attrib\'
AND `widgetoption`.option = \'dataSetId\'
AND `widgetoption`.value = :dataSetId
', ['dataSetId' => $this->dataSetId])) {
throw new InvalidArgumentException(__('Cannot delete because DataSet is in use on one or more Layouts.'), 'dataSetId');
}
if ($this->getStore()->exists('
SELECT `eventId`
FROM `schedule`
WHERE `dataSetId` = :dataSetId
', ['dataSetId' => $this->dataSetId])) {
throw new InvalidArgumentException(
__('Cannot delete because DataSet is in use on one or more Data Connector schedules.'),
'dataSetId'
);
}
// Delete Permissions
foreach ($this->permissions as $permission) {
/* @var Permission $permission */
$permission->deleteAll();
}
// Delete Columns
foreach ($this->columns as $column) {
$column->delete(true);
}
// Delete any dataSet rss
$this->getStore()->update('DELETE FROM `datasetrss` WHERE dataSetId = :dataSetId', ['dataSetId' => $this->dataSetId]);
// Delete the data set
$this->getStore()->update('DELETE FROM `dataset` WHERE dataSetId = :dataSetId', ['dataSetId' => $this->dataSetId]);
// The last thing we do is drop the dataSet table
$this->dropTable();
}
/**
* Delete all data
*/
public function deleteData()
{
// The last thing we do is drop the dataSet table
$this->getStore()->update('TRUNCATE TABLE `dataset_' . $this->dataSetId . '`', []);
$this->getStore()->update('ALTER TABLE `dataset_' . $this->dataSetId . '` AUTO_INCREMENT = 1', []);
$this->getStore()->commitIfNecessary();
}
/**
* Add
*/
private function add()
{
$columns = 'DataSet, Description, UserID, `code`, `isLookup`, `isRemote`, `lastDataEdit`,';
$columns .= '`lastClear`, `folderId`, `permissionsFolderId`, `isRealTime`, `dataConnectorSource`';
$values = ':dataSet, :description, :userId, :code, :isLookup, :isRemote,';
$values .= ':lastDataEdit, :lastClear, :folderId, :permissionsFolderId, :isRealTime, :dataConnectorSource';
$params = [
'dataSet' => $this->dataSet,
'description' => $this->description,
'userId' => $this->userId,
'code' => ($this->code == '') ? null : $this->code,
'isLookup' => $this->isLookup,
'isRemote' => $this->isRemote,
'isRealTime' => $this->isRealTime,
'dataConnectorSource' => $this->dataConnectorSource,
'lastDataEdit' => 0,
'lastClear' => 0,
'folderId' => ($this->folderId === null) ? 1 : $this->folderId,
'permissionsFolderId' => ($this->permissionsFolderId == null) ? 1 : $this-> permissionsFolderId,
];
// Insert the extra columns we expect for a remote DataSet
if ($this->isRemote === 1) {
$columns .= ', `method`, `uri`, `postData`, `authentication`, `username`, `password`, `customHeaders`, `userAgent`, `refreshRate`, `clearRate`, `truncateOnEmpty`, `runsAfter`, `dataRoot`, `lastSync`, `summarize`, `summarizeField`, `sourceId`, `ignoreFirstRow`, `rowLimit`, `limitPolicy`, `csvSeparator`';
$values .= ', :method, :uri, :postData, :authentication, :username, :password, :customHeaders, :userAgent, :refreshRate, :clearRate, :truncateOnEmpty, :runsAfter, :dataRoot, :lastSync, :summarize, :summarizeField, :sourceId, :ignoreFirstRow, :rowLimit, :limitPolicy, :csvSeparator';
$params['method'] = $this->method;
$params['uri'] = $this->uri;
$params['postData'] = $this->postData;
$params['authentication'] = $this->authentication;
$params['username'] = $this->username;
$params['password'] = $this->password;
$params['customHeaders'] = $this->customHeaders;
$params['userAgent'] = $this->userAgent;
$params['refreshRate'] = $this->refreshRate;
$params['clearRate'] = $this->clearRate;
$params['truncateOnEmpty'] = $this->truncateOnEmpty ?? 0;
$params['runsAfter'] = $this->runsAfter;
$params['dataRoot'] = $this->dataRoot;
$params['summarize'] = $this->summarize;
$params['summarizeField'] = $this->summarizeField;
$params['sourceId'] = $this->sourceId;
$params['ignoreFirstRow'] = $this->ignoreFirstRow;
$params['lastSync'] = 0;
$params['rowLimit'] = $this->rowLimit;
$params['limitPolicy'] = $this->limitPolicy;
$params['csvSeparator'] = $this->csvSeparator;
}
// Do the insert
$this->dataSetId = $this->getStore()->insert('INSERT INTO `dataset` (' . $columns . ') VALUES (' . $values . ')', $params);
// Create the data table for this dataSet
$this->createTable();
}
/**
* Edit
*/
private function edit()
{
$sql = '
`DataSet` = :dataSet,
`Description` = :description,
`userId` = :userId,
`lastDataEdit` = :lastDataEdit,
`code` = :code,
`isLookup` = :isLookup,
`isRemote` = :isRemote,
`isRealTime` = :isRealTime,
`dataConnectorSource` = :dataConnectorSource,
`folderId` = :folderId,
`permissionsFolderId` = :permissionsFolderId
';
$params = [
'dataSetId' => $this->dataSetId,
'dataSet' => $this->dataSet,
'description' => $this->description,
'userId' => $this->userId,
'lastDataEdit' => $this->lastDataEdit,
'code' => $this->code,
'isLookup' => $this->isLookup,
'isRemote' => $this->isRemote,
'isRealTime' => $this->isRealTime,
'dataConnectorSource' => $this->dataConnectorSource,
'folderId' => $this->folderId,
'permissionsFolderId' => $this->permissionsFolderId
];
if ($this->isRemote) {
$sql .= ', method = :method, uri = :uri, postData = :postData, authentication = :authentication, `username` = :username, `password` = :password, `customHeaders` = :customHeaders, `userAgent` = :userAgent, refreshRate = :refreshRate, clearRate = :clearRate, truncateOnEmpty = :truncateOnEmpty, runsAfter = :runsAfter, `dataRoot` = :dataRoot, `summarize` = :summarize, `summarizeField` = :summarizeField, `sourceId` = :sourceId, `ignoreFirstRow` = :ignoreFirstRow , `rowLimit` = :rowLimit, `limitPolicy` = :limitPolicy, `csvSeparator` = :csvSeparator ';
$params['method'] = $this->method;
$params['uri'] = $this->uri;
$params['postData'] = $this->postData;
$params['authentication'] = $this->authentication;
$params['username'] = $this->username;
$params['password'] = $this->password;
$params['customHeaders'] = $this->customHeaders;
$params['userAgent'] = $this->userAgent;
$params['refreshRate'] = $this->refreshRate;
$params['clearRate'] = $this->clearRate;
$params['truncateOnEmpty'] = $this->truncateOnEmpty ?? 0;
$params['runsAfter'] = $this->runsAfter;
$params['dataRoot'] = $this->dataRoot;
$params['summarize'] = $this->summarize;
$params['summarizeField'] = $this->summarizeField;
$params['sourceId'] = $this->sourceId;
$params['ignoreFirstRow'] = $this->ignoreFirstRow;
$params['rowLimit'] = $this->rowLimit;
$params['limitPolicy'] = $this->limitPolicy;
$params['csvSeparator'] = $this->csvSeparator;
}
$this->getStore()->update('UPDATE dataset SET ' . $sql . ' WHERE DataSetID = :dataSetId', $params);
}
/**
* Create the realised table structure for this DataSet
*/
private function createTable()
{
// Create the data table for this dataset
$this->getStore()->update('
CREATE TABLE `dataset_' . $this->dataSetId . '` (
`id` int(11) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1
', []);
}
private function dropTable()
{
$this->getStore()->update('DROP TABLE IF EXISTS dataset_' . $this->dataSetId, [], 'isolated', false, false, true);
}
/**
* Rebuild the dataSet table
* @throws GeneralException
*/
public function rebuild()
{
$this->load();
// Drop the data table
$this->dropTable();
// Add the data table
$this->createTable();
foreach ($this->columns as $column) {
/* @var \Xibo\Entity\DataSetColumn $column */
$column->dataSetId = $this->dataSetId;
$column->save(['rebuilding' => true]);
}
}
/**
* Notify displays of this campaign change
*/
public function notify()
{
$this->getLog()->debug('DataSet ' . $this->dataSetId . ' wants to notify');
$this->getDisplayNotifyService()->collectNow()->notifyByDataSetId($this->dataSetId);
}
/**
* Add a row
* @param array $row
* @return int
*/
public function addRow($row)
{
$this->getLog()->debug('Adding row ' . var_export($row, true));
// Update the last edit date on this dataSet
$this->lastDataEdit = Carbon::now()->format('U');
// Build a query to insert
$params = [];
$keys = array_keys($row);
$sql = 'INSERT INTO `dataset_' . $this->dataSetId
. '` (`' . implode('`, `', $keys) . '`) VALUES (';
$i = 0;
foreach ($row as $value) {
$i++;
$sql .= ':value' . $i . ',';
$params['value' . $i] = $value;
}
$sql = rtrim($sql, ',');
$sql .= ')';
return $this->getStore()->insert($sql, $params);
}
/**
* Edit a row
* @param int $rowId
* @param array $row
*/
public function editRow($rowId, $row)
{
$this->getLog()->debug(sprintf('Editing row %s', var_export($row, true)));
// Update the last edit date on this dataSet
$this->lastDataEdit = Carbon::now()->format('U');
// Params
$params = ['id' => $rowId];
// Generate a SQL statement
$sql = 'UPDATE `dataset_' . $this->dataSetId . '` SET';
$i = 0;
foreach ($row as $key => $value) {
$i++;
$sql .= ' `' . $key . '` = :value' . $i . ',';
$params['value' . $i] = $value;
}
$sql = rtrim($sql, ',');
$sql .= ' WHERE `id` = :id ';
$this->getStore()->update($sql, $params);
}
/**
* Delete Row
* @param $rowId
*/
public function deleteRow($rowId)
{
$this->lastDataEdit = Carbon::now()->format('U');
$this->getStore()->update('DELETE FROM `dataset_' . $this->dataSetId . '` WHERE id = :id', [
'id' => $rowId
]);
}
/**
* Copy Row
* @param int $dataSetIdSource
* @param int $dataSetIdTarget
*/
public function copyRows($dataSetIdSource, $dataSetIdTarget)
{
$this->getStore()->insert('INSERT INTO `dataset_' . $dataSetIdTarget . '` SELECT * FROM `dataset_' . $dataSetIdSource . '` ' ,[]);
}
/**
* Clear DataSet cache
*/
public function clearCache()
{
$this->getLog()->debug('Force sync detected, clear cache for remote dataSet ID ' . $this->dataSetId);
$this->pool->deleteItem('/dataset/cache/' . $this->dataSetId);
}
private function getScriptPath(): string
{
return $this->config->getSetting('LIBRARY_LOCATION')
. 'data_connectors' . DIRECTORY_SEPARATOR
. 'dataSet_' . $this->dataSetId . '.js';
}
public function getScript(): string
{
if ($this->isRealTime == 0) {
return '';
}
$path = $this->getScriptPath();
return (file_exists($path))
? file_get_contents($path)
: '';
}
public function saveScript(string $script): void
{
if ($this->isRealTime == 1) {
$path = $this->getScriptPath();
file_put_contents($path, $script);
file_put_contents($path . '.md5', md5_file($path));
}
}
}