Passed
Push — master ( bb3b01...ed8c75 )
by MusikAnimal
06:02
created

XtoolsController::getParams()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 39
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 25
nc 3
nop 1
dl 0
loc 39
ccs 8
cts 8
cp 1
crap 5
rs 8.439
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file contains the abstract XtoolsController,
4
 * which all other controllers will extend.
5
 */
6
7
namespace AppBundle\Controller;
8
9
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
10
use Symfony\Component\HttpFoundation\Request;
11
use Symfony\Component\HttpFoundation\RedirectResponse;
12
use Xtools\ProjectRepository;
13
use Xtools\UserRepository;
14
use Xtools\Project;
15
use Xtools\Page;
16
use Xtools\PageRepository;
17
18
/**
19
 * XtoolsController supplies a variety of methods around parsing and validing
20
 * parameters, and initializing Project/User instances. These are used in
21
 * other controllers in the AppBundle\Controller namespace.
22
 * @abstract
23
 */
24
abstract class XtoolsController extends Controller
25
{
26
    /**
27
     * Given the request object, parse out common parameters. These include the
28
     * 'project', 'username', 'namespace' and 'article', along with their legacy
29
     * counterparts (e.g. 'lang' and 'wiki').
30
     * @param  Request $request
31
     * @return string[] Normalized parameters (no legacy params).
32
     */
33 9
    public function parseQueryParams(Request $request)
34
    {
35
        /** @var string[] Each parameter and value that was detected. */
36 9
        $params = $this->getParams($request);
37
38
        // Covert any legacy parameters, if present.
39 9
        $params = $this->convertLegacyParams($params);
40
41
        // Remove blank values.
42 9
        return array_filter($params, function ($param) {
43
            // 'namespace' or 'username' could be '0'.
44 2
            return $param !== null && $param !== '';
45 9
        });
46
    }
47
48
    /**
49
     * Get a Project instance from the project string, using defaults if the
50
     * given project string is invalid.
51
     * @param  string[] $params Query params.
52
     * @return Project
53
     */
54 7
    public function getProjectFromQuery($params)
55
    {
56
        // Set default project so we can populate the namespace selector
57
        // on index pages.
58 7
        if (empty($params['project'])) {
59 5
            $project = $this->container->getParameter('default_project');
60
        } else {
61 2
            $project = $params['project'];
62
        }
63
64 7
        $projectData = ProjectRepository::getProject($project, $this->container);
65
66
        // Revert back to defaults if we've established the given project was invalid.
67 7
        if (!$projectData->exists()) {
68
            $projectData = ProjectRepository::getProject(
69
                $this->container->getParameter('default_project'),
70
                $this->container
71
            );
72
        }
73
74 7
        return $projectData;
75
    }
76
77
    /**
78
     * If the project and username in the given params hash are valid, Project and User instances
79
     * are returned. User validation only occurs if 'username' is in the params.
80
     * Otherwise a redirect is returned that goes back to the index page.
81
     * @param Request $request The HTTP request.
82
     * @param string $tooHighEditCountAction If the requested user has more than the configured
83
     *   max edit count, they will be redirect to this route, passing in available params.
84
     * @return RedirectResponse|array Array contains [Project|null, User|null]
85
     */
86
    public function validateProjectAndUser(Request $request, $tooHighEditCountAction = null)
87
    {
88
        $params = $this->getParams($request);
89
90
        $projectData = $this->validateProject($params);
91
        if ($projectData instanceof RedirectResponse) {
0 ignored issues
show
introduced by
The condition $projectData instanceof ...dation\RedirectResponse can never be true since $projectData is never a sub-type of Symfony\Component\HttpFoundation\RedirectResponse.
Loading history...
92
            return $projectData;
93
        }
94
95
        $userData = null;
96
97
        if (isset($params['username'])) {
98
            $userData = $this->validateUser($params, $projectData, $tooHighEditCountAction);
99
            if ($userData instanceof RedirectResponse) {
0 ignored issues
show
introduced by
The condition $userData instanceof Sym...dation\RedirectResponse can never be false since $userData is always a sub-type of Symfony\Component\HttpFoundation\RedirectResponse.
Loading history...
100
                return $userData;
101
            }
102
        }
103
104
        return [$projectData, $userData];
105
    }
106
107
    /**
108
     * Validate the given project, returning a Project if it is valid or false otherwise.
109
     * @param string|string[] $params Project domain or database name, or params hash as
110
     *   retrieved by self::getParams().
111
     * @return Project|false
112
     */
113
    public function validateProject($params)
114
    {
115
        if (is_string($params)) {
116
            $params = ['project' => $params];
117
        }
118
119
        $projectData = ProjectRepository::getProject($params['project'], $this->container);
120
121
        if (!$projectData->exists()) {
122
            $this->addFlash('danger', ['invalid-project', $params['project']]);
0 ignored issues
show
Bug introduced by
array('invalid-project', $params['project']) of type array<integer,mixed|string> is incompatible with the type string expected by parameter $message of Symfony\Bundle\Framework...\Controller::addFlash(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

122
            $this->addFlash('danger', /** @scrutinizer ignore-type */ ['invalid-project', $params['project']]);
Loading history...
123
            unset($params['project']); // Remove invalid parameter.
124
            return $this->redirectToRoute($this->getToolShortname(), $params);
0 ignored issues
show
Bug introduced by
The method getToolShortname() does not exist on AppBundle\Controller\XtoolsController. It seems like you code against a sub-type of said class. However, the method does not exist in AppBundle\Controller\DefaultController or AppBundle\Controller\MetaController. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

124
            return $this->redirectToRoute($this->/** @scrutinizer ignore-call */ getToolShortname(), $params);
Loading history...
125
        }
126
127
        return $projectData;
128
    }
129
130
    /**
131
     * Validate the given user, returning a User or Redirect if they don't exist.
132
     * @param string|string[] $params Username or params hash as retrieved by self::getParams().
133
     * @param Project $project Project to get check against.
134
     * @param string $tooHighEditCountAction If the requested user has more than the configured
135
     *   max edit count, they will be redirect to this route, passing in available params.
136
     * @return RedirectResponse|User
0 ignored issues
show
Bug introduced by
The type AppBundle\Controller\User was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
137
     */
138
    public function validateUser($params, Project $project, $tooHighEditCountAction = null)
139
    {
140
        if (is_string($params)) {
141
            $params = ['username' => $params];
142
        }
143
144
        $userData = UserRepository::getUser($params['username'], $this->container);
145
146
        // Don't continue if the user doesn't exist.
147
        if (!$userData->existsOnProject($project)) {
148
            $this->addFlash('danger', 'user-not-found');
149
            unset($params['username']);
150
            return $this->redirectToRoute($this->getToolShortname(), $params);
151
        }
152
153
        // Reject users with a crazy high edit count.
154
        if ($tooHighEditCountAction && $userData->hasTooManyEdits($project)) {
155
            $this->addFlash('danger', ['too-many-edits', number_format($userData->maxEdits())]);
0 ignored issues
show
Bug introduced by
array('too-many-edits', ...$userData->maxEdits())) of type array<integer,string> is incompatible with the type string expected by parameter $message of Symfony\Bundle\Framework...\Controller::addFlash(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

155
            $this->addFlash('danger', /** @scrutinizer ignore-type */ ['too-many-edits', number_format($userData->maxEdits())]);
Loading history...
156
157
            // If redirecting to a different controller, show an informative message accordingly.
158
            if ($tooHighEditCountAction !== $this->getToolShortname()) {
159
                // FIXME: This is currently only done for Edit Counter, redirecting to Simple Edit Counter,
160
                // so this bit is hardcoded. We need to instead give the i18n key of the route.
161
                $this->addFlash('info', ['too-many-edits-redir', 'Simple Counter']);
162
            } else {
163
                // Redirecting back to index, so remove username (otherwise we'd get a redirect loop).
164
                unset($params['username']);
165
            }
166
167
            return $this->redirectToRoute($tooHighEditCountAction, $params);
168
        }
169
170
        return $userData;
171
    }
172
173
    /**
174
     * Get a Page instance from the given page title, and validate that it exists.
175
     * @param  Project $project
176
     * @param  string $pageTitle
177
     * @return Page|RedirectResponse Page or redirect back to index if page doesn't exist.
178
     */
179
    public function getAndValidatePage($project, $pageTitle)
180
    {
181
        $page = new Page($project, $pageTitle);
182
        $pageRepo = new PageRepository();
183
        $pageRepo->setContainer($this->container);
184
        $page->setRepository($pageRepo);
185
186
        if (!$page->exists()) {
187
            // Redirect if the page doesn't exist.
188
            $this->addFlash('notice', ['no-result', $pageTitle]);
0 ignored issues
show
Bug introduced by
array('no-result', $pageTitle) of type array<integer,string> is incompatible with the type string expected by parameter $message of Symfony\Bundle\Framework...\Controller::addFlash(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

188
            $this->addFlash('notice', /** @scrutinizer ignore-type */ ['no-result', $pageTitle]);
Loading history...
189
            return $this->redirectToRoute($this->getToolShortname());
190
        }
191
192
        return $page;
193
    }
194
195
    /**
196
     * Get all standardized parameters from the Request, either via URL query string or routing.
197
     * @param Request $request
198
     * @return string[]
199
     */
200 10
    public function getParams(Request $request)
201
    {
202
        $paramsToCheck = [
203 10
            'project',
204
            'username',
205
            'namespace',
206
            'article',
207
            'redirects',
208
            'deleted',
209
            'start',
210
            'end',
211
            'offset',
212
            'format',
213
214
            // Legacy parameters.
215
            'user',
216
            'name',
217
            'page',
218
            'wiki',
219
            'wikifam',
220
            'lang',
221
            'wikilang',
222
            'begin',
223
        ];
224
225
        /** @var string[] Each parameter that was detected along with its value. */
226 10
        $params = [];
227
228 10
        foreach ($paramsToCheck as $param) {
229
            // Pull in either from URL query string or route.
230 10
            $value = $request->query->get($param) ?: $request->get($param);
231
232
            // Only store if value is given ('namespace' or 'username' could be '0').
233 10
            if ($value !== null && $value !== '') {
234 10
                $params[$param] = rawurldecode($value);
235
            }
236
        }
237
238 10
        return $params;
239
    }
240
241
    /**
242
     * Get UTC timestamps from given start and end string parameters.
243
     * This also makes $start on month before $end if not present,
244
     * and makes $end the current time if not present.
245
     * @param  string $start
246
     * @param  string $end
247
     * @param  bool   $useDefaults Whether to use defaults if the values
248
     *   are blank. The start date is set to one month before the end date,
249
     *   and the end date is set to the present.
250
     * @return mixed[] Start and end date as UTC timestamps or 'false' if empty.
251
     */
252 1
    public function getUTCFromDateParams($start, $end, $useDefaults = true)
253
    {
254 1
        $start = strtotime($start);
255 1
        $end = strtotime($end);
256
257
        // Use current time if end is not present (and is required),
258
        // or if it exceeds the current time.
259 1
        if (($useDefaults && $end === false) || $end > time()) {
260
            $end = time();
261
        }
262
263
        // Default to one month before end time if start is not present,
264
        // as is not optional.
265 1
        if ($useDefaults && $start === false) {
0 ignored issues
show
introduced by
The condition $useDefaults && $start === false can never be true.
Loading history...
266 1
            $start = strtotime('-1 month', $end);
267
        }
268
269
        // Reverse if start date is after end date.
270 1
        if ($start > $end && $start !== false && $end !== false) {
271 1
            $newEnd = $start;
272 1
            $start = $end;
273 1
            $end = $newEnd;
274
        }
275
276 1
        return [$start, $end];
277
    }
278
279
    /**
280
     * Given the params hash, normalize any legacy parameters to thier modern equivalent.
281
     * @param  string[] $params
282
     * @return string[]
283
     */
284 9
    private function convertLegacyParams($params)
285
    {
286
        $paramMap = [
287 9
            'user' => 'username',
288
            'name' => 'username',
289
            'page' => 'article',
290
            'begin' => 'start',
291
292
            // Copy super legacy project params to legacy so we can concatenate below.
293
            'wikifam' => 'wiki',
294
            'wikilang' => 'lang',
295
        ];
296
297
        // Copy legacy parameters to modern equivalent.
298 9
        foreach ($paramMap as $legacy => $modern) {
299 9
            if (isset($params[$legacy])) {
300
                $params[$modern] = $params[$legacy];
301 9
                unset($params[$legacy]);
302
            }
303
        }
304
305
        // Separate parameters for language and wiki.
306 9
        if (isset($params['wiki']) && isset($params['lang'])) {
307
            // 'wikifam' will be like '.wikipedia.org', vs just 'wikipedia',
308
            // so we must remove leading periods and trailing .org's.
309
            $params['project'] = rtrim(ltrim($params['wiki'], '.'), '.org').'.org';
310
311
            /** @var string[] Projects for which there is no specific language association. */
312
            $languagelessProjects = $this->container->getParameter('languageless_wikis');
313
314
            // Prepend language if applicable.
315
            if (isset($params['lang']) && !in_array($params['wiki'], $languagelessProjects)) {
316
                $params['project'] = $params['lang'].'.'.$params['project'];
317
            }
318
319
            unset($params['wiki']);
320
            unset($params['lang']);
321
        }
322
323 9
        return $params;
324
    }
325
326
    /**
327
     * Record usage of an API endpoint.
328
     * @param string $endpoint
329
     * @codeCoverageIgnore
330
     */
331
    public function recordApiUsage($endpoint)
332
    {
333
        $conn = $this->container->get('doctrine')
334
            ->getManager('default')
335
            ->getConnection();
336
        $date =  date('Y-m-d');
337
338
        // Increment count in timeline
339
        $existsSql = "SELECT 1 FROM usage_api_timeline
340
                      WHERE date = '$date'
341
                      AND endpoint = '$endpoint'";
342
343
        if (count($conn->query($existsSql)->fetchAll()) === 0) {
344
            $createSql = "INSERT INTO usage_api_timeline
345
                          VALUES(NULL, '$date', '$endpoint', 1)";
346
            $conn->query($createSql);
347
        } else {
348
            $updateSql = "UPDATE usage_api_timeline
349
                          SET count = count + 1
350
                          WHERE endpoint = '$endpoint'
351
                          AND date = '$date'";
352
            $conn->query($updateSql);
353
        }
354
    }
355
356
    /**
357
     * Get the rendered template for the requested format.
358
     * @param  Request $request
359
     * @param  string  $templatePath Path to template without format,
360
     *   such as '/editCounter/latest_global'.
361
     * @param  array   $ret Data that should be passed to the view.
362
     * @return array
363
     * @codeCoverageIgnore
364
     */
365
    public function getFormattedReponse(Request $request, $templatePath, $ret)
366
    {
367
        $format = $request->query->get('format', 'html');
368
        if ($format == '') {
369
            // The default above doesn't work when the 'format' parameter is blank.
370
            $format = 'html';
371
        }
372
373
        $formatMap = [
374
            'wikitext' => 'text/plain',
375
            'csv' => 'text/csv',
376
            'json' => 'application/json',
377
        ];
378
379
        $response = $this->render("$templatePath.$format.twig", $ret);
380
381
        $contentType = isset($formatMap[$format]) ? $formatMap[$format] : 'text/html';
382
        $response->headers->set('Content-Type', $contentType);
383
384
        return $response;
385
    }
386
}
387