Completed
Push — 1.4 ( f2fa86...1c2218 )
by Paweł
08:49
created

AnalyticsEventConsumer::execute()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 31
rs 8.8017
c 0
b 0
f 0
cc 6
nc 6
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Superdesk Web Publisher Core Bundle.
7
 *
8
 * Copyright 2017 Sourcefabric z.ú. and contributors.
9
 *
10
 * For the full copyright and license information, please see the
11
 * AUTHORS and LICENSE files distributed with this source code.
12
 *
13
 * @copyright 2017 Sourcefabric z.ú
14
 * @license http://www.superdesk.org/license
15
 */
16
17
namespace SWP\Bundle\CoreBundle\Consumer;
18
19
use OldSound\RabbitMqBundle\RabbitMq\ConsumerInterface;
20
use PhpAmqpLib\Message\AMQPMessage;
21
use SWP\Bundle\AnalyticsBundle\Model\ArticleEventInterface;
22
use SWP\Bundle\AnalyticsBundle\Services\ArticleStatisticsServiceInterface;
23
use SWP\Bundle\ContentBundle\Model\RouteInterface;
24
use SWP\Bundle\CoreBundle\Model\ArticleInterface;
25
use SWP\Bundle\CoreBundle\Resolver\ArticleResolverInterface;
26
use SWP\Component\MultiTenancy\Context\TenantContextInterface;
27
use SWP\Component\MultiTenancy\Exception\TenantNotFoundException;
28
use SWP\Component\MultiTenancy\Resolver\TenantResolver;
29
use Symfony\Component\HttpFoundation\Request;
30
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
31
32
/**
33
 * Class AnalyticsEventConsumer.
34
 */
35
final class AnalyticsEventConsumer implements ConsumerInterface
36
{
37
    /**
38
     * @var ArticleStatisticsServiceInterface
39
     */
40
    private $articleStatisticsService;
41
42
    /**
43
     * @var TenantResolver
44
     */
45
    private $tenantResolver;
46
47
    /**
48
     * @var TenantContextInterface
49
     */
50
    private $tenantContext;
51
52
    /**
53
     * @var UrlMatcherInterface
54
     */
55
    private $matcher;
56
57
    /**
58
     * @var ArticleResolverInterface
59
     */
60
    private $articleResolver;
61
62
    public function __construct(
63
        ArticleStatisticsServiceInterface $articleStatisticsService,
64
        TenantResolver $tenantResolver,
65
        TenantContextInterface $tenantContext,
66
        UrlMatcherInterface $matcher,
67
        ArticleResolverInterface $articleResolver
68
    ) {
69
        $this->articleStatisticsService = $articleStatisticsService;
70
        $this->tenantResolver = $tenantResolver;
71
        $this->tenantContext = $tenantContext;
72
        $this->matcher = $matcher;
73
        $this->articleResolver = $articleResolver;
74
    }
75
76
    /**
77
     * @param AMQPMessage $message
78
     *
79
     * @return bool|mixed
80
     */
81
    public function execute(AMQPMessage $message)
82
    {
83
        /** @var Request $request */
84
        $request = unserialize($message->getBody());
85
        if (!$request instanceof Request) {
86
            return ConsumerInterface::MSG_REJECT;
87
        }
88
89
        try {
90
            $this->setTenant($request);
91
        } catch (TenantNotFoundException $e) {
92
            echo $e->getMessage()."\n";
93
94
            return ConsumerInterface::MSG_REJECT;
95
        }
96
        echo 'Set tenent: '.$this->tenantContext->getTenant()->getCode()."\n";
97
98
        if ($request->query->has('articleId')) {
99
            $this->handleArticlePageViews($request);
100
101
            echo 'Pageview for article '.$request->query->get('articleId')." was processed \n";
0 ignored issues
show
Security Cross-Site Scripting introduced by
'Pageview for article ' ...') . ' was processed ' can contain request data and is used in output context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. ParameterBag::get() returns tainted data
    in src/SWP/Bundle/CoreBundle/Consumer/AnalyticsEventConsumer.php on line 101

Preventing Cross-Site-Scripting Attacks

Cross-Site-Scripting allows an attacker to inject malicious code into your website - in particular Javascript code, and have that code executed with the privileges of a visiting user. This can be used to obtain data, or perform actions on behalf of that visiting user.

In order to prevent this, make sure to escape all user-provided data:

// for HTML
$sanitized = htmlentities($tainted, ENT_QUOTES);

// for URLs
$sanitized = urlencode($tainted);

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
102
        }
103
104
        if ($request->attributes->has('data') && ArticleEventInterface::ACTION_IMPRESSION === $request->query->get('type')) {
105
            $this->handleArticleImpressions($request);
106
107
            echo "Article impressions were processed \n";
108
        }
109
110
        return ConsumerInterface::MSG_ACK;
111
    }
112
113
    private function handleArticleImpressions(Request $request): void
114
    {
115
        $articles = [];
116
        if (!\is_array($request->attributes->get('data'))) {
117
            return;
118
        }
119
120
        foreach ($request->attributes->get('data') as $url) {
121
            $article = $this->articleResolver->resolve($url);
122
            if (null !== $article) {
123
                $articleId = $article->getId();
124
                if (!\array_key_exists($articleId, $articles)) {
125
                    $articles[$articleId] = $article;
126
                }
127
            }
128
        }
129
130
        foreach ($articles as $article) {
131
            $this->articleStatisticsService->addArticleEvent(
132
                (int) $article->getId(),
133
                ArticleEventInterface::ACTION_IMPRESSION,
134
                $this->getImpressionSource($request)
135
            );
136
        }
137
    }
138
139
    private function handleArticlePageViews(Request $request): void
140
    {
141
        $articleId = $request->query->get('articleId', null);
142
        if (null !== $articleId) {
143
            $this->articleStatisticsService->addArticleEvent((int) $articleId, ArticleEventInterface::ACTION_PAGEVIEW, [
144
                'pageViewSource' => $this->getPageViewSource($request),
145
            ]);
146
        }
147
    }
148
149
    private function getImpressionSource(Request $request): array
150
    {
151
        $source = [];
152
        $referrer = $request->server->get('HTTP_REFERER');
153
        if (null === $referrer) {
154
            return $source;
155
        }
156
157
        $route = $this->matcher->match($this->getFragmentFromUrl($referrer, 'path'));
158
        if (isset($route['_article_meta']) && $route['_article_meta']->getValues() instanceof ArticleInterface) {
159
            $source[ArticleStatisticsServiceInterface::KEY_IMPRESSION_TYPE] = 'article';
160
            $source[ArticleStatisticsServiceInterface::KEY_IMPRESSION_SOURCE_ARTICLE] = $route['_article_meta']->getValues();
161
        } elseif (isset($route['_route_meta']) && $route['_route_meta']->getValues() instanceof RouteInterface) {
162
            $source[ArticleStatisticsServiceInterface::KEY_IMPRESSION_TYPE] = 'route';
163
            $source[ArticleStatisticsServiceInterface::KEY_IMPRESSION_SOURCE_ROUTE] = $route['_route_meta']->getValues();
164
        } elseif (isset($route['_route']) && 'homepage' === $route['_route']) {
165
            $source[ArticleStatisticsServiceInterface::KEY_IMPRESSION_TYPE] = 'homepage';
166
        }
167
168
        return $source;
169
    }
170
171
    private function getPageViewSource(Request $request): string
172
    {
173
        $pageViewReferer = $request->query->get('ref', null);
174
        if (null !== $pageViewReferer) {
175
            $refererHost = $this->getFragmentFromUrl($pageViewReferer, 'host');
176
            if ($refererHost && $this->isHostMatchingTenant($refererHost)) {
177
                return ArticleEventInterface::PAGEVIEW_SOURCE_INTERNAL;
178
            }
179
        }
180
181
        return ArticleEventInterface::PAGEVIEW_SOURCE_EXTERNAL;
182
    }
183
184
    private function getFragmentFromUrl(string $url, string $fragment): ?string
185
    {
186
        $fragments = \parse_url($url);
187
        if (!\array_key_exists($fragment, $fragments)) {
188
            return null;
189
        }
190
191
        return str_replace('/app_dev.php', '', $fragments[$fragment]);
192
    }
193
194
    private function isHostMatchingTenant(string $host): bool
195
    {
196
        $tenant = $this->tenantContext->getTenant();
197
        $tenantHost = $tenant->getDomainName();
198
        if (null !== ($subdomain = $tenant->getSubdomain())) {
199
            $tenantHost = $subdomain.'.'.$tenantHost;
200
        }
201
202
        return $host === $tenantHost;
203
    }
204
205
    /**
206
     * @param Request $request
207
     */
208
    private function setTenant(Request $request): void
209
    {
210
        $this->tenantContext->setTenant(
211
            $this->tenantResolver->resolve(
212
                $request->server->get('HTTP_REFERER',
213
                    $request->query->get('host',
214
                        $request->getHost()
215
                    )
216
                )
217
            )
218
        );
219
    }
220
}
221