|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* This file contains only the ApiController class. |
|
4
|
|
|
*/ |
|
5
|
|
|
|
|
6
|
|
|
namespace AppBundle\Controller; |
|
7
|
|
|
|
|
8
|
|
|
use Exception; |
|
9
|
|
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; |
|
10
|
|
|
use Symfony\Bundle\FrameworkBundle\Controller\Controller; |
|
11
|
|
|
use Symfony\Component\HttpFoundation\Request; |
|
12
|
|
|
use Symfony\Component\HttpFoundation\Response; |
|
13
|
|
|
use Symfony\Component\Debug\Exception\FatalErrorException; |
|
14
|
|
|
use FOS\RestBundle\Controller\Annotations as Rest; |
|
15
|
|
|
use FOS\RestBundle\Controller\FOSRestController; |
|
16
|
|
|
use FOS\RestBundle\View\View; |
|
17
|
|
|
use Xtools\ProjectRepository; |
|
18
|
|
|
use Xtools\UserRepository; |
|
19
|
|
|
use Xtools\Page; |
|
20
|
|
|
use Xtools\PagesRepository; |
|
21
|
|
|
use Xtools\Edit; |
|
22
|
|
|
use DateTime; |
|
23
|
|
|
|
|
24
|
|
|
/** |
|
25
|
|
|
* Serves the external API of XTools. |
|
26
|
|
|
*/ |
|
27
|
|
|
class ApiController extends FOSRestController |
|
28
|
|
|
{ |
|
29
|
|
|
/** |
|
30
|
|
|
* Get domain name, URL, and API URL of the given project. |
|
31
|
|
|
* @Rest\Get("/api/project/normalize/{project}") |
|
32
|
|
|
* @param string $project Project database name, URL, or domain name. |
|
33
|
|
|
* @return View |
|
34
|
|
|
*/ |
|
35
|
|
View Code Duplication |
public function normalizeProject($project) |
|
|
|
|
|
|
36
|
|
|
{ |
|
37
|
|
|
$proj = ProjectRepository::getProject($project, $this->container); |
|
38
|
|
|
|
|
39
|
|
|
if (!$proj->exists()) { |
|
40
|
|
|
return new View( |
|
41
|
|
|
[ |
|
42
|
|
|
'error' => "$project is not a valid project", |
|
43
|
|
|
], |
|
44
|
|
|
Response::HTTP_NOT_FOUND |
|
45
|
|
|
); |
|
46
|
|
|
} |
|
47
|
|
|
|
|
48
|
|
|
return new View( |
|
49
|
|
|
[ |
|
50
|
|
|
'domain' => $proj->getDomain(), |
|
51
|
|
|
'url' => $proj->getUrl(), |
|
52
|
|
|
'api' => $proj->getApiUrl(), |
|
53
|
|
|
'database' => $proj->getDatabaseName(), |
|
54
|
|
|
], |
|
55
|
|
|
Response::HTTP_OK |
|
56
|
|
|
); |
|
57
|
|
|
} |
|
58
|
|
|
|
|
59
|
|
|
/** |
|
60
|
|
|
* Get all namespaces of the given project. This endpoint also does the same thing |
|
61
|
|
|
* as the normalize_project endpoint, returning other basic info about the project. |
|
62
|
|
|
* @Rest\Get("/api/project/namespaces/{project}") |
|
63
|
|
|
* @param string $project The project name. |
|
64
|
|
|
* @return View |
|
65
|
|
|
*/ |
|
66
|
|
View Code Duplication |
public function namespaces($project) |
|
|
|
|
|
|
67
|
|
|
{ |
|
68
|
|
|
$proj = ProjectRepository::getProject($project, $this->container); |
|
69
|
|
|
|
|
70
|
|
|
if (!$proj->exists()) { |
|
71
|
|
|
return new View( |
|
72
|
|
|
[ |
|
73
|
|
|
'error' => "$project is not a valid project", |
|
74
|
|
|
], |
|
75
|
|
|
Response::HTTP_NOT_FOUND |
|
76
|
|
|
); |
|
77
|
|
|
} |
|
78
|
|
|
|
|
79
|
|
|
return new View( |
|
80
|
|
|
[ |
|
81
|
|
|
'domain' => $proj->getDomain(), |
|
82
|
|
|
'url' => $proj->getUrl(), |
|
83
|
|
|
'api' => $proj->getApiUrl(), |
|
84
|
|
|
'database' => $proj->getDatabaseName(), |
|
85
|
|
|
'namespaces' => $proj->getNamespaces(), |
|
86
|
|
|
], |
|
87
|
|
|
Response::HTTP_OK |
|
88
|
|
|
); |
|
89
|
|
|
} |
|
90
|
|
|
|
|
91
|
|
|
/** |
|
92
|
|
|
* Count the number of automated edits the given user has made. |
|
93
|
|
|
* @Rest\Get( |
|
94
|
|
|
* "/api/user/automated_editcount/{project}/{username}/{namespace}/{start}/{end}/{tools}", |
|
95
|
|
|
* requirements={"start" = "|\d{4}-\d{2}-\d{2}", "end" = "|\d{4}-\d{2}-\d{2}"} |
|
96
|
|
|
* ) |
|
97
|
|
|
* @param Request $request The HTTP request. |
|
98
|
|
|
* @param string $project |
|
99
|
|
|
* @param string $username |
|
100
|
|
|
* @param int|string $namespace ID of the namespace, or 'all' for all namespaces |
|
101
|
|
|
* @param string $start In the format YYYY-MM-DD |
|
102
|
|
|
* @param string $end In the format YYYY-MM-DD |
|
103
|
|
|
* @param string $tools Non-blank to show which tools were used and how many times. |
|
104
|
|
|
*/ |
|
105
|
|
|
public function automatedEditCount( |
|
106
|
|
|
Request $request, |
|
|
|
|
|
|
107
|
|
|
$project, |
|
108
|
|
|
$username, |
|
109
|
|
|
$namespace = 'all', |
|
110
|
|
|
$start = '', |
|
111
|
|
|
$end = '', |
|
112
|
|
|
$tools = '' |
|
113
|
|
|
) { |
|
114
|
|
|
$project = ProjectRepository::getProject($project, $this->container); |
|
115
|
|
|
$user = UserRepository::getUser($username, $this->container); |
|
|
|
|
|
|
116
|
|
|
|
|
117
|
|
|
$res = [ |
|
118
|
|
|
'project' => $project->getDomain(), |
|
119
|
|
|
'username' => $user->getUsername(), |
|
120
|
|
|
]; |
|
121
|
|
|
|
|
122
|
|
|
if ($tools != '') { |
|
123
|
|
|
$tools = $user->getAutomatedCounts($project, $namespace, $start, $end); |
|
124
|
|
|
$res['automated_editcount'] = 0; |
|
125
|
|
|
foreach ($tools as $tool) { |
|
126
|
|
|
$res['automated_editcount'] += $tool['count']; |
|
127
|
|
|
} |
|
128
|
|
|
$res['automated_tools'] = $tools; |
|
129
|
|
|
} else { |
|
130
|
|
|
$res['automated_editcount'] = $user->countAutomatedEdits($project, $namespace, $start, $end); |
|
131
|
|
|
} |
|
132
|
|
|
|
|
133
|
|
|
$view = View::create()->setStatusCode(Response::HTTP_OK); |
|
134
|
|
|
$view->setData($res); |
|
135
|
|
|
|
|
136
|
|
|
return $view->setFormat('json'); |
|
137
|
|
|
} |
|
138
|
|
|
|
|
139
|
|
|
/** |
|
140
|
|
|
* Get non-automated edits for the given user. |
|
141
|
|
|
* @Rest\Get( |
|
142
|
|
|
* "/api/user/nonautomated_edits/{project}/{username}/{namespace}/{start}/{end}/{offset}", |
|
143
|
|
|
* requirements={"start" = "|\d{4}-\d{2}-\d{2}", "end" = "|\d{4}-\d{2}-\d{2}"} |
|
144
|
|
|
* ) |
|
145
|
|
|
* @param Request $request The HTTP request. |
|
146
|
|
|
* @param string $project |
|
147
|
|
|
* @param string $username |
|
148
|
|
|
* @param int|string $namespace ID of the namespace, or 'all' for all namespaces |
|
149
|
|
|
* @param string $start In the format YYYY-MM-DD |
|
150
|
|
|
* @param string $end In the format YYYY-MM-DD |
|
151
|
|
|
* @param int $offset For pagination, offset results by N edits |
|
152
|
|
|
* @return View |
|
153
|
|
|
*/ |
|
154
|
|
|
public function nonautomatedEdits( |
|
155
|
|
|
Request $request, |
|
156
|
|
|
$project, |
|
157
|
|
|
$username, |
|
158
|
|
|
$namespace, |
|
159
|
|
|
$start = '', |
|
160
|
|
|
$end = '', |
|
161
|
|
|
$offset = 0 |
|
162
|
|
|
) { |
|
163
|
|
|
$twig = $this->container->get('twig'); |
|
|
|
|
|
|
164
|
|
|
$project = ProjectRepository::getProject($project, $this->container); |
|
165
|
|
|
$user = UserRepository::getUser($username, $this->container); |
|
|
|
|
|
|
166
|
|
|
$data = $user->getNonautomatedEdits($project, $namespace, $start, $end, $offset); |
|
167
|
|
|
|
|
168
|
|
|
$view = View::create()->setStatusCode(Response::HTTP_OK); |
|
169
|
|
|
|
|
170
|
|
|
if ($request->query->get('format') === 'html') { |
|
171
|
|
|
$edits = array_map(function ($attrs) use ($project, $username) { |
|
172
|
|
|
$nsName = ''; |
|
173
|
|
|
if ($attrs['page_namespace']) { |
|
174
|
|
|
$nsName = $project->getNamespaces()[$attrs['page_namespace']]; |
|
175
|
|
|
} |
|
176
|
|
|
$page = $project->getRepository() |
|
177
|
|
|
->getPage($project, $nsName . ':' . $attrs['page_title']); |
|
178
|
|
|
$attrs['id'] = $attrs['rev_id']; |
|
179
|
|
|
$attrs['username'] = $username; |
|
180
|
|
|
return new Edit($page, $attrs); |
|
181
|
|
|
}, $data); |
|
182
|
|
|
|
|
183
|
|
|
$twig = $this->container->get('twig'); |
|
|
|
|
|
|
184
|
|
|
$view->setTemplate('api/nonautomated_edits.html.twig'); |
|
185
|
|
|
$view->setTemplateData([ |
|
186
|
|
|
'edits' => $edits, |
|
187
|
|
|
'project' => $project, |
|
188
|
|
|
]); |
|
189
|
|
|
$view->setFormat('html'); |
|
190
|
|
|
} else { |
|
191
|
|
|
$res = [ |
|
192
|
|
|
'project' => $project->getDomain(), |
|
193
|
|
|
'username' => $user->getUsername(), |
|
194
|
|
|
]; |
|
195
|
|
|
if ($namespace != '' && $namespace !== 'all') { |
|
196
|
|
|
$res['namespace'] = $namespace; |
|
197
|
|
|
} |
|
198
|
|
|
if ($start != '') { |
|
199
|
|
|
$res['start'] = $start; |
|
200
|
|
|
} |
|
201
|
|
|
if ($end != '') { |
|
202
|
|
|
$res['end'] = $end; |
|
203
|
|
|
} |
|
204
|
|
|
$res['offset'] = $offset; |
|
205
|
|
|
$res['nonautomated_edits'] = $data; |
|
206
|
|
|
|
|
207
|
|
|
$view->setData($res)->setFormat('json'); |
|
208
|
|
|
} |
|
209
|
|
|
|
|
210
|
|
|
return $view; |
|
211
|
|
|
} |
|
212
|
|
|
|
|
213
|
|
|
/** |
|
214
|
|
|
* Get basic info on a given article. |
|
215
|
|
|
* @Rest\Get("/api/articleinfo/{project}/{article}", requirements={"article"=".+"}) |
|
216
|
|
|
* @Rest\Get("/api/page/articleinfo/{project}/{article}", requirements={"article"=".+"}) |
|
217
|
|
|
* @param Request $request The HTTP request. |
|
218
|
|
|
* @param string $project |
|
219
|
|
|
* @param string $article |
|
220
|
|
|
* @return View |
|
221
|
|
|
*/ |
|
222
|
|
|
public function articleInfo(Request $request, $project, $article) |
|
223
|
|
|
{ |
|
224
|
|
|
/** @var integer Number of days to query for pageviews */ |
|
225
|
|
|
$pageviewsOffset = 30; |
|
226
|
|
|
|
|
227
|
|
|
$project = ProjectRepository::getProject($project, $this->container); |
|
228
|
|
|
if (!$project->exists()) { |
|
229
|
|
|
return new View( |
|
230
|
|
|
['error' => "$project is not a valid project"], |
|
231
|
|
|
Response::HTTP_NOT_FOUND |
|
232
|
|
|
); |
|
233
|
|
|
} |
|
234
|
|
|
|
|
235
|
|
|
$page = new Page($project, $article); |
|
236
|
|
|
$pageRepo = new PagesRepository(); |
|
237
|
|
|
$pageRepo->setContainer($this->container); |
|
|
|
|
|
|
238
|
|
|
$page->setRepository($pageRepo); |
|
239
|
|
|
|
|
240
|
|
|
if (!$page->exists()) { |
|
241
|
|
|
return new View( |
|
242
|
|
|
['error' => "$article was not found"], |
|
243
|
|
|
Response::HTTP_NOT_FOUND |
|
244
|
|
|
); |
|
245
|
|
|
} |
|
246
|
|
|
|
|
247
|
|
|
$info = $page->getBasicEditingInfo(); |
|
248
|
|
|
$creationDateTime = DateTime::createFromFormat('YmdHis', $info['created_at']); |
|
249
|
|
|
$modifiedDateTime = DateTime::createFromFormat('YmdHis', $info['modified_at']); |
|
250
|
|
|
$secsSinceLastEdit = (new DateTime)->getTimestamp() - $modifiedDateTime->getTimestamp(); |
|
251
|
|
|
|
|
252
|
|
|
$data = [ |
|
253
|
|
|
'revisions' => (int) $info['num_edits'], |
|
254
|
|
|
'editors' => (int) $info['num_editors'], |
|
255
|
|
|
'author' => $info['author'], |
|
256
|
|
|
'author_editcount' => (int) $info['author_editcount'], |
|
257
|
|
|
'created_at' => $creationDateTime->format('Y-m-d'), |
|
258
|
|
|
'created_rev_id' => $info['created_rev_id'], |
|
259
|
|
|
'modified_at' => $modifiedDateTime->format('Y-m-d H:i'), |
|
260
|
|
|
'secs_since_last_edit' => $secsSinceLastEdit, |
|
261
|
|
|
'last_edit_id' => (int) $info['modified_rev_id'], |
|
262
|
|
|
'watchers' => (int) $page->getWatchers(), |
|
263
|
|
|
'pageviews' => $page->getLastPageviews($pageviewsOffset), |
|
264
|
|
|
'pageviews_offset' => $pageviewsOffset, |
|
265
|
|
|
]; |
|
266
|
|
|
|
|
267
|
|
|
$view = View::create()->setStatusCode(Response::HTTP_OK); |
|
268
|
|
|
|
|
269
|
|
|
if ($request->query->get('format') === 'html') { |
|
270
|
|
|
$twig = $this->container->get('twig'); |
|
|
|
|
|
|
271
|
|
|
$view->setTemplate('api/articleinfo.html.twig'); |
|
272
|
|
|
$view->setTemplateData([ |
|
273
|
|
|
'data' => $data, |
|
274
|
|
|
'project' => $project, |
|
275
|
|
|
'page' => $page, |
|
276
|
|
|
]); |
|
277
|
|
|
$view->setFormat('html'); |
|
278
|
|
|
} else { |
|
279
|
|
|
$res = array_merge([ |
|
280
|
|
|
'project' => $project->getDomain(), |
|
281
|
|
|
'page' => $page->getTitle(), |
|
282
|
|
|
], $data); |
|
283
|
|
|
$view->setData($res)->setFormat('json'); |
|
284
|
|
|
} |
|
285
|
|
|
|
|
286
|
|
|
return $view; |
|
287
|
|
|
} |
|
288
|
|
|
|
|
289
|
|
|
/** |
|
290
|
|
|
* Record usage of a particular XTools tool. This is called automatically |
|
291
|
|
|
* in base.html.twig via JavaScript so that it is done asynchronously |
|
292
|
|
|
* @Rest\Put("/api/usage/{tool}/{project}/{token}") |
|
293
|
|
|
* @param string $tool Internal name of tool |
|
294
|
|
|
* @param string $project Project domain such as en.wikipedia.org |
|
295
|
|
|
* @param string $token Unique token for this request, so we don't have people |
|
296
|
|
|
* meddling with these statistics |
|
297
|
|
|
* @return View |
|
298
|
|
|
*/ |
|
299
|
|
|
public function recordUsage($tool, $project, $token) |
|
300
|
|
|
{ |
|
301
|
|
|
// Validate token |
|
302
|
|
|
if (!$this->isCsrfTokenValid('intention', $token)) { |
|
303
|
|
|
return new View( |
|
304
|
|
|
[], |
|
305
|
|
|
Response::HTTP_FORBIDDEN |
|
306
|
|
|
); |
|
307
|
|
|
} |
|
308
|
|
|
|
|
309
|
|
|
// Don't update counts for tools that aren't enabled |
|
310
|
|
|
if (!$this->container->getParameter("enable.$tool")) { |
|
311
|
|
|
return new View( |
|
312
|
|
|
[ |
|
313
|
|
|
'error' => 'This tool is disabled' |
|
314
|
|
|
], |
|
315
|
|
|
Response::HTTP_FORBIDDEN |
|
316
|
|
|
); |
|
317
|
|
|
} |
|
318
|
|
|
|
|
319
|
|
|
$conn = $this->getDoctrine()->getManager('default')->getConnection(); |
|
|
|
|
|
|
320
|
|
|
$date = date('Y-m-d'); |
|
321
|
|
|
|
|
322
|
|
|
// Increment count in timeline |
|
323
|
|
|
$existsSql = "SELECT 1 FROM usage_timeline |
|
324
|
|
|
WHERE date = '$date' |
|
325
|
|
|
AND tool = '$tool'"; |
|
326
|
|
|
|
|
327
|
|
View Code Duplication |
if (count($conn->query($existsSql)->fetchAll()) === 0) { |
|
|
|
|
|
|
328
|
|
|
$createSql = "INSERT INTO usage_timeline |
|
329
|
|
|
VALUES(NULL, '$date', '$tool', 1)"; |
|
330
|
|
|
$conn->query($createSql); |
|
331
|
|
|
} else { |
|
332
|
|
|
$updateSql = "UPDATE usage_timeline |
|
333
|
|
|
SET count = count + 1 |
|
334
|
|
|
WHERE tool = '$tool' |
|
335
|
|
|
AND date = '$date'"; |
|
336
|
|
|
$conn->query($updateSql); |
|
337
|
|
|
} |
|
338
|
|
|
|
|
339
|
|
|
// Update per-project usage, if applicable |
|
340
|
|
|
if (!$this->container->getParameter('app.single_wiki')) { |
|
341
|
|
|
$existsSql = "SELECT 1 FROM usage_projects |
|
342
|
|
|
WHERE tool = '$tool' |
|
343
|
|
|
AND project = '$project'"; |
|
344
|
|
|
|
|
345
|
|
View Code Duplication |
if (count($conn->query($existsSql)->fetchAll()) === 0) { |
|
|
|
|
|
|
346
|
|
|
$createSql = "INSERT INTO usage_projects |
|
347
|
|
|
VALUES(NULL, '$tool', '$project', 1)"; |
|
348
|
|
|
$conn->query($createSql); |
|
349
|
|
|
} else { |
|
350
|
|
|
$updateSql = "UPDATE usage_projects |
|
351
|
|
|
SET count = count + 1 |
|
352
|
|
|
WHERE tool = '$tool' |
|
353
|
|
|
AND project = '$project'"; |
|
354
|
|
|
$conn->query($updateSql); |
|
355
|
|
|
} |
|
356
|
|
|
} |
|
357
|
|
|
|
|
358
|
|
|
return new View( |
|
359
|
|
|
[], |
|
360
|
|
|
Response::HTTP_NO_CONTENT |
|
361
|
|
|
); |
|
362
|
|
|
} |
|
363
|
|
|
} |
|
364
|
|
|
|
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.