Completed
Push — master ( 8fc13e...c19aa3 )
by Axel
06:21
created

ExtensionController::compatibilityAction()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 2
b 0
f 0
nc 2
nop 1
dl 0
loc 8
rs 10
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\CacheClearer;
27
use Zikula\Bundle\CoreBundle\Composer\MetaData;
28
use Zikula\Bundle\CoreBundle\Controller\AbstractController;
29
use Zikula\Bundle\CoreBundle\Event\GenericEvent;
30
use Zikula\Bundle\CoreBundle\HttpKernel\ZikulaHttpKernelInterface;
31
use Zikula\Bundle\CoreBundle\HttpKernel\ZikulaKernel;
32
use Zikula\Bundle\FormExtensionBundle\Form\Type\DeletionType;
33
use Zikula\Component\SortableColumns\Column;
34
use Zikula\Component\SortableColumns\SortableColumns;
35
use Zikula\ExtensionsModule\AbstractExtension;
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\PermissionsModule\Annotation\PermissionCheck;
48
use Zikula\ThemeModule\Engine\Annotation\Theme;
49
use Zikula\ThemeModule\Engine\Engine;
50
51
/**
52
 * Class ExtensionController
53
 *
54
 * @Route("")
55
 */
56
class ExtensionController extends AbstractController
57
{
58
    private const NEW_ROUTES_AVAIL = 'new.routes.avail';
59
60
    /**
61
     * @Route("/list/{pos}")
62
     * @PermissionCheck("admin")
63
     * @Theme("admin")
64
     * @Template("@ZikulaExtensionsModule/Extension/list.html.twig")
65
     */
66
    public function listAction(
67
        Request $request,
68
        EventDispatcherInterface $eventDispatcher,
69
        ExtensionRepositoryInterface $extensionRepository,
70
        BundleSyncHelper $bundleSyncHelper,
71
        RouterInterface $router,
72
        int $pos = 1
73
    ): array {
74
        $modulesJustInstalled = $request->query->get('justinstalled');
75
        if (!empty($modulesJustInstalled)) {
76
            // notify the event dispatcher that new routes are available (ids of modules just installed avail as args)
77
            $event = new GenericEvent(null, json_decode($modulesJustInstalled));
78
            $eventDispatcher->dispatch($event, self::NEW_ROUTES_AVAIL);
79
        }
80
81
        $sortableColumns = new SortableColumns($router, 'zikulaextensionsmodule_extension_list');
82
        $sortableColumns->addColumns([new Column('displayname'), new Column('state')]);
83
        $sortableColumns->setOrderByFromRequest($request);
84
85
        $upgradedExtensions = [];
86
        $vetoEvent = new GenericEvent();
87
        $eventDispatcher->dispatch($vetoEvent, ExtensionEvents::REGENERATE_VETO);
88
        if (1 === $pos && !$vetoEvent->isPropagationStopped()) {
89
            // regenerate the extension list only when viewing the first page
90
            $extensionsInFileSystem = $bundleSyncHelper->scanForBundles();
91
            $upgradedExtensions = $bundleSyncHelper->syncExtensions($extensionsInFileSystem);
92
        }
93
94
        $pagedResult = $extensionRepository->getPagedCollectionBy([], [
95
            $sortableColumns->getSortColumn()->getName() => $sortableColumns->getSortDirection()
96
        ], $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

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

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