Passed
Push — master ( 6ef6ed...3181c4 )
by MusikAnimal
07:28
created

AdminStatsController::resultAction()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 10
nc 1
nop 1
dl 0
loc 17
rs 9.9332
c 0
b 0
f 0
ccs 0
cts 0
cp 0
crap 2
1
<?php
2
/**
3
 * This file contains the code that powers the AdminStats page of XTools.
4
 */
5
6
declare(strict_types=1);
7
8
namespace AppBundle\Controller;
9
10
use AppBundle\Helper\I18nHelper;
11
use AppBundle\Model\AdminStats;
12
use AppBundle\Repository\AdminStatsRepository;
13
use AppBundle\Repository\UserRightsRepository;
14
use Symfony\Component\DependencyInjection\ContainerInterface;
15
use Symfony\Component\HttpFoundation\JsonResponse;
16
use Symfony\Component\HttpFoundation\RequestStack;
17
use Symfony\Component\HttpFoundation\Response;
18
use Symfony\Component\Routing\Annotation\Route;
19
20
/**
21
 * The AdminStatsController serves the search form and results page of the AdminStats tool.
22
 */
23
class AdminStatsController extends XtoolsController
24
{
25
    /** @var AdminStats The admin stats instance that does all the work. */
26
    protected $adminStats;
27
28
    public const DEFAULT_DAYS = 31;
29
    public const MAX_DAYS_UI = 365;
30
    public const MAX_DAYS_API = 31;
31
32
    /**
33
     * Get the name of the tool's index route. This is also the name of the associated model.
34
     * @return string
35
     * @codeCoverageIgnore
36
     */
37
    public function getIndexRoute(): string
38
    {
39
        return 'AdminStats';
40
    }
41
42
    /**
43
     * AdminStatsController constructor.
44
     * @param RequestStack $requestStack
45
     * @param ContainerInterface $container
46
     * @param I18nHelper $i18n
47
     */
48
    public function __construct(RequestStack $requestStack, ContainerInterface $container, I18nHelper $i18n)
49
    {
50
        // Set the max length for the date range. Value is smaller for API requests.
51
        $isApi = '/api/' === substr($requestStack->getCurrentRequest()->getPathInfo(), 0, 5);
52
        $this->maxDays = $isApi ? self::MAX_DAYS_API : self::MAX_DAYS_UI;
53
        $this->defaultDays = self::DEFAULT_DAYS;
54
55
        parent::__construct($requestStack, $container, $i18n);
56
    }
57
58
    /**
59
     * Method for rendering the AdminStats Main Form.
60
     * This method redirects if valid parameters are found, making it a valid form endpoint as well.
61
     * @Route(
62
     *     "/{group}stats", name="AdminStats",
63
     *     requirements={"group"="admin|patroller|steward"},
64
     *     defaults={"group"="admin"}
65
     * )
66
     * @return Response
67
     */
68
    public function indexAction(): Response
69
    {
70
        $this->getAndSetRequestedActions();
71
72
        // Redirect if we have a project.
73
        if (isset($this->params['project'])) {
74
            // We want pretty URLs.
75
            $route = $this->generateUrl('AdminStatsResult', $this->params);
76
            $url = str_replace('%7C', '|', $route);
77
            return $this->redirect($url);
78
        }
79
80
        $adminStatsRepo = new AdminStatsRepository();
81
        $adminStatsRepo->setContainer($this->container);
82
        $actionsConfig = $adminStatsRepo->getConfig($this->project);
83
        $group = $this->params['group'];
84
        $xtPage = lcfirst($group).'Stats';
85
86
        $params = array_merge([
87
            'xtPage' => $xtPage,
88
            'xtPageTitle' => "tool-{$group}stats",
89
            'xtSubtitle' => "tool-{$group}stats-desc",
90
            'actionsConfig' => $actionsConfig,
91
92
            // Defaults that will get overridden if in $params.
93
            'start' => '',
94
            'end' => '',
95
            'group' => 'admin',
96
        ], $this->params, ['project' => $this->project]);
97
98
        $params['isAllActions'] = $params['actions'] === implode('|', $this->getActionNames($params['group']));
99
100
        // Otherwise render form.
101
        return $this->render('adminStats/index.html.twig', $params);
102
    }
103
104
    /**
105
     * Get the requested actions and set the class property.
106
     * @return string[]
107
     * @codeCoverageIgnore
108
     */
109
    private function getAndSetRequestedActions(): array
110
    {
111
        /** @var string $group The requested 'group'. See keys at admin_stats.yml for possible values. */
112
        $group = $this->params['group'] = $this->params['group'] ?? 'admin';
113
114
        // Query param for sections gets priority.
115
        $actionsQuery = $this->request->get('actions', '');
116
117
        // Either a pipe-separated string or an array.
118
        $actions = is_array($actionsQuery) ? $actionsQuery : explode('|', $actionsQuery);
119
120
        // Filter out any invalid section IDs.
121
        $actions = array_filter($actions, function ($action) use ($group) {
122
            return in_array($action, $this->getActionNames($group));
123
        });
124
125
        // Fallback for when no valid sections were requested.
126
        if (0 === count($actions)) {
127
            $actions = $this->getActionNames($group);
128
        }
129
130
        // Store as pipe-separated string for prettier URLs.
131
        $this->params['actions'] = str_replace('%7C', '|', implode('|', $actions));
132
133
        return $actions;
134
    }
135
136
    /**
137
     * Get the names of the available sections.
138
     * @param string $group Corresponds to the groups specified in admin_stats.yml
139
     * @return string[]
140
     * @codeCoverageIgnore
141
     */
142
    private function getActionNames(string $group): array
143
    {
144
        $actionsConfig = $this->container->getParameter('admin_stats');
145
        return array_keys($actionsConfig[$group]['actions']);
146
    }
147
148
    /**
149
     * Every action in this controller (other than 'index') calls this first.
150
     * @return AdminStats
151
     * @codeCoverageIgnore
152
     */
153
    public function setUpAdminStats(): AdminStats
154
    {
155
        $adminStatsRepo = new AdminStatsRepository();
156
        $adminStatsRepo->setContainer($this->container);
157
158
        $this->adminStats = new AdminStats(
159
            $this->project,
160
            (int)$this->start,
161
            (int)$this->end,
162
            $this->params['group'] ?? 'admin',
163
            $this->getAndSetRequestedActions()
164
        );
165
        $this->adminStats->setRepository($adminStatsRepo);
166
167
        // For testing purposes.
168
        return $this->adminStats;
169
    }
170
171
    /**
172
     * Method for rendering the AdminStats results.
173
     * @Route(
174
     *     "/{group}stats/{project}/{start}/{end}", name="AdminStatsResult",
175
     *     requirements={"start"="|\d{4}-\d{2}-\d{2}", "end"="|\d{4}-\d{2}-\d{2}", "group"="admin|patroller|steward"},
176
     *     defaults={"start"=false, "end"=false, "group"="admin"}
177
     * )
178
     * @param I18nHelper $i18n
179
     * @return Response
180
     * @codeCoverageIgnore
181
     */
182
    public function resultAction(I18nHelper $i18n): Response
183
    {
184
        $this->setUpAdminStats();
185
186
        $this->adminStats->prepareStats();
187
188
        // For the HTML view, we want the localized name of the user groups.
189
        // These are in the 'title' attribute of the icons for each user group.
190
        $userRightsRepo = new UserRightsRepository();
191
        $userRightsRepo->setContainer($this->container);
192
        $rightsNames = $userRightsRepo->getRightsNames($this->project, $i18n->getLang());
193
194
        return $this->getFormattedResponse('adminStats/result', [
195
            'xtPage' => lcfirst($this->params['group']).'Stats',
196
            'xtTitle' => $this->project->getDomain(),
197
            'as' => $this->adminStats,
198
            'rightsNames' => $rightsNames,
199
        ]);
200
    }
201
202
    /************************ API endpoints ************************/
203
204
    /**
205
     * Get users of the project that are capable of making 'admin actions',
206
     * keyed by user name with a list of the relevant user groups as the values.
207
     * @Route("/api/project/admins_groups/{project}", name="ProjectApiAdminsGroups")
208
     * @Route("/api/project/users_groups/{project}/{group}")
209
     * @return JsonResponse
210
     * @codeCoverageIgnore
211
     */
212
    public function adminsGroupsApiAction(): JsonResponse
213
    {
214
        $this->recordApiUsage('project/admins_groups');
215
216
        $this->setUpAdminStats();
217
218
        return new JsonResponse(
219
            $this->adminStats->getUsersAndGroups(),
220
            Response::HTTP_OK
221
        );
222
    }
223
224
    /**
225
     * Same as adminStatsApiAction except it accepts dates.
226
     * @Route(
227
     *     "/api/project/{group}_stats/{project}/{start}/{end}",
228
     *     name="ProjectApiAdminStatsDates",
229
     *     requirements={"start"="|\d{4}-\d{2}-\d{2}", "end"="|\d{4}-\d{2}-\d{2}", "group"="admin|patroller|steward"},
230
     *     defaults={"start"=false, "end"=false, "group"="admin"}
231
     * )
232
     * @return JsonResponse
233
     * @codeCoverageIgnore
234
     */
235
    public function adminStatsDatesApiAction(): JsonResponse
236
    {
237
        $this->recordApiUsage('project/adminstats');
238
239
        $this->setUpAdminStats();
240
        $this->adminStats->prepareStats();
241
242
        return $this->getFormattedApiResponse([
243
            'users' => $this->adminStats->getStats(),
244
        ]);
245
    }
246
247
    /**
248
     * Get users of the project that are capable of making 'admin actions', along with various stats
249
     * about which actions they took. Time period is limited to one month.
250
     * @Route(
251
     *     "/api/project/adminstats/{project}/{days}",
252
     *     name="ProjectApiAdminStatsLegacy",
253
     *     requirements={"days"="\d+"},
254
     *     defaults={"days"=31}
255
     * )
256
     * @Route(
257
     *     "/api/project/admin_stats/{project}/{days}",
258
     *     name="ProjectApiAdminStats",
259
     *     requirements={"days"="\d+"},
260
     *     defaults={"days"=31}
261
     * )
262
     * @param int $days Number of days from present to grab data for. Maximum 31.
263
     * @return JsonResponse
264
     * @deprecated Use adminStatsDatesApiAction().
265
     * @todo Remove this endpoint.
266
     * @codeCoverageIgnore
267
     */
268
    public function adminStatsApiAction(int $days = 31): JsonResponse
269
    {
270
        $this->recordApiUsage('project/adminstats');
271
272
        // $this->start and $this->end are normally set by the parent XtoolsController, but here we're going off of
273
        // the relative $days parameter. So we run XtoolsController::getUTCFromDateParams() once more.
274
        $end = time();
275
        $start = strtotime("-$days days", time());
276
        [$this->start, $this->end] = $this->getUTCFromDateParams($start, $end);
277
278
        $this->setUpAdminStats();
279
        $this->adminStats->prepareStats();
280
281
        $this->addFlash('warning', 'This endpoint is deprecated and will be removed in the future. '.
282
            'Use /api/project/admin_stats/{project}/{start}/{end} instead. '.
283
            'See https://xtools.readthedocs.io/en/stable/api/project.html#admin-statistics for documentation.');
284
285
        return $this->getFormattedApiResponse([
286
            // Start/end must be explicitly provided because $this->params['start'] and ['end'] are empty.
287
            'start' => date('Y-m-d', $start),
288
            'end' => date('Y-m-d', $end),
289
290
            'users' => $this->adminStats->getStats(),
291
        ]);
292
    }
293
}
294