aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--autoload.php1
-rw-r--r--includes/ExternalLinks/LinkFilter.php22
-rw-r--r--includes/libs/rdbms/database/IReadableDatabase.php4
-rw-r--r--includes/libs/rdbms/expression/AndExpressionGroup.php4
-rw-r--r--includes/libs/rdbms/expression/Expression.php20
-rw-r--r--includes/libs/rdbms/expression/IExpression.php6
-rw-r--r--includes/libs/rdbms/expression/LikeValue.php62
-rw-r--r--includes/libs/rdbms/expression/OrExpressionGroup.php4
-rw-r--r--includes/libs/rdbms/platform/SQLPlatform.php22
-rw-r--r--includes/page/PageArchive.php14
-rw-r--r--tests/phpunit/includes/ExternalLinks/LinkFilterTest.php54
11 files changed, 145 insertions, 68 deletions
diff --git a/autoload.php b/autoload.php
index 07d713e28710..9d544f1b725d 100644
--- a/autoload.php
+++ b/autoload.php
@@ -3115,6 +3115,7 @@ $wgAutoloadLocalClasses = [
'Wikimedia\\Rdbms\\LBFactorySimple' => __DIR__ . '/includes/libs/rdbms/lbfactory/LBFactorySimple.php',
'Wikimedia\\Rdbms\\LBFactorySingle' => __DIR__ . '/includes/libs/rdbms/lbfactory/LBFactorySingle.php',
'Wikimedia\\Rdbms\\LikeMatch' => __DIR__ . '/includes/libs/rdbms/encasing/LikeMatch.php',
+ 'Wikimedia\\Rdbms\\LikeValue' => __DIR__ . '/includes/libs/rdbms/expression/LikeValue.php',
'Wikimedia\\Rdbms\\LoadBalancer' => __DIR__ . '/includes/libs/rdbms/loadbalancer/LoadBalancer.php',
'Wikimedia\\Rdbms\\LoadBalancerDisabled' => __DIR__ . '/includes/libs/rdbms/loadbalancer/LoadBalancerDisabled.php',
'Wikimedia\\Rdbms\\LoadBalancerSingle' => __DIR__ . '/includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php',
diff --git a/includes/ExternalLinks/LinkFilter.php b/includes/ExternalLinks/LinkFilter.php
index ca994602c170..7ed7afb75617 100644
--- a/includes/ExternalLinks/LinkFilter.php
+++ b/includes/ExternalLinks/LinkFilter.php
@@ -26,8 +26,11 @@ use MediaWiki\MediaWikiServices;
use StringUtils;
use TextContent;
use Wikimedia\IPUtils;
+use Wikimedia\Rdbms\AndExpressionGroup;
+use Wikimedia\Rdbms\IExpression;
use Wikimedia\Rdbms\LikeMatch;
-use Wikimedia\Rdbms\Platform\ISQLPlatform;
+use Wikimedia\Rdbms\LikeValue;
+use Wikimedia\Rdbms\OrExpressionGroup;
/**
* Utilities for formatting and querying the externallinks table.
@@ -361,15 +364,16 @@ class LinkFilter {
if ( $options['oneWildcard'] && $likePath[0] != '/' ) {
$thisDomainConditions[] = $db->expr( 'el_to_domain_index', '=', $index1 );
} else {
- $thisDomainConditions[] = "el_to_domain_index" . $db->buildLike( $index1, $db->anyString() );
+ $thisDomainConditions[] = $db->expr(
+ 'el_to_domain_index',
+ IExpression::LIKE,
+ new LikeValue( $index1, $db->anyString() )
+ );
}
foreach ( $domainGaps[$index1] ?? [] as $from => $to ) {
- $thisDomainConditions[] = $db->makeList( [
- $db->expr( 'el_id', '<', $from ),
- $db->expr( 'el_id', '>', $to ),
- ], ISQLPlatform::LIST_OR );
+ $thisDomainConditions[] = $db->expr( 'el_id', '<', $from )->or( 'el_id', '>', $to );
}
- $domainConditions[] = $db->makeList( $thisDomainConditions, ISQLPlatform::LIST_AND );
+ $domainConditions[] = new AndExpressionGroup( ...$thisDomainConditions );
}
if ( !$domainConditions ) {
@@ -383,8 +387,8 @@ class LinkFilter {
$index2 = implode( '', $trimmedlikePath );
return [
- $db->makeList( $domainConditions, ISQLPlatform::LIST_OR ),
- "el_to_path" . $db->buildLike( $index2, $db->anyString() ),
+ new OrExpressionGroup( ...$domainConditions ),
+ $db->expr( 'el_to_path', IExpression::LIKE, new LikeValue( $index2, $db->anyString() ) ),
];
}
diff --git a/includes/libs/rdbms/database/IReadableDatabase.php b/includes/libs/rdbms/database/IReadableDatabase.php
index 5daa0520a2fd..a8e8fea0d3aa 100644
--- a/includes/libs/rdbms/database/IReadableDatabase.php
+++ b/includes/libs/rdbms/database/IReadableDatabase.php
@@ -671,9 +671,9 @@ interface IReadableDatabase extends ISQLPlatform, DbQuoter, IDatabaseFlags {
* @since 1.42
* @param string $field
* @param-taint $field exec_sql
- * @param string $op One of '>', '<', '!=', '=', '>=', '<='
+ * @param string $op One of '>', '<', '!=', '=', '>=', '<=', IExpression::LIKE
* @param-taint $op exec_sql
- * @param string|int|float|bool|Blob|null|non-empty-list<string|int|float|bool|Blob> $value
+ * @param string|int|float|bool|Blob|null|LikeValue|non-empty-list<string|int|float|bool|Blob> $value
* @param-taint $value escapes_sql
* @return Expression
*/
diff --git a/includes/libs/rdbms/expression/AndExpressionGroup.php b/includes/libs/rdbms/expression/AndExpressionGroup.php
index 6fa27c334298..58fe4146a81e 100644
--- a/includes/libs/rdbms/expression/AndExpressionGroup.php
+++ b/includes/libs/rdbms/expression/AndExpressionGroup.php
@@ -15,9 +15,9 @@ class AndExpressionGroup extends ExpressionGroup {
/**
* @param string $field
* @param-taint $field exec_sql
- * @param string $op One of '>', '<', '!=', '=', '>=', '<='
+ * @param string $op One of '>', '<', '!=', '=', '>=', '<=', IExpression::LIKE
* @param-taint $op exec_sql
- * @param string|int|float|bool|Blob|null|non-empty-list<string|int|float|bool|Blob> $value
+ * @param string|int|float|bool|Blob|null|LikeValue|non-empty-list<string|int|float|bool|Blob> $value
* @param-taint $value escapes_sql
*/
public function and( string $field, string $op, $value ): AndExpressionGroup {
diff --git a/includes/libs/rdbms/expression/Expression.php b/includes/libs/rdbms/expression/Expression.php
index 9c4c4e4100dd..6279e7686fb4 100644
--- a/includes/libs/rdbms/expression/Expression.php
+++ b/includes/libs/rdbms/expression/Expression.php
@@ -19,11 +19,12 @@ class Expression implements IExpression {
/**
* Store an expression
*
+ * @internal Outside of rdbms, Use IReadableDatabase::expr() to create an expression object.
* @param string $field
* @param-taint $field exec_sql
- * @param string $op One of '>', '<', '!=', '=', '>=', '<='
+ * @param string $op One of '>', '<', '!=', '=', '>=', '<=', IExpression::LIKE
* @param-taint $op exec_sql
- * @param string|int|float|bool|Blob|null|non-empty-list<string|int|float|bool|Blob> $value
+ * @param string|int|float|bool|Blob|null|LikeValue|non-empty-list<string|int|float|bool|Blob> $value
* @param-taint $value escapes_sql
*/
public function __construct( string $field, string $op, $value ) {
@@ -40,6 +41,10 @@ class Expression implements IExpression {
if ( is_array( $value ) && in_array( null, $value, true ) ) {
throw new InvalidArgumentException( "NULL can't be in the array of values" );
}
+
+ if ( $op === IExpression::LIKE && !( $value instanceof LikeValue ) ) {
+ throw new InvalidArgumentException( "Value for like expression must be of LikeValue type" );
+ }
$this->field = $field;
$this->op = $op;
$this->value = $value;
@@ -48,9 +53,9 @@ class Expression implements IExpression {
/**
* @param string $field
* @param-taint $field exec_sql
- * @param string $op One of '>', '<', '!=', '=', '>=', '<='
+ * @param string $op One of '>', '<', '!=', '=', '>=', '<=', IExpression::LIKE
* @param-taint $op exec_sql
- * @param string|int|float|bool|Blob|null|non-empty-list<string|int|float|bool|Blob> $value
+ * @param string|int|float|bool|Blob|null|LikeValue|non-empty-list<string|int|float|bool|Blob> $value
* @param-taint $value escapes_sql
*/
public function and( string $field, string $op, $value ): AndExpressionGroup {
@@ -61,9 +66,9 @@ class Expression implements IExpression {
/**
* @param string $field
* @param-taint $field exec_sql
- * @param string $op One of '>', '<', '!=', '=', '>=', '<='
+ * @param string $op One of '>', '<', '!=', '=', '>=', '<=', IExpression::LIKE
* @param-taint $op exec_sql
- * @param string|int|float|bool|Blob|null|non-empty-list<string|int|float|bool|Blob> $value
+ * @param string|int|float|bool|Blob|null|LikeValue|non-empty-list<string|int|float|bool|Blob> $value
* @param-taint $value escapes_sql
*/
public function or( string $field, string $op, $value ): OrExpressionGroup {
@@ -108,6 +113,9 @@ class Expression implements IExpression {
throw new LogicException( "Operator $this->op can't take null as value" );
}
}
+ if ( $this->op === IExpression::LIKE && $this->value instanceof LikeValue ) {
+ return $this->field . ' ' . $this->op . ' ' . $this->value->toSql( $dbQuoter );
+ }
return $this->field . ' ' . $this->op . ' ' . $dbQuoter->addQuotes( $this->value );
}
diff --git a/includes/libs/rdbms/expression/IExpression.php b/includes/libs/rdbms/expression/IExpression.php
index a8d8610b71d4..d333dc306488 100644
--- a/includes/libs/rdbms/expression/IExpression.php
+++ b/includes/libs/rdbms/expression/IExpression.php
@@ -5,11 +5,13 @@ namespace Wikimedia\Rdbms;
use Wikimedia\Rdbms\Database\DbQuoter;
/**
- * @internal
+ * @since 1.42
*/
interface IExpression {
- public const ACCEPTABLE_OPERATORS = [ '>', '<', '!=', '=', '>=', '<=' ];
+ public const ACCEPTABLE_OPERATORS = [ '>', '<', '!=', '=', '>=', '<=', self::LIKE ];
+
+ public const LIKE = 'LIKE';
/**
* Return SQL for execution.
diff --git a/includes/libs/rdbms/expression/LikeValue.php b/includes/libs/rdbms/expression/LikeValue.php
new file mode 100644
index 000000000000..8ba454a338d8
--- /dev/null
+++ b/includes/libs/rdbms/expression/LikeValue.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace Wikimedia\Rdbms;
+
+use InvalidArgumentException;
+use Wikimedia\Rdbms\Database\DbQuoter;
+
+/**
+ * Content of like value
+ *
+ * @newable
+ * @since 1.42
+ */
+class LikeValue {
+ private array $values = [];
+
+ public function __construct( $value, ...$values ) {
+ if ( !is_string( $value ) && !( $value instanceof LikeMatch ) ) {
+ throw new InvalidArgumentException( "Value $value must be either string or LikeMatch" );
+ }
+ $this->values = [ $value ];
+
+ foreach ( $values as $value ) {
+ if ( !is_string( $value ) && !( $value instanceof LikeMatch ) ) {
+ throw new InvalidArgumentException( "Value $value must be either string or LikeMatch" );
+ }
+ $this->values[] = $value;
+ }
+ }
+
+ /**
+ * @internal to be used by rdbms library only
+ * @return-taint none
+ */
+ public function toSql( DbQuoter $dbQuoter ): string {
+ $s = '';
+
+ // We use ` instead of \ as the default LIKE escape character, since addQuotes()
+ // may escape backslashes, creating problems of double escaping. The `
+ // character has good cross-DBMS compatibility, avoiding special operators
+ // in MS SQL like ^ and %
+ $escapeChar = '`';
+
+ foreach ( $this->values as $value ) {
+ if ( $value instanceof LikeMatch ) {
+ $s .= $value->toString();
+ } else {
+ $s .= $this->escapeLikeInternal( $value, $escapeChar );
+ }
+ }
+
+ return $dbQuoter->addQuotes( $s ) . ' ESCAPE ' . $dbQuoter->addQuotes( $escapeChar ) . ' ';
+ }
+
+ private function escapeLikeInternal( $s, $escapeChar = '`' ) {
+ return str_replace(
+ [ $escapeChar, '%', '_' ],
+ [ "{$escapeChar}{$escapeChar}", "{$escapeChar}%", "{$escapeChar}_" ],
+ $s
+ );
+ }
+}
diff --git a/includes/libs/rdbms/expression/OrExpressionGroup.php b/includes/libs/rdbms/expression/OrExpressionGroup.php
index bc89436e6e0f..c64d07e5300b 100644
--- a/includes/libs/rdbms/expression/OrExpressionGroup.php
+++ b/includes/libs/rdbms/expression/OrExpressionGroup.php
@@ -15,9 +15,9 @@ class OrExpressionGroup extends ExpressionGroup {
/**
* @param string $field
* @param-taint $field exec_sql
- * @param string $op One of '>', '<', '!=', '=', '>=', '<='
+ * @param string $op One of '>', '<', '!=', '=', '>=', '<=', IExpression::LIKE
* @param-taint $op exec_sql
- * @param string|int|float|bool|Blob|null|non-empty-list<string|int|float|bool|Blob> $value
+ * @param string|int|float|bool|Blob|null|LikeValue|non-empty-list<string|int|float|bool|Blob> $value
* @param-taint $value escapes_sql
*/
public function or( string $field, string $op, $value ): OrExpressionGroup {
diff --git a/includes/libs/rdbms/platform/SQLPlatform.php b/includes/libs/rdbms/platform/SQLPlatform.php
index cfec055cef81..d4d8be845b59 100644
--- a/includes/libs/rdbms/platform/SQLPlatform.php
+++ b/includes/libs/rdbms/platform/SQLPlatform.php
@@ -30,6 +30,7 @@ use Wikimedia\Rdbms\DatabaseDomain;
use Wikimedia\Rdbms\DBLanguageError;
use Wikimedia\Rdbms\IExpression;
use Wikimedia\Rdbms\LikeMatch;
+use Wikimedia\Rdbms\LikeValue;
use Wikimedia\Rdbms\Query;
use Wikimedia\Rdbms\QueryBuilderFromRawSql;
use Wikimedia\Rdbms\Subquery;
@@ -437,25 +438,10 @@ class SQLPlatform implements ISQLPlatform {
} else {
$params = func_get_args();
}
+ // @phan-suppress-next-line PhanParamTooFewUnpack
+ $likeValue = new LikeValue( ...$params );
- $s = '';
-
- // We use ` instead of \ as the default LIKE escape character, since addQuotes()
- // may escape backslashes, creating problems of double escaping. The `
- // character has good cross-DBMS compatibility, avoiding special operators
- // in MS SQL like ^ and %
- $escapeChar = '`';
-
- foreach ( $params as $value ) {
- if ( $value instanceof LikeMatch ) {
- $s .= $value->toString();
- } else {
- $s .= $this->escapeLikeInternal( $value, $escapeChar );
- }
- }
-
- return ' LIKE ' .
- $this->quoter->addQuotes( $s ) . ' ESCAPE ' . $this->quoter->addQuotes( $escapeChar ) . ' ';
+ return ' LIKE ' . $likeValue->toSql( $this->quoter );
}
public function anyChar() {
diff --git a/includes/page/PageArchive.php b/includes/page/PageArchive.php
index a57c37860792..40cbdf171cd5 100644
--- a/includes/page/PageArchive.php
+++ b/includes/page/PageArchive.php
@@ -25,8 +25,10 @@ use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Status\Status;
use MediaWiki\Title\Title;
use MediaWiki\User\UserIdentity;
+use Wikimedia\Rdbms\IExpression;
use Wikimedia\Rdbms\IReadableDatabase;
use Wikimedia\Rdbms\IResultWrapper;
+use Wikimedia\Rdbms\LikeValue;
use Wikimedia\Rdbms\SelectQueryBuilder;
/**
@@ -88,15 +90,13 @@ class PageArchive {
}
$dbr = MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->getReplicaDatabase();
- $condTitles = array_unique( array_map( static function ( Title $t ) {
+ $condTitles = array_values( array_unique( array_map( static function ( Title $t ) {
return $t->getDBkey();
- }, $results ) );
+ }, $results ) ) );
$conds = [
'ar_namespace' => $ns,
- $dbr->makeList( [
- 'ar_title' => $condTitles,
- 'ar_title' . $dbr->buildLike( $termDb, $dbr->anyString() ),
- ], LIST_OR ),
+ $dbr->expr( 'ar_title', '=', $condTitles )
+ ->or( 'ar_title', IExpression::LIKE, new LikeValue( $termDb, $dbr->anyString() ) ),
];
return self::listPages( $dbr, $conds );
@@ -125,7 +125,7 @@ class PageArchive {
$conds = [
'ar_namespace' => $ns,
- 'ar_title' . $dbr->buildLike( $prefix, $dbr->anyString() ),
+ $dbr->expr( 'ar_title', IExpression::LIKE, new LikeValue( $prefix, $dbr->anyString() ) ),
];
return self::listPages( $dbr, $conds );
diff --git a/tests/phpunit/includes/ExternalLinks/LinkFilterTest.php b/tests/phpunit/includes/ExternalLinks/LinkFilterTest.php
index 992562c88e19..da35d76afe09 100644
--- a/tests/phpunit/includes/ExternalLinks/LinkFilterTest.php
+++ b/tests/phpunit/includes/ExternalLinks/LinkFilterTest.php
@@ -2,6 +2,8 @@
use MediaWiki\ExternalLinks\LinkFilter;
use MediaWiki\MainConfigNames;
+use MediaWiki\Tests\Unit\Libs\Rdbms\AddQuoterMock;
+use Wikimedia\Rdbms\IExpression;
use Wikimedia\Rdbms\LikeMatch;
/**
@@ -497,7 +499,19 @@ class LinkFilterTest extends MediaWikiLangTestCase {
],
] );
$conds = LinkFilter::getQueryConditions( $query, $options );
- $this->assertEquals( $expected, $conds );
+ if ( !$conds ) {
+ $this->assertEquals( $expected, $conds );
+ return;
+ }
+ $sqlConds = [];
+ foreach ( $conds as $cond ) {
+ if ( $cond instanceof IExpression ) {
+ $sqlConds[] = $cond->toSql( new AddQuoterMock() );
+ } else {
+ $sqlConds[] = $cond;
+ }
+ }
+ $this->assertEquals( $expected, $sqlConds );
}
public static function provideGetQueryConditions() {
@@ -506,8 +520,8 @@ class LinkFilterTest extends MediaWikiLangTestCase {
'example.com',
[],
[
- '((el_to_domain_index LIKE \'http://com.example.%\' ESCAPE \'`\' )) OR ' .
- '((el_to_domain_index LIKE \'https://com.example.%\' ESCAPE \'`\' ))',
+ '((el_to_domain_index LIKE \'http://com.example.%\' ESCAPE \'`\' ) OR ' .
+ '(el_to_domain_index LIKE \'https://com.example.%\' ESCAPE \'`\' ))',
'el_to_path LIKE \'/%\' ESCAPE \'`\' ',
],
],
@@ -515,8 +529,8 @@ class LinkFilterTest extends MediaWikiLangTestCase {
'example.com/foobar',
[],
[
- '((el_to_domain_index LIKE \'http://com.example.%\' ESCAPE \'`\' )) OR ' .
- '((el_to_domain_index LIKE \'https://com.example.%\' ESCAPE \'`\' ))',
+ '((el_to_domain_index LIKE \'http://com.example.%\' ESCAPE \'`\' ) OR ' .
+ '(el_to_domain_index LIKE \'https://com.example.%\' ESCAPE \'`\' ))',
'el_to_path LIKE \'/foobar%\' ESCAPE \'`\' ',
],
],
@@ -524,8 +538,8 @@ class LinkFilterTest extends MediaWikiLangTestCase {
'*.example.com',
[],
[
- '((el_to_domain_index LIKE \'http://com.example.%\' ESCAPE \'`\' )) OR ' .
- '((el_to_domain_index LIKE \'https://com.example.%\' ESCAPE \'`\' ))',
+ '((el_to_domain_index LIKE \'http://com.example.%\' ESCAPE \'`\' ) OR ' .
+ '(el_to_domain_index LIKE \'https://com.example.%\' ESCAPE \'`\' ))',
'el_to_path LIKE \'/%\' ESCAPE \'`\' ',
],
],
@@ -533,8 +547,8 @@ class LinkFilterTest extends MediaWikiLangTestCase {
'*.example.com/foobar',
[],
[
- '((el_to_domain_index LIKE \'http://com.example.%\' ESCAPE \'`\' )) OR ' .
- '((el_to_domain_index LIKE \'https://com.example.%\' ESCAPE \'`\' ))',
+ '((el_to_domain_index LIKE \'http://com.example.%\' ESCAPE \'`\' ) OR ' .
+ '(el_to_domain_index LIKE \'https://com.example.%\' ESCAPE \'`\' ))',
'el_to_path LIKE \'/foobar%\' ESCAPE \'`\' ',
],
],
@@ -542,8 +556,8 @@ class LinkFilterTest extends MediaWikiLangTestCase {
'*.example.com/foobar',
[ 'oneWildcard' => true ],
[
- '((el_to_domain_index = \'http://com.example.\')) OR ' .
- '((el_to_domain_index = \'https://com.example.\'))',
+ '((el_to_domain_index = \'http://com.example.\') OR ' .
+ '(el_to_domain_index = \'https://com.example.\'))',
'el_to_path LIKE \'/foobar%\' ESCAPE \'`\' ',
],
],
@@ -551,8 +565,8 @@ class LinkFilterTest extends MediaWikiLangTestCase {
'example.com/blah/blah/blah/blah/blah/blah/blah/blah/blah/blah?foo=',
[],
[
- '((el_to_domain_index LIKE \'http://com.example.%\' ESCAPE \'`\' )) OR ' .
- '((el_to_domain_index LIKE \'https://com.example.%\' ESCAPE \'`\' ))',
+ '((el_to_domain_index LIKE \'http://com.example.%\' ESCAPE \'`\' ) OR ' .
+ '(el_to_domain_index LIKE \'https://com.example.%\' ESCAPE \'`\' ))',
'el_to_path LIKE ' .
'\'/blah/blah/blah/blah/blah/blah/blah/blah/blah/blah?foo=%\' ' .
'ESCAPE \'`\' ',
@@ -567,13 +581,13 @@ class LinkFilterTest extends MediaWikiLangTestCase {
'gaps.example',
[],
[
- '((el_to_domain_index LIKE \'http://example.gaps.%\' ESCAPE \'`\' ) AND ' .
- '((el_id < 10) OR (el_id > 20)) AND ' .
- '((el_id < 100) OR (el_id > 200)) AND ' .
- '((el_id < 1000) OR (el_id > 2000))) OR ' .
- '((el_to_domain_index LIKE \'https://example.gaps.%\' ESCAPE \'`\' ) AND ' .
- '((el_id < 30) OR (el_id > 40)) AND ' .
- '((el_id < 5000) OR (el_id > 60000)))',
+ '((el_to_domain_index LIKE \'http://example.gaps.%\' ESCAPE \'`\' AND ' .
+ '(el_id < 10 OR el_id > 20) AND ' .
+ '(el_id < 100 OR el_id > 200) AND ' .
+ '(el_id < 1000 OR el_id > 2000)) OR ' .
+ '(el_to_domain_index LIKE \'https://example.gaps.%\' ESCAPE \'`\' AND ' .
+ '(el_id < 30 OR el_id > 40) AND ' .
+ '(el_id < 5000 OR el_id > 60000)))',
'el_to_path LIKE \'/%\' ESCAPE \'`\' ',
],
],