Passed
Push — master ( f5769e...cca843 )
by MusikAnimal
10:28
created

automatedEditsApiAction()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 9
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 17
rs 9.9666
ccs 0
cts 0
cp 0
crap 6
1
<?php
2
/**
3
 * This file contains only the AutomatedEditsController class.
4
 */
5
6
declare(strict_types=1);
7
8
namespace AppBundle\Controller;
9
10
use AppBundle\Helper\I18nHelper;
11
use AppBundle\Model\AutoEdits;
12
use AppBundle\Repository\AutoEditsRepository;
13
use DateTime;
14
use Symfony\Component\DependencyInjection\ContainerInterface;
15
use Symfony\Component\HttpFoundation\JsonResponse;
16
use Symfony\Component\HttpFoundation\RedirectResponse;
17
use Symfony\Component\HttpFoundation\RequestStack;
18
use Symfony\Component\HttpFoundation\Response;
19
use Symfony\Component\Routing\Annotation\Route;
20
21
/**
22
 * This controller serves the AutomatedEdits tool.
23
 */
24
class AutomatedEditsController extends XtoolsController
25
{
26
    /** @var AutoEdits The AutoEdits instance. */
27
    protected $autoEdits;
28
29
    /** @var array Data that is passed to the view. */
30
    private $output;
31
32
    /**
33
     * Get the name of the tool's index route.
34
     * This is also the name of the associated model.
35
     * @return string
36
     * @codeCoverageIgnore
37
     */
38
    public function getIndexRoute(): string
39
    {
40
        return 'AutoEdits';
41
    }
42
43
    /**
44
     * AutomatedEditsController constructor.
45
     * @param RequestStack $requestStack
46
     * @param ContainerInterface $container
47
     * @param I18nHelper $i18n
48
     */
49 1
    public function __construct(RequestStack $requestStack, ContainerInterface $container, I18nHelper $i18n)
50
    {
51
        // This will cause the tool to redirect back to the index page, with an error,
52
        // if the user has too high of an edit count.
53 1
        $this->tooHighEditCountAction = $this->getIndexRoute();
54
55 1
        parent::__construct($requestStack, $container, $i18n);
56 1
    }
57
58
    /**
59
     * Display the search form.
60
     * @Route("/autoedits", name="AutoEdits")
61
     * @Route("/automatededits", name="AutoEditsLong")
62
     * @Route("/autoedits/index.php", name="AutoEditsIndexPhp")
63
     * @Route("/automatededits/index.php", name="AutoEditsLongIndexPhp")
64
     * @Route("/autoedits/{project}", name="AutoEditsProject")
65
     * @return Response
66
     */
67 1
    public function indexAction(): Response
68
    {
69
        // Redirect if at minimum project and username are provided.
70 1
        if (isset($this->params['project']) && isset($this->params['username'])) {
71
            // If 'tool' param is given, redirect to corresponding action.
72
            $tool = $this->request->query->get('tool');
73
74
            if ('all' === $tool) {
75
                unset($this->params['tool']);
76
                return $this->redirectToRoute('AutoEditsContributionsResult', $this->params);
77
            } elseif ('' != $tool && 'none' !== $tool) {
78
                $this->params['tool'] = $tool;
79
                return $this->redirectToRoute('AutoEditsContributionsResult', $this->params);
80
            } elseif ('none' === $tool) {
81
                unset($this->params['tool']);
82
            }
83
84
            // Otherwise redirect to the normal result action.
85
            return $this->redirectToRoute('AutoEditsResult', $this->params);
86
        }
87
88 1
        return $this->render('autoEdits/index.html.twig', array_merge([
89 1
            'xtPageTitle' => 'tool-autoedits',
90
            'xtSubtitle' => 'tool-autoedits-desc',
91
            'xtPage' => 'AutoEdits',
92
93
            // Defaults that will get overridden if in $this->params.
94
            'username' => '',
95
            'namespace' => 0,
96
            'start' => '',
97
            'end' => '',
98 1
        ], $this->params, ['project' => $this->project]));
99
    }
100
101
    /**
102
     * Set defaults, and instantiate the AutoEdits model. This is called at the top of every view action.
103
     * @codeCoverageIgnore
104
     */
105
    private function setupAutoEdits(): void
106
    {
107
        $tool = $this->request->query->get('tool', null);
108
        $useSandbox = (bool)$this->request->query->get('usesandbox', false);
109
110
        if ($useSandbox && !$this->request->getSession()->get('logged_in_user')) {
111
            $this->addFlashMessage('danger', 'auto-edits-logged-out');
112
            $useSandbox = false;
113
        }
114
115
        $autoEditsRepo = new AutoEditsRepository($useSandbox);
116
        $autoEditsRepo->setContainer($this->container);
117
118
        $misconfigured = $autoEditsRepo->getInvalidTools($this->project);
119
        $helpLink = "https://w.wiki/ppr";
120
        foreach ($misconfigured as $tool) {
121
            $this->addFlashMessage('warning', 'auto-edits-misconfiguration', [$tool, $helpLink]);
122
        }
123
124
        // Validate tool.
125
        // FIXME: instead of redirecting to index page, show result page listing all tools for that project,
126
        //  clickable to show edits by the user, etc.
127
        if ($tool && !isset($autoEditsRepo->getTools($this->project)[$tool])) {
128
            $this->throwXtoolsException(
129
                $this->getIndexRoute(),
130
                'auto-edits-unknown-tool',
131
                [$tool],
132
                'tool'
133
            );
134
        }
135
136
        $this->autoEdits = new AutoEdits(
137
            $this->project,
138
            $this->user,
139
            $this->namespace,
140
            $this->start,
141
            $this->end,
142
            $tool,
143
            $this->offset,
144
            $this->limit
145
        );
146
        $this->autoEdits->setRepository($autoEditsRepo);
147
148
        $this->output = [
149
            'xtPage' => 'AutoEdits',
150
            'xtTitle' => $this->user->getUsername(),
151
            'ae' => $this->autoEdits,
152
            'is_sub_request' => $this->isSubRequest,
153
        ];
154
    }
155
156
    /**
157
     * Display the results.
158
     * @Route(
159
     *     "/autoedits/{project}/{username}/{namespace}/{start}/{end}", name="AutoEditsResult",
160
     *     requirements={
161
     *         "namespace"="|all|\d+",
162
     *         "start"="|\d{4}-\d{2}-\d{2}",
163
     *         "end"="|\d{4}-\d{2}-\d{2}",
164
     *     },
165
     *     defaults={"namespace"=0, "start"=false, "end"=false}
166
     * )
167
     * @return Response
168
     * @codeCoverageIgnore
169
     */
170
    public function resultAction(): Response
171
    {
172
        // Will redirect back to index if the user has too high of an edit count.
173
        $this->setupAutoEdits();
174
175
        if (in_array('bot', $this->user->getUserRights($this->project))) {
176
            $this->addFlashMessage('warning', 'auto-edits-bot');
177
        }
178
179
        return $this->getFormattedResponse('autoEdits/result', $this->output);
180
    }
181
182
    /**
183
     * Get non-automated edits for the given user.
184
     * @Route(
185
     *   "/nonautoedits-contributions/{project}/{username}/{namespace}/{start}/{end}/{offset}",
186
     *   name="NonAutoEditsContributionsResult",
187
     *   requirements={
188
     *       "namespace"="|all|\d+",
189
     *       "start"="|\d{4}-\d{2}-\d{2}",
190
     *       "end"="|\d{4}-\d{2}-\d{2}",
191
     *       "offset"="|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}",
192
     *   },
193
     *   defaults={"namespace"=0, "start"=false, "end"=false, "offset"=false}
194
     * )
195
     * @return Response|RedirectResponse
196
     * @codeCoverageIgnore
197
     */
198
    public function nonAutomatedEditsAction(): Response
199
    {
200
        $this->setupAutoEdits();
201
202
        return $this->getFormattedResponse('autoEdits/nonautomated_edits', $this->output);
203
    }
204
205
    /**
206
     * Get automated edits for the given user using the given tool.
207
     * @Route(
208
     *   "/autoedits-contributions/{project}/{username}/{namespace}/{start}/{end}/{offset}",
209
     *   name="AutoEditsContributionsResult",
210
     *   requirements={
211
     *       "namespace"="|all|\d+",
212
     *       "start"="|\d{4}-\d{2}-\d{2}",
213
     *       "end"="|\d{4}-\d{2}-\d{2}",
214
     *       "offset"="|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}",
215
     *   },
216
     *   defaults={"namespace"=0, "start"=false, "end"=false, "offset"=false}
217
     * )
218
     * @return Response
219
     * @codeCoverageIgnore
220
     */
221
    public function automatedEditsAction(): Response
222
    {
223
        $this->setupAutoEdits();
224
225
        return $this->getFormattedResponse('autoEdits/automated_edits', $this->output);
226
    }
227
228
    /************************ API endpoints ************************/
229
230
    /**
231
     * Get a list of the automated tools and their regex/tags/etc.
232
     * @Route("/api/user/automated_tools/{project}", name="UserApiAutoEditsTools")
233
     * @Route("/api/project/automated_tools/{project}", name="ProjectApiAutoEditsTools")
234
     * @return JsonResponse
235
     * @codeCoverageIgnore
236
     */
237
    public function automatedToolsApiAction(): JsonResponse
238
    {
239
        $this->recordApiUsage('user/automated_tools');
240
241
        $aeh = $this->container->get('app.automated_edits_helper');
242
        return $this->getFormattedApiResponse($aeh->getTools($this->project));
243
    }
244
245
    /**
246
     * Count the number of automated edits the given user has made.
247
     * @Route(
248
     *   "/api/user/automated_editcount/{project}/{username}/{namespace}/{start}/{end}/{tools}",
249
     *   name="UserApiAutoEditsCount",
250
     *   requirements={
251
     *       "namespace"="|all|\d+",
252
     *       "start"="|\d{4}-\d{2}-\d{2}",
253
     *       "end"="|\d{4}-\d{2}-\d{2}"
254
     *   },
255
     *   defaults={"namespace"="all", "start"=false, "end"=false, "tools"=false}
256
     * )
257
     * @return JsonResponse
258
     * @codeCoverageIgnore
259
     */
260
    public function automatedEditCountApiAction(): JsonResponse
261
    {
262
        $this->recordApiUsage('user/automated_editcount');
263
264
        $this->setupAutoEdits();
265
266
        $ret = [
267
            'total_editcount' => $this->autoEdits->getEditCount(),
268
            'automated_editcount' => $this->autoEdits->getAutomatedCount(),
269
        ];
270
        $ret['nonautomated_editcount'] = $ret['total_editcount'] - $ret['automated_editcount'];
271
272
        if (isset($this->params['tools'])) {
273
            $tools = $this->autoEdits->getToolCounts();
274
            $ret['automated_tools'] = $tools;
275
        }
276
277
        return $this->getFormattedApiResponse($ret);
278
    }
279
280
    /**
281
     * Get non-automated edits for the given user.
282
     * @Route(
283
     *   "/api/user/nonautomated_edits/{project}/{username}/{namespace}/{start}/{end}/{offset}",
284
     *   name="UserApiNonAutoEdits",
285
     *   requirements={
286
     *       "namespace"="|all|\d+",
287
     *       "start"="|\d{4}-\d{2}-\d{2}",
288
     *       "end"="|\d{4}-\d{2}-\d{2}",
289
     *       "offset"="|\d{4}-?\d{2}-?\d{2}T?\d{2}:?\d{2}:?\d{2}"
290
     *   },
291
     *   defaults={"namespace"=0, "start"=false, "end"=false, "offset"=false, "limit"=50}
292
     * )
293
     * @return JsonResponse
294
     * @codeCoverageIgnore
295
     */
296
    public function nonAutomatedEditsApiAction(): JsonResponse
297
    {
298
        $this->recordApiUsage('user/nonautomated_edits');
299
300
        $this->setupAutoEdits();
301
302
        $out = $this->addFullPageTitlesAndContinue(
303
            'nonautomated_edits',
304
            [],
305
            $this->autoEdits->getNonAutomatedEdits(true)
306
        );
307
308
        return $this->getFormattedApiResponse($out);
309
    }
310
311
    /**
312
     * Get (semi-)automated edits for the given user, optionally using the given tool.
313
     * @Route(
314
     *   "/api/user/automated_edits/{project}/{username}/{namespace}/{start}/{end}/{offset}",
315
     *   name="UserApiAutoEdits",
316
     *   requirements={
317
     *       "namespace"="|all|\d+",
318
     *       "start"="|\d{4}-\d{2}-\d{2}",
319
     *       "end"="|\d{4}-\d{2}-\d{2}",
320
     *       "offset"="|\d{4}-?\d{2}-?\d{2}T?\d{2}:?\d{2}:?\d{2}",
321
     *   },
322
     *   defaults={"namespace"=0, "start"=false, "end"=false, "offset"=false, "limit"=50}
323
     * )
324
     * @return Response
325
     * @codeCoverageIgnore
326
     */
327
    public function automatedEditsApiAction(): Response
328
    {
329
        $this->recordApiUsage('user/automated_edits');
330
331
        $this->setupAutoEdits();
332
333
        $extras = $this->autoEdits->getTool()
334
            ? ['tool' => $this->autoEdits->getTool()]
335
            : [];
336
337
        $out = $this->addFullPageTitlesAndContinue(
338
            'automated_edits',
339
            $extras,
340
            $this->autoEdits->getAutomatedEdits(true)
341
        );
342
343
        return $this->getFormattedApiResponse($out);
344
    }
345
346
    /**
347
     * Adds a 'full_page_title' key and value to each entry in $data.
348
     * If there are as many entries in $data as there are $this->limit, pagination is assumed
349
     *   and a 'continue' key is added to the end of the response body.
350
     * @param string $type Either 'nonautomated_edits' or 'automated_edits'.
351
     * @param array $out Whatever data needs to appear above the $data in the response body.
352
     * @param array $data The data set itself.
353
     * @return array
354
     */
355
    private function addFullPageTitlesAndContinue(string $type, array $out, array $data): array
356
    {
357
        // Add full_page_title (in addition to the existing page_title and page_namespace keys).
358
        $out[$type] = array_map(function ($rev) {
359
            return array_merge([
360
                'full_page_title' => $this->getPageFromNsAndTitle(
361
                    (int)$rev['page_namespace'],
362
                    $rev['page_title'],
363
                    true
364
                ),
365
            ], $rev);
366
        }, $data);
367
368
        // Check if pagination is needed.
369
        if (count($out[$type]) === $this->limit && count($out[$type]) > 0) {
370
            // Use the timestamp of the last Edit as the value for the 'continue' return key,
371
            //   which can be used as a value for 'offset' in order to paginate results.
372
            $timestamp = array_slice($out[$type], -1, 1)[0]['timestamp'];
373
            $out['continue'] = (new DateTime($timestamp))->format('Y-m-d\TH:i:s');
374
        }
375
376
        return $out;
377
    }
378
}
379