CategoryEditsRepository::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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

Used in sql context

  1. Connection::executeQuery() is called
    in src/Repository/CategoryEditsRepository.php on line 267
  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...
268
    }
269
270
    /**
271
     * Get Edits given revision rows (JOINed on the page table).
272
     * @param Project $project
273
     * @param User $user
274
     * @param array $revs Each must contain 'page_title' and 'namespace'.
275
     * @return Edit[]
276
     */
277
    public function getEditsFromRevs(Project $project, User $user, array $revs): array
278
    {
279
        return Edit::getEditsFromRevs(
280
            $this->pageRepo,
281
            $this->editRepo,
282
            $this->userRepo,
283
            $project,
284
            $user,
285
            $revs
286
        );
287
    }
288
}
289