|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
/** |
|
4
|
|
|
* This file contains the code that powers the AdminStats page of XTools. |
|
5
|
|
|
* |
|
6
|
|
|
* @version 1.5.1 |
|
7
|
|
|
*/ |
|
8
|
|
|
|
|
9
|
|
|
namespace AppBundle\Controller; |
|
10
|
|
|
|
|
11
|
|
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; |
|
12
|
|
|
use Symfony\Bundle\FrameworkBundle\Controller\Controller; |
|
13
|
|
|
use Symfony\Component\HttpFoundation\Request; |
|
14
|
|
|
use Symfony\Component\HttpFoundation\RedirectResponse; |
|
15
|
|
|
use Xtools\ProjectRepository; |
|
16
|
|
|
|
|
17
|
|
|
/** |
|
18
|
|
|
* Class AdminStatsController |
|
19
|
|
|
* |
|
20
|
|
|
* @category AdminStats |
|
21
|
|
|
* @package AppBundle\Controller |
|
22
|
|
|
* @author XTools Team <[email protected]> |
|
23
|
|
|
* @license GPL 3.0 |
|
24
|
|
|
*/ |
|
25
|
|
|
class AdminStatsController extends XtoolsController |
|
26
|
|
|
{ |
|
27
|
|
|
/** |
|
28
|
|
|
* Get the tool's shortname. |
|
29
|
|
|
* @return string |
|
30
|
|
|
* @codeCoverageIgnore |
|
31
|
|
|
*/ |
|
32
|
|
|
public function getToolShortname() |
|
33
|
|
|
{ |
|
34
|
|
|
return 'adminstats'; |
|
35
|
|
|
} |
|
36
|
|
|
|
|
37
|
|
|
/** |
|
38
|
|
|
* Method for rendering the AdminStats Main Form. |
|
39
|
|
|
* This method redirects if valid parameters are found, making it a |
|
40
|
|
|
* valid form endpoint as well. |
|
41
|
|
|
* |
|
42
|
|
|
* @param Request $request Generated by Symfony |
|
43
|
|
|
* |
|
44
|
|
|
* @Route("/adminstats", name="adminstats") |
|
45
|
|
|
* @Route("/adminstats/", name="AdminStatsSlash") |
|
46
|
|
|
* @Route("/adminstats/index.php", name="AdminStatsIndexPhp") |
|
47
|
|
|
* |
|
48
|
|
|
* @return Route|\Symfony\Component\HttpFoundation\Response |
|
49
|
|
|
*/ |
|
50
|
|
View Code Duplication |
public function indexAction(Request $request) |
|
|
|
|
|
|
51
|
|
|
{ |
|
52
|
|
|
$params = $this->parseQueryParams($request); |
|
53
|
|
|
|
|
54
|
|
|
// Redirect if we have a project. $results may also include start and/or end date. |
|
55
|
|
|
if (isset($params['project'])) { |
|
56
|
|
|
return $this->redirectToRoute('AdminStatsResult', $params); |
|
57
|
|
|
} |
|
58
|
|
|
|
|
59
|
|
|
// Otherwise render form. |
|
60
|
|
|
return $this->render('adminStats/index.html.twig', [ |
|
61
|
|
|
'xtPage' => 'adminstats', |
|
62
|
|
|
'xtPageTitle' => 'tool-adminstats', |
|
63
|
|
|
'xtSubtitle' => 'tool-adminstats-desc', |
|
64
|
|
|
]); |
|
65
|
|
|
} |
|
66
|
|
|
|
|
67
|
|
|
/** |
|
68
|
|
|
* Method for rendering the AdminStats Results |
|
69
|
|
|
* |
|
70
|
|
|
* @param Request $request The HTTP request. |
|
71
|
|
|
* @param string $project Project to run the results against |
|
72
|
|
|
* @param string $start Date to start on. Must parse by strtotime. |
|
73
|
|
|
* @param string $end Date to end on. Must parse by strtotime. |
|
74
|
|
|
* |
|
75
|
|
|
* @Route( |
|
76
|
|
|
* "/adminstats/{project}/{start}/{end}", name="AdminStatsResult", |
|
77
|
|
|
* requirements={"start" = "|\d{4}-\d{2}-\d{2}", "end" = "|\d{4}-\d{2}-\d{2}"} |
|
78
|
|
|
* ) |
|
79
|
|
|
* |
|
80
|
|
|
* @return Route|\Symfony\Component\HttpFoundation\Response |
|
81
|
|
|
* @todo Move SQL to a model. |
|
82
|
|
|
* @codeCoverageIgnore |
|
83
|
|
|
*/ |
|
84
|
|
|
public function resultAction(Request $request, $project, $start = null, $end = null) |
|
|
|
|
|
|
85
|
|
|
{ |
|
86
|
|
|
// Load the database information for the tool. |
|
87
|
|
|
// $projectData will be a redirect if the project is invalid. |
|
88
|
|
|
$projectData = $this->validateProject($project); |
|
89
|
|
|
if ($projectData instanceof RedirectResponse) { |
|
90
|
|
|
return $projectData; |
|
91
|
|
|
} |
|
92
|
|
|
|
|
93
|
|
|
list($start, $end) = $this->getUTCFromDateParams($start, $end); |
|
94
|
|
|
|
|
95
|
|
|
// Initialize variables - prevents variable undefined errors |
|
96
|
|
|
$adminIdArr = []; |
|
97
|
|
|
$adminsWithoutAction = 0; |
|
98
|
|
|
$adminsWithoutActionPct = 0; |
|
99
|
|
|
|
|
100
|
|
|
// Pull the API helper and database. Then check if we can use this tool |
|
101
|
|
|
$api = $this->get('app.api_helper'); |
|
102
|
|
|
$conn = $this->get('doctrine')->getManager('replicas')->getConnection(); |
|
103
|
|
|
|
|
104
|
|
|
// Generate a diff for the dates - this is the number of days we're spanning. |
|
105
|
|
|
$days = ($end - $start) / 60 / 60 / 24; |
|
106
|
|
|
|
|
107
|
|
|
// Get admin ID's, used to account for inactive admins |
|
108
|
|
|
$userGroupsTable = $projectData->getTableName('user_groups'); |
|
109
|
|
|
$ufgTable = $projectData->getTableName('user_former_groups'); |
|
110
|
|
|
$query = " |
|
111
|
|
|
SELECT ug_user AS user_id |
|
112
|
|
|
FROM $userGroupsTable |
|
113
|
|
|
WHERE ug_group = 'sysop' |
|
114
|
|
|
UNION |
|
115
|
|
|
SELECT ufg_user AS user_id |
|
116
|
|
|
FROM $ufgTable |
|
117
|
|
|
WHERE ufg_group = 'sysop' |
|
118
|
|
|
"; |
|
119
|
|
|
|
|
120
|
|
|
$res = $conn->prepare($query); |
|
121
|
|
|
$res->execute(); |
|
122
|
|
|
|
|
123
|
|
|
// Iterate over query results, loading each user id into the array |
|
124
|
|
|
while ($row = $res->fetch()) { |
|
125
|
|
|
$adminIdArr[] = $row['user_id'] ; |
|
126
|
|
|
} |
|
127
|
|
|
|
|
128
|
|
|
// Set the query results to be useful in a sql statement. |
|
129
|
|
|
$adminIds = implode(',', $adminIdArr); |
|
130
|
|
|
|
|
131
|
|
|
// Load up the tables we need and run the mega query. |
|
132
|
|
|
// This query provides all of the statistics |
|
133
|
|
|
$userTable = $projectData->getTableName('user'); |
|
134
|
|
|
$loggingTable = $projectData->getTableName('logging', 'userindex'); |
|
135
|
|
|
|
|
136
|
|
|
$startDb = date('Ymd000000', $start); |
|
137
|
|
|
$endDb = date('Ymd235959', $end); |
|
138
|
|
|
|
|
139
|
|
|
// TODO: Fix this - inactive admins aren't getting shown |
|
140
|
|
|
$query = " |
|
141
|
|
|
SELECT user_name, user_id, |
|
142
|
|
|
SUM(IF( (log_type = 'delete' AND log_action != 'restore'),1,0)) AS mdelete, |
|
143
|
|
|
SUM(IF( (log_type = 'delete' AND log_action = 'restore'),1,0)) AS mrestore, |
|
144
|
|
|
SUM(IF( (log_type = 'block' AND log_action != 'unblock'),1,0)) AS mblock, |
|
145
|
|
|
SUM(IF( (log_type = 'block' AND log_action = 'unblock'),1,0)) AS munblock, |
|
146
|
|
|
SUM(IF( (log_type = 'protect' AND log_action != 'unprotect'),1,0)) AS mprotect, |
|
147
|
|
|
SUM(IF( (log_type = 'protect' AND log_action = 'unprotect'),1,0)) AS munprotect, |
|
148
|
|
|
SUM(IF( log_type = 'rights',1,0)) AS mrights, |
|
149
|
|
|
SUM(IF( log_type = 'import',1,0)) AS mimport, |
|
150
|
|
|
SUM(IF(log_type != '',1,0)) AS mtotal |
|
151
|
|
|
FROM $loggingTable |
|
152
|
|
|
JOIN $userTable ON user_id = log_user |
|
153
|
|
|
WHERE log_timestamp > '$startDb' AND log_timestamp <= '$endDb' |
|
154
|
|
|
AND log_type IS NOT NULL |
|
155
|
|
|
AND log_action IS NOT NULL |
|
156
|
|
|
AND log_type IN ('block', 'delete', 'protect', 'import', 'rights') |
|
157
|
|
|
GROUP BY user_name |
|
158
|
|
|
HAVING mdelete > 0 OR user_id IN ($adminIds) |
|
159
|
|
|
ORDER BY mtotal DESC"; |
|
160
|
|
|
|
|
161
|
|
|
$res = $conn->prepare($query); |
|
162
|
|
|
$res->execute(); |
|
163
|
|
|
|
|
164
|
|
|
// Fetch all the information out. Because of pre-processing done |
|
165
|
|
|
// in the query, we can use this practically raw. |
|
166
|
|
|
$users = $res->fetchAll(); |
|
167
|
|
|
|
|
168
|
|
|
// Pull the admins from the API, for merging. |
|
169
|
|
|
$admins = $api->getAdmins($project); |
|
170
|
|
|
|
|
171
|
|
|
// Get the total number of admins, the number of admins without |
|
172
|
|
|
// action, and then later we'll run percentage calculations |
|
173
|
|
|
$adminCount = count($admins); |
|
174
|
|
|
|
|
175
|
|
|
// Combine the two arrays. We can't use array_merge here because |
|
176
|
|
|
// the arrays contain fundamentally different data. Instead, it's |
|
177
|
|
|
// done by hand. Only two values are needed, edit count and groups. |
|
178
|
|
|
foreach ($users as $key => $value) { |
|
179
|
|
|
$username = $value['user_name']; |
|
180
|
|
|
|
|
181
|
|
|
if (empty($admins[$username])) { |
|
182
|
|
|
$admins[$username] = [ |
|
183
|
|
|
'groups' => '', |
|
184
|
|
|
]; |
|
185
|
|
|
} |
|
186
|
|
|
$users[$key]['groups'] = $admins[$username]['groups']; |
|
187
|
|
|
|
|
188
|
|
|
if ($users[$key]['mtotal'] === 0) { |
|
189
|
|
|
$adminsWithoutAction++; |
|
190
|
|
|
} |
|
191
|
|
|
|
|
192
|
|
|
unset($admins[$username]); |
|
193
|
|
|
} |
|
194
|
|
|
|
|
195
|
|
|
// push any inactive admins back to $users with zero values |
|
196
|
|
|
if (count($admins)) { |
|
197
|
|
|
foreach ($admins as $username => $stats) { |
|
198
|
|
|
$users[] = [ |
|
199
|
|
|
'user_name' => $username, |
|
200
|
|
|
'mdelete' => 0, |
|
201
|
|
|
'mrestore' => 0, |
|
202
|
|
|
'mblock' => 0, |
|
203
|
|
|
'munblock' => 0, |
|
204
|
|
|
'mprotect' => 0, |
|
205
|
|
|
'munprotect' => 0, |
|
206
|
|
|
'mrights' => 0, |
|
207
|
|
|
'mimport' => 0, |
|
208
|
|
|
'mtotal' => 0, |
|
209
|
|
|
'groups' => $stats['groups'], |
|
210
|
|
|
]; |
|
211
|
|
|
$adminsWithoutAction++; |
|
212
|
|
|
} |
|
213
|
|
|
} |
|
214
|
|
|
|
|
215
|
|
|
if ($adminCount > 0) { |
|
216
|
|
|
$adminsWithoutActionPct = ($adminsWithoutAction / $adminCount) * 100; |
|
217
|
|
|
} |
|
218
|
|
|
|
|
219
|
|
|
// Render the result! |
|
220
|
|
|
return $this->render('adminStats/result.html.twig', [ |
|
221
|
|
|
'xtPage' => 'adminstats', |
|
222
|
|
|
'xtTitle' => $project, |
|
223
|
|
|
|
|
224
|
|
|
'project' => $projectData, |
|
225
|
|
|
|
|
226
|
|
|
'start_date' => date('Y-m-d', $start), |
|
227
|
|
|
'end_date' => date('Y-m-d', $end), |
|
228
|
|
|
'days' => $days, |
|
229
|
|
|
|
|
230
|
|
|
'adminsWithoutAction' => $adminsWithoutAction, |
|
231
|
|
|
'admins_without_action_pct' => $adminsWithoutActionPct, |
|
232
|
|
|
'adminCount' => $adminCount, |
|
233
|
|
|
|
|
234
|
|
|
'users' => $users, |
|
235
|
|
|
'usersCount' => count($users), |
|
236
|
|
|
]); |
|
237
|
|
|
} |
|
238
|
|
|
} |
|
239
|
|
|
|
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.