Issues (3099)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

EventListener/NodeTranslationListener.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Kunstmaan\NodeBundle\EventListener;
4
5
use Doctrine\Common\Persistence\Mapping\MappingException;
6
use Doctrine\ORM\EntityManagerInterface;
7
use Doctrine\ORM\Event\LifecycleEventArgs;
8
use Doctrine\ORM\Event\OnFlushEventArgs;
9
use Doctrine\ORM\Mapping\ClassMetadata;
10
use Kunstmaan\AdminBundle\FlashMessages\FlashTypes;
11
use Kunstmaan\AdminBundle\Helper\DomainConfigurationInterface;
12
use Kunstmaan\NodeBundle\Entity\HasNodeInterface;
13
use Kunstmaan\NodeBundle\Entity\Node;
14
use Kunstmaan\NodeBundle\Entity\NodeTranslation;
15
use Kunstmaan\NodeBundle\Helper\PagesConfiguration;
16
use Kunstmaan\NodeBundle\Repository\NodeTranslationRepository;
17
use Kunstmaan\UtilitiesBundle\Helper\SlugifierInterface;
18
use Psr\Log\LoggerInterface;
19
use Symfony\Component\HttpFoundation\RequestStack;
20
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
21
use Symfony\Component\HttpFoundation\Session\SessionInterface;
22
23
/**
24
 * Class NodeTranslationListener
25
 * Listens to doctrine postFlush event and updates the urls if the entities are nodetranslations
26
 */
27
class NodeTranslationListener
28
{
29
    /** @var SessionInterface|FlashBagInterface */
30
    private $flashBag;
31
32
    /** @var LoggerInterface */
33
    private $logger;
34
35
    /** @var SlugifierInterface */
36
    private $slugifier;
37
38
    /** @var RequestStack */
39
    private $requestStack;
40
41
    /** @var DomainConfigurationInterface */
42
    private $domainConfiguration;
43
44
    /** @var PagesConfiguration */
45
    private $pagesConfiguration;
46
47
    /**
48
     * NodeTranslationListener constructor.
49
     *
50
     * @param SessionInterface|FlashBagInterface $session
51
     * @param LoggerInterface                    $logger
52
     * @param SlugifierInterface                 $slugifier
53
     * @param RequestStack                       $requestStack
54
     * @param DomainConfigurationInterface       $domainConfiguration
55
     * @param PagesConfiguration                 $pagesConfiguration
56
     */
57
    public function __construct(
58
        /* SessionInterface */ $flashBag,
59
        LoggerInterface $logger,
60
        SlugifierInterface $slugifier,
61
        RequestStack $requestStack,
62
        DomainConfigurationInterface $domainConfiguration,
63
        PagesConfiguration $pagesConfiguration
64
    ) {
65
        $this->flashBag = $flashBag;
66
67
        if ($flashBag instanceof FlashBagInterface) {
68
            @trigger_error('Passing the "@session.flash_bag" service as first argument is deprecated since KunstmaanNodeBundle 5.6 and will be replaced by the session in KunstmaanNodeBundle 6.0. Inject the "@session" service instead.', E_USER_DEPRECATED);
69
        }
70
71
        $this->logger = $logger;
72
        $this->slugifier = $slugifier;
73
        $this->requestStack = $requestStack;
74
        $this->domainConfiguration = $domainConfiguration;
75
        $this->pagesConfiguration = $pagesConfiguration;
76
    }
77
78
    /**
79
     * @param LifecycleEventArgs $args
80
     */
81 View Code Duplication
    public function prePersist(LifecycleEventArgs $args)
82
    {
83
        $entity = $args->getEntity();
84
85
        if ($entity instanceof NodeTranslation) {
86
            $this->setSlugWhenEmpty($entity, $args->getEntityManager());
87
            $this->ensureSlugIsSlugified($entity);
88
        }
89
    }
90
91
    /**
92
     * @param LifecycleEventArgs $args
93
     */
94 View Code Duplication
    public function preUpdate(LifecycleEventArgs $args)
95
    {
96
        $entity = $args->getEntity();
97
98
        if ($entity instanceof NodeTranslation) {
99
            $this->setSlugWhenEmpty($entity, $args->getEntityManager());
100
            $this->ensureSlugIsSlugified($entity);
101
        }
102
    }
103
104
    /**
105
     * @param NodeTranslation        $nodeTranslation
106
     * @param EntityManagerInterface $em
107
     */
108
    private function setSlugWhenEmpty(NodeTranslation $nodeTranslation, EntityManagerInterface $em)
109
    {
110
        $publicNode = $nodeTranslation->getRef($em);
0 ignored issues
show
$em of type object<Doctrine\ORM\EntityManagerInterface> is not a sub-type of object<Doctrine\ORM\EntityManager>. It seems like you assume a concrete implementation of the interface Doctrine\ORM\EntityManagerInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
111
112
        // Do nothing for StructureNode objects, skip.
113
        if ($publicNode instanceof HasNodeInterface && $publicNode->isStructureNode()) {
114
            return;
115
        }
116
117
        // If no slug is set and no structure node, apply title as slug.
118 View Code Duplication
        if ($nodeTranslation->getSlug() === null && $nodeTranslation->getNode()->getParent() !== null) {
119
            $nodeTranslation->setSlug(
120
                $this->slugifier->slugify($nodeTranslation->getTitle())
121
            );
122
        }
123
    }
124
125
    /**
126
     * @param NodeTranslation $nodeTranslation
127
     */
128
    private function ensureSlugIsSlugified(NodeTranslation $nodeTranslation)
129
    {
130
        if ($nodeTranslation->getSlug() !== null) {
131
            $nodeTranslation->setSlug(
132
                $this->slugifier->slugify($nodeTranslation->getSlug())
133
            );
134
        }
135
    }
136
137
    /**
138
     * OnFlush doctrine event - updates the nodetranslation urls if needed
139
     *
140
     * @param OnFlushEventArgs $args
141
     */
142
    public function onFlush(OnFlushEventArgs $args)
143
    {
144
        try {
145
            $em = $args->getEntityManager();
146
147
            $class = $em->getClassMetadata(NodeTranslation::class);
148
149
            // Collect all nodetranslations that are updated
150
            foreach ($em->getUnitOfWork()->getScheduledEntityUpdates() as $entity) {
151
                if ($entity instanceof NodeTranslation) {
152
                    /** @var Node $publicNode */
153
                    $publicNode = $entity->getPublicNodeVersion()->getRef($em);
154
155
                    // Do nothing for StructureNode objects, skip.
156
                    if ($publicNode instanceof HasNodeInterface && $publicNode->isStructureNode()) {
157
                        continue;
158
                    }
159
160
                    $entity = $this->updateUrl($entity, $em);
161
162 View Code Duplication
                    if ($entity !== false) {
163
                        $em->persist($entity);
164
                        $em->getUnitOfWork()->recomputeSingleEntityChangeSet($class, $entity);
165
166
                        $this->updateNodeChildren($entity, $em, $class);
167
                    }
168
                }
169
            }
170
        } catch (MappingException $e) {
171
            // Different entity manager without this entity configured in namespace chain. Ignore
172
        }
173
    }
174
175
    /**
176
     * Checks if a nodetranslation has children and update their url
177
     *
178
     * @param NodeTranslation        $node  The node
179
     * @param EntityManagerInterface $em    The entity manager
180
     * @param ClassMetadata          $class The class meta daat
181
     */
182
    private function updateNodeChildren(NodeTranslation $node, EntityManagerInterface $em, ClassMetadata $class)
183
    {
184
        $children = $node->getNode()->getChildren();
185
        if (\count($children) > 0) {
186
            /* @var Node $child */
187
            foreach ($children as $child) {
188
                $translation = $child->getNodeTranslation($node->getLang(), true);
189
                if ($translation) {
190
                    $translation = $this->updateUrl($translation, $em);
191
192 View Code Duplication
                    if ($translation !== false) {
193
                        $em->persist($translation);
194
                        $em->getUnitOfWork()->recomputeSingleEntityChangeSet($class, $translation);
195
196
                        $this->updateNodeChildren($translation, $em, $class);
197
                    }
198
                }
199
            }
200
        }
201
    }
202
203
    /**
204
     * Update the url for a nodetranslation
205
     *
206
     * @param NodeTranslation        $nodeTranslation The node translation
207
     * @param EntityManagerInterface $em              The entity manager
208
     *
209
     * @return NodeTranslation|bool returns the node when all is well because it has to be saved
210
     */
211
    private function updateUrl(NodeTranslation $nodeTranslation, EntityManagerInterface $em)
212
    {
213
        $result = $this->ensureUniqueUrl($nodeTranslation, $em);
214
215
        if ($result) {
216
            return $nodeTranslation;
217
        }
218
219
        $this->logger->info(
220
            sprintf('Found NT %s needed NO change', $nodeTranslation->getId())
221
        );
222
223
        return false;
224
    }
225
226
    /**
227
     * A function that checks the URL and sees if it's unique.
228
     * It's allowed to be the same when the node is a StructureNode.
229
     * When a node is deleted it needs to be ignored in the check.
230
     * Offline nodes need to be included as well.
231
     *
232
     * It sluggifies the slug, updates the URL
233
     * and checks all existing NodeTranslations ([1]), excluding itself. If a
234
     * URL existsthat has the same url. If an existing one is found the slug is
235
     * modified, the URL is updated and the check is repeated until no prior
236
     * urls exist.
237
     *
238
     * NOTE: We need a way to tell if the slug has been modified or not.
239
     * NOTE: Would be cool if we could increment a number after the slug. Like
240
     * check if it matches -v# and increment the number.
241
     *
242
     * [1] For all languages for now. The issue is that we need a way to know
243
     * if a node's URL is prepended with the language or not. For now both
244
     * scenarios are possible so we check for all languages.
245
     *
246
     * @param NodeTranslation        $translation Reference to the NodeTranslation.
247
     *                                            This is modified in place.
248
     * @param EntityManagerInterface $em          The entity manager
249
     * @param array                  $flashes     The flash messages array
250
     *
251
     * @return bool
252
     */
253
    private function ensureUniqueUrl(NodeTranslation $translation, EntityManagerInterface $em, array $flashes = [])
254
    {
255
        // Can't use GetRef here yet since the NodeVersions aren't loaded yet for some reason.
256
        $nodeVersion = $translation->getPublicNodeVersion();
257
        $page = $em->getRepository($nodeVersion->getRefEntityName())
258
            ->find($nodeVersion->getRefId());
259
260
        if (null === $page) {
261
            return false;
262
        }
263
264
        $isStructureNode = $page->isStructureNode();
265
266
        // If it's a StructureNode the slug and url should be empty.
267
        if ($isStructureNode) {
268
            $translation->setSlug('');
269
            $translation->setUrl($translation->getFullSlug());
270
271
            return true;
272
        }
273
274
        /* @var NodeTranslationRepository $nodeTranslationRepository */
275
        $nodeTranslationRepository = $em->getRepository(NodeTranslation::class);
276
277
        if ($translation->getUrl() === $translation->getFullSlug()) {
278
            $this->logger->debug(
279
                sprintf(
280
                    'Evaluating URL for NT %s getUrl: "%s" getFullSlug: "%s"',
281
                    $translation->getId(),
282
                    $translation->getUrl(),
283
                    $translation->getFullSlug()
284
                )
285
            );
286
287
            return false;
288
        }
289
290
        // Adjust the URL.
291
        $translation->setUrl($translation->getFullSlug());
292
293
        // Find all translations with this new URL, whose nodes are not deleted.
294
        $translations = $nodeTranslationRepository->getAllNodeTranslationsForUrl(
295
            $translation->getUrl(),
296
            $translation->getLang(),
297
            false,
298
            $translation,
299
            $this->domainConfiguration->getRootNode()
300
        );
301
302
        $this->logger->debug(
303
            sprintf(
304
                'Found %s node(s) that math url "%s"',
305
                \count($translations),
306
                $translation->getUrl()
307
            )
308
        );
309
310
        $translationsWithSameUrl = [];
311
312
        /** @var NodeTranslation $trans */
313
        foreach ($translations as $trans) {
314
            if (!$this->pagesConfiguration->isStructureNode($trans->getPublicNodeVersion()->getRefEntityName())) {
315
                $translationsWithSameUrl[] = $trans;
316
            }
317
        }
318
319
        if (\count($translationsWithSameUrl) > 0) {
320
            $oldUrl = $translation->getFullSlug();
321
            $translation->setSlug(
322
                $this->slugifier->slugify(
323
                    $this->incrementString($translation->getSlug())
324
                )
325
            );
326
            $newUrl = $translation->getFullSlug();
327
328
            $message = sprintf(
329
                'The URL of the page has been changed from %s to %s since another page already uses this URL',
330
                $oldUrl,
331
                $newUrl
332
            );
333
            $this->logger->info($message);
334
            $flashes[] = $message;
335
336
            $this->ensureUniqueUrl($translation, $em, $flashes);
337
        } elseif (\count($flashes) > 0 && $this->isInRequestScope()) {
338
            // No translations found so we're certain we can show this message.
339
            $flash = current(\array_slice($flashes, -1));
340
            $this->getFlashBag()->add(FlashTypes::WARNING, $flash);
341
        }
342
343
        return true;
344
    }
345
346
    /**
347
     * Increment a string that ends with a number.
348
     * If the string does not end in a number we'll add the append and then add
349
     * the first number.
350
     *
351
     * @param string $string the string we want to increment
352
     * @param string $append the part we want to append before we start adding
353
     *                       a number
354
     *
355
     * @return string incremented string
356
     */
357 View Code Duplication
    private function incrementString($string, $append = '-v')
358
    {
359
        $finalDigitGrabberRegex = '/\d+$/';
360
        $matches = [];
361
362
        preg_match($finalDigitGrabberRegex, $string, $matches);
363
364
        if (\count($matches) > 0) {
365
            $digit = (int) $matches[0];
366
            ++$digit;
367
368
            // Replace the integer with the new digit.
369
            return preg_replace($finalDigitGrabberRegex, $digit, $string);
370
        }
371
372
        return $string.$append.'1';
373
    }
374
375
    /**
376
     * @return bool
377
     */
378
    private function isInRequestScope()
379
    {
380
        return $this->requestStack && $this->requestStack->getCurrentRequest();
381
    }
382
383
    private function getFlashBag()
384
    {
385
        if ($this->flashBag instanceof SessionInterface) {
386
            return $this->flashBag->getFlashBag();
387
        }
388
389
        return $this->flashBag;
390
    }
391
}
392