get('internalClassName'); if (!$name) { throw new Error('Internal report name is not specified.'); } $className = $this->metadata->get("app.advancedReport.internalReports.$name.className"); if (!$className) { if (stripos($name, ':') === false) { throw new Error("Internal report $name is not defined."); } [$moduleName, $reportName] = explode(':', $name); if ($moduleName === 'Custom') { $className = "Espo\\Custom\\Reports\\$reportName"; } else { $className = "Espo\\Modules\\$moduleName\\Reports\\$reportName"; } } if (!class_exists($className)) { throw new Error("Class $className for report $name does not exist."); } /** @var class-string $className */ $binding = BindingContainerBuilder::create() ->bindInstance(Report::class, $report) ->build(); return $this->injectableFactory->createWithBinding($className, $binding); } /** * @throws Forbidden */ public function checkReportCanBeRun(Report $report): void { if ( in_array( $report->getTargetEntityType(), $this->metadata->get('entityDefs.Report.entityListToIgnore', []) ) ) { throw new Forbidden("Entity type is not allowed."); } } /** * @throws Error * @throws Forbidden */ public function fetchGridDataFromReport(Report $report): GridData { if ($report->getType() !== Report::TYPE_GRID) { throw new Error("Non-grid report."); } return new GridData( entityType: $report->getTargetEntityType(), columns: $report->getColumns(), groupBy: $report->getGroupBy(), orderBy: $report->getOrderBy(), applyAcl: $report->getApplyAcl(), filtersWhere: $this->fetchFiltersWhereFromReport($report), chartType: $report->getChartType(), chartColors: get_object_vars($report->get('chartColors') ?? (object) []), chartColor: $report->get('chartColor'), chartDataList: $report->get('chartDataList'), success: ($report->get('data') ?? (object) [])->success ?? null, columnsData: $report->getColumnsData(), tableMode: $report->getTableMode(), ); } /** * @throws Error * @throws Forbidden */ public function fetchListDataFromReport(Report $report): ListData { if ($report->getType() !== Report::TYPE_LIST) { throw new Error("Non-list report."); } return new ListData( $report->getTargetEntityType(), $report->getColumns(), $report->getOrderByList(), $report->getColumnsData(), $this->fetchFiltersWhereFromReport($report) ); } /** * @throws Error */ public function fetchJointDataFromReport(Report $report): JointData { if ($report->getType() !== Report::TYPE_JOINT_GRID) { throw new Error("Non-joint-grid report."); } return new JointData( $report->get('joinedReportDataList') ?? [], $report->get('chartType') ); } /** * @throws Error * @throws Forbidden */ public function fetchFiltersWhereFromReport(Report $report): ?WhereItem { $isNotList = $report->getType() !== Report::TYPE_LIST; $raw = $report->get('filtersData') && !$report->get('filtersDataList') ? $this->convertFiltersData($report->get('filtersData')) : $this->convertFiltersDataList($report->get('filtersDataList') ?? [], $isNotList); if (!$raw) { return null; } $raw = json_decode( /** @phpstan-ignore-next-line */ json_encode($raw), true ); return WhereItem::fromRawAndGroup($raw); } /** * @param array|null $filtersData * @return stdClass[]|null */ private function convertFiltersData(?array $filtersData): ?array { if (empty($filtersData)) { return null; } $arr = []; foreach ($filtersData as $name => $defs) { $field = $name; if (empty($defs)) { continue; } if (isset($defs->where)) { $arr[] = $defs->where; } else { if (isset($defs->field)) { $field = $defs->field; } $type = $defs->type ?? null; if (!empty($defs->dateTime)) { $arr[] = $this->fixDateTimeWhere( $type, $field, $defs->value ?? null, false ); } else { $o = new stdClass(); $o->type = $type; $o->field = $field; $o->value = $defs->value ?? null; $arr[] = $o; } } } return $arr; } /** * @param object{ * type?: string, * name?: ?string, * params?: object{ * type?: string, * where?: mixed, * attribute?: string, * field?: string, * dateTime?: string, * value?: mixed, * function?: string, * expression?: string, * operator?: string, * }, * }[] $filtersDataList * @return stdClass[]|null * @throws Error * @throws Forbidden */ private function convertFiltersDataList(array $filtersDataList, bool $useSystemTimeZone): ?array { if (empty($filtersDataList)) { return null; } $arr = []; foreach ($filtersDataList as $defs) { $field = null; if (isset($defs->name)) { $field = $defs->name; } if (empty($defs) || empty($defs->params)) { continue; } $params = $defs->params; $type = $defs->type ?? null; if ( in_array($type, [ self::WHERE_TYPE_OR, self::WHERE_TYPE_AND, self::WHERE_TYPE_NOT, self::WHERE_TYPE_SUB_QUERY_IN, self::WHERE_TYPE_SUB_QUERY_NOT_IN, self::WHERE_TYPE_HAVING, ]) ) { if (empty($params->value)) { continue; } $o = new stdClass(); $o->type = $params->type ?? null; if ($o->type === self::WHERE_TYPE_NOT) { $o->type = self::WHERE_TYPE_SUB_QUERY_NOT_IN; } if ($o->type === self::WHERE_TYPE_HAVING) { $o->type = self::WHERE_TYPE_AND; $o->attribute = self::ATTR_HAVING; } $o->value = $this->convertFiltersDataList($params->value, $useSystemTimeZone); $arr[] = $o; continue; } if ($type === 'complexExpression') { $o = (object) []; $function = $params->function ?? null; if ($function === 'custom') { if (empty($params->expression)) { continue; } $o->attribute = $params->expression; $o->type = 'expression'; } else if ($function === 'customWithOperator') { if (empty($params->expression)) { continue; } if (empty($params->operator)) { continue; } $o->attribute = $params->expression; $o->type = $params->operator; } else { if (empty($params->attribute)) { continue; } if (empty($params->operator)) { continue; } $o->attribute = $params->attribute; if ($function) { $o->attribute = $function . ':' . $o->attribute; } $o->type = $params->operator; } if (isset($params->value) && is_string($params->value) && strlen($params->value)) { try { $o->value = $this->runFormula($params->value); } catch (FormulaError $e) { throw new Error($e->getMessage()); } } $arr[] = $o; continue; } if (isset($params->where)) { $arr[] = $params->where; continue; } if (isset($params->field)) { $field = $params->field; } if (empty($params->type)) { continue; } $type = $params->type; if (!empty($params->dateTime)) { $arr[] = $this->fixDateTimeWhere( $type, $field, $params->value ?? null, $useSystemTimeZone ); continue; } $o = new stdClass(); $o->type = $type; $o->field = $field; $o->attribute = $field; $o->value = $params->value ?? null; $arr[] = $o; } return $arr; } /** * @param mixed $value */ private function fixDateTimeWhere(string $type, string $field, $value, bool $useSystemTimeZone): object { $timeZone = null; if (!$useSystemTimeZone) { $timeZone = $this->preferences->get('timeZone'); } if (!$timeZone) { $timeZone = $this->config->get('timeZone') ?? 'UTC'; } return (object) [ 'attribute' => $field, 'type' => $type, 'value' => $value, 'dateTime' => true, 'timeZone' => $timeZone, ]; } /** * @throws Forbidden */ public function checkRuntimeFilters(WhereItem $where, Report $report): void { $this->checkRuntimeFiltersItem($where, $report->getRuntimeFilters()); } /** * @param string[] $allowedFilterList * @throws Forbidden */ private function checkRuntimeFiltersItem(WhereItem $item, array $allowedFilterList): void { $type = $item->getType(); if ($type === self::WHERE_TYPE_AND || $type === self::WHERE_TYPE_OR) { foreach ($item->getItemList() as $subItem) { $this->checkRuntimeFiltersItem($subItem, $allowedFilterList); } return; } $attribute = $item->getAttribute(); if (!$attribute) { throw new Forbidden("Not allowed runtime filter item."); } if ($attribute === 'id') { return; } if (str_contains($attribute, ':')) { throw new Forbidden("Expressions are not allowed in runtime filter."); } if (!str_contains($attribute, '.')) { return; } $isAllowed = in_array($attribute, $allowedFilterList); if (!$isAllowed && str_ends_with($attribute, 'Id')) { $isAllowed = in_array(substr($attribute, 0, -2), $allowedFilterList); } if (!$isAllowed) { throw new Forbidden("Not allowed runtime filter $attribute."); } } /** * @return mixed * @throws Forbidden * @throws FormulaError */ private function runFormula(string $script) { $this->formulaChecker->check($script); $script = $this->formulaChecker->sanitize($script); return $this->formulaManager->run($script); } }