Completed
Push — master ( 05bb5a...5289b8 )
by Rafał
34:32 queued 04:15
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 tenant: '.$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
            try {
122
                $article = $this->articleResolver->resolve($url);
123
            } catch (\Exception $e) {
124
                $article = null;
125
            }
126
127
            if (null !== $article) {
128
                $articleId = $article->getId();
129
                if (!\array_key_exists($articleId, $articles)) {
130
                    $articles[$articleId] = $article;
131
                }
132
            }
133
        }
134
135
        foreach ($articles as $article) {
136
            try {
137
                $impressionSource = $this->getImpressionSource($request);
138
            } catch (\Exception $e) {
139
                continue;
140
            }
141
142
            $this->articleStatisticsService->addArticleEvent(
143
                (int) $article->getId(),
144
                ArticleEventInterface::ACTION_IMPRESSION,
145
                $impressionSource
146
            );
147
            echo 'Article '.$article->getId()." impression was added \n";
148
        }
149
    }
150
151
    private function handleArticlePageViews(Request $request): void
152
    {
153
        $articleId = $request->query->get('articleId', null);
154
        if (null !== $articleId) {
155
            $this->articleStatisticsService->addArticleEvent((int) $articleId, ArticleEventInterface::ACTION_PAGEVIEW, [
156
                'pageViewSource' => $this->getPageViewSource($request),
157
            ]);
158
        }
159
    }
160
161
    private function getImpressionSource(Request $request): array
162
    {
163
        $source = [];
164
        $referrer = $request->server->get('HTTP_REFERER');
165
        if (null === $referrer) {
166
            return $source;
167
        }
168
169
        $route = $this->matcher->match($this->getFragmentFromUrl($referrer, 'path'));
170
        if (isset($route['_article_meta']) && $route['_article_meta']->getValues() instanceof ArticleInterface) {
171
            $source[ArticleStatisticsServiceInterface::KEY_IMPRESSION_TYPE] = 'article';
172
            $source[ArticleStatisticsServiceInterface::KEY_IMPRESSION_SOURCE_ARTICLE] = $route['_article_meta']->getValues();
173
        } elseif (isset($route['_route_meta']) && $route['_route_meta']->getValues() instanceof RouteInterface) {
174
            $source[ArticleStatisticsServiceInterface::KEY_IMPRESSION_TYPE] = 'route';
175
            $source[ArticleStatisticsServiceInterface::KEY_IMPRESSION_SOURCE_ROUTE] = $route['_route_meta']->getValues();
176
        } elseif (isset($route['_route']) && 'homepage' === $route['_route']) {
177
            $source[ArticleStatisticsServiceInterface::KEY_IMPRESSION_TYPE] = 'homepage';
178
        }
179
180
        return $source;
181
    }
182
183
    private function getPageViewSource(Request $request): string
184
    {
185
        $pageViewReferer = $request->query->get('ref', null);
186
        if (null !== $pageViewReferer) {
187
            $refererHost = \str_replace('www.', '', $this->getFragmentFromUrl($pageViewReferer, 'host'));
188
            if ($refererHost && $this->isHostMatchingTenant($refererHost)) {
189
                return ArticleEventInterface::PAGEVIEW_SOURCE_INTERNAL;
190
            }
191
        }
192
193
        return ArticleEventInterface::PAGEVIEW_SOURCE_EXTERNAL;
194
    }
195
196
    private function getFragmentFromUrl(string $url, string $fragment): ?string
197
    {
198
        $fragments = \parse_url($url);
199
        if (!\array_key_exists($fragment, $fragments)) {
200
            return null;
201
        }
202
203
        return str_replace('/app_dev.php', '', $fragments[$fragment]);
204
    }
205
206
    private function isHostMatchingTenant(string $host): bool
207
    {
208
        $tenant = $this->tenantContext->getTenant();
209
        $tenantHost = $tenant->getDomainName();
210
        if (null !== ($subdomain = $tenant->getSubdomain())) {
211
            $tenantHost = $subdomain.'.'.$tenantHost;
212
        }
213
214
        return $host === $tenantHost;
215
    }
216
217
    /**
218
     * @param Request $request
219
     */
220
    private function setTenant(Request $request): void
221
    {
222
        $this->tenantContext->setTenant(
223
            $this->tenantResolver->resolve(
224
                $request->server->get('HTTP_REFERER',
225
                    $request->query->get('host',
226
                        $request->getHost()
227
                    )
228
                )
229
            )
230
        );
231
    }
232
}
233