Passed
Push — master ( 44ae05...a9392a )
by MusikAnimal
11:19
created

MetaController::resultAction()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 18
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 14
nc 4
nop 1
dl 0
loc 18
ccs 0
cts 0
cp 0
crap 12
rs 9.7998
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file contains only the MetaController class.
4
 */
5
6
declare(strict_types=1);
7
8
namespace AppBundle\Controller;
9
10
use DateTime;
11
use Doctrine\DBAL\Connection;
12
use Symfony\Component\HttpFoundation\Request;
13
use Symfony\Component\HttpFoundation\Response;
14
use Symfony\Component\Routing\Annotation\Route;
15
16
/**
17
 * This controller serves everything for the Meta tool.
18
 */
19
class MetaController extends XtoolsController
20
{
21
    /**
22
     * Get the name of the tool's index route.
23
     * @return string
24
     * @codeCoverageIgnore
25
     */
26
    public function getIndexRoute(): string
27
    {
28
        return 'Meta';
29
    }
30
31
    /**
32
     * Display the form.
33
     * @Route("/meta", name="meta")
34
     * @Route("/meta", name="Meta")
35
     * @Route("/meta/index.php", name="MetaIndexPhp")
36
     * @return Response
37
     */
38 1
    public function indexAction(): Response
39
    {
40 1
        if (isset($this->params['start']) && isset($this->params['end'])) {
41
            return $this->redirectToRoute('MetaResult', $this->params);
42
        }
43
44 1
        return $this->render('meta/index.html.twig', [
45 1
            'xtPage' => 'Meta',
46
            'xtPageTitle' => 'tool-meta',
47
            'xtSubtitle' => 'tool-meta-desc',
48
        ]);
49
    }
50
51
    /**
52
     * Display the results.
53
     * @Route("/meta/{start}/{end}/{legacy}", name="MetaResult")
54
     * @param bool $legacy Non-blank value indicates to show stats for legacy XTools
55
     * @return Response
56
     * @codeCoverageIgnore
57
     */
58
    public function resultAction(bool $legacy = false): Response
59
    {
60
        $db = $legacy ? 'toolsdb' : 'default';
61
        $table = $legacy ? 's51187__metadata.xtools_timeline' : 'usage_timeline';
62
        $client = $this->container
63
            ->get('doctrine')
64
            ->getManager($db)
65
            ->getConnection();
66
67
        $toolUsage = $this->getToolUsageStats($client, $table);
68
        $apiUsage = $this->getApiUsageStats($client);
69
70
        return $this->render('meta/result.html.twig', [
71
            'xtPage' => 'Meta',
72
            'start' => $this->start,
73
            'end' => $this->end,
74
            'toolUsage' => $toolUsage,
75
            'apiUsage' => $apiUsage,
76
        ]);
77
    }
78
79
    /**
80
     * Get usage statistics of the core tools.
81
     * @param Connection $client
82
     * @param string $table Table to query.
83
     * @return array
84
     * @codeCoverageIgnore
85
     */
86
    private function getToolUsageStats(Connection $client, string $table): array
87
    {
88
        $query = $client->prepare("SELECT * FROM $table
89
                                   WHERE date >= :start AND date <= :end");
90
        $start = date('Y-m-d', $this->start);
0 ignored issues
show
Bug introduced by
It seems like $this->start can also be of type boolean; however, parameter $timestamp of date() 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

90
        $start = date('Y-m-d', /** @scrutinizer ignore-type */ $this->start);
Loading history...
91
        $end = date('Y-m-d', $this->end);
92
        $query->bindParam('start', $start);
93
        $query->bindParam('end', $end);
94
        $query->execute();
95
96
        $data = $query->fetchAll();
97
98
        // Create array of totals, along with formatted timeline data as needed by Chart.js
99
        $totals = [];
100
        $dateLabels = [];
101
        $timeline = [];
102
        $startObj = new DateTime($start);
103
        $endObj = new DateTime($end);
104
        $numDays = (int) $endObj->diff($startObj)->format("%a");
105
        $grandSum = 0;
106
107
        // Generate array of date labels
108
        for ($dateObj = new DateTime($start); $dateObj <= $endObj; $dateObj->modify('+1 day')) {
109
            $dateLabels[] = $dateObj->format('Y-m-d');
110
        }
111
112
        foreach ($data as $entry) {
113
            if (!isset($totals[$entry['tool']])) {
114
                $totals[$entry['tool']] = (int) $entry['count'];
115
116
                // Create arrays for each tool, filled with zeros for each date in the timeline
117
                $timeline[$entry['tool']] = array_fill(0, $numDays, 0);
118
            } else {
119
                $totals[$entry['tool']] += (int) $entry['count'];
120
            }
121
122
            $date = new DateTime($entry['date']);
123
            $dateIndex = (int) $date->diff($startObj)->format("%a");
124
            $timeline[$entry['tool']][$dateIndex] = (int) $entry['count'];
125
126
            $grandSum += $entry['count'];
127
        }
128
        arsort($totals);
129
130
        return [
131
            'totals' => $totals,
132
            'grandSum' => $grandSum,
133
            'dateLabels' => $dateLabels,
134
            'timeline' => $timeline,
135
        ];
136
    }
137
138
    /**
139
     * Get usage statistics of the API.
140
     * @param Connection $client
141
     * @return array
142
     * @codeCoverageIgnore
143
     */
144
    private function getApiUsageStats(Connection $client): array
145
    {
146
        $query = $client->prepare("SELECT * FROM usage_api_timeline
147
                                   WHERE date >= :start AND date <= :end");
148
        $start = date('Y-m-d', $this->start);
0 ignored issues
show
Bug introduced by
It seems like $this->start can also be of type boolean; however, parameter $timestamp of date() 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

148
        $start = date('Y-m-d', /** @scrutinizer ignore-type */ $this->start);
Loading history...
149
        $end = date('Y-m-d', $this->end);
150
        $query->bindParam('start', $start);
151
        $query->bindParam('end', $end);
152
        $query->execute();
153
154
        $data = $query->fetchAll();
155
156
        // Create array of totals, along with formatted timeline data as needed by Chart.js
157
        $totals = [];
158
        $dateLabels = [];
159
        $timeline = [];
160
        $startObj = new DateTime($start);
161
        $endObj = new DateTime($end);
162
        $numDays = (int) $endObj->diff($startObj)->format("%a");
163
        $grandSum = 0;
164
165
        // Generate array of date labels
166
        for ($dateObj = new DateTime($start); $dateObj <= $endObj; $dateObj->modify('+1 day')) {
167
            $dateLabels[] = $dateObj->format('Y-m-d');
168
        }
169
170
        foreach ($data as $entry) {
171
            if (!isset($totals[$entry['endpoint']])) {
172
                $totals[$entry['endpoint']] = (int) $entry['count'];
173
174
                // Create arrays for each endpoint, filled with zeros for each date in the timeline
175
                $timeline[$entry['endpoint']] = array_fill(0, $numDays, 0);
176
            } else {
177
                $totals[$entry['endpoint']] += (int) $entry['count'];
178
            }
179
180
            $date = new DateTime($entry['date']);
181
            $dateIndex = (int) $date->diff($startObj)->format("%a");
182
            $timeline[$entry['endpoint']][$dateIndex] = (int) $entry['count'];
183
184
            $grandSum += $entry['count'];
185
        }
186
        arsort($totals);
187
188
        return [
189
            'totals' => $totals,
190
            'grandSum' => $grandSum,
191
            'dateLabels' => $dateLabels,
192
            'timeline' => $timeline,
193
        ];
194
    }
195
196
    /**
197
     * Record usage of a particular XTools tool. This is called automatically
198
     *   in base.html.twig via JavaScript so that it is done asynchronously.
199
     * @Route("/meta/usage/{tool}/{project}/{token}")
200
     * @param Request $request
201
     * @param string $tool Internal name of tool.
202
     * @param string $project Project domain such as en.wikipedia.org
203
     * @param string $token Unique token for this request, so we don't have people meddling with these statistics.
204
     * @return Response
205
     * @codeCoverageIgnore
206
     */
207
    public function recordUsageAction(Request $request, string $tool, string $project, string $token): Response
208
    {
209
        // Ready the response object.
210
        $response = new Response();
211
        $response->headers->set('Content-Type', 'application/json');
212
213
        // Validate method and token.
214
        if ('PUT' !== $request->getMethod() || !$this->isCsrfTokenValid('intention', $token)) {
215
            $response->setStatusCode(Response::HTTP_FORBIDDEN);
216
            $response->setContent(json_encode([
217
                'error' => 'This endpoint is for internal use only.',
218
            ]));
219
            return $response;
220
        }
221
222
        // Don't update counts for tools that aren't enabled
223
        $configKey = 'enable.'.ucfirst($tool);
224
        if (!$this->container->hasParameter($configKey) || !$this->container->getParameter($configKey)) {
225
            $response->setStatusCode(Response::HTTP_FORBIDDEN);
226
            $response->setContent(json_encode([
227
                'error' => 'This tool is disabled',
228
            ]));
229
            return $response;
230
        }
231
232
        /** @var Connection $conn */
233
        $conn = $this->container->get('doctrine')->getManager('default')->getConnection();
234
        $date =  date('Y-m-d');
235
236
        // Tool name needs to be lowercase.
237
        $tool = strtolower($tool);
238
239
        $sql = "INSERT INTO usage_timeline
240
                      VALUES(NULL, :date, :tool, 1)
241
                      ON DUPLICATE KEY UPDATE `count` = `count` + 1";
242
        $stmt = $conn->prepare($sql);
243
        $stmt->bindParam('date', $date);
244
        $stmt->bindParam('tool', $tool);
245
        $stmt->execute();
246
247
        // Update per-project usage, if applicable
248
        if (!$this->container->getParameter('app.single_wiki')) {
249
            $sql = "INSERT INTO usage_projects
250
                    VALUES(NULL, :tool, :project, 1)
251
                    ON DUPLICATE KEY UPDATE `count` = `count` + 1";
252
            $stmt = $conn->prepare($sql);
253
            $stmt->bindParam('tool', $tool);
254
            $stmt->bindParam('project', $project);
255
            $stmt->execute();
256
        }
257
258
        $response->setStatusCode(Response::HTTP_NO_CONTENT);
259
        $response->setContent(json_encode([]));
260
        return $response;
261
    }
262
}
263