Test Failed
Push — dependency-injection ( 7565fa )
by MusikAnimal
07:05
created

CategoryEditsRepository::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

Used in sql context

  1. Connection::executeQuery() is called
    in src/Repository/CategoryEditsRepository.php on line 242
  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...
243
    }
244
}
245