Completed
Push — master ( 89c3e8...c161b3 )
by Craig
05:56 queued 40s
created

ExtensionController::modifyAction()   B

Complexity

Conditions 8
Paths 11

Size

Total Lines 45
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 25
c 0
b 0
f 0
nc 11
nop 5
dl 0
loc 45
rs 8.4444
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Zikula package.
7
 *
8
 * Copyright Zikula Foundation - https://ziku.la/
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Zikula\ExtensionsModule\Controller;
15
16
use RuntimeException;
17
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
18
use Symfony\Component\HttpFoundation\RedirectResponse;
19
use Symfony\Component\HttpFoundation\Request;
20
use Symfony\Component\HttpFoundation\Response;
21
use Symfony\Component\Routing\Annotation\Route;
22
use Symfony\Component\Routing\RouterInterface;
23
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
24
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
25
use Zikula\BlocksModule\Entity\RepositoryInterface\BlockRepositoryInterface;
26
use Zikula\Bundle\CoreBundle\AbstractBundle;
27
use Zikula\Bundle\CoreBundle\CacheClearer;
28
use Zikula\Bundle\CoreBundle\Composer\MetaData;
29
use Zikula\Bundle\CoreBundle\Controller\AbstractController;
30
use Zikula\Bundle\CoreBundle\Event\GenericEvent;
31
use Zikula\Bundle\CoreBundle\HttpKernel\ZikulaHttpKernelInterface;
32
use Zikula\Bundle\CoreBundle\HttpKernel\ZikulaKernel;
33
use Zikula\Bundle\FormExtensionBundle\Form\Type\DeletionType;
34
use Zikula\Component\SortableColumns\Column;
35
use Zikula\Component\SortableColumns\SortableColumns;
36
use Zikula\ExtensionsModule\Constant;
37
use Zikula\ExtensionsModule\Entity\ExtensionEntity;
38
use Zikula\ExtensionsModule\Entity\RepositoryInterface\ExtensionRepositoryInterface;
39
use Zikula\ExtensionsModule\Event\ExtensionStateEvent;
40
use Zikula\ExtensionsModule\ExtensionEvents;
41
use Zikula\ExtensionsModule\Form\Type\ExtensionInstallType;
42
use Zikula\ExtensionsModule\Form\Type\ExtensionModifyType;
43
use Zikula\ExtensionsModule\Helper\BundleSyncHelper;
44
use Zikula\ExtensionsModule\Helper\ExtensionDependencyHelper;
45
use Zikula\ExtensionsModule\Helper\ExtensionHelper;
46
use Zikula\ExtensionsModule\Helper\ExtensionStateHelper;
47
use Zikula\ThemeModule\Engine\Annotation\Theme;
48
49
/**
50
 * Class ExtensionController
51
 * @Route("")
52
 */
53
class ExtensionController extends AbstractController
54
{
55
    private const NEW_ROUTES_AVAIL = 'new.routes.avail';
56
57
    /**
58
     * @Route("/list/{pos}")
59
     * @Theme("admin")
60
     * @Template("@ZikulaExtensionsModule/Extension/list.html.twig")
61
     *
62
     * @throws AccessDeniedException Thrown if the user doesn't have admin permissions for the module
63
     */
64
    public function listAction(
65
        Request $request,
66
        EventDispatcherInterface $eventDispatcher,
67
        ExtensionRepositoryInterface $extensionRepository,
68
        BundleSyncHelper $bundleSyncHelper,
69
        RouterInterface $router,
70
        int $pos = 1
71
    ): array {
72
        if (!$this->hasPermission('ZikulaExtensionsModule::', '::', ACCESS_ADMIN)) {
73
            throw new AccessDeniedException();
74
        }
75
        $modulesJustInstalled = $request->query->get('justinstalled');
76
        if (!empty($modulesJustInstalled)) {
77
            // notify the event dispatcher that new routes are available (ids of modules just installed avail as args)
78
            $event = new GenericEvent(null, json_decode($modulesJustInstalled));
79
            $eventDispatcher->dispatch($event, self::NEW_ROUTES_AVAIL);
80
        }
81
82
        $sortableColumns = new SortableColumns($router, 'zikulaextensionsmodule_extension_list');
83
        $sortableColumns->addColumns([new Column('displayname'), new Column('state')]);
84
        $sortableColumns->setOrderByFromRequest($request);
85
86
        $upgradedExtensions = [];
87
        $vetoEvent = new GenericEvent();
88
        $eventDispatcher->dispatch($vetoEvent, ExtensionEvents::REGENERATE_VETO);
89
        if (1 === $pos && !$vetoEvent->isPropagationStopped()) {
90
            // regenerate the extension list only when viewing the first page
91
            $extensionsInFileSystem = $bundleSyncHelper->scanForBundles();
92
            $upgradedExtensions = $bundleSyncHelper->syncExtensions($extensionsInFileSystem);
93
        }
94
95
        $pagedResult = $extensionRepository->getPagedCollectionBy([], [
96
            $sortableColumns->getSortColumn()->getName() => $sortableColumns->getSortDirection()
97
        ], $this->getVar('itemsperpage'), $pos);
0 ignored issues
show
Bug introduced by
It seems like $this->getVar('itemsperpage') can also be of type false; however, parameter $limit of Zikula\ExtensionsModule\...:getPagedCollectionBy() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

97
        ], /** @scrutinizer ignore-type */ $this->getVar('itemsperpage'), $pos);
Loading history...
98
99
        return [
100
            'sort' => $sortableColumns->generateSortableColumns(),
101
            'pager' => [
102
                'limit' => $this->getVar('itemsperpage'),
103
                'count' => count($pagedResult)
104
            ],
105
            'extensions' => $pagedResult,
106
            'upgradedExtensions' => $upgradedExtensions
107
        ];
108
    }
109
110
    /**
111
     * @Route("/activate/{id}/{token}", methods = {"GET"}, requirements={"id" = "^[1-9]\d*$"})
112
     *
113
     * Activate an extension.
114
     *
115
     * @throws AccessDeniedException Thrown if the user doesn't have admin permissions for the module
116
     */
117
    public function activateAction(
118
        int $id,
119
        string $token,
120
        ExtensionRepositoryInterface $extensionRepository,
121
        ExtensionStateHelper $extensionStateHelper,
122
        CacheClearer $cacheClearer
123
    ): RedirectResponse {
124
        if (!$this->hasPermission('ZikulaExtensionsModule::', '::', ACCESS_ADMIN)) {
125
            throw new AccessDeniedException();
126
        }
127
128
        if (!$this->isCsrfTokenValid('activate-extension', $token)) {
129
            throw new AccessDeniedException();
130
        }
131
132
        /** @var ExtensionEntity $extension */
133
        $extension = $extensionRepository->find($id);
134
        if (Constant::STATE_NOTALLOWED === $extension->getState()) {
135
            $this->addFlash('error', $this->trans('Error! Activation of %name% not allowed.', ['%name%' => $extension->getName()]));
136
        } else {
137
            // Update state
138
            $extensionStateHelper->updateState($id, Constant::STATE_ACTIVE);
139
            $cacheClearer->clear('symfony');
140
            $this->addFlash('status', $this->trans('Done! Activated %name%.', ['%name%' => $extension->getName()]));
141
        }
142
143
        return $this->redirectToRoute('zikulaextensionsmodule_extension_list');
144
    }
145
146
    /**
147
     * @Route("/deactivate/{id}/{token}", methods = {"GET"}, requirements={"id" = "^[1-9]\d*$"})
148
     *
149
     * Deactivate an extension
150
     *
151
     * @throws AccessDeniedException Thrown if the user doesn't have admin permissions for the module
152
     */
153
    public function deactivateAction(
154
        int $id,
155
        string $token,
156
        ExtensionRepositoryInterface $extensionRepository,
157
        ExtensionStateHelper $extensionStateHelper,
158
        CacheClearer $cacheClearer
159
    ): RedirectResponse {
160
        if (!$this->hasPermission('ZikulaExtensionsModule::', '::', ACCESS_ADMIN)) {
161
            throw new AccessDeniedException();
162
        }
163
164
        if (!$this->isCsrfTokenValid('deactivate-extension', $token)) {
165
            throw new AccessDeniedException();
166
        }
167
        // @todo check if this is a theme and currently set as default or admin theme
168
169
        /** @var ExtensionEntity $extension */
170
        $extension = $extensionRepository->find($id);
171
        if (null !== $extension) {
172
            if (ZikulaKernel::isCoreExtension($extension->getName())) {
173
                $this->addFlash('error', $this->trans('Error! You cannot deactivate the %name%. It is required by the system.', ['%name%' => $extension->getName()]));
174
            } else {
175
                // Update state
176
                $extensionStateHelper->updateState($id, Constant::STATE_INACTIVE);
177
                $cacheClearer->clear('symfony');
178
                $this->addFlash('status', $this->trans('Done! Deactivated %name%.', ['%name%' => $extension->getName()]));
179
            }
180
        }
181
182
        return $this->redirectToRoute('zikulaextensionsmodule_extension_list');
183
    }
184
185
    /**
186
     * @Route("/modify/{id}/{forceDefaults}", requirements={"id" = "^[1-9]\d*$", "forceDefaults" = "0|1"})
187
     * @Theme("admin")
188
     * @Template("@ZikulaExtensionsModule/Extension/modify.html.twig")
189
     *
190
     * Modify a module.
191
     *
192
     * @return array|RedirectResponse
193
     * @throws AccessDeniedException Thrown if the user doesn't have admin permissions for modifying the extension
194
     */
195
    public function modifyAction(
196
        Request $request,
197
        ZikulaHttpKernelInterface $kernel,
198
        ExtensionEntity $extension,
199
        CacheClearer $cacheClearer,
200
        bool $forceDefaults = false
201
    ) {
202
        if (!$this->hasPermission('ZikulaExtensionsModule::modify', $extension->getName() . '::' . $extension->getId(), ACCESS_ADMIN)) {
203
            throw new AccessDeniedException();
204
        }
205
206
        /** @var AbstractBundle $bundle */
207
        $bundle = $kernel->getBundle($extension->getName());
208
        $metaData = $bundle->getMetaData()->getFilteredVersionInfoArray();
209
210
        if ($forceDefaults) {
211
            $extension->setName($metaData['name']);
212
            $extension->setUrl($metaData['url']);
213
            $extension->setDescription($metaData['description']);
214
        }
215
216
        $form = $this->createForm(ExtensionModifyType::class, $extension);
217
        $form->handleRequest($request);
218
        if ($form->isSubmitted() && $form->isValid()) {
219
            if ($form->get('defaults')->isClicked()) {
0 ignored issues
show
Bug introduced by
The method isClicked() does not exist on Symfony\Component\Form\FormInterface. It seems like you code against a sub-type of Symfony\Component\Form\FormInterface such as Symfony\Component\Form\SubmitButton. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

219
            if ($form->get('defaults')->/** @scrutinizer ignore-call */ isClicked()) {
Loading history...
220
                $this->addFlash('info', 'Default values reloaded. Save to confirm.');
221
222
                return $this->redirectToRoute('zikulaextensionsmodule_extension_modify', ['id' => $extension->getId(), 'forceDefaults' => 1]);
223
            }
224
            if ($form->get('save')->isClicked()) {
225
                $em = $this->getDoctrine()->getManager();
226
                $em->persist($extension);
227
                $em->flush();
228
229
                $cacheClearer->clear('symfony');
230
                $this->addFlash('status', 'Done! Extension updated.');
231
            } elseif ($form->get('cancel')->isClicked()) {
232
                $this->addFlash('status', 'Operation cancelled.');
233
            }
234
235
            return $this->redirectToRoute('zikulaextensionsmodule_extension_list');
236
        }
237
238
        return [
239
            'form' => $form->createView()
240
        ];
241
    }
242
243
    /**
244
     * @Route("/compatibility/{id}", methods = {"GET"}, requirements={"id" = "^[1-9]\d*$"})
245
     * @Theme("admin")
246
     * @Template("@ZikulaExtensionsModule/Extension/compatibility.html.twig")
247
     *
248
     * Display information of a module compatibility with the version of the core
249
     *
250
     * @throws AccessDeniedException Thrown if the user doesn't have admin permission to the requested module
251
     */
252
    public function compatibilityAction(ExtensionEntity $extension): array
253
    {
254
        if (!$this->hasPermission('ZikulaExtensionsModule::', $extension->getName() . '::' . $extension->getId(), ACCESS_ADMIN)) {
255
            throw new AccessDeniedException();
256
        }
257
258
        return [
259
            'extension' => $extension
260
        ];
261
    }
262
263
    /**
264
     * @Route("/install/{id}/{token}", requirements={"id" = "^[1-9]\d*$"})
265
     * @Theme("admin")
266
     * @Template("@ZikulaExtensionsModule/Extension/install.html.twig")
267
     *
268
     * Install and initialise an extension.
269
     *
270
     * @return array|RedirectResponse
271
     * @throws AccessDeniedException Thrown if the user doesn't have admin permission for the module
272
     */
273
    public function installAction(
274
        Request $request,
275
        ExtensionEntity $extension,
276
        string $token,
277
        ZikulaHttpKernelInterface $kernel,
278
        ExtensionRepositoryInterface $extensionRepository,
279
        ExtensionHelper $extensionHelper,
280
        ExtensionStateHelper $extensionStateHelper,
281
        ExtensionDependencyHelper $dependencyHelper,
282
        CacheClearer $cacheClearer
283
    ) {
284
        if (!$this->hasPermission('ZikulaExtensionsModule::', '::', ACCESS_ADMIN)) {
285
            throw new AccessDeniedException();
286
        }
287
288
        $id = $extension->getId();
289
        if (!$this->isCsrfTokenValid('install-extension', $token)) {
290
            throw new AccessDeniedException();
291
        }
292
293
        if (!$kernel->isBundle($extension->getName())) {
294
            $extensionStateHelper->updateState($id, Constant::STATE_TRANSITIONAL);
295
            $cacheClearer->clear('symfony');
296
297
            return $this->redirectToRoute('zikulaextensionsmodule_extension_install', ['id' => $id, 'token' => $token]);
298
        }
299
        $unsatisfiedDependencies = $dependencyHelper->getUnsatisfiedExtensionDependencies($extension);
300
        $form = $this->createForm(ExtensionInstallType::class, [
301
            'dependencies' => $this->formatDependencyCheckboxArray($extensionRepository, $unsatisfiedDependencies)
302
        ]);
303
        $hasNoUnsatisfiedDependencies = empty($unsatisfiedDependencies);
304
        $form->handleRequest($request);
305
        if ($hasNoUnsatisfiedDependencies || ($form->isSubmitted() && $form->isValid())) {
306
            if ($hasNoUnsatisfiedDependencies || $form->get('install')->isClicked()) {
307
                $extensionsInstalled = [];
308
                $data = $form->getData();
309
                foreach ($data['dependencies'] as $dependencyId => $installSelected) {
310
                    if (!$installSelected && MetaData::DEPENDENCY_REQUIRED !== $unsatisfiedDependencies[$dependencyId]->getStatus()) {
311
                        continue;
312
                    }
313
                    $dependencyExtensionEntity = $extensionRepository->get($unsatisfiedDependencies[$dependencyId]->getModname());
314
                    if (isset($dependencyExtensionEntity)) {
315
                        if (!$extensionHelper->install($dependencyExtensionEntity)) {
316
                            $this->addFlash('error', $this->trans('Failed to install dependency "%name%"!', ['%name%' => $dependencyExtensionEntity->getName()]));
317
318
                            return $this->redirectToRoute('zikulaextensionsmodule_extension_list');
319
                        }
320
                        $extensionsInstalled[] = $dependencyExtensionEntity->getId();
321
                        $this->addFlash('status', $this->trans('Installed dependency "%name%".', ['%name%' => $dependencyExtensionEntity->getName()]));
322
                    } else {
323
                        $this->addFlash('warning', $this->trans('Warning: could not install selected dependency "%name%".', ['%name%' => $unsatisfiedDependencies[$dependencyId]->getModname()]));
324
                    }
325
                }
326
                if ($extensionHelper->install($extension)) {
327
                    $this->addFlash('status', $this->trans('Done! Installed "%name%".', ['%name%' => $extension->getName()]));
328
                    $extensionsInstalled[] = $id;
329
                    $cacheClearer->clear('symfony');
330
331
                    return $this->redirectToRoute('zikulaextensionsmodule_extension_postinstall', ['extensions' => json_encode($extensionsInstalled)]);
332
                }
333
                $extensionStateHelper->updateState($id, Constant::STATE_UNINITIALISED);
334
                $this->addFlash('error', $this->trans('Initialization of "%name%" failed!', ['%name%' => $extension->getName()]));
335
            }
336
            if ($form->get('cancel')->isClicked()) {
337
                $extensionStateHelper->updateState($id, Constant::STATE_UNINITIALISED);
338
                $this->addFlash('status', 'Operation cancelled.');
339
            }
340
341
            return $this->redirectToRoute('zikulaextensionsmodule_extension_list');
342
        }
343
344
        return [
345
            'dependencies' => $unsatisfiedDependencies,
346
            'extension' => $extension,
347
            'form' => $form->createView()
348
        ];
349
    }
350
351
    /**
352
     * Post-installation action to trigger the MODULE_POSTINSTALL event.
353
     * The additional Action is required because this event must occur AFTER the rebuild of the cache which occurs on Request.
354
     *
355
     * @Route("/postinstall/{extensions}", methods = {"GET"})
356
     */
357
    public function postInstallAction(
358
        ExtensionRepositoryInterface $extensionRepository,
359
        ZikulaHttpKernelInterface $kernel,
360
        EventDispatcherInterface $eventDispatcher,
361
        string $extensions = null
362
    ): RedirectResponse {
363
        if (!empty($extensions)) {
364
            $extensions = json_decode($extensions);
365
            foreach ($extensions as $extensionId) {
366
                /** @var ExtensionEntity $extensionEntity */
367
                $extensionEntity = $extensionRepository->find($extensionId);
368
                if (null === $extensionRepository) {
369
                    continue;
370
                }
371
                /** @var AbstractBundle $bundle */
372
                $bundle = $kernel->getBundle($extensionEntity->getName());
373
                if (null === $bundle) {
374
                    continue;
375
                }
376
                $event = new ExtensionStateEvent($bundle, $extensionEntity->toArray());
377
                $eventDispatcher->dispatch($event, ExtensionEvents::EXTENSION_POSTINSTALL);
378
            }
379
        }
380
381
        return $this->redirectToRoute('zikulaextensionsmodule_extension_list', ['justinstalled' => json_encode($extensions)]);
382
    }
383
384
    /**
385
     * Create array suitable for checkbox FormType [[ID => bool][ID => bool]].
386
     */
387
    private function formatDependencyCheckboxArray(
388
        ExtensionRepositoryInterface $extensionRepository,
389
        array $dependencies
390
    ): array {
391
        $return = [];
392
        foreach ($dependencies as $dependency) {
393
            /** @var ExtensionEntity $dependencyExtension */
394
            $dependencyExtension = $extensionRepository->get($dependency->getModname());
395
            $return[$dependency->getId()] = null !== $dependencyExtension;
396
        }
397
398
        return $return;
399
    }
400
401
    /**
402
     * @Route("/upgrade/{id}/{token}", requirements={"id" = "^[1-9]\d*$"})
403
     *
404
     * Upgrade an extension.
405
     *
406
     * @throws AccessDeniedException Thrown if the user doesn't have admin permission for the module
407
     */
408
    public function upgradeAction(
409
        ExtensionEntity $extension,
410
        $token,
411
        ExtensionHelper $extensionHelper
412
    ): RedirectResponse {
413
        if (!$this->hasPermission('ZikulaExtensionsModule::', '::', ACCESS_ADMIN)) {
414
            throw new AccessDeniedException();
415
        }
416
417
        if (!$this->isCsrfTokenValid('upgrade-extension', $token)) {
418
            throw new AccessDeniedException();
419
        }
420
421
        $result = $extensionHelper->upgrade($extension);
422
        if ($result) {
423
            $this->addFlash('status', $this->trans('%name% upgraded to new version and activated.', ['%name%' => $extension->getDisplayname()]));
424
        } else {
425
            $this->addFlash('error', 'Extension upgrade failed!');
426
        }
427
428
        return $this->redirectToRoute('zikulaextensionsmodule_extension_list');
429
    }
430
431
    /**
432
     * @Route("/uninstall/{id}/{token}", requirements={"id" = "^[1-9]\d*$"})
433
     * @Theme("admin")
434
     * @Template("@ZikulaExtensionsModule/Extension/uninstall.html.twig")
435
     *
436
     * Uninstall an extension.
437
     *
438
     * @return array|Response|RedirectResponse
439
     * @throws AccessDeniedException Thrown if the user doesn't have admin permission for the module
440
     */
441
    public function uninstallAction(
442
        Request $request,
443
        ExtensionEntity $extension,
444
        string $token,
445
        ZikulaHttpKernelInterface $kernel,
446
        BlockRepositoryInterface $blockRepository,
447
        ExtensionHelper $extensionHelper,
448
        ExtensionStateHelper $extensionStateHelper,
449
        ExtensionDependencyHelper $dependencyHelper,
450
        CacheClearer $cacheClearer
451
    ) {
452
        if (!$this->hasPermission('ZikulaExtensionsModule::', '::', ACCESS_ADMIN)) {
453
            throw new AccessDeniedException();
454
        }
455
456
        if (!$this->isCsrfTokenValid('uninstall-extension', $token)) {
457
            throw new AccessDeniedException();
458
        }
459
460
        if (Constant::STATE_MISSING === $extension->getState()) {
461
            throw new RuntimeException($this->trans('Error! The requested extension cannot be uninstalled because its files are missing!'));
462
        }
463
        if (!$kernel->isBundle($extension->getName())) {
464
            $extensionStateHelper->updateState($extension->getId(), Constant::STATE_TRANSITIONAL);
465
            $cacheClearer->clear('symfony');
466
        }
467
        $requiredDependents = $dependencyHelper->getDependentExtensions($extension);
468
        $blocks = $blockRepository->findBy(['module' => $extension]);
469
470
        $form = $this->createForm(DeletionType::class, [], [
471
            'action' => $this->generateUrl('zikulaextensionsmodule_extension_uninstall', [
472
                'id' => $extension->getId(),
473
                'token' => $token
474
            ]),
475
        ]);
476
        $form->handleRequest($request);
477
        if ($form->isSubmitted() && $form->isValid()) {
478
            if ($form->get('delete')->isClicked()) {
479
                // remove dependent extensions
480
                if (!$extensionHelper->uninstallArray($requiredDependents)) {
481
                    $this->addFlash('error', 'Error: Could not uninstall dependent extensions.');
482
483
                    return $this->redirectToRoute('zikulaextensionsmodule_extension_list');
484
                }
485
                // remove blocks
486
                $blockRepository->remove($blocks);
487
488
                // remove the extension
489
                if ($extensionHelper->uninstall($extension)) {
490
                    $this->addFlash('status', 'Done! Uninstalled extension.');
491
                } else {
492
                    $this->addFlash('error', 'Extension removal failed! (note: blocks and dependents may have been removed)');
493
                }
494
            } elseif ($form->get('cancel')->isClicked()) {
495
                $this->addFlash('status', 'Operation cancelled.');
496
            }
497
498
            return $this->redirectToRoute('zikulaextensionsmodule_extension_list');
499
        }
500
501
        return [
502
            'form' => $form->createView(),
503
            'extension' => $extension,
504
            'blocks' => $blocks,
505
            'requiredDependents' => $requiredDependents
506
        ];
507
    }
508
}
509