Skip to content

Commit 909e2ad

Browse files
AdrienClairembaulttrasher
authored andcommitted
Check custom assets availability before importing a form
1 parent 3a7efa3 commit 909e2ad

File tree

12 files changed

+395
-14
lines changed

12 files changed

+395
-14
lines changed

src/Glpi/Asset/AssetDefinitionManager.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,14 @@ public function getCapacity(string $classname): ?CapacityInterface
390390
return $this->capacities[$classname] ?? null;
391391
}
392392

393+
public function isCustomAsset(string $class): bool
394+
{
395+
return str_starts_with(
396+
$class,
397+
AssetDefinition::getCustomObjectNamespace()
398+
);
399+
}
400+
393401
private function loadConcreteClass(AssetDefinition $definition): void
394402
{
395403
$rightname = $definition->getCustomObjectRightname();

src/Glpi/Controller/Form/Import/Step2PreviewController.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,11 @@ private function previewResponse(
7979
}
8080

8181
$previewResult = $serializer->previewImport($json, $mapper, $skipped_forms);
82-
if (empty($previewResult->getValidForms()) && empty($previewResult->getInvalidForms())) {
82+
if (
83+
empty($previewResult->getValidForms())
84+
&& empty($previewResult->getInvalidForms())
85+
&& empty($previewResult->getFormsWithFatalErrors())
86+
) {
8387
return new RedirectResponse($request->getBasePath() . '/Form/Import');
8488
}
8589

src/Glpi/Dropdown/DropdownDefinitionManager.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,14 @@ public function autoloadClass(string $classname): void
124124
}
125125
}
126126

127+
public function isCustomDropdown(string $class): bool
128+
{
129+
return str_starts_with(
130+
$class,
131+
DropdownDefinition::getCustomObjectNamespace()
132+
);
133+
}
134+
127135
private function loadConcreteClass(DropdownDefinition $definition): void
128136
{
129137
$rightname = $definition->getCustomObjectRightname();

src/Glpi/Form/Export/Result/ImportError.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,5 @@
3838
enum ImportError
3939
{
4040
case MISSING_DATA_REQUIREMENT;
41+
case MISSING_CUSTOM_TYPE_REQUIREMENT;
4142
}

src/Glpi/Form/Export/Result/ImportResultPreview.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ final class ImportResultPreview
4646
/** @var string[] $skipped_forms */
4747
private array $skipped_forms = [];
4848

49+
/**
50+
* Store error messages for a given form ID.
51+
* Unlike $invalid_forms, this represent errors that aren't recoverable.
52+
*
53+
* @var array<int, array{name: string, errors: array<string>}> $fatal_errors
54+
**/
55+
private array $fatal_errors = [];
56+
4957
public function addValidForm(int $form_id, string $form_name): void
5058
{
5159
$this->valid_forms[$form_id] = $form_name;
@@ -78,4 +86,30 @@ public function getSkippedForms(): array
7886
{
7987
return $this->skipped_forms;
8088
}
89+
90+
public function addFatalErrorForForm(
91+
int $form_id,
92+
string $form_name,
93+
string $error
94+
): void {
95+
if (!isset($this->fatal_errors[$form_id])) {
96+
$this->fatal_errors[$form_id] = [
97+
'name' => $form_name,
98+
'errors' => [],
99+
];
100+
}
101+
102+
$this->fatal_errors[$form_id]['errors'][] = $error;
103+
}
104+
105+
public function hasFatalErrorForForm(int $form_id): bool
106+
{
107+
$errors = $this->fatal_errors[$form_id]['errors'] ?? [];
108+
return count($errors) > 0;
109+
}
110+
111+
public function getFormsWithFatalErrors(): array
112+
{
113+
return $this->fatal_errors;
114+
}
81115
}

src/Glpi/Form/Export/Serializer/FormSerializer.php

Lines changed: 100 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636

3737
use CommonDBTM;
3838
use Entity;
39+
use Glpi\Asset\AssetDefinitionManager;
40+
use Glpi\Dropdown\DropdownDefinitionManager;
3941
use Glpi\Form\AccessControl\FormAccessControl;
4042
use Glpi\Form\Category;
4143
use Glpi\Form\Comment;
@@ -54,6 +56,7 @@
5456
use Glpi\Form\Export\Specification\CommentContentSpecification;
5557
use Glpi\Form\Export\Specification\ConditionDataSpecification;
5658
use Glpi\Form\Export\Specification\CustomIllustrationContentSpecification;
59+
use Glpi\Form\Export\Specification\CustomTypeRequirementSpecification;
5760
use Glpi\Form\Export\Specification\DataRequirementSpecification;
5861
use Glpi\Form\Export\Specification\DestinationContentSpecification;
5962
use Glpi\Form\Export\Specification\ExportContentSpecification;
@@ -65,9 +68,12 @@
6568
use Glpi\Form\FormTranslation;
6669
use Glpi\Form\Question;
6770
use Glpi\Form\QuestionType\QuestionTypeInterface;
71+
use Glpi\Form\QuestionType\QuestionTypeItem;
72+
use Glpi\Form\QuestionType\QuestionTypeItemExtraDataConfig;
6873
use Glpi\Form\Section;
6974
use Glpi\UI\IllustrationManager;
7075
use InvalidArgumentException;
76+
use LogicException;
7177
use RuntimeException;
7278
use Session;
7379
use Throwable;
@@ -120,17 +126,32 @@ public function previewImport(
120126
// Validate each forms
121127
$results = new ImportResultPreview();
122128
foreach ($export_specification->forms as $form_spec) {
123-
$requirements = $form_spec->data_requirements;
124-
$mapper->mapExistingItemsForRequirements($requirements);
125-
129+
// Skip ignored forms
126130
$form_id = $form_spec->id;
127131
$form_name = $form_spec->name;
128132
if (in_array($form_id, $skipped_forms)) {
129133
$results->addSkippedForm($form_id, $form_name);
130134
continue;
131135
}
132136

133-
if ($mapper->validateRequirements($requirements)) {
137+
// Validate custom types requirements
138+
$types_requirements = $form_spec->custom_types_requirements;
139+
$missing_types = $this->getMissingCustomTypes($types_requirements);
140+
foreach ($missing_types as $missing_type) {
141+
$message = sprintf(__('Unknown custom type: %s'), $missing_type);
142+
$results->addFatalErrorForForm($form_id, $form_name, $message);
143+
}
144+
145+
// Do not process others requirements if we reached a fatal error
146+
if ($results->hasFatalErrorForForm($form_id)) {
147+
continue;
148+
}
149+
150+
// Validate data requirements
151+
$data_requirements = $form_spec->data_requirements;
152+
$mapper->mapExistingItemsForRequirements($data_requirements);
153+
154+
if ($mapper->validateRequirements($data_requirements)) {
134155
$results->addValidForm($form_id, $form_name);
135156
} else {
136157
$results->addInvalidForm($form_id, $form_name);
@@ -180,15 +201,27 @@ public function importFormsFromJson(
180201
// Import each forms
181202
$result = new ImportResult();
182203
foreach ($export_specification->forms as $form_spec) {
183-
$requirements = $form_spec->data_requirements;
184-
$mapper->mapExistingItemsForRequirements($requirements);
185-
204+
// Skip form if needed
186205
$form_id = $form_spec->id;
187206
if (in_array($form_id, $skipped_forms)) {
188207
continue;
189208
}
190209

191-
if (!$mapper->validateRequirements($requirements)) {
210+
// Validate custom types
211+
$types_requirements = $form_spec->custom_types_requirements;
212+
if (!$this->validateCustomTypesRequirements($types_requirements)) {
213+
$result->addFailedFormImport(
214+
$form_spec->name,
215+
ImportError::MISSING_CUSTOM_TYPE_REQUIREMENT,
216+
);
217+
continue;
218+
}
219+
220+
// Validate data requirements
221+
$data_requirements = $form_spec->data_requirements;
222+
$mapper->mapExistingItemsForRequirements($data_requirements);
223+
224+
if (!$mapper->validateRequirements($data_requirements)) {
192225
$result->addFailedFormImport(
193226
$form_spec->name,
194227
ImportError::MISSING_DATA_REQUIREMENT
@@ -235,6 +268,7 @@ private function exportFormToSpec(Form $form): FormContentSpecification
235268
$form_spec = $this->exportAccesControlPolicies($form, $form_spec);
236269
$form_spec = $this->exportDestinations($form, $form_spec);
237270
$form_spec = $this->exportTranslations($form, $form_spec);
271+
$form_spec = $this->addCustomTypesRequirements($form, $form_spec);
238272

239273
return $form_spec;
240274
}
@@ -1108,4 +1142,62 @@ private function prepareIllustrationDataForImport(
11081142

11091143
return $prefix . $illustration->key;
11101144
}
1145+
1146+
private function addCustomTypesRequirements(
1147+
Form $form,
1148+
FormContentSpecification $form_spec,
1149+
): FormContentSpecification {
1150+
$asset_manager = AssetDefinitionManager::getInstance();
1151+
$dropdown_manager = DropdownDefinitionManager::getInstance();
1152+
1153+
// Look for item question on custom assets types
1154+
foreach ($form->getQuestions() as $question) {
1155+
$type = $question->getQuestionType();
1156+
if (!$type instanceof QuestionTypeItem) {
1157+
continue;
1158+
}
1159+
1160+
$config = $question->getExtraDataConfig();
1161+
if (!$config instanceof QuestionTypeItemExtraDataConfig) {
1162+
throw new LogicException(); // Impossible
1163+
}
1164+
1165+
$itemtype = $config->getItemtype();
1166+
if (
1167+
$asset_manager->isCustomAsset($itemtype)
1168+
|| $dropdown_manager->isCustomDropdown($itemtype)
1169+
) {
1170+
$form_spec->addCustomTypeRequirement(
1171+
new CustomTypeRequirementSpecification($itemtype)
1172+
);
1173+
}
1174+
}
1175+
1176+
return $form_spec;
1177+
}
1178+
1179+
/** @param CustomTypeRequirementSpecification[] $requirements */
1180+
private function validateCustomTypesRequirements(array $requirements): bool
1181+
{
1182+
foreach ($requirements as $requirement) {
1183+
if (!class_exists($requirement->itemtype)) {
1184+
return false;
1185+
}
1186+
}
1187+
1188+
return true;
1189+
}
1190+
1191+
/** @param CustomTypeRequirementSpecification[] $requirements */
1192+
private function getMissingCustomTypes(array $requirements): array
1193+
{
1194+
$missing_types = [];
1195+
foreach ($requirements as $requirement) {
1196+
if (!class_exists($requirement->itemtype)) {
1197+
$missing_types[] = $requirement->itemtype;
1198+
}
1199+
}
1200+
1201+
return $missing_types;
1202+
}
11111203
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
/**
4+
* ---------------------------------------------------------------------
5+
*
6+
* GLPI - Gestionnaire Libre de Parc Informatique
7+
*
8+
* http://glpi-project.org
9+
*
10+
* @copyright 2015-2025 Teclib' and contributors.
11+
* @licence https://www.gnu.org/licenses/gpl-3.0.html
12+
*
13+
* ---------------------------------------------------------------------
14+
*
15+
* LICENSE
16+
*
17+
* This file is part of GLPI.
18+
*
19+
* This program is free software: you can redistribute it and/or modify
20+
* it under the terms of the GNU General Public License as published by
21+
* the Free Software Foundation, either version 3 of the License, or
22+
* (at your option) any later version.
23+
*
24+
* This program is distributed in the hope that it will be useful,
25+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
26+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
27+
* GNU General Public License for more details.
28+
*
29+
* You should have received a copy of the GNU General Public License
30+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
31+
*
32+
* ---------------------------------------------------------------------
33+
*/
34+
35+
namespace Glpi\Form\Export\Specification;
36+
37+
final class CustomTypeRequirementSpecification
38+
{
39+
public function __construct(
40+
public string $itemtype = "",
41+
) {}
42+
}

src/Glpi/Form/Export/Specification/FormContentSpecification.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,14 @@ final class FormContentSpecification
7777
/** @var DataRequirementSpecification[] $data_requirements */
7878
public array $data_requirements = [];
7979

80+
/** @var CustomTypeRequirementSpecification[] $data_requirements */
81+
public array $custom_types_requirements = [];
82+
8083
/** @return DataRequirementSpecification[] */
8184
public function getDataRequirements(): array
8285
{
8386
return $this->data_requirements;
8487
}
85-
8688
public function addDataRequirement(
8789
DataRequirementSpecification $requirement
8890
): void {
@@ -93,4 +95,14 @@ public function addRequirementsFromDynamicData(DynamicExportData $data): void
9395
{
9496
array_push($this->data_requirements, ...$data->getRequirements());
9597
}
98+
99+
public function getCustomTypesRequirements(): array
100+
{
101+
return $this->custom_types_requirements;
102+
}
103+
public function addCustomTypeRequirement(
104+
CustomTypeRequirementSpecification $requirement
105+
): void {
106+
$this->custom_types_requirements[] = $requirement;
107+
}
96108
}

templates/pages/admin/form/import/step2_preview.html.twig

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,17 @@
5656
</tr>
5757
</thead>
5858
<tbody>
59-
{% macro render_row(form_id, form_name, status_icon, status_text, show_resolve_issues_action, show_remove_action) %}
59+
{% macro render_row(form_id, form_name, status_icon, messages, show_resolve_issues_action, show_remove_action) %}
6060
<tr>
6161
<td class="w-50 px-4 align-middle">{{ form_name }}</td>
6262
<td class="w-50 px-4 align-middle">
6363
<div class="d-flex align-items-center">
6464
<i class="ti {{ status_icon }} me-2"></i>
65-
<span>{{ status_text }}</span>
65+
<div>
66+
{% for message in messages %}
67+
<span>{{ message }}</span>
68+
{% endfor %}
69+
</div>
6670
</div>
6771
</td>
6872
<td class="w-25 px-4 align-middle">
@@ -108,7 +112,7 @@
108112
form_id,
109113
form_name,
110114
'ti-check text-success',
111-
__("Ready to be imported"),
115+
[__("Ready to be imported")],
112116
false,
113117
true
114118
) }}
@@ -118,11 +122,21 @@
118122
form_id,
119123
form_name,
120124
'ti-x text-danger',
121-
__("Can't be imported"),
125+
[__("Can't be imported")],
122126
true,
123127
true
124128
) }}
125129
{% endfor %}
130+
{% for form_id, details in preview.getFormsWithFatalErrors() %}
131+
{{ _self.render_row(
132+
form_id,
133+
details.name,
134+
'ti-x text-danger',
135+
details.errors,
136+
false,
137+
true
138+
) }}
139+
{% endfor %}
126140
</tbody>
127141
</table>
128142
</div>

0 commit comments

Comments
 (0)