Initial Upload
This commit is contained in:
917
lib/Factory/DataSetFactory.php
Normal file
917
lib/Factory/DataSetFactory.php
Normal file
@@ -0,0 +1,917 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2025 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\Factory;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use Stash\Interfaces\PoolInterface;
|
||||
use Xibo\Entity\DataSet;
|
||||
use Xibo\Entity\DataSetColumn;
|
||||
use Xibo\Helper\DateFormatHelper;
|
||||
use Xibo\Helper\Environment;
|
||||
use Xibo\Service\ConfigServiceInterface;
|
||||
use Xibo\Service\DisplayNotifyServiceInterface;
|
||||
use Xibo\Support\Exception\InvalidArgumentException;
|
||||
use Xibo\Support\Exception\NotFoundException;
|
||||
|
||||
/**
|
||||
* Class DataSetFactory
|
||||
* @package Xibo\Factory
|
||||
*/
|
||||
class DataSetFactory extends BaseFactory
|
||||
{
|
||||
/** @var ConfigServiceInterface */
|
||||
private $config;
|
||||
|
||||
/** @var PoolInterface */
|
||||
private $pool;
|
||||
|
||||
/** @var DataSetColumnFactory */
|
||||
private $dataSetColumnFactory;
|
||||
|
||||
/** @var PermissionFactory */
|
||||
private $permissionFactory;
|
||||
|
||||
/** @var DisplayNotifyServiceInterface */
|
||||
private $displayNotifyService;
|
||||
|
||||
/**
|
||||
* Construct a factory
|
||||
* @param \Xibo\Entity\User $user
|
||||
* @param UserFactory $userFactory
|
||||
* @param ConfigServiceInterface $config
|
||||
* @param PoolInterface $pool
|
||||
* @param DataSetColumnFactory $dataSetColumnFactory
|
||||
* @param PermissionFactory $permissionFactory
|
||||
* @param DisplayNotifyServiceInterface $displayNotifyService
|
||||
*/
|
||||
public function __construct($user, $userFactory, $config, $pool, $dataSetColumnFactory, $permissionFactory, $displayNotifyService)
|
||||
{
|
||||
$this->setAclDependencies($user, $userFactory);
|
||||
$this->config = $config;
|
||||
$this->pool = $pool;
|
||||
$this->dataSetColumnFactory = $dataSetColumnFactory;
|
||||
$this->permissionFactory = $permissionFactory;
|
||||
$this->displayNotifyService = $displayNotifyService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataSetColumnFactory
|
||||
*/
|
||||
public function getDataSetColumnFactory()
|
||||
{
|
||||
return $this->dataSetColumnFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataSet
|
||||
*/
|
||||
public function createEmpty()
|
||||
{
|
||||
return new DataSet(
|
||||
$this->getStore(),
|
||||
$this->getLog(),
|
||||
$this->getDispatcher(),
|
||||
$this->getSanitizerService(),
|
||||
$this->config,
|
||||
$this->pool,
|
||||
$this,
|
||||
$this->dataSetColumnFactory,
|
||||
$this->permissionFactory,
|
||||
$this->displayNotifyService
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DataSets by ID
|
||||
* @param $dataSetId
|
||||
* @return DataSet
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function getById($dataSetId)
|
||||
{
|
||||
$dataSets = $this->query(null, ['disableUserCheck' => 1, 'dataSetId' => $dataSetId]);
|
||||
|
||||
if (count($dataSets) <= 0) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return $dataSets[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DataSets by Code
|
||||
* @param $code
|
||||
* @return DataSet
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function getByCode($code)
|
||||
{
|
||||
$dataSets = $this->query(null, ['disableUserCheck' => 1, 'code' => $code]);
|
||||
|
||||
if (count($dataSets) <= 0) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return $dataSets[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DataSets by Name
|
||||
* @param $dataSet
|
||||
* @param int|null $userId the userId
|
||||
* @return DataSet
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function getByName($dataSet, $userId = null)
|
||||
{
|
||||
$dataSets = $this->query(null, ['dataSetExact' => $dataSet, 'userId' => $userId]);
|
||||
|
||||
if (count($dataSets) <= 0) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return $dataSets[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $userId
|
||||
* @return DataSet[]
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function getByOwnerId($userId)
|
||||
{
|
||||
$dataSets = $this->query(null, ['disableUserCheck' => 1, 'userId' => $userId]);
|
||||
|
||||
return $dataSets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $folderId
|
||||
* @return DataSet[]
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function getByFolderId(int $folderId)
|
||||
{
|
||||
return $this->query(null, ['disableUserCheck' => 1, 'folderId' => $folderId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $sortOrder
|
||||
* @param array $filterBy
|
||||
* @return array[DataSet]
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function query($sortOrder = null, $filterBy = [])
|
||||
{
|
||||
$entries = [];
|
||||
$params = [];
|
||||
$parsedFilter = $this->getSanitizer($filterBy);
|
||||
|
||||
if ($sortOrder === null) {
|
||||
$sortOrder = ['dataSet'];
|
||||
}
|
||||
|
||||
$select = '
|
||||
SELECT dataset.dataSetId,
|
||||
dataset.dataSet,
|
||||
dataset.description,
|
||||
dataset.userId,
|
||||
dataset.lastDataEdit,
|
||||
dataset.`code`,
|
||||
dataset.`isLookup`,
|
||||
dataset.`isRemote`,
|
||||
dataset.`isRealTime`,
|
||||
dataset.`dataConnectorSource`,
|
||||
dataset.`method`,
|
||||
dataset.`uri`,
|
||||
dataset.`postData`,
|
||||
dataset.`authentication`,
|
||||
dataset.`username`,
|
||||
dataset.`password`,
|
||||
dataset.`customHeaders`,
|
||||
dataset.`userAgent`,
|
||||
dataset.`refreshRate`,
|
||||
dataset.`clearRate`,
|
||||
dataset.`truncateOnEmpty`,
|
||||
dataset.`runsAfter`,
|
||||
dataset.`dataRoot`,
|
||||
dataset.`summarize`,
|
||||
dataset.`summarizeField`,
|
||||
dataset.`lastSync`,
|
||||
dataset.`lastClear`,
|
||||
dataset.`sourceId`,
|
||||
dataset.`ignoreFirstRow`,
|
||||
dataset.`rowLimit`,
|
||||
dataset.`limitPolicy`,
|
||||
dataset.`csvSeparator`,
|
||||
dataset.`folderId`,
|
||||
dataset.`permissionsFolderId`,
|
||||
user.userName AS owner,
|
||||
(
|
||||
SELECT GROUP_CONCAT(DISTINCT `group`.group)
|
||||
FROM `permission`
|
||||
INNER JOIN `permissionentity`
|
||||
ON `permissionentity`.entityId = permission.entityId
|
||||
INNER JOIN `group`
|
||||
ON `group`.groupId = `permission`.groupId
|
||||
WHERE entity = :groupsWithPermissionsEntity
|
||||
AND objectId = dataset.dataSetId
|
||||
) AS groupsWithPermissions
|
||||
';
|
||||
|
||||
$params['groupsWithPermissionsEntity'] = 'Xibo\\Entity\\DataSet';
|
||||
|
||||
$body = '
|
||||
FROM dataset
|
||||
INNER JOIN `user` ON user.userId = dataset.userId
|
||||
WHERE 1 = 1
|
||||
';
|
||||
|
||||
if ($parsedFilter->getInt('dataSetId') !== null) {
|
||||
$body .= ' AND dataset.dataSetId = :dataSetId ';
|
||||
$params['dataSetId'] = $parsedFilter->getInt('dataSetId');
|
||||
}
|
||||
|
||||
if ($parsedFilter->getInt('userId') !== null) {
|
||||
$body .= ' AND dataset.userId = :userId ';
|
||||
$params['userId'] = $parsedFilter->getInt('userId');
|
||||
}
|
||||
|
||||
if ($parsedFilter->getInt('isRemote') !== null) {
|
||||
$body .= ' AND dataset.isRemote = :isRemote ';
|
||||
$params['isRemote'] = $parsedFilter->getInt('isRemote');
|
||||
}
|
||||
|
||||
if ($parsedFilter->getInt('isRealTime') !== null) {
|
||||
$body .= ' AND dataset.isRealTime = :isRealTime ';
|
||||
$params['isRealTime'] = $parsedFilter->getInt('isRealTime');
|
||||
}
|
||||
|
||||
if ($parsedFilter->getString('dataSet') != null) {
|
||||
$terms = explode(',', $parsedFilter->getString('dataSet'));
|
||||
$logicalOperator = $parsedFilter->getString('logicalOperatorName', ['default' => 'OR']);
|
||||
$this->nameFilter(
|
||||
'dataset',
|
||||
'dataSet',
|
||||
$terms,
|
||||
$body,
|
||||
$params,
|
||||
($parsedFilter->getCheckbox('useRegexForName') == 1),
|
||||
$logicalOperator
|
||||
);
|
||||
}
|
||||
|
||||
if ($parsedFilter->getString('dataSetExact') != '') {
|
||||
$body.= " AND dataset.dataSet = :exact ";
|
||||
$params['exact'] = $parsedFilter->getString('dataSetExact');
|
||||
}
|
||||
|
||||
if ($parsedFilter->getString('code') != null) {
|
||||
$body .= ' AND `dataset`.`code` = :code ';
|
||||
$params['code'] = $parsedFilter->getString('code');
|
||||
}
|
||||
|
||||
if ($parsedFilter->getInt('folderId') !== null) {
|
||||
$body .= ' AND dataset.folderId = :folderId ';
|
||||
$params['folderId'] = $parsedFilter->getInt('folderId');
|
||||
}
|
||||
|
||||
// View Permissions
|
||||
$this->viewPermissionSql('Xibo\Entity\DataSet', $body, $params, '`dataset`.dataSetId', '`dataset`.userId', $filterBy, '`dataset`.permissionsFolderId');
|
||||
|
||||
// Sorting?
|
||||
$order = '';
|
||||
if (is_array($sortOrder))
|
||||
$order .= 'ORDER BY ' . implode(',', $sortOrder);
|
||||
|
||||
$limit = '';
|
||||
// Paging
|
||||
if ($filterBy !== null && $parsedFilter->getInt('start') !== null && $parsedFilter->getInt('length') !== null) {
|
||||
$limit = ' LIMIT ' . $parsedFilter->getInt('start', ['default' => 0]) . ', ' . $parsedFilter->getInt('length', ['default' => 10]);
|
||||
}
|
||||
|
||||
$sql = $select . $body . $order . $limit;
|
||||
|
||||
foreach ($this->getStore()->select($sql, $params) as $row) {
|
||||
$entries[] = $this->createEmpty()->hydrate($row, [
|
||||
'intProperties' => [
|
||||
'isLookup',
|
||||
'isRemote',
|
||||
'isRealTime',
|
||||
'clearRate',
|
||||
'refreshRate',
|
||||
'lastDataEdit',
|
||||
'runsAfter',
|
||||
'lastSync',
|
||||
'lastClear',
|
||||
'ignoreFirstRow',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// Paging
|
||||
if ($limit != '' && count($entries) > 0) {
|
||||
unset($params['groupsWithPermissionsEntity']);
|
||||
$results = $this->getStore()->select('SELECT COUNT(*) AS total ' . $body, $params);
|
||||
$this->_countLast = intval($results[0]['total']);
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a call to a Remote Dataset and returns all received data as a JSON decoded Object.
|
||||
* In case of an Error, null is returned instead.
|
||||
* @param DataSet $dataSet The Dataset to get Data for
|
||||
* @param DataSet|null $dependant The Dataset $dataSet depends on
|
||||
* @param bool $enableCaching Should we cache check the results and store the resulting cache
|
||||
* @return \stdClass {entries:[], number:int, isEligibleToTruncate:bool}
|
||||
* @throws InvalidArgumentException
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function callRemoteService(DataSet $dataSet, ?DataSet $dependant = null, $enableCaching = true)
|
||||
{
|
||||
$this->getLog()->debug('Calling remote service for DataSet: ' . $dataSet->dataSet
|
||||
. ' and URL ' . $dataSet->uri);
|
||||
|
||||
// Record our max memory
|
||||
$maxMemory = Environment::getMemoryLimitBytes() / 2;
|
||||
|
||||
// Guzzle for this and add proxy support.
|
||||
$client = new Client($this->config->getGuzzleProxy());
|
||||
|
||||
$result = new \stdClass();
|
||||
$result->entries = [];
|
||||
$result->number = 0;
|
||||
$result->isEligibleToTruncate = false;
|
||||
|
||||
// Getting all dependant values if needed
|
||||
// just an empty array if we don't have a dependent
|
||||
$values = [
|
||||
[]
|
||||
];
|
||||
|
||||
if ($dependant != null && $dataSet->containsDependantFieldsInRequest()) {
|
||||
$this->getLog()->debug('Dependant provided with fields in the request.');
|
||||
|
||||
$values = $dependant->getData();
|
||||
}
|
||||
|
||||
// Fetching data for every field in the dependant dataSet
|
||||
foreach ($values as $options) {
|
||||
// Make some request params to provide to the HTTP client
|
||||
$resolvedUri = $this->replaceParams($dataSet->uri, $options);
|
||||
$requestParams = [];
|
||||
|
||||
// Auth
|
||||
switch ($dataSet->authentication) {
|
||||
case 'basic':
|
||||
$requestParams['auth'] = [$dataSet->username, $dataSet->password];
|
||||
break;
|
||||
|
||||
case 'digest':
|
||||
$requestParams['auth'] = [$dataSet->username, $dataSet->password, 'digest'];
|
||||
break;
|
||||
|
||||
case 'ntlm':
|
||||
$requestParams['auth'] = [$dataSet->username, $dataSet->password, 'ntlm'];
|
||||
break;
|
||||
|
||||
case 'bearer':
|
||||
$requestParams['headers'] = ['Authorization' => 'Bearer ' . $dataSet->password];
|
||||
break;
|
||||
|
||||
case 'none':
|
||||
default:
|
||||
$this->getLog()->debug('No authentication required');
|
||||
}
|
||||
|
||||
if (isset($dataSet->customHeaders)) {
|
||||
$arrayOfCustomHeaders = array_filter(explode(',', $dataSet->customHeaders));
|
||||
|
||||
foreach ($arrayOfCustomHeaders as $customHeader) {
|
||||
$header = array_filter(explode(':', $customHeader));
|
||||
$requestParams['headers'][$header[0]] = $header[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Post request?
|
||||
if ($dataSet->method === 'POST') {
|
||||
parse_str($this->replaceParams($dataSet->postData, $options), $requestParams['form_params']);
|
||||
} else {
|
||||
// Get the query params from the URL.
|
||||
$queryParamsArray = [];
|
||||
$dataSetPostData = [];
|
||||
$parsedUrl = parse_url($resolvedUri, PHP_URL_QUERY);
|
||||
if ($parsedUrl) {
|
||||
parse_str($parsedUrl, $queryParamsArray);
|
||||
}
|
||||
parse_str($this->replaceParams($dataSet->postData, $options), $dataSetPostData);
|
||||
$requestParams['query'] = array_merge($queryParamsArray, $dataSetPostData);
|
||||
}
|
||||
|
||||
if (!empty($dataSet->userAgent)) {
|
||||
$requestParams['headers']['User-Agent'] = trim($dataSet->userAgent);
|
||||
}
|
||||
|
||||
$this->getLog()->debug('Making request to ' . $resolvedUri . ' with params: ' . var_export($requestParams, true));
|
||||
|
||||
try {
|
||||
// Make a HEAD request to the URI and see if we are able to process this.
|
||||
if ($dataSet->method === 'GET') {
|
||||
try {
|
||||
$request = $client->head($resolvedUri, $requestParams);
|
||||
|
||||
$contentLength = $request->getHeader('Content-Length');
|
||||
if ($maxMemory > 0 && count($contentLength) > 0 && $contentLength[0] > $maxMemory)
|
||||
throw new InvalidArgumentException(__('The request %d is too large to fit inside the configured memory limit. %d', $contentLength[0], $maxMemory), 'contentLength');
|
||||
} catch (RequestException $requestException) {
|
||||
$this->getLog()->info('Cannot make head request for remote dataSet ' . $dataSet->dataSetId);
|
||||
}
|
||||
}
|
||||
|
||||
$request = $client->request($dataSet->method, $resolvedUri, $requestParams);
|
||||
|
||||
// Check the cache control situation
|
||||
if ($enableCaching) {
|
||||
// recache if necessary
|
||||
$cacheControlKey = $this->pool->getItem('/dataset/cache/' . $dataSet->dataSetId . '/' . md5($resolvedUri . json_encode($requestParams)));
|
||||
$cacheControlKeyValue = ($cacheControlKey->isMiss()) ? '' : $cacheControlKey->get();
|
||||
|
||||
$this->getLog()->debug('Cache Control Key is ' . $cacheControlKeyValue);
|
||||
|
||||
$etags = $request->getHeader('E-Tag');
|
||||
$lastModifieds = $request->getHeader('Last-Modified');
|
||||
|
||||
if (count($etags) > 0) {
|
||||
// Compare the etag with the cache key and see if they are the same, if they are
|
||||
// then we stop processing this data set
|
||||
if ($cacheControlKeyValue === $etags[0]) {
|
||||
$this->getLog()->debug('Skipping due to eTag');
|
||||
continue;
|
||||
}
|
||||
|
||||
$cacheControlKeyValue = $etags[0];
|
||||
} else if (count($lastModifieds) > 0) {
|
||||
if ($cacheControlKeyValue === $lastModifieds[0]) {
|
||||
$this->getLog()->debug('Skipping due to Last-Modified');
|
||||
continue;
|
||||
}
|
||||
|
||||
$cacheControlKeyValue = $lastModifieds[0];
|
||||
} else {
|
||||
// Request doesn't have any cache control of its own
|
||||
// use the md5
|
||||
$md5 = md5($request->getBody());
|
||||
|
||||
// Rewind so we can use it again
|
||||
$request->getBody()->rewind();
|
||||
|
||||
if ($cacheControlKeyValue === $md5) {
|
||||
$this->getLog()->debug('Skipping due to MD5');
|
||||
continue;
|
||||
}
|
||||
|
||||
$cacheControlKeyValue = $md5;
|
||||
}
|
||||
|
||||
$this->getLog()->debug('Cache Control Key is now ' . $cacheControlKeyValue);
|
||||
|
||||
// Store the cache key
|
||||
$cacheControlKey->set($cacheControlKeyValue);
|
||||
$cacheControlKey->expiresAfter(86400 * 365);
|
||||
$this->pool->saveDeferred($cacheControlKey);
|
||||
}
|
||||
|
||||
// We have passed any caching and therefore expect results
|
||||
$result->isEligibleToTruncate = true;
|
||||
|
||||
if ($dataSet->sourceId === 1) {
|
||||
// Make sure we have JSON in the response
|
||||
$body = $request->getBody()->getContents();
|
||||
|
||||
try {
|
||||
$json = \GuzzleHttp\json_decode($body);
|
||||
} catch (\GuzzleHttp\Exception\InvalidArgumentException $invalidArgumentException) {
|
||||
$this->getLog()->debug('JSON decode error: ' . $invalidArgumentException->getMessage());
|
||||
throw new InvalidArgumentException(__('Unable to get Data for %s because the response was not valid JSON.', $dataSet->dataSet), 'url');
|
||||
}
|
||||
|
||||
$result->entries[] = $json;
|
||||
foreach ($result->entries as $entry) {
|
||||
$data = $this->getDataRootFromResult($dataSet->dataRoot, $entry);
|
||||
|
||||
if (is_array($data)) {
|
||||
$result->number = count($data);
|
||||
} elseif (is_object($data)) {
|
||||
$result->number = count(get_object_vars($data));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$csv = $request->getBody()->getContents();
|
||||
$array = array_map(
|
||||
function ($v) use ($dataSet) {
|
||||
return str_getcsv($v, $dataSet->csvSeparator ?? ',');
|
||||
},
|
||||
explode("\n", $csv)
|
||||
);
|
||||
|
||||
if ($dataSet->ignoreFirstRow == 1) {
|
||||
array_shift($array);
|
||||
}
|
||||
|
||||
// Filter out rows that are entirely empty
|
||||
$array = array_filter($array, function($row) {
|
||||
// Check if the row is empty (all elements are empty or null)
|
||||
return array_filter($row, function($value) {
|
||||
return !empty($value);
|
||||
});
|
||||
});
|
||||
|
||||
$result->entries = $array;
|
||||
$result->number = count($array);
|
||||
}
|
||||
} catch (RequestException $requestException) {
|
||||
$this->getLog()->error('Error making request. ' . $requestException->getMessage());
|
||||
|
||||
// No point in carrying on through this stack of requests, dependent or original data will be
|
||||
// missing
|
||||
throw new InvalidArgumentException(__('Unable to get Data for %s because %s.', $dataSet->dataSet, $requestException->getMessage()), 'dataSetId');
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all URI/PostData parameters
|
||||
* @param string String to replace {{DATE}}, {{TIME}} and {{COL.xxx}}
|
||||
* @param array $values ColumnValues to use on {{COL.xxx}} parts
|
||||
* @return string
|
||||
*/
|
||||
private function replaceParams($string = '', array $values = [])
|
||||
{
|
||||
if (empty($string)) {
|
||||
return $string;
|
||||
}
|
||||
|
||||
$string = str_replace('{{DATE}}', date('Y-m-d'), $string);
|
||||
$string = str_replace('%7B%7BDATE%7D%7D', date('Y-m-d'), $string);
|
||||
$string = str_replace('{{TIME}}', date('H:m:s'), $string);
|
||||
$string = str_replace('%7B%7BTIME%7D%7D', date('H:m:s'), $string);
|
||||
|
||||
foreach ($values as $k => $v) {
|
||||
$string = str_replace('{{COL.' . $k . '}}', urlencode($v), $string);
|
||||
$string = str_replace('%7B%7BCOL.' . $k . '%7D%7D', urlencode($v), $string);
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to process received Data against the configured DataSet with all Columns
|
||||
*
|
||||
* @param DataSet $dataSet The RemoteDataset to process
|
||||
* @param \stdClass $results A simple Object with one Property 'entries' which contains all results
|
||||
* @param bool $save
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function processResults(DataSet $dataSet, \stdClass $results, $save = true)
|
||||
{
|
||||
$results->processed = [];
|
||||
|
||||
if (property_exists($results, 'entries') && is_array($results->entries)) {
|
||||
// Load the DataSet fully
|
||||
$dataSet->load();
|
||||
|
||||
$results->messages = [__('Processing %d results into %d potential columns', count($results->entries), count($dataSet->columns))];
|
||||
|
||||
foreach ($results->entries as $result) {
|
||||
$results->messages[] = __('Processing Result with Data Root %s', $dataSet->dataRoot);
|
||||
|
||||
// Remote Data has to have the configured DataRoot which has to be an Array
|
||||
$data = $this->getDataRootFromResult($dataSet->dataRoot, $result);
|
||||
|
||||
$columns = $dataSet->columns;
|
||||
$entries = [];
|
||||
|
||||
// Process the data root according to its type
|
||||
if (is_array($data)) {
|
||||
// An array of results as the DataRoot
|
||||
$results->messages[] = 'DataRoot is an array';
|
||||
|
||||
// First process each entry form the remote and try to map the values to the configured columns
|
||||
foreach ($data as $k => $entry) {
|
||||
$this->getLog()->debug('Processing key ' . $k . ' from the remote results');
|
||||
$this->getLog()->debug('Entry is: ' . var_export($entry, true));
|
||||
|
||||
$results->messages[] = 'Processing ' . $k;
|
||||
|
||||
if (is_array($entry) || is_object($entry)) {
|
||||
$entries[] = $this->processEntry((array)$entry, $columns);
|
||||
} else {
|
||||
$this->getLog()->error('DataSet ' . $dataSet->dataSet . ' failed: DataRoot ' . $dataSet->dataRoot . ' contains data which is not arrays or objects.');
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (is_object($data)) {
|
||||
// An object as the DataRoot.
|
||||
$results->messages[] = 'DataRoot is an object';
|
||||
|
||||
// We should treat this as a single row? Or as multiple rows?
|
||||
// we could try and guess from the configuration of the dataset columns
|
||||
$singleRow = false;
|
||||
foreach ($columns as $column) {
|
||||
if ($column->dataSetColumnTypeId === 3 && $column->remoteField != null && !is_numeric($column->remoteField)) {
|
||||
$singleRow = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($singleRow) {
|
||||
// Process as a single row
|
||||
$results->messages[] = __('Processing as a Single Row');
|
||||
|
||||
$entries[] = $this->processEntry((array)$data, $columns);
|
||||
} else {
|
||||
// Process as multiple rows
|
||||
$results->messages[] = __('Processing as Multiple Rows');
|
||||
|
||||
foreach (get_object_vars($data) as $property => $value) {
|
||||
// Treat each property as an index key (flattening the array)
|
||||
$results->messages[] = 'Processing ' . $property;
|
||||
|
||||
$entries[] = $this->processEntry([$property, $value], $columns);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new InvalidArgumentException(__('No data found at the DataRoot %s', $dataSet->dataRoot), 'dataRoot');
|
||||
}
|
||||
|
||||
$results->messages[] = __('Consolidating entries');
|
||||
|
||||
// If there is a Consolidation-Function, use the Data against it
|
||||
$entries = $this->consolidateEntries($dataSet, $entries, $columns);
|
||||
|
||||
$results->messages[] = __('There are %d entries in total', count($entries));
|
||||
|
||||
// Finally add each entry as a new Row in the DataSet
|
||||
if ($save) {
|
||||
foreach ($entries as $entry) {
|
||||
$dataSet->addRow($entry);
|
||||
}
|
||||
}
|
||||
|
||||
$results->processed[] = $entries;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the RemoteResult to get the main DataRoot value which can be stay in a structure as well as the values
|
||||
*
|
||||
* @param String Chunks split by a Dot where the main entries are hold
|
||||
* @param array|\stdClass The Value from the remote request
|
||||
* @return array|\stdClass The Data hold in the configured dataRoot
|
||||
*/
|
||||
private function getDataRootFromResult($dataRoot, $result)
|
||||
{
|
||||
$this->getLog()->debug('Getting ' . $dataRoot . 'from result.');
|
||||
|
||||
if (empty($dataRoot)) {
|
||||
return $result;
|
||||
}
|
||||
$chunks = explode('.', $dataRoot);
|
||||
$entries = $this->getFieldValueFromEntry($chunks, $result);
|
||||
return $entries[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single Data-Entry form the remote system and map it to the configured Columns
|
||||
*
|
||||
* @param array $entry The Data from the remote system
|
||||
* @param DataSetColumn[] $dataSetColumns The configured Columns form the current DataSet
|
||||
* @return array The processed $entry as a List of Fields from $columns
|
||||
*/
|
||||
private function processEntry(array $entry, array $dataSetColumns)
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($dataSetColumns as $column) {
|
||||
if ($column->dataSetColumnTypeId === 3 && $column->remoteField != null) {
|
||||
$this->getLog()->debug('Trying to match dataSetColumn ' . $column->heading . ' with remote field ' . $column->remoteField);
|
||||
|
||||
// The Field may be a Date, timestamp or a real field
|
||||
if ($column->remoteField == '{{DATE}}') {
|
||||
$value = [0, date('Y-m-d')];
|
||||
} else if ($column->remoteField == '{{TIMESTAMP}}') {
|
||||
$value = [0, Carbon::now()->format('U')];
|
||||
} else {
|
||||
$chunks = explode('.', $column->remoteField);
|
||||
$value = $this->getFieldValueFromEntry($chunks, $entry);
|
||||
}
|
||||
|
||||
$this->getLog()->debug('Resolved value: ' . var_export($value, true));
|
||||
|
||||
// Only add it to the result if we were able to process the field
|
||||
if ($value != null && $value[1] !== null) {
|
||||
// 1,String
|
||||
// 2,Number
|
||||
// 3,Date
|
||||
// 4,External Image
|
||||
// 5,Library Image
|
||||
// 6,HTML
|
||||
$validator = $this->getValidator();
|
||||
|
||||
switch ($column->dataTypeId) {
|
||||
case 2:
|
||||
// Number
|
||||
if (empty($value[1]) || !($validator->double($value[1]) || $validator->int($value[1]))) {
|
||||
$result[$column->heading] = 0;
|
||||
} else {
|
||||
$result[$column->heading] = doubleval($value[1]);
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
// Date
|
||||
// This expects an ISO date
|
||||
// check if we were provided with custom dateFormat
|
||||
$dateFormat = $column->dateFormat ?: DateFormatHelper::getSystemFormat();
|
||||
|
||||
try {
|
||||
// Parse into a date object from any format, and then save using the system format
|
||||
$result[$column->heading] = Carbon::createFromFormat($dateFormat, $value[1])
|
||||
->format(DateFormatHelper::getSystemFormat());
|
||||
} catch (\Exception $e) {
|
||||
$this->getLog()->error(
|
||||
sprintf(
|
||||
'Incorrect date provided %s, expected date format %s',
|
||||
$value[1],
|
||||
$dateFormat
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
case 5:
|
||||
// Library Image
|
||||
if (empty($value[1]) || !$validator->int($value[1])) {
|
||||
$result[$column->heading] = 0;
|
||||
} else {
|
||||
$result[$column->heading] = intval($value[1]);
|
||||
}
|
||||
break;
|
||||
case 6:
|
||||
// HTML, without any sanitization
|
||||
$result[$column->heading] = $value[1];
|
||||
break;
|
||||
default:
|
||||
// Default value, assume it will be a string and filter it accordingly.
|
||||
$result[$column->heading] = strip_tags($value[1]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->getLog()->debug('Column not matched');
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Value of the remote DataEntry based on the remoteField definition split into chunks
|
||||
*
|
||||
* This function is recursive, so be sure you remove the first value from chunks and pass it in again
|
||||
*
|
||||
* @param array List of Chunks which interprets the FieldNames in the actual DataEntry
|
||||
* @param array|\stdClass $entry Current DataEntry
|
||||
* @return array of the last FieldName and the corresponding value
|
||||
*/
|
||||
private function getFieldValueFromEntry(array $chunks, $entry)
|
||||
{
|
||||
$value = null;
|
||||
$key = array_shift($chunks);
|
||||
|
||||
$this->getLog()->debug('Entry: ' . var_export($entry, true));
|
||||
$this->getLog()->debug('Looking for key: ' . $key . '. Chunks: ' . var_export($chunks, true));
|
||||
|
||||
if (($entry instanceof \stdClass) && property_exists($entry, $key)) {
|
||||
$value = $entry->{$key};
|
||||
} else if (array_key_exists($key, $entry)) {
|
||||
$value = $entry[$key];
|
||||
}
|
||||
|
||||
$this->getLog()->debug('Value found is: ' . var_export($value, true));
|
||||
|
||||
if (($value != null) && (count($chunks) > 0)) {
|
||||
return $this->getFieldValueFromEntry($chunks, (array) $value);
|
||||
}
|
||||
|
||||
return [ $key, $value ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Consolidates all Entries by the defined Function in the DataSet
|
||||
*
|
||||
* This Method *sums* or *counts* all same entries and returns them.
|
||||
* If no consolidation function is configured, nothing is done here.
|
||||
*
|
||||
* @param DataSet $dataSet the current DataSet
|
||||
* @param array $entries All processed entries which may be consolidated
|
||||
* @param array $columns The columns form this DataSet
|
||||
* @return array which contains all Entries to be added to the DataSet
|
||||
*/
|
||||
private function consolidateEntries(DataSet $dataSet, array $entries, array $columns)
|
||||
{
|
||||
// Do we need to consolidate?
|
||||
if ((count($entries) > 0) && $dataSet->doConsolidate()) {
|
||||
// Yes
|
||||
$this->getLog()->debug('Consolidate Required on field ' . $dataSet->getConsolidationField());
|
||||
|
||||
$consolidated = [];
|
||||
$field = $dataSet->getConsolidationField();
|
||||
|
||||
// Get the Field-Heading based on the consolidation field
|
||||
foreach ($columns as $k => $column) {
|
||||
if ($column->remoteField == $dataSet->summarizeField) {
|
||||
$field = $column->heading;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check each entry and consolidate the value form the defined field
|
||||
foreach ($entries as $entry) {
|
||||
if (array_key_exists($field, $entry)) {
|
||||
$key = $field . '-' . $entry[$field];
|
||||
$existing = (isset($consolidated[$key])) ? $consolidated[$key] : null;
|
||||
|
||||
// Create a new one if there is no currently consolidated field for this value
|
||||
if ($existing == null) {
|
||||
$existing = $entry;
|
||||
$existing[$field] = 0;
|
||||
}
|
||||
|
||||
// Consolidate: Summarize, Count, Unknown
|
||||
if ($dataSet->summarize == 'sum') {
|
||||
$existing[$field] = $existing[$field] + $entry[$field];
|
||||
|
||||
} else if ($dataSet->summarize == 'count') {
|
||||
$existing[$field] = $existing[$field] + 1;
|
||||
|
||||
} else {
|
||||
// Unknown consolidation type :?
|
||||
$existing[$field] = 0;
|
||||
}
|
||||
|
||||
$consolidated[$key] = $existing;
|
||||
}
|
||||
}
|
||||
|
||||
return $consolidated;
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
public function processCsvEntries(DataSet $dataSet, \stdClass $results, $save = true)
|
||||
{
|
||||
$this->getLog()->debug('Processing CSV results');
|
||||
|
||||
$dataSet->load();
|
||||
$entries = [];
|
||||
|
||||
foreach ($results->entries as $entry) {
|
||||
$entries[] = $this->processEntry((array)$entry, $dataSet->columns);
|
||||
}
|
||||
|
||||
$results->processed = $entries;
|
||||
|
||||
if ($save) {
|
||||
foreach ($entries as $row) {
|
||||
$dataSet->addRow($row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user