Fix file check of `database` instruction when using DevTools
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / acp / form / DevtoolsProjectAddForm.class.php
1 <?php
2
3 namespace wcf\acp\form;
4
5 use wcf\data\AbstractDatabaseObjectAction;
6 use wcf\data\devtools\project\DevtoolsProject;
7 use wcf\data\devtools\project\DevtoolsProjectAction;
8 use wcf\data\devtools\project\DevtoolsProjectList;
9 use wcf\data\package\installation\plugin\PackageInstallationPlugin;
10 use wcf\data\package\installation\plugin\PackageInstallationPluginList;
11 use wcf\data\package\Package;
12 use wcf\form\AbstractForm;
13 use wcf\form\AbstractFormBuilderForm;
14 use wcf\system\devtools\package\DevtoolsPackageXmlWriter;
15 use wcf\system\form\builder\container\FormContainer;
16 use wcf\system\form\builder\container\TabFormContainer;
17 use wcf\system\form\builder\container\TabMenuFormContainer;
18 use wcf\system\form\builder\field\BooleanFormField;
19 use wcf\system\form\builder\field\DateFormField;
20 use wcf\system\form\builder\field\dependency\NonEmptyFormFieldDependency;
21 use wcf\system\form\builder\field\dependency\ValueFormFieldDependency;
22 use wcf\system\form\builder\field\devtools\project\DevtoolsProjectExcludedPackagesFormField;
23 use wcf\system\form\builder\field\devtools\project\DevtoolsProjectInstructionsFormField;
24 use wcf\system\form\builder\field\devtools\project\DevtoolsProjectOptionalPackagesFormField;
25 use wcf\system\form\builder\field\devtools\project\DevtoolsProjectRequiredPackagesFormField;
26 use wcf\system\form\builder\field\MultipleSelectionFormField;
27 use wcf\system\form\builder\field\RadioButtonFormField;
28 use wcf\system\form\builder\field\TextFormField;
29 use wcf\system\form\builder\field\UrlFormField;
30 use wcf\system\form\builder\field\validation\FormFieldValidationError;
31 use wcf\system\form\builder\field\validation\FormFieldValidator;
32 use wcf\system\form\builder\field\validation\FormFieldValidatorUtil;
33 use wcf\system\package\plugin\AbstractXMLPackageInstallationPlugin;
34 use wcf\system\Regex;
35 use wcf\system\WCF;
36 use wcf\util\DirectoryUtil;
37 use wcf\util\FileUtil;
38
39 /**
40 * Shows the devtools project add form.
41 *
42 * @author Alexander Ebert, Matthias Schmidt
43 * @copyright 2001-2019 WoltLab GmbH
44 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
45 * @package WoltLabSuite\Core\Acp\Form
46 * @since 3.1
47 *
48 * @property null|DevtoolsProject $formObject
49 */
50 class DevtoolsProjectAddForm extends AbstractFormBuilderForm
51 {
52 /**
53 * @inheritDoc
54 */
55 public $activeMenuItem = 'wcf.acp.menu.link.devtools.project.add';
56
57 /**
58 * @inheritDoc
59 */
60 public $formAction = 'create';
61
62 /**
63 * @inheritDoc
64 */
65 public $neededPermissions = ['admin.configuration.package.canInstallPackage'];
66
67 /**
68 * @inheritDoc
69 */
70 public $objectActionClass = DevtoolsProjectAction::class;
71
72 /**
73 * newly added or edited project
74 * @var DevtoolsProject
75 */
76 public $project;
77
78 /**
79 * ids of the fields containing object data
80 * @var string[]
81 */
82 public $projectFields = ['name', 'path'];
83
84 /**
85 * @inheritDoc
86 */
87 protected function createForm()
88 {
89 parent::createForm();
90
91 $tabMenu = TabMenuFormContainer::create('project');
92 $this->form->appendChild($tabMenu);
93
94 $dataTab = TabFormContainer::create('dataTab');
95 $dataTab->label('wcf.global.form.data');
96 $tabMenu->appendChild($dataTab);
97
98 $mode = RadioButtonFormField::create('mode')
99 ->label('wcf.acp.devtools.project.add.mode')
100 ->options(function () {
101 $options = [
102 'import' => 'wcf.acp.devtools.project.add.mode.import',
103 'setup' => 'wcf.acp.devtools.project.add.mode.setup',
104 ];
105
106 if ($this->formObject !== null) {
107 $options['edit'] = 'wcf.acp.devtools.project.add.mode.edit';
108 }
109
110 return $options;
111 })
112 ->immutable($this->formObject !== null)
113 ->value($this->formObject ? 'edit' : 'import');
114
115 $dataContainer = FormContainer::create('data')
116 ->label('wcf.global.form.data')
117 ->appendChildren([
118 $mode,
119 TextFormField::create('name')
120 ->label('wcf.acp.devtools.project.name')
121 ->required()
122 ->addValidator(new FormFieldValidator('uniqueness', function (TextFormField $formField) {
123 $name = $formField->getSaveValue();
124
125 if ($this->formObject === null || $this->formObject->name !== $name) {
126 $projectList = new DevtoolsProjectList();
127 $projectList->getConditionBuilder()->add('name = ?', [$name]);
128
129 if ($projectList->countObjects() > 0) {
130 $formField->addValidationError(
131 new FormFieldValidationError(
132 'notUnique',
133 'wcf.acp.devtools.project.name.error.notUnique'
134 )
135 );
136 }
137 }
138 })),
139
140 TextFormField::create('path')
141 ->label('wcf.acp.devtools.project.path')
142 ->required()
143 ->addValidator(new FormFieldValidator('validPath', function (TextFormField $formField) {
144 // ensure that unified directory separators are used
145 // and that there is a trailing slash
146 $formField->value(
147 FileUtil::addTrailingSlash(
148 FileUtil::unifyDirSeparator($formField->getSaveValue() ?? '')
149 )
150 );
151
152 $path = $formField->getSaveValue();
153
154 /** @var RadioButtonFormField $modeField */
155 $modeField = $formField->getDocument()->getNodeById('mode');
156
157 switch ($modeField->getSaveValue()) {
158 case 'import':
159 case 'edit':
160 if ($this->formObject === null || $this->formObject->path !== $path) {
161 $errorType = DevtoolsProject::validatePath($path);
162 if ($errorType !== '') {
163 $formField->addValidationError(
164 new FormFieldValidationError(
165 $errorType,
166 'wcf.acp.devtools.project.path.error.' . $errorType
167 )
168 );
169 }
170 }
171
172 break;
173
174 case 'setup':
175 if (\is_dir($path)) {
176 $formField->addValidationError(
177 new FormFieldValidationError(
178 'pathExists',
179 'wcf.acp.devtools.project.path.error.pathExists'
180 )
181 );
182 } elseif (!\is_dir(\dirname($path))) {
183 $formField->addValidationError(
184 new FormFieldValidationError(
185 'parentDoesNotExist',
186 'wcf.acp.devtools.project.path.error.parentDoesNotExist'
187 )
188 );
189 } else {
190 if (!FileUtil::makePath($path)) {
191 $formField->addValidationError(
192 new FormFieldValidationError(
193 'cannotMakeDirectory',
194 'wcf.acp.devtools.project.path.error.cannotMakeDirectory'
195 )
196 );
197 } else {
198 // remove directory for now again
199 \rmdir($path);
200 }
201 }
202
203 break;
204
205 default:
206 throw new \LogicException("Unknown mode '{$modeField->getSaveValue()}'.");
207 break;
208 }
209 }))
210 ->addValidator(new FormFieldValidator('uniqueness', function (TextFormField $formField) {
211 $path = $formField->getSaveValue();
212
213 if ($this->formObject === null || $this->formObject->path !== $path) {
214 $projectList = new DevtoolsProjectList();
215 $projectList->getConditionBuilder()->add('path = ?', [$path]);
216
217 if ($projectList->countObjects() > 0) {
218 $formField->addValidationError(
219 new FormFieldValidationError(
220 'notUnique',
221 'wcf.acp.devtools.project.path.error.notUnique'
222 )
223 );
224 }
225 }
226 })),
227 ]);
228 $dataTab->appendChild($dataContainer);
229
230 $packageInformation = FormContainer::create('packageInformation')
231 ->label('wcf.acp.devtools.project.packageInformation')
232 ->appendChildren([
233 TextFormField::create('packageIdentifier')
234 ->label('wcf.acp.devtools.project.packageIdentifier')
235 ->description('wcf.acp.devtools.project.packageIdentifier.description')
236 ->required()
237 ->maximumLength(191)
238 ->addValidator(new FormFieldValidator('format', static function (TextFormField $formField) {
239 if (!Package::isValidPackageName($formField->getSaveValue())) {
240 $formField->addValidationError(
241 new FormFieldValidationError(
242 'format',
243 'wcf.acp.devtools.project.packageIdentifier.error.format'
244 )
245 );
246 }
247 })),
248
249 TextFormField::create('packageName')
250 ->label('wcf.acp.devtools.project.packageName')
251 ->required()
252 ->maximumLength(255)
253 ->i18n()
254 ->languageItemPattern('__NONE__'),
255
256 TextFormField::create('packageDescription')
257 ->label('wcf.global.description')
258 ->required()
259 ->maximumLength(255)
260 ->i18n()
261 ->languageItemPattern('__NONE__'),
262
263 BooleanFormField::create('isApplication')
264 ->label('wcf.acp.devtools.project.isApplication')
265 ->description('wcf.acp.devtools.project.isApplication.description'),
266
267 TextFormField::create('applicationDirectory')
268 ->label('wcf.acp.devtools.project.applicationDirectory')
269 ->description('wcf.acp.devtools.project.applicationDirectory.description')
270 ->available($this->formObject === null || !$this->formObject->isCore())
271 ->addValidator(FormFieldValidatorUtil::getRegularExpressionValidator(
272 '[A-z0-9\-\_]+$',
273 'wcf.acp.devtools.project.applicationDirectory'
274 )),
275
276 TextFormField::create('version')
277 ->label('wcf.acp.devtools.project.packageVersion')
278 ->description('wcf.acp.devtools.project.packageVersion.description')
279 ->required()
280 ->maximumLength(255),
281
282 DateFormField::create('date')
283 ->label('wcf.acp.devtools.project.packageDate')
284 ->description('wcf.acp.devtools.project.packageDate.description')
285 ->required()
286 ->saveValueFormat('Y-m-d'),
287
288 UrlFormField::create('packageUrl')
289 ->label('wcf.acp.devtools.project.packageUrl')
290 ->description('wcf.acp.devtools.project.packageUrl.description')
291 ->maximumLength(255),
292
293 TextFormField::create('license')
294 ->label('wcf.acp.devtools.project.license')
295 ->maximumLength(255)
296 ->i18n()
297 ->languageItemPattern('__NONE__'),
298 ])
299 ->addDependency(
300 ValueFormFieldDependency::create('mode')
301 ->field($mode)
302 ->values(['edit', 'setup'])
303 );
304 $dataTab->appendChild($packageInformation);
305
306 /** @var BooleanFormField $isApplication */
307 $isApplication = $packageInformation->getNodeById('isApplication');
308 $packageInformation->getNodeById('applicationDirectory')
309 ->addDependency(
310 NonEmptyFormFieldDependency::create('isApplication')
311 ->field($isApplication)
312 );
313
314 $authorInformation = FormContainer::create('authorInformation')
315 ->label('wcf.acp.devtools.project.authorInformation')
316 ->appendChildren([
317 TextFormField::create('author')
318 ->label('wcf.acp.devtools.project.author')
319 ->required()
320 ->maximumLength(255),
321
322 UrlFormField::create('authorUrl')
323 ->label('wcf.acp.devtools.project.authorUrl')
324 ->maximumLength(255),
325 ])
326 ->addDependency(
327 ValueFormFieldDependency::create('mode')
328 ->field($mode)
329 ->values(['edit', 'setup'])
330 );
331 $dataTab->appendChild($authorInformation);
332
333 $compatibility = FormContainer::create('compatibility')
334 ->label('wcf.acp.devtools.project.compatibility')
335 ->appendChildren([
336 MultipleSelectionFormField::create('apiVersions')
337 ->label('wcf.acp.devtools.project.apiVersions')
338 ->description('wcf.acp.devtools.project.apiVersions.description')
339 ->options(static function () {
340 $apiVersions = \array_filter(\array_merge(
341 WCF::getSupportedLegacyApiVersions(),
342 [WSC_API_VERSION]
343 ), static function ($value) {
344 return $value !== 2017;
345 });
346
347 \sort($apiVersions);
348
349 return \array_combine($apiVersions, $apiVersions);
350 })
351 ->available($this->formObject === null || !$this->formObject->isCore()),
352 ])
353 ->addDependency(
354 ValueFormFieldDependency::create('mode')
355 ->field($mode)
356 ->values(['edit', 'setup'])
357 );
358 $dataTab->appendChild($compatibility);
359
360 $requiredPackages = FormContainer::create('requiredPackagesContainer')
361 ->label('wcf.acp.devtools.project.requiredPackages')
362 ->description('wcf.acp.devtools.project.requiredPackages.description')
363 ->appendChild(
364 DevtoolsProjectRequiredPackagesFormField::create()
365 ->addValidator(new FormFieldValidator(
366 'selfRequirement',
367 static function (DevtoolsProjectRequiredPackagesFormField $formField) {
368 /** @var TextFormField $packageIdentifier */
369 $packageIdentifier = $formField->getDocument()->getNodeById('packageIdentifier');
370
371 // ensure that the package does not require itself
372 foreach ($formField->getSaveValue() as $requirement) {
373 if ($requirement['packageIdentifier'] === $packageIdentifier->getSaveValue()) {
374 $formField->addValidationError(
375 new FormFieldValidationError(
376 'selfRequirement',
377 'wcf.acp.devtools.project.requiredPackage.error.selfRequirement'
378 )
379 );
380 }
381 }
382 }
383 ))
384 ->addValidator(new FormFieldValidator(
385 'missingFiles',
386 function (DevtoolsProjectRequiredPackagesFormField $formField) {
387 /** @var TextFormField $pathField */
388 $pathField = $this->form->getNodeById('path');
389 $path = FileUtil::addTrailingSlash($pathField->getSaveValue());
390
391 $missingFiles = [];
392 foreach ($formField->getSaveValue() as $requirement) {
393 if ($requirement['file'] && !\is_file($path . "requirements/{$requirement['packageIdentifier']}.tar")) {
394 $missingFiles[] = "requirements/{$requirement['packageIdentifier']}.tar";
395 }
396 }
397
398 if (!empty($missingFiles)) {
399 $formField->addValidationError(
400 new FormFieldValidationError(
401 'missingFiles',
402 'wcf.acp.devtools.project.requiredPackage.error.missingFiles',
403 ['missingFiles' => $missingFiles]
404 )
405 );
406 }
407 }
408 ))
409 );
410 $tabMenu->appendChild(
411 TabFormContainer::create('requiredPackagesTab')
412 ->label('wcf.acp.devtools.project.requiredPackages.shortTitle')
413 ->appendChild($requiredPackages)
414 ->addDependency(
415 ValueFormFieldDependency::create('mode')
416 ->field($mode)
417 ->values(['edit', 'setup'])
418 )
419 );
420
421 $optionalPackages = FormContainer::create('optionalPackagesContainer')
422 ->label('wcf.acp.devtools.project.optionalPackages')
423 ->description('wcf.acp.devtools.project.optionalPackages.description')
424 ->appendChild(
425 DevtoolsProjectOptionalPackagesFormField::create()
426 ->addValidator(new FormFieldValidator(
427 'selfOptional',
428 static function (DevtoolsProjectOptionalPackagesFormField $formField) {
429 /** @var TextFormField $packageIdentifier */
430 $packageIdentifier = $formField->getDocument()->getNodeById('packageIdentifier');
431
432 // ensure that the package does not mark itself as optional
433 foreach ($formField->getSaveValue() as $requirement) {
434 if ($requirement['packageIdentifier'] === $packageIdentifier->getSaveValue()) {
435 $formField->addValidationError(
436 new FormFieldValidationError(
437 'selfExclusion',
438 'wcf.acp.devtools.project.optionalPackage.error.selfOptional'
439 )
440 );
441 }
442 }
443 }
444 ))
445 ->addValidator(new FormFieldValidator(
446 'requirementOptional',
447 static function (DevtoolsProjectOptionalPackagesFormField $formField) {
448 /** @var DevtoolsProjectRequiredPackagesFormField $requiredPackagesField */
449 $requiredPackagesField = $formField->getDocument()->getNodeById('requiredPackages');
450 $requiredPackages = [];
451 foreach ($requiredPackagesField->getSaveValue() as $requiredPackage) {
452 $requiredPackages[$requiredPackage['packageIdentifier']] = $requiredPackage;
453 }
454
455 // ensure that the optionals and requirements do not conflict
456 foreach ($formField->getSaveValue() as $optional) {
457 if (isset($requiredPackages[$optional['packageIdentifier']])) {
458 $erroneousPackages[] = $optional['packageIdentifier'];
459 }
460 }
461
462 if (!empty($erroneousPackages)) {
463 $formField->addValidationError(
464 new FormFieldValidationError(
465 'requirementOptional',
466 'wcf.acp.devtools.project.optionalPackage.error.requirementOptional',
467 ['affectedPackages' => $erroneousPackages]
468 )
469 );
470 }
471 }
472 ))
473 ->addValidator(new FormFieldValidator(
474 'exclusionOptional',
475 static function (DevtoolsProjectOptionalPackagesFormField $formField) {
476 /** @var DevtoolsProjectExcludedPackagesFormField $excludedPackagesField */
477 $excludedPackagesField = $formField->getDocument()->getNodeById('excludedPackages');
478 $excludedPackages = [];
479 foreach ($excludedPackagesField->getSaveValue() as $requiredPackage) {
480 $excludedPackages[$requiredPackage['packageIdentifier']] = $requiredPackage;
481 }
482
483 // ensure that the exclusions and requirements do not conflict
484 foreach ($formField->getSaveValue() as $optional) {
485 if (isset($excludedPackages[$optional['packageIdentifier']])) {
486 $erroneousPackages[] = $optional['packageIdentifier'];
487 }
488 }
489
490 if (!empty($erroneousPackages)) {
491 $formField->addValidationError(
492 new FormFieldValidationError(
493 'requirementOptional',
494 'wcf.acp.devtools.project.optionalPackage.error.exclusionOptional',
495 ['affectedPackages' => $erroneousPackages]
496 )
497 );
498 }
499 }
500 ))
501 ->addValidator(new FormFieldValidator(
502 'missingFiles',
503 function (DevtoolsProjectOptionalPackagesFormField $formField) {
504 /** @var TextFormField $pathField */
505 $pathField = $this->form->getNodeById('path');
506 $path = FileUtil::addTrailingSlash($pathField->getSaveValue());
507
508 $missingFiles = [];
509 foreach ($formField->getSaveValue() as $optional) {
510 if (!\is_file($path . "optionals/{$optional['packageIdentifier']}.tar")) {
511 $missingFiles[] = "optionals/{$optional['packageIdentifier']}.tar";
512 }
513 }
514
515 if (!empty($missingFiles)) {
516 $formField->addValidationError(
517 new FormFieldValidationError(
518 'missingFiles',
519 'wcf.acp.devtools.project.optionalPackage.error.missingFiles',
520 ['missingFiles' => $missingFiles]
521 )
522 );
523 }
524 }
525 ))
526 );
527 $tabMenu->appendChild(
528 TabFormContainer::create('optionalPackagesTab')
529 ->label('wcf.acp.devtools.project.optionalPackages.shortTitle')
530 ->appendChild($optionalPackages)
531 ->addDependency(
532 ValueFormFieldDependency::create('mode')
533 ->field($mode)
534 ->values(['edit'])
535 )
536 );
537
538 $excludedPackages = FormContainer::create('excludedPackagesContainer')
539 ->label('wcf.acp.devtools.project.excludedPackages')
540 ->description('wcf.acp.devtools.project.excludedPackages.description')
541 ->appendChild(
542 DevtoolsProjectExcludedPackagesFormField::create()
543 ->addValidator(new FormFieldValidator(
544 'selfExclusion',
545 static function (DevtoolsProjectExcludedPackagesFormField $formField) {
546 /** @var TextFormField $packageIdentifier */
547 $packageIdentifier = $formField->getDocument()->getNodeById('packageIdentifier');
548
549 // ensure that the package does not exclude itself
550 foreach ($formField->getSaveValue() as $requirement) {
551 if ($requirement['packageIdentifier'] === $packageIdentifier->getSaveValue()) {
552 $formField->addValidationError(
553 new FormFieldValidationError(
554 'selfExclusion',
555 'wcf.acp.devtools.project.excludedPackage.error.selfExclusion'
556 )
557 );
558 }
559 }
560 }
561 ))
562 ->addValidator(new FormFieldValidator(
563 'requirementExclusion',
564 static function (DevtoolsProjectExcludedPackagesFormField $formField) {
565 /** @var DevtoolsProjectRequiredPackagesFormField $requiredPackagesField */
566 $requiredPackagesField = $formField->getDocument()->getNodeById('requiredPackages');
567 $requiredPackageVersions = [];
568 foreach ($requiredPackagesField->getSaveValue() as $requiredPackage) {
569 $requiredPackageVersions[$requiredPackage['packageIdentifier']] = $requiredPackage['minVersion'];
570 }
571
572 // ensure that the exclusions and requirements do not conflict
573 $affectedPackages = [];
574 foreach ($formField->getSaveValue() as $exclusion) {
575 if (isset($requiredPackageVersions[$exclusion['packageIdentifier']])) {
576 $requiredVersion = $requiredPackageVersions[$exclusion['packageIdentifier']];
577 $excludedVersion = $exclusion['version'];
578
579 // we enfore a hard rule: if a package is both an exclusion
580 // and a requirement, both must specify a version
581 if ($requiredVersion === '' || $excludedVersion === '') {
582 $affectedPackages[] = $exclusion['packageIdentifier'];
583 } elseif (Package::compareVersion($excludedVersion, $requiredVersion) <= 0) {
584 $affectedPackages[] = $exclusion['packageIdentifier'];
585 }
586 }
587 }
588
589 if (!empty($affectedPackages)) {
590 $formField->addValidationError(
591 new FormFieldValidationError(
592 'requirementExclusion',
593 'wcf.acp.devtools.project.excludedPackage.error.requirementExclusion',
594 ['affectedPackages' => $affectedPackages]
595 )
596 );
597 }
598 }
599 ))
600 );
601 $tabMenu->appendChild(
602 TabFormContainer::create('excludedPackagesTab')
603 ->label('wcf.acp.devtools.project.excludedPackages.shortTitle')
604 ->appendChild($excludedPackages)
605 ->addDependency(
606 ValueFormFieldDependency::create('mode')
607 ->field($mode)
608 ->values(['edit', 'setup'])
609 )
610 );
611
612 $instructions = FormContainer::create('instructionsContainer')
613 ->label('wcf.acp.devtools.project.instructions')
614 ->description('wcf.acp.devtools.project.instructions.description')
615 ->appendChild(
616 DevtoolsProjectInstructionsFormField::create()
617 ->label('wcf.acp.devtools.project.instructions')
618 ->addValidator(new FormFieldValidator(
619 'updateFromPreviousVersion',
620 function (DevtoolsProjectInstructionsFormField $formField) {
621 /** @var TextFormField $versionField */
622 $versionField = $this->form->getNodeById('version');
623 $version = $versionField->getSaveValue();
624
625 foreach ($formField->getValue() as $key => $instructions) {
626 if ($instructions['type'] === 'install') {
627 continue;
628 }
629
630 $fromVersion = $instructions['fromVersion'];
631 if (\strpos($fromVersion, '*') !== false) {
632 // assume the smallest version by replacing
633 // all wildcards with zeros
634 $checkedFromVersion = \str_replace('*', '0', $fromVersion);
635 if (Package::compareVersion($version, $checkedFromVersion) <= 0) {
636 $formField->addValidationError(
637 new FormFieldValidationError(
638 'updateForFutureVersion',
639 'wcf.acp.devtools.project.instructions.type.update.error.updateForFutureVersion',
640 [
641 'fromVersion' => $fromVersion,
642 'instructions' => $key,
643 'version' => $version,
644 ]
645 )
646 );
647 }
648 } elseif (Package::compareVersion($version, $fromVersion) <= 0) {
649 $formField->addValidationError(
650 new FormFieldValidationError(
651 'updateForFutureVersion',
652 'wcf.acp.devtools.project.instructions.type.update.error.updateForFutureVersion',
653 [
654 'fromVersion' => $fromVersion,
655 'instructions' => $key,
656 'version' => $version,
657 ]
658 )
659 );
660 }
661 }
662 }
663 ))
664 ->addValidator($this->getInstructionValuesValidator())
665 );
666
667 $tabMenu->appendChild(
668 TabFormContainer::create('instructionsTab')
669 ->label('wcf.acp.devtools.project.instructions')
670 ->appendChild($instructions)
671 ->addDependency(
672 ValueFormFieldDependency::create('mode')
673 ->field($mode)
674 ->values(['edit'])
675 )
676 );
677 }
678
679 /**
680 * Returns the form field validator for the instructions form field to check
681 * the values of all instructions.
682 *
683 * @return FormFieldValidator
684 */
685 protected function getInstructionValuesValidator()
686 {
687 return new FormFieldValidator('instructionValues', function (DevtoolsProjectInstructionsFormField $formField) {
688 /** @var TextFormField $pathField */
689 $pathField = $this->form->getNodeById('path');
690 $path = FileUtil::addTrailingSlash($pathField->getSaveValue());
691
692 /** @var TextFormField $packageIdentifierField */
693 $packageIdentifierField = $this->form->getNodeById('packageIdentifier');
694 $packageIdentifier = $packageIdentifierField->getSaveValue();
695
696 /** @var BooleanFormField $isApplicationField */
697 $isApplicationField = $this->form->getNodeById('isApplication');
698 $isApplication = $isApplicationField->getSaveValue();
699
700 $packageInstallationPluginList = new PackageInstallationPluginList();
701 $packageInstallationPluginList->readObjects();
702
703 /** @var PackageInstallationPlugin[] $packageInstallationPlugins */
704 $packageInstallationPlugins = [];
705 foreach ($packageInstallationPluginList as $packageInstallationPlugin) {
706 $packageInstallationPlugins[$packageInstallationPlugin->pluginName] = $packageInstallationPlugin;
707 }
708
709 foreach ($formField->getValue() as $instructionsKey => $instructions) {
710 if (empty($instructions['instructions'])) {
711 $formField->addValidationError(
712 new FormFieldValidationError(
713 'missingInstructions',
714 'wcf.acp.devtools.project.instructions.error.missingInstructions',
715 ['instructions' => $instructionsKey]
716 )
717 );
718
719 continue;
720 }
721
722 foreach ($instructions['instructions'] as $instructionKey => $instruction) {
723 $value = $instruction['value'];
724 $packageInstallationPlugin = $packageInstallationPlugins[$instruction['pip']];
725
726 // explicity set value for valiation if instruction value
727 // is empty but supports default filename
728 if ($value === '' && $packageInstallationPlugin->getDefaultFilename() !== null) {
729 $value = $packageInstallationPlugin->getDefaultFilename();
730 }
731
732 switch ($instruction['pip']) {
733 case 'acpTemplate':
734 case 'file':
735 case 'template':
736 // core is too special, ignore it
737 if ($this->formObject !== null && $this->formObject->isCore()) {
738 break;
739 }
740
741 // only tar archives are supported for file-based pips
742 if (\substr($value, -4) !== '.tar') {
743 $formField->addValidationError(
744 new FormFieldValidationError(
745 'noArchive',
746 'wcf.acp.devtools.project.instruction.error.noArchive',
747 [
748 'instruction' => $instructionKey,
749 'instructions' => $instructionsKey,
750 ]
751 )
752 );
753 }
754 // the associated directory with the source fles
755 // has to exist ...
756 elseif (!\is_dir($path . \substr($value, 0, -4))) {
757 // ... unless it is an update and an archive
758 // with updated files only
759 if (
760 $instructions['type'] === 'update' && \preg_match(
761 '~^(.+)_update\.tar$~',
762 $value,
763 $match
764 )
765 ) {
766 if (!\is_dir($path . $match[1])) {
767 $formField->addValidationError(
768 new FormFieldValidationError(
769 'missingDirectoryForUpdatedFiles',
770 'wcf.acp.devtools.project.instruction.error.missingDirectoryForUpdatedFiles',
771 [
772 'directory' => $path . $match[1] . '/',
773 'instruction' => $instructionKey,
774 'instructions' => $instructionsKey,
775 ]
776 )
777 );
778 }
779 } else {
780 $formField->addValidationError(
781 new FormFieldValidationError(
782 'missingDirectory',
783 'wcf.acp.devtools.project.instruction.error.missingDirectory',
784 [
785 'directory' => $path . \substr($value, 0, -4) . '/',
786 'instruction' => $instructionKey,
787 'instructions' => $instructionsKey,
788 ]
789 )
790 );
791 }
792 }
793 break;
794
795 case 'language':
796 if ($value === 'language/*.xml') {
797 $directory = FileUtil::addTrailingSlash(\dirname($path . $value));
798 if ($this->formObject !== null && $this->formObject->isCore()) {
799 $directory = FileUtil::addTrailingSlash(\dirname($path . 'wcfsetup/install/lang/*.xml'));
800 }
801
802 $directoryUtil = DirectoryUtil::getInstance($directory);
803 if (empty($directoryUtil->getFiles(\SORT_ASC, Regex::compile('.+\.xml')))) {
804 $formField->addValidationError(
805 new FormFieldValidationError(
806 'missingFiles',
807 'wcf.acp.devtools.project.instruction.language.error.missingFiles',
808 [
809 'directory' => $directory,
810 'instruction' => $instructionKey,
811 'instructions' => $instructionsKey,
812 ]
813 )
814 );
815 }
816 } elseif (\substr($value, -4) !== '.xml') {
817 $formField->addValidationError(
818 new FormFieldValidationError(
819 'noXmlFile',
820 'wcf.acp.devtools.project.instruction.error.noXmlFile',
821 [
822 'instruction' => $instructionKey,
823 'instructions' => $instructionsKey,
824 ]
825 )
826 );
827 }
828
829 break;
830
831 case 'database':
832 case 'script':
833 // only PHP files are supported for file-based pips
834 if (\substr($value, -4) !== '.php') {
835 $formField->addValidationError(
836 new FormFieldValidationError(
837 'noPhpFile',
838 'wcf.acp.devtools.project.instruction.script.error.noPhpFile',
839 [
840 'instruction' => $instructionKey,
841 'instructions' => $instructionsKey,
842 ]
843 )
844 );
845 } else {
846 $application = 'wcf';
847 if (!empty($instruction['application'])) {
848 $application = $instruction['application'];
849 } elseif ($isApplication) {
850 $application = Package::getAbbreviation($packageIdentifier);
851 }
852
853 $missingFile = true;
854 $checkedFileLocations = [];
855 if ($this->formObject !== null && $this->formObject->isCore()) {
856 $scriptLocation = $path . 'wcfsetup/install/files/' . $instruction['value'];
857 if (!\is_file($scriptLocation)) {
858 $checkedFileLocations[] = $scriptLocation;
859 } else {
860 $missingFile = false;
861 }
862 } else {
863 // try to find matching `file` instruction
864 // for determined application
865 foreach ($instructions['instructions'] as $fileSearchInstruction) {
866 if ($fileSearchInstruction['pip'] === 'file') {
867 $fileSearchValue = $fileSearchInstruction['value'];
868
869 // ignore empty instructions with default filename
870 if ($fileSearchValue === '' && $packageInstallationPlugins['file']->getDefaultFilename() !== null) {
871 $fileSearchValue = $packageInstallationPlugins['file']->getDefaultFilename();
872 }
873
874 $fileApplication = 'wcf';
875 if (!empty($fileSearchInstruction['application'])) {
876 $fileApplication = $fileSearchInstruction['application'];
877 } elseif ($isApplication) {
878 $fileApplication = Package::getAbbreviation($packageIdentifier);
879 }
880
881 if ($fileApplication === $application) {
882 $scriptLocation = $path . \substr(
883 $fileSearchValue,
884 0,
885 -4
886 ) . '/' . $instruction['value'];
887 if (!\is_file($scriptLocation)) {
888 $checkedFileLocations[] = $scriptLocation;
889 } else {
890 $missingFile = false;
891 break;
892 }
893 }
894 }
895 }
896 }
897
898 if ($missingFile) {
899 $formField->addValidationError(
900 new FormFieldValidationError(
901 'missingFile',
902 'wcf.acp.devtools.project.instruction.script.error.missingFile',
903 [
904 'checkedFileLocations' => $checkedFileLocations,
905 'instruction' => $instructionKey,
906 'instructions' => $instructionsKey,
907 ]
908 )
909 );
910 }
911 }
912
913 break;
914
915 default:
916 $filePath = $path . $value;
917 if ($this->formObject !== null && $this->formObject->isCore()) {
918 $filePath = $path . 'com.woltlab.wcf/' . $value;
919 }
920
921 if (!\file_exists($filePath)) {
922 $formField->addValidationError(
923 new FormFieldValidationError(
924 'missingFile',
925 'wcf.acp.devtools.project.instruction.error.missingFile',
926 [
927 'file' => $filePath,
928 'instruction' => $instructionKey,
929 'instructions' => $instructionsKey,
930 ]
931 )
932 );
933 } elseif (
934 \is_subclass_of(
935 $packageInstallationPlugin->className,
936 AbstractXMLPackageInstallationPlugin::class
937 )
938 && \substr($value, -4) !== '.xml'
939 ) {
940 $formField->addValidationError(
941 new FormFieldValidationError(
942 'noXmlFile',
943 'wcf.acp.devtools.project.instruction.error.noXmlFile',
944 [
945 'instruction' => $instructionKey,
946 'instructions' => $instructionsKey,
947 ]
948 )
949 );
950 }
951
952 break;
953 }
954 }
955 }
956 });
957 }
958
959 /**
960 * @inheritDoc
961 */
962 public function save()
963 {
964 AbstractForm::save();
965
966 $data = $this->form->getData();
967 $projectData = [];
968 foreach ($this->projectFields as $projectField) {
969 if (isset($data['data'][$projectField])) {
970 $projectData[$projectField] = $data['data'][$projectField];
971 unset($data['data'][$projectField]);
972 }
973 }
974
975 $action = $this->formAction;
976 if ($this->objectActionName) {
977 $action = $this->objectActionName;
978 } elseif ($this->formAction === 'edit') {
979 $action = 'update';
980 }
981
982 /** @var AbstractDatabaseObjectAction objectAction */
983 $this->objectAction = new $this->objectActionClass(
984 \array_filter([$this->formObject]),
985 $action,
986 ['data' => $projectData]
987 );
988 $project = $this->objectAction->executeAction()['returnValues'];
989
990 if (!($project instanceof DevtoolsProject)) {
991 if ($this->formObject instanceof DevtoolsProject) {
992 $project = new DevtoolsProject($this->formObject->projectID);
993 } else {
994 throw new \LogicException('Cannot determine project object.');
995 }
996 }
997
998 if ($data['data']['mode'] !== 'import') {
999 $this->writePackageXml($project, $data);
1000 }
1001
1002 $this->saved();
1003
1004 WCF::getTPL()->assign('success', true);
1005 }
1006
1007 /**
1008 * Writes the updated `package.xml` file for the given project using the given data.
1009 *
1010 * @param DevtoolsProject $project
1011 * @param array $data
1012 */
1013 protected function writePackageXml(DevtoolsProject $project, array $data)
1014 {
1015 $xmlData = \array_merge($data, $data['data']);
1016 unset($xmlData['data'], $xmlData['mode']);
1017 $packageXmlWriter = new DevtoolsPackageXmlWriter($project, $xmlData);
1018 $packageXmlWriter->write();
1019 }
1020 }