|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* This file contains only the MetaController class. |
|
4
|
|
|
*/ |
|
5
|
|
|
|
|
6
|
|
|
namespace AppBundle\Controller; |
|
7
|
|
|
|
|
8
|
|
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; |
|
9
|
|
|
use Symfony\Bundle\FrameworkBundle\Controller\Controller; |
|
10
|
|
|
use Symfony\Component\HttpFoundation\Request; |
|
11
|
|
|
use DateTime; |
|
12
|
|
|
use Symfony\Component\HttpFoundation\Response; |
|
13
|
|
|
use Doctrine\DBAL\Connection; |
|
14
|
|
|
|
|
15
|
|
|
/** |
|
16
|
|
|
* This controller serves everything for the Meta tool. |
|
17
|
|
|
*/ |
|
18
|
|
|
class MetaController extends XtoolsController |
|
19
|
|
|
{ |
|
20
|
|
|
/** |
|
21
|
|
|
* Display the form. |
|
22
|
|
|
* @Route("/meta", name="meta") |
|
23
|
|
|
* @Route("/meta", name="Meta") |
|
24
|
|
|
* @Route("/meta/", name="MetaSlash") |
|
25
|
|
|
* @Route("/meta/index.php", name="MetaIndexPhp") |
|
26
|
|
|
* @param Request $request |
|
27
|
|
|
* @return Response |
|
28
|
|
|
*/ |
|
29
|
1 |
|
public function indexAction(Request $request) |
|
30
|
|
|
{ |
|
31
|
1 |
|
$params = $this->parseQueryParams($request); |
|
32
|
|
|
|
|
33
|
1 |
|
if (isset($params['start']) && isset($params['end'])) { |
|
34
|
|
|
return $this->redirectToRoute('MetaResult', $params); |
|
35
|
|
|
} |
|
36
|
|
|
|
|
37
|
1 |
|
return $this->render('meta/index.html.twig', [ |
|
38
|
1 |
|
'xtPage' => 'meta', |
|
39
|
|
|
'xtPageTitle' => 'tool-meta', |
|
40
|
|
|
'xtSubtitle' => 'tool-meta-desc', |
|
41
|
|
|
]); |
|
42
|
|
|
} |
|
43
|
|
|
|
|
44
|
|
|
/** |
|
45
|
|
|
* Display the results. |
|
46
|
|
|
* @Route("/meta/{start}/{end}/{legacy}", name="MetaResult") |
|
47
|
|
|
* @param string $start Start date |
|
48
|
|
|
* @param string $end End date |
|
49
|
|
|
* @param string $legacy Non-blank value indicates to show stats for legacy XTools |
|
50
|
|
|
* @return Response |
|
51
|
|
|
* @codeCoverageIgnore |
|
52
|
|
|
*/ |
|
53
|
|
|
public function resultAction($start, $end, $legacy = false) |
|
54
|
|
|
{ |
|
55
|
|
|
$db = $legacy ? 'toolsdb' : 'default'; |
|
56
|
|
|
$table = $legacy ? 's51187__metadata.xtools_timeline' : 'usage_timeline'; |
|
57
|
|
|
$client = $this->container |
|
58
|
|
|
->get('doctrine') |
|
59
|
|
|
->getManager($db) |
|
60
|
|
|
->getConnection(); |
|
61
|
|
|
|
|
62
|
|
|
$toolUsage = $this->getToolUsageStats($client, $table, $start, $end); |
|
63
|
|
|
$apiUsage = $this->getApiUsageStats($client, $start, $end); |
|
64
|
|
|
|
|
65
|
|
|
return $this->render('meta/result.html.twig', [ |
|
66
|
|
|
'xtPage' => 'meta', |
|
67
|
|
|
'start' => $start, |
|
68
|
|
|
'end' => $end, |
|
69
|
|
|
'toolUsage' => $toolUsage, |
|
70
|
|
|
'apiUsage' => $apiUsage, |
|
71
|
|
|
]); |
|
72
|
|
|
} |
|
73
|
|
|
|
|
74
|
|
|
/** |
|
75
|
|
|
* Get usage statistics of the core tools. |
|
76
|
|
|
* @param Connection $client |
|
77
|
|
|
* @param string $table Table to query. |
|
78
|
|
|
* @param string $start Start date. |
|
79
|
|
|
* @param string $end End date. |
|
80
|
|
|
* @return array |
|
81
|
|
|
*/ |
|
82
|
|
View Code Duplication |
private function getToolUsageStats(Connection $client, $table, $start, $end) |
|
|
|
|
|
|
83
|
|
|
{ |
|
84
|
|
|
$query = $client->prepare("SELECT * FROM $table |
|
85
|
|
|
WHERE date >= :start AND date <= :end"); |
|
86
|
|
|
$query->bindParam('start', $start); |
|
87
|
|
|
$query->bindParam('end', $end); |
|
88
|
|
|
$query->execute(); |
|
89
|
|
|
|
|
90
|
|
|
$data = $query->fetchAll(); |
|
91
|
|
|
|
|
92
|
|
|
// Create array of totals, along with formatted timeline data as needed by Chart.js |
|
93
|
|
|
$totals = []; |
|
94
|
|
|
$dateLabels = []; |
|
95
|
|
|
$timeline = []; |
|
96
|
|
|
$startObj = new DateTime($start); |
|
97
|
|
|
$endObj = new DateTime($end); |
|
98
|
|
|
$numDays = (int) $endObj->diff($startObj)->format("%a"); |
|
99
|
|
|
$grandSum = 0; |
|
100
|
|
|
|
|
101
|
|
|
// Generate array of date labels |
|
102
|
|
|
for ($dateObj = new DateTime($start); $dateObj <= $endObj; $dateObj->modify('+1 day')) { |
|
103
|
|
|
$dateLabels[] = $dateObj->format('Y-m-d'); |
|
104
|
|
|
} |
|
105
|
|
|
|
|
106
|
|
|
foreach ($data as $entry) { |
|
107
|
|
|
if (!isset($totals[$entry['tool']])) { |
|
108
|
|
|
$totals[$entry['tool']] = (int) $entry['count']; |
|
109
|
|
|
|
|
110
|
|
|
// Create arrays for each tool, filled with zeros for each date in the timeline |
|
111
|
|
|
$timeline[$entry['tool']] = array_fill(0, $numDays, 0); |
|
112
|
|
|
} else { |
|
113
|
|
|
$totals[$entry['tool']] += (int) $entry['count']; |
|
114
|
|
|
} |
|
115
|
|
|
|
|
116
|
|
|
$date = new DateTime($entry['date']); |
|
117
|
|
|
$dateIndex = (int) $date->diff($startObj)->format("%a"); |
|
118
|
|
|
$timeline[$entry['tool']][$dateIndex] = (int) $entry['count']; |
|
119
|
|
|
|
|
120
|
|
|
$grandSum += $entry['count']; |
|
121
|
|
|
} |
|
122
|
|
|
arsort($totals); |
|
123
|
|
|
|
|
124
|
|
|
return [ |
|
125
|
|
|
'totals' => $totals, |
|
126
|
|
|
'grandSum' => $grandSum, |
|
127
|
|
|
'dateLabels' => $dateLabels, |
|
128
|
|
|
'timeline' => $timeline, |
|
129
|
|
|
]; |
|
130
|
|
|
} |
|
131
|
|
|
|
|
132
|
|
|
/** |
|
133
|
|
|
* Get usage statistics of the API. |
|
134
|
|
|
* @param Connection $client |
|
135
|
|
|
* @param string $start Start date. |
|
136
|
|
|
* @param string $end End date. |
|
137
|
|
|
* @return array |
|
138
|
|
|
*/ |
|
139
|
|
View Code Duplication |
private function getApiUsageStats(Connection $client, $start, $end) |
|
|
|
|
|
|
140
|
|
|
{ |
|
141
|
|
|
$query = $client->prepare("SELECT * FROM usage_api_timeline |
|
142
|
|
|
WHERE date >= :start AND date <= :end"); |
|
143
|
|
|
$query->bindParam('start', $start); |
|
144
|
|
|
$query->bindParam('end', $end); |
|
145
|
|
|
$query->execute(); |
|
146
|
|
|
|
|
147
|
|
|
$data = $query->fetchAll(); |
|
148
|
|
|
|
|
149
|
|
|
// Create array of totals, along with formatted timeline data as needed by Chart.js |
|
150
|
|
|
$totals = []; |
|
151
|
|
|
$dateLabels = []; |
|
152
|
|
|
$timeline = []; |
|
153
|
|
|
$startObj = new DateTime($start); |
|
154
|
|
|
$endObj = new DateTime($end); |
|
155
|
|
|
$numDays = (int) $endObj->diff($startObj)->format("%a"); |
|
156
|
|
|
$grandSum = 0; |
|
157
|
|
|
|
|
158
|
|
|
// Generate array of date labels |
|
159
|
|
|
for ($dateObj = new DateTime($start); $dateObj <= $endObj; $dateObj->modify('+1 day')) { |
|
160
|
|
|
$dateLabels[] = $dateObj->format('Y-m-d'); |
|
161
|
|
|
} |
|
162
|
|
|
|
|
163
|
|
|
foreach ($data as $entry) { |
|
164
|
|
|
if (!isset($totals[$entry['endpoint']])) { |
|
165
|
|
|
$totals[$entry['endpoint']] = (int) $entry['count']; |
|
166
|
|
|
|
|
167
|
|
|
// Create arrays for each endpoint, filled with zeros for each date in the timeline |
|
168
|
|
|
$timeline[$entry['endpoint']] = array_fill(0, $numDays, 0); |
|
169
|
|
|
} else { |
|
170
|
|
|
$totals[$entry['endpoint']] += (int) $entry['count']; |
|
171
|
|
|
} |
|
172
|
|
|
|
|
173
|
|
|
$date = new DateTime($entry['date']); |
|
174
|
|
|
$dateIndex = (int) $date->diff($startObj)->format("%a"); |
|
175
|
|
|
$timeline[$entry['endpoint']][$dateIndex] = (int) $entry['count']; |
|
176
|
|
|
|
|
177
|
|
|
$grandSum += $entry['count']; |
|
178
|
|
|
} |
|
179
|
|
|
arsort($totals); |
|
180
|
|
|
|
|
181
|
|
|
return [ |
|
182
|
|
|
'totals' => $totals, |
|
183
|
|
|
'grandSum' => $grandSum, |
|
184
|
|
|
'dateLabels' => $dateLabels, |
|
185
|
|
|
'timeline' => $timeline, |
|
186
|
|
|
]; |
|
187
|
|
|
} |
|
188
|
|
|
|
|
189
|
|
|
/** |
|
190
|
|
|
* Record usage of a particular XTools tool. This is called automatically |
|
191
|
|
|
* in base.html.twig via JavaScript so that it is done asynchronously |
|
192
|
|
|
* @Route("/meta/usage/{tool}/{project}/{token}") |
|
193
|
|
|
* @param Request $request |
|
194
|
|
|
* @param string $tool Internal name of tool |
|
195
|
|
|
* @param string $project Project domain such as en.wikipedia.org |
|
196
|
|
|
* @param string $token Unique token for this request, so we don't have people |
|
197
|
|
|
* meddling with these statistics |
|
198
|
|
|
* @return Response |
|
199
|
|
|
* @codeCoverageIgnore |
|
200
|
|
|
*/ |
|
201
|
|
|
public function recordUsage(Request $request, $tool, $project, $token) |
|
202
|
|
|
{ |
|
203
|
|
|
// Validate method and token. |
|
204
|
|
|
if ($request->getMethod() !== 'PUT' || !$this->isCsrfTokenValid('intention', $token)) { |
|
205
|
|
|
throw $this->createAccessDeniedException('This endpoint is for internal use only.'); |
|
206
|
|
|
} |
|
207
|
|
|
|
|
208
|
|
|
// Ready the response object. |
|
209
|
|
|
$response = new Response(); |
|
210
|
|
|
$response->headers->set('Content-Type', 'application/json'); |
|
211
|
|
|
|
|
212
|
|
|
// Don't update counts for tools that aren't enabled |
|
213
|
|
|
if (!$this->container->getParameter("enable.$tool")) { |
|
214
|
|
|
$response->setStatusCode(Response::HTTP_FORBIDDEN); |
|
215
|
|
|
$response->setContent(json_encode([ |
|
216
|
|
|
'error' => 'This tool is disabled' |
|
217
|
|
|
])); |
|
218
|
|
|
return $response; |
|
219
|
|
|
} |
|
220
|
|
|
|
|
221
|
|
|
$conn = $this->container->get('doctrine')->getManager('default')->getConnection(); |
|
222
|
|
|
$date = date('Y-m-d'); |
|
223
|
|
|
|
|
224
|
|
|
// Increment count in timeline |
|
225
|
|
|
$existsSql = "SELECT 1 FROM usage_timeline |
|
226
|
|
|
WHERE date = '$date' |
|
227
|
|
|
AND tool = '$tool'"; |
|
228
|
|
|
|
|
229
|
|
View Code Duplication |
if (count($conn->query($existsSql)->fetchAll()) === 0) { |
|
|
|
|
|
|
230
|
|
|
$createSql = "INSERT INTO usage_timeline |
|
231
|
|
|
VALUES(NULL, '$date', '$tool', 1)"; |
|
232
|
|
|
$conn->query($createSql); |
|
233
|
|
|
} else { |
|
234
|
|
|
$updateSql = "UPDATE usage_timeline |
|
235
|
|
|
SET count = count + 1 |
|
236
|
|
|
WHERE tool = '$tool' |
|
237
|
|
|
AND date = '$date'"; |
|
238
|
|
|
$conn->query($updateSql); |
|
239
|
|
|
} |
|
240
|
|
|
|
|
241
|
|
|
// Update per-project usage, if applicable |
|
242
|
|
|
if (!$this->container->getParameter('app.single_wiki')) { |
|
243
|
|
|
$existsSql = "SELECT 1 FROM usage_projects |
|
244
|
|
|
WHERE tool = '$tool' |
|
245
|
|
|
AND project = '$project'"; |
|
246
|
|
|
|
|
247
|
|
View Code Duplication |
if (count($conn->query($existsSql)->fetchAll()) === 0) { |
|
|
|
|
|
|
248
|
|
|
$createSql = "INSERT INTO usage_projects |
|
249
|
|
|
VALUES(NULL, '$tool', '$project', 1)"; |
|
250
|
|
|
$conn->query($createSql); |
|
251
|
|
|
} else { |
|
252
|
|
|
$updateSql = "UPDATE usage_projects |
|
253
|
|
|
SET count = count + 1 |
|
254
|
|
|
WHERE tool = '$tool' |
|
255
|
|
|
AND project = '$project'"; |
|
256
|
|
|
$conn->query($updateSql); |
|
257
|
|
|
} |
|
258
|
|
|
} |
|
259
|
|
|
|
|
260
|
|
|
$response->setStatusCode(Response::HTTP_NO_CONTENT); |
|
261
|
|
|
$response->setContent(json_encode([])); |
|
262
|
|
|
return $response; |
|
263
|
|
|
} |
|
264
|
|
|
} |
|
265
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.