From ae57ab1eec9f9051fc0e6786e8eff9d01988be19 Mon Sep 17 00:00:00 2001 From: Max Semenik Date: Wed, 21 Oct 2009 19:53:03 +0000 Subject: [PATCH] (bug 20275) Fixed LIKE queries on SQLite backend * All manually built LIKE queries in the core are replaced with a wrapper function Database::buildLike() * This function automatically performs all escaping, so Database::escapeLike() is now almost never used --- RELEASE-NOTES | 1 + includes/AutoLoader.php | 1 + includes/Block.php | 2 +- includes/LinkFilter.php | 97 +++++++++++++++++++ includes/LogEventsList.php | 10 +- includes/MessageCache.php | 5 +- includes/Title.php | 3 +- includes/api/ApiQueryAllCategories.php | 2 +- includes/api/ApiQueryAllLinks.php | 2 +- includes/api/ApiQueryAllUsers.php | 2 +- includes/api/ApiQueryAllimages.php | 2 +- includes/api/ApiQueryAllpages.php | 2 +- includes/api/ApiQueryBlocks.php | 4 +- includes/api/ApiQueryExtLinksUsage.php | 9 +- includes/api/ApiQueryUserContributions.php | 2 +- includes/db/Database.php | 62 +++++++++++- includes/db/DatabaseSqlite.php | 8 ++ includes/filerepo/LocalRepo.php | 2 +- includes/specials/SpecialIpblocklist.php | 3 +- includes/specials/SpecialLinkSearch.php | 10 +- includes/specials/SpecialListfiles.php | 6 +- includes/specials/SpecialMovepage.php | 2 +- includes/specials/SpecialNewimages.php | 3 +- includes/specials/SpecialPrefixindex.php | 2 +- includes/specials/SpecialUndelete.php | 5 +- includes/specials/SpecialWithoutinterwiki.php | 2 +- maintenance/cleanupSpam.php | 6 +- maintenance/deleteSelfExternals.php | 4 +- maintenance/namespaceDupes.php | 3 +- maintenance/storage/compressOld.inc | 3 +- maintenance/storage/moveToExternal.php | 2 +- maintenance/storage/resolveStubs.php | 2 +- maintenance/storage/trackBlobs.php | 6 +- 33 files changed, 219 insertions(+), 56 deletions(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index ed1a868d7d..87f8e67e91 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -581,6 +581,7 @@ it from source control: http://www.mediawiki.org/wiki/Download_from_SVN dummy * (bug 21161) Changing $wgCacheEpoch now always invalidates file cache * (bug 20268) Fixed row count estimation on SQLite backend +* (bug 20275) Fixed LIKE queries on SQLite backend == API changes in 1.16 == diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index b24163dd59..5e2fce79f2 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -343,6 +343,7 @@ $wgAutoloadLocalClasses = array( 'LBFactory' => 'includes/db/LBFactory.php', 'LBFactory_Multi' => 'includes/db/LBFactory_Multi.php', 'LBFactory_Simple' => 'includes/db/LBFactory.php', + 'LikeMatch' => 'includes/db/Database.php', 'LoadBalancer' => 'includes/db/LoadBalancer.php', 'LoadMonitor' => 'includes/db/LoadMonitor.php', 'LoadMonitor_MySQL' => 'includes/db/LoadMonitor.php', diff --git a/includes/Block.php b/includes/Block.php index 7aa3065efb..02469080cf 100644 --- a/includes/Block.php +++ b/includes/Block.php @@ -286,7 +286,7 @@ class Block { $options = array(); $db =& $this->getDBOptions( $options ); $conds = array( - "ipb_range_start LIKE '$range%'", + 'ipb_range_start' . $db->buildLike( $range, $db->anyString() ), "ipb_range_start <= '$iaddr'", "ipb_range_end >= '$iaddr'" ); diff --git a/includes/LinkFilter.php b/includes/LinkFilter.php index dc4c125646..af90a9bf20 100644 --- a/includes/LinkFilter.php +++ b/includes/LinkFilter.php @@ -49,8 +49,10 @@ class LinkFilter { * @static * @param $filterEntry String: domainparts * @param $prot String: protocol + * @deprecated Use makeLikeArray() and pass result to Database::buildLike() instead */ public static function makeLike( $filterEntry , $prot = 'http://' ) { + wfDeprecated( __METHOD__ ); $db = wfGetDB( DB_MASTER ); if ( substr( $filterEntry, 0, 2 ) == '*.' ) { $subdomains = true; @@ -105,4 +107,99 @@ class LinkFilter { } return $like; } + + /** + * Make an array to be used for calls to Database::like(), which will match the specified + * string. There are several kinds of filter entry: + * *.domain.com - Produces http://com.domain.%, matches domain.com + * and www.domain.com + * domain.com - Produces http://com.domain./%, matches domain.com + * or domain.com/ but not www.domain.com + * *.domain.com/x - Produces http://com.domain.%/x%, matches + * www.domain.com/xy + * domain.com/x - Produces http://com.domain./x%, matches + * domain.com/xy but not www.domain.com/xy + * + * Asterisks in any other location are considered invalid. + * + * @static + * @param $filterEntry String: domainparts + * @param $prot String: protocol + */ + public static function makeLikeArray( $filterEntry , $prot = 'http://' ) { + $db = wfGetDB( DB_MASTER ); + if ( substr( $filterEntry, 0, 2 ) == '*.' ) { + $subdomains = true; + $filterEntry = substr( $filterEntry, 2 ); + if ( $filterEntry == '' ) { + // We don't want to make a clause that will match everything, + // that could be dangerous + return false; + } + } else { + $subdomains = false; + } + // No stray asterisks, that could cause confusion + // It's not simple or efficient to handle it properly so we don't + // handle it at all. + if ( strpos( $filterEntry, '*' ) !== false ) { + return false; + } + $slash = strpos( $filterEntry, '/' ); + if ( $slash !== false ) { + $path = substr( $filterEntry, $slash ); + $host = substr( $filterEntry, 0, $slash ); + } else { + $path = '/'; + $host = $filterEntry; + } + // Reverse the labels in the hostname, convert to lower case + // For emails reverse domainpart only + if ( $prot == 'mailto:' && strpos($host, '@') ) { + // complete email adress + $mailparts = explode( '@', $host ); + $domainpart = strtolower( implode( '.', array_reverse( explode( '.', $mailparts[1] ) ) ) ); + $host = $domainpart . '@' . $mailparts[0]; + $like = array( "$prot$host", $db->anyString() ); + } elseif ( $prot == 'mailto:' ) { + // domainpart of email adress only. do not add '.' + $host = strtolower( implode( '.', array_reverse( explode( '.', $host ) ) ) ); + $like = array( "$prot$host", $db->anyString() ); + } else { + $host = strtolower( implode( '.', array_reverse( explode( '.', $host ) ) ) ); + if ( substr( $host, -1, 1 ) !== '.' ) { + $host .= '.'; + } + $like = array( "$prot$host" ); + + if ( $subdomains ) { + $like[] = $db->anyString(); + } + if ( !$subdomains || $path !== '/' ) { + $like[] = $path; + $like[] = $db->anyString(); + } + } + return $like; + } + + /** + * Filters an array returned by makeLikeArray(), removing everything past first pattern placeholder. + * @static + * @param $arr array: array to filter + * @return filtered array + */ + public static function keepOneWildcard( $arr ) { + if( !is_array( $arr ) ) { + return $arr; + } + + foreach( $arr as $key => $value ) { + if ( $value instanceof LikeMatch ) { + return array_slice( $arr, 0, $key + 1 ); + } + } + + return $arr; + } } diff --git a/includes/LogEventsList.php b/includes/LogEventsList.php index fce4e9cc31..fcd86e4fb4 100644 --- a/includes/LogEventsList.php +++ b/includes/LogEventsList.php @@ -854,6 +854,8 @@ class LogPager extends ReverseChronologicalPager { $this->title = $title->getPrefixedText(); $ns = $title->getNamespace(); + $db = $this->mDb; + # Using the (log_namespace, log_title, log_timestamp) index with a # range scan (LIKE) on the first two parts, instead of simple equality, # makes it unusable for sorting. Sorted retrieval using another index @@ -866,10 +868,8 @@ class LogPager extends ReverseChronologicalPager { # log entries for even the busiest pages, so it can be safely scanned # in full to satisfy an impossible condition on user or similar. if( $pattern && !$wgMiserMode ) { - # use escapeLike to avoid expensive search patterns like 't%st%' - $safetitle = $this->mDb->escapeLike( $title->getDBkey() ); $this->mConds['log_namespace'] = $ns; - $this->mConds[] = "log_title LIKE '$safetitle%'"; + $this->mConds[] = 'log_title ' . $db->buildLike( $title->getDBkey(), $db->anyString() ); $this->pattern = $pattern; } else { $this->mConds['log_namespace'] = $ns; @@ -877,9 +877,9 @@ class LogPager extends ReverseChronologicalPager { } // Paranoia: avoid brute force searches (bug 17342) if( !$wgUser->isAllowed( 'deletedhistory' ) ) { - $this->mConds[] = $this->mDb->bitAnd('log_deleted', LogPage::DELETED_ACTION) . ' = 0'; + $this->mConds[] = $db->bitAnd('log_deleted', LogPage::DELETED_ACTION) . ' = 0'; } else if( !$wgUser->isAllowed( 'suppressrevision' ) ) { - $this->mConds[] = $this->mDb->bitAnd('log_deleted', LogPage::SUPPRESSED_ACTION) . + $this->mConds[] = $db->bitAnd('log_deleted', LogPage::SUPPRESSED_ACTION) . ' != ' . LogPage::SUPPRESSED_ACTION; } } diff --git a/includes/MessageCache.php b/includes/MessageCache.php index 2928117056..e5eb7057eb 100644 --- a/includes/MessageCache.php +++ b/includes/MessageCache.php @@ -318,12 +318,11 @@ class MessageCache { # database or in code. if ( $code !== $wgContLanguageCode ) { # Messages for particular language - $escapedCode = $dbr->escapeLike( $code ); - $conds[] = "page_title like '%%/$escapedCode'"; + $conds[] = 'page_title' . $dbr->buildLike( $dbr->anyString(), "/$code" ); } else { # Effectively disallows use of '/' character in NS_MEDIAWIKI for uses # other than language code. - $conds[] = "page_title not like '%%/%%'"; + $conds[] = 'page_title NOT' . $dbr->buildLike( $dbr->anyString(), '/', $dbr->anyString() ); } } diff --git a/includes/Title.php b/includes/Title.php index 422a50b554..44df7314b9 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -1663,8 +1663,7 @@ class Title { $dbr = wfGetDB( DB_SLAVE ); $conds['page_namespace'] = $this->getNamespace(); - $conds[] = 'page_title LIKE ' . $dbr->addQuotes( - $dbr->escapeLike( $this->getDBkey() ) . '/%' ); + $conds[] = 'page_title ' . $dbr->buildLike( $this->getDBkey() . '/', $dbr->anyString() ); $options = array(); if( $limit > -1 ) $options['LIMIT'] = $limit; diff --git a/includes/api/ApiQueryAllCategories.php b/includes/api/ApiQueryAllCategories.php index b8a9380dd5..b0927dc334 100644 --- a/includes/api/ApiQueryAllCategories.php +++ b/includes/api/ApiQueryAllCategories.php @@ -60,7 +60,7 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { $from = (is_null($params['from']) ? null : $this->titlePartToKey($params['from'])); $this->addWhereRange('cat_title', $dir, $from, null); if (isset ($params['prefix'])) - $this->addWhere("cat_title LIKE '" . $db->escapeLike($this->titlePartToKey($params['prefix'])) . "%'"); + $this->addWhere('cat_title' . $db->buildLike( $this->titlePartToKey($params['prefix']), $db->anyString() ) ); $this->addOption('LIMIT', $params['limit']+1); $this->addOption('ORDER BY', 'cat_title' . ($params['dir'] == 'descending' ? ' DESC' : '')); diff --git a/includes/api/ApiQueryAllLinks.php b/includes/api/ApiQueryAllLinks.php index 0f6e6bc69b..4df2373701 100644 --- a/includes/api/ApiQueryAllLinks.php +++ b/includes/api/ApiQueryAllLinks.php @@ -84,7 +84,7 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { if (!is_null($params['from'])) $this->addWhere('pl_title>=' . $db->addQuotes($this->titlePartToKey($params['from']))); if (isset ($params['prefix'])) - $this->addWhere("pl_title LIKE '" . $db->escapeLike($this->titlePartToKey($params['prefix'])) . "%'"); + $this->addWhere('pl_title' . $db->buildLike( $this->titlePartToKey($params['prefix']), $db->anyString() ) ); $this->addFields(array ( 'pl_title', diff --git a/includes/api/ApiQueryAllUsers.php b/includes/api/ApiQueryAllUsers.php index fd36b19bb3..21dcbb22a7 100644 --- a/includes/api/ApiQueryAllUsers.php +++ b/includes/api/ApiQueryAllUsers.php @@ -61,7 +61,7 @@ class ApiQueryAllUsers extends ApiQueryBase { $this->addWhere('u1.user_name >= ' . $db->addQuotes($this->keyToTitle($params['from']))); if (!is_null($params['prefix'])) - $this->addWhere('u1.user_name LIKE "' . $db->escapeLike($this->keyToTitle( $params['prefix'])) . '%"'); + $this->addWhere('u1.user_name' . $db->buildLike($this->keyToTitle($params['prefix']), $db->anyString())); if (!is_null($params['group'])) { // Filter only users that belong to a given group diff --git a/includes/api/ApiQueryAllimages.php b/includes/api/ApiQueryAllimages.php index 39a7ac73c1..e108a8fef3 100644 --- a/includes/api/ApiQueryAllimages.php +++ b/includes/api/ApiQueryAllimages.php @@ -76,7 +76,7 @@ class ApiQueryAllimages extends ApiQueryGeneratorBase { $from = (is_null($params['from']) ? null : $this->titlePartToKey($params['from'])); $this->addWhereRange('img_name', $dir, $from, null); if (isset ($params['prefix'])) - $this->addWhere("img_name LIKE '" . $db->escapeLike($this->titlePartToKey($params['prefix'])) . "%'"); + $this->addWhere('img_name' . $db->buildLike( $this->titlePartToKey($params['prefix']), $db->anyString() ) ); if (isset ($params['minsize'])) { $this->addWhere('img_size>=' . intval($params['minsize'])); diff --git a/includes/api/ApiQueryAllpages.php b/includes/api/ApiQueryAllpages.php index 439129063d..558efd4c16 100644 --- a/includes/api/ApiQueryAllpages.php +++ b/includes/api/ApiQueryAllpages.php @@ -65,7 +65,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { $from = (is_null($params['from']) ? null : $this->titlePartToKey($params['from'])); $this->addWhereRange('page_title', $dir, $from, null); if (isset ($params['prefix'])) - $this->addWhere("page_title LIKE '" . $db->escapeLike($this->titlePartToKey($params['prefix'])) . "%'"); + $this->addWhere('page_title' . $db->buildLike($this->titlePartToKey($params['prefix']), $db->->anyString())); if (is_null($resultPageSet)) { $selectFields = array ( diff --git a/includes/api/ApiQueryBlocks.php b/includes/api/ApiQueryBlocks.php index f2f4a7f420..62de51cf29 100644 --- a/includes/api/ApiQueryBlocks.php +++ b/includes/api/ApiQueryBlocks.php @@ -109,8 +109,10 @@ class ApiQueryBlocks extends ApiQueryBase { else $lower = $upper = IP::toHex($params['ip']); $prefix = substr($lower, 0, 4); + + $db = $this->getDB(); $this->addWhere(array( - "ipb_range_start LIKE '$prefix%'", + 'ipb_range_start' . $db->buildLike($prefix, $db->anyString()), "ipb_range_start <= '$lower'", "ipb_range_end >= '$upper'" )); diff --git a/includes/api/ApiQueryExtLinksUsage.php b/includes/api/ApiQueryExtLinksUsage.php index 8d76e8b2ea..537a04d187 100644 --- a/includes/api/ApiQueryExtLinksUsage.php +++ b/includes/api/ApiQueryExtLinksUsage.php @@ -77,14 +77,15 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { if(is_null($protocol)) $protocol = 'http://'; - $likeQuery = LinkFilter::makeLike($query, $protocol); + $likeQuery = LinkFilter::makeLikeArray($query, $protocol); if (!$likeQuery) $this->dieUsage('Invalid query', 'bad_query'); - $likeQuery = substr($likeQuery, 0, strpos($likeQuery,'%')+1); - $this->addWhere('el_index LIKE ' . $db->addQuotes( $likeQuery )); + + $likeQuery = LinkFilter::keepOneWildcard($likeQuery); + $this->addWhere('el_index ' . $db->buildLike( $likeQuery )); } else if(!is_null($protocol)) - $this->addWhere('el_index LIKE ' . $db->addQuotes( "$protocol%" )); + $this->addWhere('el_index ' . $db->buildLike( "$protocol", $db->anyString() )); $prop = array_flip($params['prop']); $fld_ids = isset($prop['ids']); diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php index 9f739ecaba..756805c506 100644 --- a/includes/api/ApiQueryUserContributions.php +++ b/includes/api/ApiQueryUserContributions.php @@ -165,7 +165,7 @@ class ApiQueryContributions extends ApiQueryBase { $this->addWhere($this->getDB()->bitAnd('rev_deleted',Revision::DELETED_USER) . ' = 0'); // We only want pages by the specified users. if($this->prefixMode) - $this->addWhere("rev_user_text LIKE '" . $this->getDB()->escapeLike($this->userprefix) . "%'"); + $this->addWhere('rev_user_text' . $this->getDB()->buildLike($this->userprefix, $this->getDB()->anyString())); else $this->addWhereFld('rev_user_text', $this->usernames); // ... and in the specified timeframe. diff --git a/includes/db/Database.php b/includes/db/Database.php index e9e87535da..e3d390653e 100644 --- a/includes/db/Database.php +++ b/includes/db/Database.php @@ -1490,7 +1490,9 @@ abstract class DatabaseBase { } /** - * Escape string for safe LIKE usage + * Escape string for safe LIKE usage. + * WARNING: you should almost never use this function directly, + * instead use buildLike() that escapes everything automatically */ function escapeLike( $s ) { $s = str_replace( '\\', '\\\\', $s ); @@ -1499,6 +1501,48 @@ abstract class DatabaseBase { return $s; } + /** + * LIKE statement wrapper, receives a variable-length argument list with parts of pattern to match + * containing either string literals that will be escaped or tokens returned by anyChar() or anyString(). + * Alternatively, the function could be provided with an array of aforementioned parameters. + * + * Example: $dbr->buildLike( 'My_page_title/', $dbr->anyString() ) returns a LIKE clause that searches + * for subpages of 'My page title'. + * Alternatively: $pattern = array( 'My_page_title/', $dbr->anyString() ); $query .= $dbr->buildLike( $pattern ); + * + * @ return String: fully built LIKE statement + */ + function buildLike() { + $params = func_get_args(); + if (count($params) > 0 && is_array($params[0])) { + $params = $params[0]; + } + + $s = ''; + foreach( $params as $value) { + if( $value instanceof LikeMatch ) { + $s .= $value->toString(); + } else { + $s .= $this->escapeLike( $value ); + } + } + return " LIKE '" . $s . "' "; + } + + /** + * Returns a token for buildLike() that denotes a '_' to be used in a LIKE query + */ + function anyChar() { + return new LikeMatch( '_' ); + } + + /** + * Returns a token for buildLike() that denotes a '%' to be used in a LIKE query + */ + function anyString() { + return new LikeMatch( '%' ); + } + /** * Returns an appropriately quoted sequence value for inserting a new row. * MySQL has autoincrement fields, so this is just NULL. But the PostgreSQL @@ -2779,3 +2823,19 @@ class ResultWrapper implements Iterator { return $this->current() !== false; } } + +/** + * Used by DatabaseBase::buildLike() to represent characters that have special meaning in SQL LIKE clauses + * and thus need no escaping. Don't instantiate it manually, use Database::anyChar() and anyString() instead. + */ +class LikeMatch { + private $str; + + public function __construct( $s ) { + $this->str = $s; + } + + public function toString() { + return $this->str; + } +} \ No newline at end of file diff --git a/includes/db/DatabaseSqlite.php b/includes/db/DatabaseSqlite.php index 720c3a1135..138e9ca25e 100644 --- a/includes/db/DatabaseSqlite.php +++ b/includes/db/DatabaseSqlite.php @@ -418,6 +418,14 @@ class DatabaseSqlite extends DatabaseBase { return $s; } + function buildLike() { + $params = func_get_args(); + if ( count( $params ) > 0 && is_array( $params[0] ) ) { + $params = $params[0]; + } + return parent::buildLike( $params ) . "ESCAPE '\' "; + } + /** * How lagged is this slave? */ diff --git a/includes/filerepo/LocalRepo.php b/includes/filerepo/LocalRepo.php index 3eca504831..6c4d21a26c 100644 --- a/includes/filerepo/LocalRepo.php +++ b/includes/filerepo/LocalRepo.php @@ -49,7 +49,7 @@ class LocalRepo extends FSRepo { $ext = File::normalizeExtension($ext); $inuse = $dbw->selectField( 'oldimage', '1', array( 'oi_sha1' => $sha1, - "oi_archive_name LIKE '%.{$ext}'", + 'oi_archive_name ' . $dbw->buildLike( $dbw->anyString(), ".$ext" ), $dbw->bitAnd('oi_deleted', File::DELETED_FILE) => File::DELETED_FILE ), __METHOD__, array( 'FOR UPDATE' ) ); } diff --git a/includes/specials/SpecialIpblocklist.php b/includes/specials/SpecialIpblocklist.php index e017cec12c..c81ae49eea 100644 --- a/includes/specials/SpecialIpblocklist.php +++ b/includes/specials/SpecialIpblocklist.php @@ -262,10 +262,9 @@ class IPUnblockForm { // Fixme -- encapsulate this sort of query-building. $dbr = wfGetDB( DB_SLAVE ); $encIp = $dbr->addQuotes( IP::sanitizeIP($this->ip) ); - $encRange = $dbr->addQuotes( "$range%" ); $encAddr = $dbr->addQuotes( $iaddr ); $conds[] = "(ipb_address = $encIp) OR - (ipb_range_start LIKE $encRange AND + (ipb_range_start" . $dbr->buildLike( $range, $dbr->anyString() ) . " AND ipb_range_start <= $encAddr AND ipb_range_end >= $encAddr)"; } else { diff --git a/includes/specials/SpecialLinkSearch.php b/includes/specials/SpecialLinkSearch.php index 5c343ed3ea..8df21a6bbf 100644 --- a/includes/specials/SpecialLinkSearch.php +++ b/includes/specials/SpecialLinkSearch.php @@ -96,11 +96,11 @@ class LinkSearchPage extends QueryPage { */ static function mungeQuery( $query , $prot ) { $field = 'el_index'; - $rv = LinkFilter::makeLike( $query , $prot ); + $rv = LinkFilter::makeLikeArray( $query , $prot ); if ($rv === false) { //makeLike doesn't handle wildcard in IP, so we'll have to munge here. if (preg_match('/^(:?[0-9]{1,3}\.)+\*\s*$|^(:?[0-9]{1,3}\.){3}[0-9]{1,3}:[0-9]*\*\s*$/', $query)) { - $rv = $prot . rtrim($query, " \t*") . '%'; + $rv = array( $prot . rtrim($query, " \t*"), $dbr->anyString() ); $field = 'el_to'; } } @@ -125,8 +125,8 @@ class LinkSearchPage extends QueryPage { /* strip everything past first wildcard, so that index-based-only lookup would be done */ list( $munged, $clause ) = self::mungeQuery( $this->mQuery, $this->mProt ); - $stripped = substr($munged,0,strpos($munged,'%')+1); - $encSearch = $dbr->addQuotes( $stripped ); + $stripped = LinkFilter::keepOneWildcard( $munged ); + $like = $dbr->buildLike( $stripped ); $encSQL = ''; if ( isset ($this->mNs) && !$wgMiserMode ) @@ -144,7 +144,7 @@ class LinkSearchPage extends QueryPage { $externallinks $use_index WHERE page_id=el_from - AND $clause LIKE $encSearch + AND $clause $like $encSQL"; } diff --git a/includes/specials/SpecialListfiles.php b/includes/specials/SpecialListfiles.php index ae19ea9ad7..788009764f 100644 --- a/includes/specials/SpecialListfiles.php +++ b/includes/specials/SpecialListfiles.php @@ -37,10 +37,8 @@ class ImageListPager extends TablePager { $nt = Title::newFromUrl( $search ); if( $nt ) { $dbr = wfGetDB( DB_SLAVE ); - $m = $dbr->strencode( strtolower( $nt->getDBkey() ) ); - $m = str_replace( "%", "\\%", $m ); - $m = str_replace( "_", "\\_", $m ); - $this->mQueryConds = array( "LOWER(img_name) LIKE '%{$m}%'" ); + $this->mQueryConds = array( 'LOWER(img_name)' . $dbr->buildLike( $dbr->anyString(), + strtolower( $nt->getDBkey() ), $dbr->anyString() ) ); } } diff --git a/includes/specials/SpecialMovepage.php b/includes/specials/SpecialMovepage.php index 6ee29ccafe..ed5d03cc53 100644 --- a/includes/specials/SpecialMovepage.php +++ b/includes/specials/SpecialMovepage.php @@ -416,7 +416,7 @@ class MovePageForm { ) ) ) { $conds = array( - 'page_title LIKE '.$dbr->addQuotes( $dbr->escapeLike( $ot->getDBkey() ) . '/%' ) + 'page_title' . $dbr->buildLike( $ot->getDBkey() . '/', $dbr->anyString() ) .' OR page_title = ' . $dbr->addQuotes( $ot->getDBkey() ) ); $conds['page_namespace'] = array(); diff --git a/includes/specials/SpecialNewimages.php b/includes/specials/SpecialNewimages.php index 96fea669db..61f5230c33 100644 --- a/includes/specials/SpecialNewimages.php +++ b/includes/specials/SpecialNewimages.php @@ -69,8 +69,7 @@ function wfSpecialNewimages( $par, $specialPage ) { if ( $wpIlMatch != '' && !$wgMiserMode) { $nt = Title::newFromUrl( $wpIlMatch ); if( $nt ) { - $m = $dbr->escapeLike( strtolower( $nt->getDBkey() ) ); - $where[] = "LOWER(img_name) LIKE '%{$m}%'"; + $where[] = 'LOWER(img_name) ' . $dbr->buildLike( $dbr->anyString(), strtolower( $nt->getDBkey() ), $dbr->anyString() ); $searchpar['wpIlMatch'] = $wpIlMatch; } } diff --git a/includes/specials/SpecialPrefixindex.php b/includes/specials/SpecialPrefixindex.php index 3a6b3c2d0d..8b5f0c93ba 100644 --- a/includes/specials/SpecialPrefixindex.php +++ b/includes/specials/SpecialPrefixindex.php @@ -115,7 +115,7 @@ class SpecialPrefixindex extends SpecialAllpages { array( 'page_namespace', 'page_title', 'page_is_redirect' ), array( 'page_namespace' => $namespace, - 'page_title LIKE \'' . $dbr->escapeLike( $prefixKey ) .'%\'', + 'page_title' . $dbr->buildLike( $prefixKey, $dbr->anyString() ), 'page_title >= ' . $dbr->addQuotes( $fromKey ), ), __METHOD__, diff --git a/includes/specials/SpecialUndelete.php b/includes/specials/SpecialUndelete.php index fd9801b22c..0733d05fee 100644 --- a/includes/specials/SpecialUndelete.php +++ b/includes/specials/SpecialUndelete.php @@ -58,16 +58,15 @@ class PageArchive { $title = Title::newFromText( $prefix ); if( $title ) { $ns = $title->getNamespace(); - $encPrefix = $dbr->escapeLike( $title->getDBkey() ); + $prefix = $title->getDBkey(); } else { // Prolly won't work too good // @todo handle bare namespace names cleanly? $ns = 0; - $encPrefix = $dbr->escapeLike( $prefix ); } $conds = array( 'ar_namespace' => $ns, - "ar_title LIKE '$encPrefix%'", + 'ar_title' . $dbr->buildLike( $prefix, $dbr->anyString() ), ); return self::listPages( $dbr, $conds ); } diff --git a/includes/specials/SpecialWithoutinterwiki.php b/includes/specials/SpecialWithoutinterwiki.php index 740b43466f..a5d60d2f2e 100644 --- a/includes/specials/SpecialWithoutinterwiki.php +++ b/includes/specials/SpecialWithoutinterwiki.php @@ -53,7 +53,7 @@ class WithoutInterwikiPage extends PageQueryPage { function getSQL() { $dbr = wfGetDB( DB_SLAVE ); list( $page, $langlinks ) = $dbr->tableNamesN( 'page', 'langlinks' ); - $prefix = $this->prefix ? "AND page_title LIKE '" . $dbr->escapeLike( $this->prefix ) . "%'" : ''; + $prefix = $this->prefix ? 'AND page_title' . $dbr->buildLike( $this->prefix , $dbr->anyString() ) : ''; return "SELECT 'Withoutinterwiki' AS type, page_namespace AS namespace, diff --git a/maintenance/cleanupSpam.php b/maintenance/cleanupSpam.php index fb76e31758..d1927f9006 100644 --- a/maintenance/cleanupSpam.php +++ b/maintenance/cleanupSpam.php @@ -40,7 +40,7 @@ class CleanupSpam extends Maintenance { $wgUser->addToDatabase(); } $spec = $this->getArg(); - $like = LinkFilter::makeLike( $spec ); + $like = LinkFilter::makeLikeArray( $spec ); if ( !$like ) { $this->error( "Not a valid hostname specification: $spec", true ); } @@ -53,7 +53,7 @@ class CleanupSpam extends Maintenance { $dbr = wfGetDB( DB_SLAVE, array(), $wikiID ); $count = $dbr->selectField( 'externallinks', 'COUNT(*)', - array( 'el_index LIKE ' . $dbr->addQuotes( $like ) ), __METHOD__ ); + array( 'el_index' . $dbr->buildLike( $like ) ), __METHOD__ ); if ( $count ) { $found = true; passthru( "php cleanupSpam.php --wiki='$wikiID' $spec | sed 's/^/$wikiID: /'" ); @@ -69,7 +69,7 @@ class CleanupSpam extends Maintenance { $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( 'externallinks', array( 'DISTINCT el_from' ), - array( 'el_index LIKE ' . $dbr->addQuotes( $like ) ), __METHOD__ ); + array( 'el_index' . $dbr->buildLike( $like ) ), __METHOD__ ); $count = $dbr->numRows( $res ); $this->output( "Found $count articles containing $spec\n" ); foreach ( $res as $row ) { diff --git a/maintenance/deleteSelfExternals.php b/maintenance/deleteSelfExternals.php index e2e9789598..7beda5dcc2 100644 --- a/maintenance/deleteSelfExternals.php +++ b/maintenance/deleteSelfExternals.php @@ -7,8 +7,8 @@ $db = wfGetDB(DB_MASTER); while (1) { wfWaitForSlaves( 2 ); $db->commit(); - $encServer = $db->escapeLike( $wgServer ); - $q="DELETE /* deleteSelfExternals */ FROM externallinks WHERE el_to LIKE '$encServer/%' LIMIT 1000\n"; + $q = $db->limitResult( "DELETE /* deleteSelfExternals */ FROM externallinks WHERE el_to" + . $db->buildLike( $wgServer . '/', $db->anyString() ), 1000 ); print "Deleting a batch\n"; $db->query($q); if (!$db->affectedRows()) exit(0); diff --git a/maintenance/namespaceDupes.php b/maintenance/namespaceDupes.php index b17011e3a6..0e48a6713d 100644 --- a/maintenance/namespaceDupes.php +++ b/maintenance/namespaceDupes.php @@ -197,7 +197,6 @@ class NamespaceConflictChecker extends Maintenance { $table = $this->db->tableName( $page ); $prefix = $this->db->strencode( $name ); - $likeprefix = str_replace( '_', '\\_', $prefix); $encNamespace = $this->db->addQuotes( $ns ); $titleSql = "TRIM(LEADING '$prefix:' FROM {$page}_title)"; @@ -212,7 +211,7 @@ class NamespaceConflictChecker extends Maintenance { $titleSql AS title FROM {$table} WHERE {$page}_namespace=0 - AND {$page}_title LIKE '$likeprefix:%'"; + AND {$page}_title " . $this->db->buildLike( $name . ':', $this-db->anyString() ); $result = $this->db->query( $sql, __METHOD__ ); diff --git a/maintenance/storage/compressOld.inc b/maintenance/storage/compressOld.inc index d79354f5d4..60096ff75c 100644 --- a/maintenance/storage/compressOld.inc +++ b/maintenance/storage/compressOld.inc @@ -105,7 +105,8 @@ function compressWithConcat( $startId, $maxChunkSize, $beginDate, # overwriting bulk storage concat rows. Don't compress external references, because # the script doesn't yet delete rows from external storage. $conds = array( - "old_flags NOT LIKE '%object%' AND old_flags NOT LIKE '%external%'"); + 'old_flags NOT ' . $dbr->buildLike( MATCH_STRING, 'object', MATCH_STRING ) . ' AND old_flags NOT ' + . $dbr->buildLike( MATCH_STRING, 'external', MATCH_STRING ) ); if ( $beginDate ) { if ( !preg_match( '/^\d{14}$/', $beginDate ) ) { diff --git a/maintenance/storage/moveToExternal.php b/maintenance/storage/moveToExternal.php index a8b2f93bc5..dc11856aaa 100644 --- a/maintenance/storage/moveToExternal.php +++ b/maintenance/storage/moveToExternal.php @@ -62,7 +62,7 @@ function moveToExternal( $cluster, $maxID, $minID = 1 ) { $res = $dbr->select( 'text', array( 'old_id', 'old_flags', 'old_text' ), array( "old_id BETWEEN $blockStart AND $blockEnd", - "old_flags NOT LIKE '%external%'", + 'old_flags NOT ' . $dbr->buildLike( $dbr->anyString(), 'external', $dbr->anyString() ), ), $fname ); while ( $row = $dbr->fetchObject( $res ) ) { # Resolve stubs diff --git a/maintenance/storage/resolveStubs.php b/maintenance/storage/resolveStubs.php index 3db9e48027..0e51f0b098 100644 --- a/maintenance/storage/resolveStubs.php +++ b/maintenance/storage/resolveStubs.php @@ -69,7 +69,7 @@ function resolveStub( $id, $stubText, $flags ) { # Get the (maybe) external row $externalRow = $dbr->selectRow( 'text', array( 'old_text' ), - array( 'old_id' => $stub->mOldId, "old_flags LIKE '%external%'" ), + array( 'old_id' => $stub->mOldId, 'old_flags' . $dbr->buildLike( $dbr->anyString(), 'external', $dbr->anyString() ) ), $fname ); diff --git a/maintenance/storage/trackBlobs.php b/maintenance/storage/trackBlobs.php index 8ac9474818..5f25e390ea 100644 --- a/maintenance/storage/trackBlobs.php +++ b/maintenance/storage/trackBlobs.php @@ -60,7 +60,7 @@ class TrackBlobs { if ( $this->textClause != '' ) { $this->textClause .= ' OR '; } - $this->textClause .= 'old_text LIKE ' . $dbr->addQuotes( $dbr->escapeLike( "DB://$cluster/" ) . '%' ); + $this->textClause .= 'old_text' . $dbr->buildLike( "DB://$cluster/", $dbr->anyString() ); } } return $this->textClause; @@ -99,7 +99,7 @@ class TrackBlobs { 'rev_id > ' . $dbr->addQuotes( $startId ), 'rev_text_id=old_id', $textClause, - "old_flags LIKE '%external%'", + 'old_flags ' . $dbr->buildLike( $dbr->anyString(), 'external', $dbr->anyString() ), ), __METHOD__, array( @@ -175,7 +175,7 @@ class TrackBlobs { array( 'old_id>' . $dbr->addQuotes( $startId ), $textClause, - "old_flags LIKE '%external%'", + 'old_flags ' . $dbr->buildLike( $dbr->anyString(), 'external', $dbr->anyString() ), 'bt_text_id IS NULL' ), __METHOD__, -- 2.20.1