770 lines
17 KiB
PHP
770 lines
17 KiB
PHP
<?php
|
|
namespace Bitrix\Landing;
|
|
|
|
use Bitrix\Main\Application;
|
|
use Bitrix\Rest\AppTable;
|
|
use Bitrix\Landing\Site\Type;
|
|
use Bitrix\Main\Localization\Loc;
|
|
use Bitrix\Main\ModuleManager;
|
|
|
|
Loc::loadMessages(__FILE__);
|
|
|
|
class PublicAction
|
|
{
|
|
/**
|
|
* Scope for REST (default commands).
|
|
*/
|
|
const REST_SCOPE_DEFAULT = 'landing';
|
|
|
|
/**
|
|
* Scope for REST (cloud repo commands).
|
|
*/
|
|
const REST_SCOPE_CLOUD = 'landing_cloud';
|
|
|
|
/**
|
|
* Code indicating used blocks in REST statistics.
|
|
*/
|
|
public const REST_USAGE_TYPE_BLOCK = 'blocks';
|
|
|
|
/**
|
|
* Code indicating used pages in REST statistics.
|
|
*/
|
|
public const REST_USAGE_TYPE_PAGE = 'pages';
|
|
|
|
/**
|
|
* REST application.
|
|
* @var array
|
|
*/
|
|
protected static $restApp = null;
|
|
|
|
/**
|
|
* Raw data from waf.
|
|
* @var mixed
|
|
*/
|
|
protected static $rawData = null;
|
|
|
|
/**
|
|
* Get full namespace for public classes.
|
|
* @return string
|
|
*/
|
|
protected static function getNamespacePublicClasses()
|
|
{
|
|
return __NAMESPACE__ . '\\PublicAction';
|
|
}
|
|
|
|
/**
|
|
* Get info about method - class/method/params.
|
|
* @param string $action Full name of action (\Namespace\Class::method).
|
|
* @param array $data Array of data.
|
|
* @return array
|
|
* @throws \ReflectionException
|
|
*/
|
|
protected static function getMethodInfo($action, $data = array())
|
|
{
|
|
$info = array();
|
|
|
|
// if action exist and is callable
|
|
if ($action && mb_strpos($action, '::'))
|
|
{
|
|
$actionOriginal = $action;
|
|
$action = self::getNamespacePublicClasses() . '\\' . $action;
|
|
if (is_callable(explode('::', $action)))
|
|
{
|
|
[$class, $method] = explode('::', $action);
|
|
$info = array(
|
|
'action' => $actionOriginal,
|
|
'class' => $class,
|
|
'method' => $method,
|
|
'params_init' => array(),
|
|
'params_missing' => array()
|
|
);
|
|
// parse func params
|
|
$reflection = new \ReflectionMethod($class, $method);
|
|
$static = $reflection->getStaticVariables();
|
|
$mixedParams = isset($static['mixedParams'])
|
|
? $static['mixedParams']
|
|
: [];
|
|
foreach ($reflection->getParameters() as $param)
|
|
{
|
|
$name = $param->getName();
|
|
if (isset($data[$name]))
|
|
{
|
|
if (!in_array($name, $mixedParams))
|
|
{
|
|
if (
|
|
$param->isArray() &&
|
|
!is_array($data[$name])
|
|
||
|
|
!$param->isArray() &&
|
|
is_array($data[$name])
|
|
|
|
)
|
|
{
|
|
throw new \Bitrix\Main\ArgumentTypeException(
|
|
$name
|
|
);
|
|
}
|
|
}
|
|
$info['params_init'][$name] = $data[$name];
|
|
}
|
|
elseif ($param->isDefaultValueAvailable())
|
|
{
|
|
$info['params_init'][$name] = $param->getDefaultValue();
|
|
}
|
|
else
|
|
{
|
|
$info['params_missing'][] = $name;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $info;
|
|
}
|
|
|
|
/**
|
|
* Returns true if current user out of the extranet.
|
|
* In extranet case allowed only GROUP scope.
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected static function checkForExtranet(): bool
|
|
{
|
|
if (\Bitrix\Landing\Manager::isAdmin())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (Type::getCurrentScopeId() === Type::SCOPE_CODE_GROUP)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (\Bitrix\Main\Loader::includeModule('extranet'))
|
|
{
|
|
return \CExtranet::isIntranetUser(
|
|
\CExtranet::getExtranetSiteID(),
|
|
\Bitrix\Landing\Manager::getUserId()
|
|
);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Processing the AJAX/REST action.
|
|
* @param string $action Action name.
|
|
* @param mixed $data Data.
|
|
* @param boolean $isRest Is rest call.
|
|
* @return array|null
|
|
* @throws \ReflectionException
|
|
*/
|
|
protected static function actionProcessing($action, $data, $isRest = false)
|
|
{
|
|
if (!is_array($data))
|
|
{
|
|
$data = array();
|
|
}
|
|
|
|
if (isset($data['scope']))
|
|
{
|
|
Type::setScope($data['scope']);
|
|
}
|
|
|
|
$error = new Error;
|
|
|
|
// not for guest
|
|
if (!Manager::getUserId() || !self::checkForExtranet())
|
|
{
|
|
$error->addError(
|
|
'ACCESS_DENIED',
|
|
Loc::getMessage('LANDING_ACCESS_DENIED2')
|
|
);
|
|
}
|
|
// tmp flag for compatibility
|
|
else if (
|
|
ModuleManager::isModuleInstalled('bitrix24') &&
|
|
Manager::getOption('temp_permission_admin_only') &&
|
|
!\CBitrix24::isPortalAdmin(Manager::getUserId())
|
|
)
|
|
{
|
|
$error->addError(
|
|
'ACCESS_DENIED',
|
|
Loc::getMessage('LANDING_ACCESS_DENIED2')
|
|
);
|
|
}
|
|
// check common permission
|
|
else if (
|
|
!Rights::hasAdditionalRight(
|
|
Rights::ADDITIONAL_RIGHTS['menu24'],
|
|
null,
|
|
true
|
|
)
|
|
)
|
|
{
|
|
$error->addError(
|
|
'ACCESS_DENIED',
|
|
Loc::getMessage('LANDING_ACCESS_DENIED2')
|
|
);
|
|
}
|
|
else if (!Manager::isB24() && Manager::getApplication()->getGroupRight('landing') < 'W')
|
|
{
|
|
$error->addError(
|
|
'ACCESS_DENIED',
|
|
Loc::getMessage('LANDING_ACCESS_DENIED2')
|
|
);
|
|
}
|
|
// if method::action exist in PublicAction, call it
|
|
elseif (($action = self::getMethodInfo($action, $data)))
|
|
{
|
|
if (!$isRest && !check_bitrix_sessid())
|
|
{
|
|
$error->addError(
|
|
'SESSION_EXPIRED',
|
|
Loc::getMessage('LANDING_SESSION_EXPIRED')
|
|
);
|
|
}
|
|
if (!empty($action['params_missing']))
|
|
{
|
|
$error->addError(
|
|
'MISSING_PARAMS',
|
|
Loc::getMessage('LANDING_MISSING_PARAMS', array(
|
|
'#MISSING#' => implode(', ', $action['params_missing'])
|
|
))
|
|
);
|
|
}
|
|
if (method_exists($action['class'], 'init'))
|
|
{
|
|
$result = call_user_func_array(
|
|
array($action['class'], 'init'),
|
|
[]
|
|
);
|
|
if (!$result->isSuccess())
|
|
{
|
|
$error->copyError($result->getError());
|
|
}
|
|
}
|
|
// all right - execute
|
|
if ($error->isEmpty())
|
|
{
|
|
try
|
|
{
|
|
$result = call_user_func_array(
|
|
array($action['class'], $action['method']),
|
|
$action['params_init']
|
|
);
|
|
// answer
|
|
if ($result === null)// void is accepted as success
|
|
{
|
|
return array(
|
|
'type' => 'success',
|
|
'result' => true
|
|
);
|
|
}
|
|
else if ($result->isSuccess())
|
|
{
|
|
$restResult = $result->getResult();
|
|
$event = new \Bitrix\Main\Event('landing', 'onSuccessRest', [
|
|
'result' => $restResult,
|
|
'action' => $action
|
|
]);
|
|
$event->send();
|
|
foreach ($event->getResults() as $eventResult)
|
|
{
|
|
if (($modified = $eventResult->getModified()))
|
|
{
|
|
if (isset($modified['result']))
|
|
{
|
|
$restResult = $modified['result'];
|
|
}
|
|
}
|
|
}
|
|
return array(
|
|
'type' => 'success',
|
|
'result' => $restResult
|
|
);
|
|
}
|
|
else
|
|
{
|
|
$error->copyError($result->getError());
|
|
}
|
|
}
|
|
catch (\TypeError $e)
|
|
{
|
|
$error->addError(
|
|
'TYPE_ERROR',
|
|
$e->getMessage()
|
|
);
|
|
}
|
|
catch (\Exception $e)
|
|
{
|
|
$error->addError(
|
|
'SYSTEM_ERROR',
|
|
$e->getMessage()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
// error
|
|
$errors = array();
|
|
foreach ($error->getErrors() as $error)
|
|
{
|
|
$errors[] = array(
|
|
'error' => $error->getCode(),
|
|
'error_description' => $error->getMessage()
|
|
);
|
|
}
|
|
if (!$isRest)
|
|
{
|
|
return [
|
|
'sessid' => bitrix_sessid(),
|
|
'type' => 'error',
|
|
'result' => $errors
|
|
];
|
|
}
|
|
else
|
|
{
|
|
return [
|
|
'type' => 'error',
|
|
'result' => $errors
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get raw data of curring processing.
|
|
* @return mixed
|
|
*/
|
|
public static function getRawData()
|
|
{
|
|
return self::$rawData;
|
|
}
|
|
|
|
/**
|
|
* Listen commands from ajax.
|
|
* @return array|null
|
|
* @throws \ReflectionException
|
|
*/
|
|
public static function ajaxProcessing()
|
|
{
|
|
$context = Application::getInstance()->getContext();
|
|
$request = $context->getRequest();
|
|
$files = $request->getFileList();
|
|
$postlist = $context->getRequest()->getPostList();
|
|
|
|
Type::setScope($request->get('type'));
|
|
|
|
// multiple commands
|
|
if (
|
|
$request->offsetExists('batch')
|
|
&& is_array($request->get('batch'))
|
|
)
|
|
{
|
|
$result = array();
|
|
// additional site id detect
|
|
if ($request->offsetExists('site_id'))
|
|
{
|
|
$siteId = $request->get('site_id');
|
|
}
|
|
foreach ($request->get('batch') as $key => $batchItem)
|
|
{
|
|
if (
|
|
isset($batchItem['action']) &&
|
|
isset($batchItem['data'])
|
|
)
|
|
{
|
|
$batchItem['data'] = (array)$batchItem['data'];
|
|
if (isset($siteId))
|
|
{
|
|
$batchItem['data']['siteId'] = $siteId;
|
|
}
|
|
if ($files)
|
|
{
|
|
foreach ($files as $code => $file)
|
|
{
|
|
$batchItem['data'][$code] = $file;
|
|
}
|
|
}
|
|
$rawData = $postlist->getRaw('batch');
|
|
if (isset($rawData[$key]['data']))
|
|
{
|
|
self::$rawData = $rawData[$key]['data'];
|
|
}
|
|
$result[$key] = self::actionProcessing(
|
|
$batchItem['action'],
|
|
$batchItem['data']
|
|
);
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
// or single command
|
|
else if (
|
|
$request->offsetExists('action')
|
|
&& $request->offsetExists('data')
|
|
&& is_array($request->get('data'))
|
|
)
|
|
{
|
|
$data = $request->get('data');
|
|
// additional site id detect
|
|
if ($request->offsetExists('site_id'))
|
|
{
|
|
$data['siteId'] = $request->get('site_id');
|
|
}
|
|
if ($files)
|
|
{
|
|
foreach ($files as $code => $file)
|
|
{
|
|
$data[$code] = $file;
|
|
}
|
|
}
|
|
$rawData = $postlist->getRaw('data');
|
|
if (isset($rawData['data']))
|
|
{
|
|
self::$rawData = $rawData['data'];
|
|
}
|
|
return self::actionProcessing(
|
|
$request->get('action'),
|
|
$data
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Register methods in REST.
|
|
* @return array
|
|
* @throws \ReflectionException
|
|
*/
|
|
public static function restBase()
|
|
{
|
|
static $restMethods = array();
|
|
|
|
if (empty($restMethods))
|
|
{
|
|
$restMethods[self::REST_SCOPE_DEFAULT] = array();
|
|
$restMethods[self::REST_SCOPE_CLOUD] = array();
|
|
|
|
$classes = array(
|
|
self::REST_SCOPE_DEFAULT => array(
|
|
'block', 'site', 'landing', 'repo', 'template',
|
|
'demos', 'role', 'syspage', 'chat', 'repowidget'
|
|
),
|
|
self::REST_SCOPE_CLOUD => array(
|
|
'cloud'
|
|
)
|
|
);
|
|
|
|
// then methods list for each class
|
|
foreach ($classes as $scope => $classList)
|
|
{
|
|
foreach ($classList as $className)
|
|
{
|
|
$fullClassName = self::getNamespacePublicClasses() . '\\' . $className;
|
|
$class = new \ReflectionClass($fullClassName);
|
|
$methods = $class->getMethods(\ReflectionMethod::IS_PUBLIC);
|
|
foreach ($methods as $method)
|
|
{
|
|
$static = $method->getStaticVariables();
|
|
if (!isset($static['internal']) || !$static['internal'])
|
|
{
|
|
$command = $scope.'.'.
|
|
mb_strtolower($className).'.'.
|
|
mb_strtolower($method->getName());
|
|
$restMethods[$scope][$command] = array(
|
|
__CLASS__, 'restGateway'
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return array(
|
|
self::REST_SCOPE_DEFAULT => $restMethods[self::REST_SCOPE_DEFAULT],
|
|
self::REST_SCOPE_CLOUD => $restMethods[self::REST_SCOPE_CLOUD]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Gateway between REST and publicaction.
|
|
* @param array $fields Rest fields.
|
|
* @param mixed $t Var.
|
|
* @param \CRestServer $server Server instance.
|
|
* @return mixed
|
|
* @throws \ReflectionException
|
|
*/
|
|
public static function restGateway($fields, $t, $server)
|
|
{
|
|
// get context app
|
|
self::$restApp = AppTable::getByClientId($server->getClientId());
|
|
// prepare method and call action
|
|
$method = $server->getMethod();
|
|
$method = mb_substr($method, mb_strpos($method, '.') + 1);// delete module-prefix
|
|
$method = preg_replace('/\./', '\\', $method, substr_count($method, '.') - 1);
|
|
$method = str_replace('.', '::', $method);
|
|
$result = self::actionProcessing(
|
|
$method,
|
|
$fields,
|
|
true
|
|
);
|
|
// prepare answer
|
|
if ($result['type'] == 'error')
|
|
{
|
|
foreach ($result['result'] as $error)
|
|
{
|
|
throw new \Bitrix\Rest\RestException(
|
|
$error['error_description'],
|
|
$error['error']
|
|
);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return $result['result'];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get current REST application.
|
|
* @return array
|
|
*/
|
|
public static function restApplication()
|
|
{
|
|
return self::$restApp;
|
|
}
|
|
|
|
/**
|
|
* On REST app delete.
|
|
* @param array $app App info.
|
|
* @return void
|
|
*/
|
|
public static function restApplicationDelete($app)
|
|
{
|
|
if (isset($app['APP_ID']) && $app['APP_ID'])
|
|
{
|
|
if (($app = AppTable::getByClientId($app['APP_ID'])))
|
|
{
|
|
Rights::setOff();
|
|
Repo::deleteByAppCode($app['CODE']);
|
|
Placement::deleteByAppId($app['ID']);
|
|
Demos::deleteByAppCode($app['CODE']);
|
|
Rights::setOn();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Before REST app delete.
|
|
* @param \Bitrix\Main\Event $event Event data.
|
|
* @return \Bitrix\Main\EventResult
|
|
*/
|
|
public static function beforeRestApplicationDelete(\Bitrix\Main\Event $event)
|
|
{
|
|
$parameters = $event->getParameters();
|
|
|
|
if ($app = AppTable::getByClientId($parameters['ID']))
|
|
{
|
|
$stat = self::getRestStat(true);
|
|
if (isset($stat[self::REST_USAGE_TYPE_BLOCK][$app['CODE']]))
|
|
{
|
|
$eventResult = new \Bitrix\Main\EventResult(
|
|
\Bitrix\Main\EventResult::ERROR,
|
|
new \Bitrix\Main\Error(
|
|
Loc::getMessage('LANDING_REST_DELETE_EXIST_BLOCKS'),
|
|
'LANDING_EXISTS_BLOCKS'
|
|
)
|
|
);
|
|
|
|
return $eventResult;
|
|
}
|
|
else if (isset($stat[self::REST_USAGE_TYPE_PAGE][$app['CODE']]))
|
|
{
|
|
$eventResult = new \Bitrix\Main\EventResult(
|
|
\Bitrix\Main\EventResult::ERROR,
|
|
new \Bitrix\Main\Error(
|
|
Loc::getMessage('LANDING_REST_DELETE_EXIST_PAGES'),
|
|
'LANDING_EXISTS_PAGES'
|
|
)
|
|
);
|
|
|
|
return $eventResult;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets stat data of using rest app.
|
|
* @param bool $humanFormat Gets data in human format.
|
|
* @param bool $onlyActive Gets data only in active states.
|
|
* @param array $additionalFilter Additional filter array.
|
|
* @return array
|
|
*/
|
|
public static function getRestStat(bool $humanFormat = false, bool $onlyActive = true, array $additionalFilter = []): array
|
|
{
|
|
$blockCnt = [];
|
|
$fullStat = [
|
|
self::REST_USAGE_TYPE_BLOCK => [],
|
|
self::REST_USAGE_TYPE_PAGE => []
|
|
];
|
|
$activeValues = $onlyActive ? 'Y' : ['Y', 'N'];
|
|
$filter = [
|
|
'=%CODE' => 'repo_%',
|
|
'=DELETED' => 'N',
|
|
'=PUBLIC' => $activeValues,
|
|
'=LANDING.ACTIVE' => $activeValues,
|
|
'=LANDING.SITE.ACTIVE' => $activeValues
|
|
];
|
|
|
|
if (isset($additionalFilter['SITE_ID']))
|
|
{
|
|
$filter['LANDING.SITE_ID'] = $additionalFilter['SITE_ID'];
|
|
}
|
|
|
|
Rights::setOff();
|
|
|
|
// gets all partners active block, placed on pages
|
|
$res = Internals\BlockTable::getList([
|
|
'select' => [
|
|
'CODE', 'CNT'
|
|
],
|
|
'filter' => $filter,
|
|
'group' => [
|
|
'CODE'
|
|
],
|
|
'runtime' => [
|
|
new \Bitrix\Main\Entity\ExpressionField('CNT', 'COUNT(*)')
|
|
]
|
|
]);
|
|
while ($row = $res->fetch())
|
|
{
|
|
$blockCnt[mb_substr($row['CODE'], 5)] = $row['CNT'];
|
|
}
|
|
|
|
// gets apps for this blocks
|
|
$res = Repo::getList([
|
|
'select' => [
|
|
'ID', 'APP_CODE'
|
|
],
|
|
'filter' => [
|
|
'ID' => array_keys($blockCnt)
|
|
]
|
|
]);
|
|
while ($row = $res->fetch())
|
|
{
|
|
if (!$row['APP_CODE'])
|
|
{
|
|
continue;
|
|
}
|
|
if (!isset($fullStat[self::REST_USAGE_TYPE_BLOCK][$row['APP_CODE']]))
|
|
{
|
|
$fullStat[self::REST_USAGE_TYPE_BLOCK][$row['APP_CODE']] = 0;
|
|
}
|
|
$fullStat[self::REST_USAGE_TYPE_BLOCK][$row['APP_CODE']] += $blockCnt[$row['ID']];
|
|
}
|
|
unset($blockCnt);
|
|
|
|
// gets additional partners active block with not empty INITIATOR_APP_CODE, placed on pages
|
|
$filter['!CODE'] = $filter['CODE'];
|
|
unset($filter['CODE']);
|
|
$filter['!=INITIATOR_APP_CODE'] = null;
|
|
$res = Internals\BlockTable::getList([
|
|
'select' => [
|
|
'INITIATOR_APP_CODE', 'CNT'
|
|
],
|
|
'filter' => $filter,
|
|
'group' => [
|
|
'INITIATOR_APP_CODE'
|
|
],
|
|
'runtime' => [
|
|
new \Bitrix\Main\Entity\ExpressionField('CNT', 'COUNT(*)')
|
|
]
|
|
]);
|
|
while ($row = $res->fetch())
|
|
{
|
|
$appCode = $row['INITIATOR_APP_CODE'];
|
|
if (!isset($fullStat[self::REST_USAGE_TYPE_BLOCK][$appCode]))
|
|
{
|
|
$fullStat[self::REST_USAGE_TYPE_BLOCK][$appCode] = 0;
|
|
}
|
|
$fullStat[self::REST_USAGE_TYPE_BLOCK][$appCode] += $row['CNT'];
|
|
}
|
|
|
|
// gets all partners active pages
|
|
$filter = [
|
|
'=DELETED' => 'N',
|
|
'=ACTIVE' => $activeValues,
|
|
'=SITE.ACTIVE' => $activeValues,
|
|
'!=INITIATOR_APP_CODE' => null
|
|
];
|
|
if (isset($additionalFilter['SITE_ID']))
|
|
{
|
|
$filter['SITE_ID'] = $additionalFilter['SITE_ID'];
|
|
}
|
|
$res = Landing::getList([
|
|
'select' => [
|
|
'INITIATOR_APP_CODE', 'CNT'
|
|
],
|
|
'filter' => $filter,
|
|
'group' => [
|
|
'INITIATOR_APP_CODE'
|
|
],
|
|
'runtime' => [
|
|
new \Bitrix\Main\Entity\ExpressionField('CNT', 'COUNT(*)')
|
|
]
|
|
]);
|
|
while ($row = $res->fetch())
|
|
{
|
|
$appCode = $row['INITIATOR_APP_CODE'];
|
|
if (!isset($fullStat[self::REST_USAGE_TYPE_PAGE][$appCode]))
|
|
{
|
|
$fullStat[self::REST_USAGE_TYPE_PAGE][$appCode] = 0;
|
|
}
|
|
$fullStat[self::REST_USAGE_TYPE_PAGE][$appCode] += $row['CNT'];
|
|
}
|
|
|
|
// get client id for apps
|
|
if (!$humanFormat && \Bitrix\Main\Loader::includeModule('rest'))
|
|
{
|
|
$appsCode = array_merge(
|
|
array_keys($fullStat[self::REST_USAGE_TYPE_BLOCK]),
|
|
array_keys($fullStat[self::REST_USAGE_TYPE_PAGE])
|
|
);
|
|
$fullStatNew = [
|
|
self::REST_USAGE_TYPE_BLOCK => [],
|
|
self::REST_USAGE_TYPE_PAGE => []
|
|
];
|
|
if ($appsCode)
|
|
{
|
|
$appsCode = array_unique($appsCode);
|
|
$res = AppTable::getList([
|
|
'select' => [
|
|
'CLIENT_ID', 'CODE'
|
|
],
|
|
'filter' => [
|
|
'=CODE' => $appsCode
|
|
]
|
|
]);
|
|
while ($row = $res->fetch())
|
|
{
|
|
foreach ($fullStat as $code => $stat)
|
|
{
|
|
if (isset($stat[$row['CODE']]))
|
|
{
|
|
$fullStatNew[$code][$row['CLIENT_ID']] = $stat[$row['CODE']];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $fullStatNew;
|
|
}
|
|
|
|
Rights::setOn();
|
|
|
|
return $fullStat;
|
|
}
|
|
} |