Passed
Push — master ( e6ce91...429907 )
by MusikAnimal
04:46
created

MetaController   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 255
Duplicated Lines 0 %

Test Coverage

Coverage 80%

Importance

Changes 0
Metric Value
eloc 128
dl 0
loc 255
ccs 4
cts 5
cp 0.8
rs 10
c 0
b 0
f 0
wmc 22

6 Methods

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

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

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