Passed
Pull Request — main (#442)
by MusikAnimal
08:40 queued 04:21
created

CategoryEditsRepository::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 10
dl 0
loc 17
rs 10
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types = 1);
4
5
namespace App\Repository;
6
7
use App\Helper\AutomatedEditsHelper;
8
use App\Model\Edit;
9
use App\Model\Project;
10
use App\Model\User;
11
use Doctrine\DBAL\Connection;
12
use Doctrine\DBAL\Driver\ResultStatement;
13
use Doctrine\DBAL\ParameterType;
14
use GuzzleHttp\Client;
15
use Psr\Cache\CacheItemPoolInterface;
16
use Psr\Container\ContainerInterface;
17
use Psr\Log\LoggerInterface;
18
use Wikimedia\IPUtils;
19
20
/**
21
 * CategoryEditsRepository is responsible for retrieving data from the database
22
 * about the edits made by a user to pages in a set of given categories.
23
 * @codeCoverageIgnore
24
 */
25
class CategoryEditsRepository extends Repository
26
{
27
    protected AutomatedEditsHelper $autoEditsHelper;
28
    protected EditRepository $editRepo;
29
    protected PageRepository $pageRepo;
30
    protected UserRepository $userRepo;
31
32
    /**
33
     * @param ContainerInterface $container
34
     * @param CacheItemPoolInterface $cache
35
     * @param Client $guzzle
36
     * @param LoggerInterface $logger
37
     * @param bool $isWMF
38
     * @param int $queryTimeout
39
     * @param AutomatedEditsHelper $autoEditsHelper
40
     * @param EditRepository $editRepo
41
     * @param PageRepository $pageRepo
42
     * @param UserRepository $userRepo
43
     */
44
    public function __construct(
45
        ContainerInterface $container,
46
        CacheItemPoolInterface $cache,
47
        Client $guzzle,
48
        LoggerInterface $logger,
49
        bool $isWMF,
50
        int $queryTimeout,
51
        AutomatedEditsHelper $autoEditsHelper,
52
        EditRepository $editRepo,
53
        PageRepository $pageRepo,
54
        UserRepository $userRepo
55
    ) {
56
        $this->autoEditsHelper= $autoEditsHelper;
57
        $this->editRepo = $editRepo;
58
        $this->pageRepo = $pageRepo;
59
        $this->userRepo = $userRepo;
60
        parent::__construct($container, $cache, $guzzle, $logger, $isWMF, $queryTimeout);
61
    }
62
63
    /**
64
     * Get the number of edits this user made to the given categories.
65
     * @param Project $project
66
     * @param User $user
67
     * @param string[] $categories
68
     * @param int|false $start Start date as Unix timestamp.
69
     * @param int|false $end End date as Unix timestamp.
70
     * @return int Result of query, see below.
71
     */
72
    public function countCategoryEdits(
73
        Project $project,
74
        User $user,
75
        array $categories,
76
        $start = false,
77
        $end = false
78
    ): int {
79
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_categoryeditcount');
80
        if ($this->cache->hasItem($cacheKey)) {
81
            return $this->cache->getItem($cacheKey)->get();
82
        }
83
84
        $revisionTable = $project->getTableName('revision');
85
        $categorylinksTable = $project->getTableName('categorylinks');
86
        $revDateConditions = $this->getDateConditions($start, $end, false, 'revs.');
87
        $whereClause = 'revs.rev_actor = ?';
88
        $ipcJoin = '';
89
90
        if ($user->isIpRange()) {
91
            $ipcTable = $project->getTableName('ip_changes');
92
            $ipcJoin = "JOIN $ipcTable ON ipc_rev_id = revs.rev_id";
93
            $whereClause = 'ipc_hex BETWEEN ? AND ?';
94
        }
95
96
        $sql = "SELECT COUNT(DISTINCT revs.rev_id)
97
                FROM $revisionTable revs
98
                $ipcJoin
99
                JOIN $categorylinksTable ON cl_from = rev_page
100
                WHERE $whereClause
101
                    AND cl_to IN (?)
102
                    $revDateConditions";
103
        $result = (int)$this->executeStmt($sql, $project, $user, $categories)->fetchOne();
104
105
        // Cache and return.
106
        return $this->setCache($cacheKey, $result);
107
    }
108
109
    /**
110
     * Get number of edits within each individual category.
111
     * @param Project $project
112
     * @param User $user
113
     * @param array $categories
114
     * @param int|false $start
115
     * @param int|false $end
116
     * @return string[] With categories as keys, counts as values.
117
     */
118
    public function getCategoryCounts(
119
        Project $project,
120
        User $user,
121
        array $categories,
122
        $start = false,
123
        $end = false
124
    ): array {
125
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_categorycounts');
126
        if ($this->cache->hasItem($cacheKey)) {
127
            return $this->cache->getItem($cacheKey)->get();
128
        }
129
130
        $revisionTable = $project->getTableName('revision');
131
        $categorylinksTable = $project->getTableName('categorylinks');
132
        $revDateConditions = $this->getDateConditions($start, $end, false, 'revs.');
133
        $whereClause = 'revs.rev_actor = ?';
134
        $ipcJoin = '';
135
136
        if ($user->isIpRange()) {
137
            $ipcTable = $project->getTableName('ip_changes');
138
            $ipcJoin = "JOIN $ipcTable ON ipc_rev_id = revs.rev_id";
139
            $whereClause = 'ipc_hex BETWEEN ? AND ?';
140
        }
141
142
        $sql = "SELECT cl_to AS cat, COUNT(rev_id) AS edit_count, COUNT(DISTINCT rev_page) AS page_count
143
                FROM $revisionTable revs
144
                $ipcJoin
145
                JOIN $categorylinksTable ON cl_from = rev_page
146
                WHERE $whereClause
147
                    AND cl_to IN (?)
148
                    $revDateConditions
149
                GROUP BY cl_to
150
                ORDER BY edit_count DESC";
151
152
        $counts = [];
153
        $stmt = $this->executeStmt($sql, $project, $user, $categories);
154
        while ($result = $stmt->fetchAssociative()) {
155
            $counts[$result['cat']] = [
156
                'editCount' => (int)$result['edit_count'],
157
                'pageCount' => (int)$result['page_count'],
158
            ];
159
        }
160
161
        // Cache and return.
162
        return $this->setCache($cacheKey, $counts);
163
    }
164
165
    /**
166
     * Get contributions made to the given categories.
167
     * @param Project $project
168
     * @param User $user
169
     * @param string[] $categories
170
     * @param int|false $start Start date as Unix timestamp.
171
     * @param int|false $end End date as Unix timestamp.
172
     * @param false|int $offset Unix timestamp. Used for pagination.
173
     * @return string[] Result of query, with columns 'page_title', 'page_namespace', 'rev_id', 'timestamp', 'minor',
174
     *   'length', 'length_change', 'comment'
175
     */
176
    public function getCategoryEdits(
177
        Project $project,
178
        User $user,
179
        array $categories,
180
        $start = false,
181
        $end = false,
182
        $offset = false
183
    ): array {
184
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_categoryedits');
185
        if ($this->cache->hasItem($cacheKey)) {
186
            return $this->cache->getItem($cacheKey)->get();
187
        }
188
189
        $pageTable = $project->getTableName('page');
190
        $revisionTable = $project->getTableName('revision');
191
        $commentTable = $project->getTableName('comment');
192
        $categorylinksTable = $project->getTableName('categorylinks');
193
        $revDateConditions = $this->getDateConditions($start, $end, $offset, 'revs.');
194
        $whereClause = 'revs.rev_actor = ?';
195
        $ipcJoin = '';
196
197
        if ($user->isIpRange()) {
198
            $ipcTable = $project->getTableName('ip_changes');
199
            $ipcJoin = "JOIN $ipcTable ON ipc_rev_id = revs.rev_id";
200
            $whereClause = 'ipc_hex BETWEEN ? AND ?';
201
        }
202
203
        $sql = "SELECT page_title, page_namespace, revs.rev_id AS rev_id, revs.rev_timestamp AS timestamp,
204
                    revs.rev_minor_edit AS minor, revs.rev_len AS length,
205
                    (CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS length_change,
206
                    comment_text AS `comment`
207
                FROM $pageTable
208
                JOIN $revisionTable revs ON page_id = revs.rev_page
209
                $ipcJoin
210
                JOIN $categorylinksTable ON cl_from = rev_page
211
                LEFT JOIN $commentTable comment ON revs.rev_comment_id = comment_id
212
                LEFT JOIN $revisionTable parentrevs ON revs.rev_parent_id = parentrevs.rev_id
213
                WHERE $whereClause
214
                    AND cl_to IN (?)
215
                    $revDateConditions
216
                GROUP BY revs.rev_id
217
                ORDER BY revs.rev_timestamp DESC
218
                LIMIT 50";
219
220
        $result = $this->executeStmt($sql, $project, $user, $categories)->fetchAllAssociative();
221
222
        // Cache and return.
223
        return $this->setCache($cacheKey, $result);
224
    }
225
226
    /**
227
     * Bind dates, username and categories then execute the query.
228
     * @param string $sql
229
     * @param Project $project
230
     * @param User $user
231
     * @param string[] $categories
232
     * @return ResultStatement
233
     */
234
    private function executeStmt(
235
        string $sql,
236
        Project $project,
237
        User $user,
238
        array $categories
239
    ): ResultStatement {
240
        if ($user->isIpRange()) {
241
            [$hexStart, $hexEnd] = IPUtils::parseRange($user->getUsername());
242
            $params = [
243
                $hexStart,
244
                $hexEnd,
245
                $categories,
246
            ];
247
            $types = [
248
                ParameterType::STRING,
249
                ParameterType::STRING,
250
                Connection::PARAM_STR_ARRAY,
251
            ];
252
        } else {
253
            $params = [
254
                $user->getActorId($project),
255
                $categories,
256
            ];
257
            $types = [
258
                ParameterType::STRING,
259
                Connection::PARAM_STR_ARRAY,
260
            ];
261
        }
262
263
        return $this->getProjectsConnection($project)
264
            ->executeQuery($sql, $params, $types);
0 ignored issues
show
Security SQL Injection introduced by
$params can contain request data and is used in sql context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Data is passed through explode(), and explode('|', $this->request->get('categories')) is assigned to $categories
    in src/Controller/CategoryEditsController.php on line 110
  2. Data is passed through array_map(), and array_map(function(...) { /* ... */ }, $categories) is assigned to property CategoryEditsController::$categories
    in src/Controller/CategoryEditsController.php on line 118
  3. Read from property CategoryEditsController::$categories, and CategoryEdits::__construct() is called
    in src/Controller/CategoryEditsController.php on line 87
  4. Enters via parameter $categories
    in src/Model/CategoryEdits.php on line 43
  5. Data is passed through array_map(), and array_map(function(...) { /* ... */ }, $categories) is assigned to property CategoryEdits::$categories
    in src/Model/CategoryEdits.php on line 51
  6. Read from property CategoryEdits::$categories, and CategoryEditsRepository::getCategoryCounts() is called
    in src/Model/CategoryEdits.php on line 198
  7. Enters via parameter $categories
    in src/Repository/CategoryEditsRepository.php on line 121
  8. CategoryEditsRepository::executeStmt() is called
    in src/Repository/CategoryEditsRepository.php on line 153
  9. Enters via parameter $categories
    in src/Repository/CategoryEditsRepository.php on line 238
  10. array($hexStart, $hexEnd, $categories) is assigned to $params
    in src/Repository/CategoryEditsRepository.php on line 242

Used in sql context

  1. Connection::executeQuery() is called
    in src/Repository/CategoryEditsRepository.php on line 264
  2. Enters via parameter $params
    in vendor/doctrine/dbal/lib/Doctrine/DBAL/Connection.php on line 1284
  3. Data is passed through expandListParameters()
    in vendor/doctrine/dbal/lib/Doctrine/DBAL/Connection.php on line 1299
  4. Doctrine\DBAL\SQLParserUtils::expandListParameters($sql, $params, $types) is assigned to $params
    in vendor/doctrine/dbal/lib/Doctrine/DBAL/Connection.php on line 1299
  5. DB2Statement::execute() is called
    in vendor/doctrine/dbal/lib/Doctrine/DBAL/Connection.php on line 1306
  6. Enters via parameter $params
    in vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/DB2Statement.php on line 216
  7. db2_execute() is called
    in vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/DB2Statement.php on line 238

Preventing SQL Injection

There are two options to prevent SQL injection. Generally, it is recommended to use parameter binding:

$stmt = mysqli_prepare("SELECT * FROM users WHERE name = ?");
$stmt->bind_param("s", $taintedUserName);

An alternative – although generally not recommended – is to escape your data manually:

$mysqli = new mysqli('localhost', 'user', 'pass', 'dbname');

$escaped = $mysqli->real_escape_string($taintedUserName);
$mysqli->query("SELECT * FROM users WHERE name = '".$escaped."'");

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
265
    }
266
267
    /**
268
     * Get Edits given revision rows (JOINed on the page table).
269
     * @param Project $project
270
     * @param User $user
271
     * @param array $revs Each must contain 'page_title' and 'page_namespace'.
272
     * @return Edit[]
273
     */
274
    public function getEditsFromRevs(Project $project, User $user, array $revs): array
275
    {
276
        return Edit::getEditsFromRevs(
277
            $this->pageRepo,
278
            $this->editRepo,
279
            $this->userRepo,
280
            $project,
281
            $user,
282
            $revs
283
        );
284
    }
285
}
286