Completed
Push — master ( 0b2c58...a04bc9 )
by Craig
05:22
created

ExtensionController::previewAction()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 6
rs 10
c 1
b 0
f 1
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\Api\VariableApi;
37
use Zikula\ExtensionsModule\Constant;
38
use Zikula\ExtensionsModule\Entity\ExtensionEntity;
39
use Zikula\ExtensionsModule\Entity\RepositoryInterface\ExtensionRepositoryInterface;
40
use Zikula\ExtensionsModule\Event\ExtensionStateEvent;
41
use Zikula\ExtensionsModule\ExtensionEvents;
42
use Zikula\ExtensionsModule\Form\Type\ExtensionInstallType;
43
use Zikula\ExtensionsModule\Form\Type\ExtensionModifyType;
44
use Zikula\ExtensionsModule\Helper\BundleSyncHelper;
45
use Zikula\ExtensionsModule\Helper\ExtensionDependencyHelper;
46
use Zikula\ExtensionsModule\Helper\ExtensionHelper;
47
use Zikula\ExtensionsModule\Helper\ExtensionStateHelper;
48
use Zikula\PermissionsModule\Annotation\PermissionCheck;
49
use Zikula\ThemeModule\Engine\Annotation\Theme;
50
use Zikula\ThemeModule\Engine\Engine;
51
52
/**
53
 * Class ExtensionController
54
 *
55
 * @Route("")
56
 */
57
class ExtensionController extends AbstractController
58
{
59
    private const NEW_ROUTES_AVAIL = 'new.routes.avail';
60
61
    /**
62
     * @Route("/list/{pos}")
63
     * @PermissionCheck("admin")
64
     * @Theme("admin")
65
     * @Template("@ZikulaExtensionsModule/Extension/list.html.twig")
66
     */
67
    public function listAction(
68
        Request $request,
69
        EventDispatcherInterface $eventDispatcher,
70
        ExtensionRepositoryInterface $extensionRepository,
71
        BundleSyncHelper $bundleSyncHelper,
72
        RouterInterface $router,
73
        int $pos = 1
74
    ): array {
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
     * @PermissionCheck("admin")
113
     *
114
     * Activate an extension.
115
     *
116
     * @throws AccessDeniedException Thrown if the CSRF token is invalid
117
     */
118
    public function activateAction(
119
        int $id,
120
        string $token,
121
        ExtensionRepositoryInterface $extensionRepository,
122
        ExtensionStateHelper $extensionStateHelper,
123
        CacheClearer $cacheClearer
124
    ): RedirectResponse {
125
        if (!$this->isCsrfTokenValid('activate-extension', $token)) {
126
            throw new AccessDeniedException();
127
        }
128
129
        /** @var ExtensionEntity $extension */
130
        $extension = $extensionRepository->find($id);
131
        if (Constant::STATE_NOTALLOWED === $extension->getState()) {
132
            $this->addFlash('error', $this->trans('Error! Activation of %name% not allowed.', ['%name%' => $extension->getName()]));
133
        } else {
134
            // Update state
135
            $extensionStateHelper->updateState($id, Constant::STATE_ACTIVE);
136
            $cacheClearer->clear('symfony');
137
            $this->addFlash('status', $this->trans('Done! Activated %name%.', ['%name%' => $extension->getName()]));
138
        }
139
140
        return $this->redirectToRoute('zikulaextensionsmodule_extension_list');
141
    }
142
143
    /**
144
     * @Route("/deactivate/{id}/{token}", methods = {"GET"}, requirements={"id" = "^[1-9]\d*$"})
145
     * @PermissionCheck("admin")
146
     *
147
     * Deactivate an extension
148
     *
149
     * @throws AccessDeniedException Thrown if the CSRF token is invalid
150
     */
151
    public function deactivateAction(
152
        int $id,
153
        string $token,
154
        ExtensionRepositoryInterface $extensionRepository,
155
        ExtensionStateHelper $extensionStateHelper,
156
        CacheClearer $cacheClearer
157
    ): RedirectResponse {
158
        if (!$this->isCsrfTokenValid('deactivate-extension', $token)) {
159
            throw new AccessDeniedException();
160
        }
161
162
        /** @var ExtensionEntity $extension */
163
        $extension = $extensionRepository->find($id);
164
        $defaultTheme = $this->getVariableApi()->get(VariableApi::CONFIG, 'defaulttheme');
165
        $adminTheme = $this->getVariableApi()->get('ZikulaAdminModule', 'admintheme');
166
167
        if (null !== $extension) {
168
            if (ZikulaKernel::isCoreExtension($extension->getName())) {
169
                $this->addFlash('error', $this->trans('Error! You cannot deactivate the %name%. It is required by the system.', ['%name%' => $extension->getName()]));
170
            } elseif (in_array($extension->getName(), [$defaultTheme, $adminTheme])) {
171
                $this->addFlash('error', $this->trans('Error! You cannot deactivate the %name%. The theme is in use.', ['%name%' => $extension->getName()]));
172
            } else {
173
                // Update state
174
                $extensionStateHelper->updateState($id, Constant::STATE_INACTIVE);
175
                $cacheClearer->clear('symfony');
176
                $this->addFlash('status', $this->trans('Done! Deactivated %name%.', ['%name%' => $extension->getName()]));
177
            }
178
        }
179
180
        return $this->redirectToRoute('zikulaextensionsmodule_extension_list');
181
    }
182
183
    /**
184
     * @Route("/modify/{id}/{forceDefaults}", requirements={"id" = "^[1-9]\d*$", "forceDefaults" = "0|1"})
185
     * @Theme("admin")
186
     * @Template("@ZikulaExtensionsModule/Extension/modify.html.twig")
187
     *
188
     * Modify a module.
189
     *
190
     * @return array|RedirectResponse
191
     * @throws AccessDeniedException Thrown if the user doesn't have admin permissions for modifying the extension
192
     */
193
    public function modifyAction(
194
        Request $request,
195
        ZikulaHttpKernelInterface $kernel,
196
        ExtensionEntity $extension,
197
        CacheClearer $cacheClearer,
198
        bool $forceDefaults = false
199
    ) {
200
        if (!$this->hasPermission('ZikulaExtensionsModule::modify', $extension->getName() . '::' . $extension->getId(), ACCESS_ADMIN)) {
201
            throw new AccessDeniedException();
202
        }
203
204
        /** @var AbstractExtension $extensionBundle */
205
        $extensionBundle = $kernel->getBundle($extension->getName());
206
        $metaData = $extensionBundle->getMetaData()->getFilteredVersionInfoArray();
207
208
        if ($forceDefaults) {
209
            $extension->setName($metaData['name']);
210
            $extension->setUrl($metaData['url']);
211
            $extension->setDescription($metaData['description']);
212
        }
213
214
        $form = $this->createForm(ExtensionModifyType::class, $extension);
215
        $form->handleRequest($request);
216
        if ($form->isSubmitted() && $form->isValid()) {
217
            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

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