summaryrefslogtreecommitdiff
path: root/includes/search
diff options
context:
space:
mode:
authorPierre Schmitz <pierre@archlinux.de>2014-12-27 15:41:37 +0100
committerPierre Schmitz <pierre@archlinux.de>2014-12-31 11:43:28 +0100
commitc1f9b1f7b1b77776192048005dcc66dcf3df2bfb (patch)
tree2b38796e738dd74cb42ecd9bfd151803108386bc /includes/search
parentb88ab0086858470dd1f644e64cb4e4f62bb2be9b (diff)
Update to MediaWiki 1.24.1
Diffstat (limited to 'includes/search')
-rw-r--r--includes/search/SearchDatabase.php57
-rw-r--r--includes/search/SearchEngine.php1116
-rw-r--r--includes/search/SearchHighlighter.php575
-rw-r--r--includes/search/SearchMssql.php110
-rw-r--r--includes/search/SearchMySQL.php122
-rw-r--r--includes/search/SearchOracle.php77
-rw-r--r--includes/search/SearchPostgres.php89
-rw-r--r--includes/search/SearchResult.php237
-rw-r--r--includes/search/SearchResultSet.php211
-rw-r--r--includes/search/SearchSqlite.php116
-rw-r--r--includes/search/SearchUpdate.php185
11 files changed, 1324 insertions, 1571 deletions
diff --git a/includes/search/SearchDatabase.php b/includes/search/SearchDatabase.php
new file mode 100644
index 00000000..82d09073
--- /dev/null
+++ b/includes/search/SearchDatabase.php
@@ -0,0 +1,57 @@
+<?php
+/**
+ * Database search engine
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Search
+ */
+
+/**
+ * Base search engine base class for database-backed searches
+ * @ingroup Search
+ * @since 1.23
+ */
+class SearchDatabase extends SearchEngine {
+ /**
+ * @var DatabaseBase Slave database for reading from for results
+ */
+ protected $db;
+
+ /**
+ * Constructor
+ * @param DatabaseBase $db The database to search from
+ */
+ public function __construct( DatabaseBase $db = null ) {
+ if ( $db ) {
+ $this->db = $db;
+ } else {
+ $this->db = wfGetDB( DB_SLAVE );
+ }
+ }
+
+ /**
+ * Return a 'cleaned up' search string
+ *
+ * @param string $text
+ * @return string
+ */
+ protected function filter( $text ) {
+ $lc = $this->legalSearchChars();
+ return trim( preg_replace( "/[^{$lc}]/", " ", $text ) );
+ }
+}
diff --git a/includes/search/SearchEngine.php b/includes/search/SearchEngine.php
index 71c05d8b..0eb87e4a 100644
--- a/includes/search/SearchEngine.php
+++ b/includes/search/SearchEngine.php
@@ -30,35 +30,33 @@
* @ingroup Search
*/
class SearchEngine {
- var $limit = 10;
- var $offset = 0;
- var $prefix = '';
- var $searchTerms = array();
- var $namespaces = array( NS_MAIN );
- var $showRedirects = false;
-
- /// Feature values
- protected $features = array();
+ /** @var string */
+ public $prefix = '';
- /**
- * @var DatabaseBase
- */
- protected $db;
+ /** @var int[] */
+ public $namespaces = array( NS_MAIN );
- function __construct( $db = null ) {
- if ( $db ) {
- $this->db = $db;
- } else {
- $this->db = wfGetDB( DB_SLAVE );
- }
- }
+ /** @var int */
+ protected $limit = 10;
+
+ /** @var int */
+ protected $offset = 0;
+
+ /** @var array|string */
+ protected $searchTerms = array();
+
+ /** @var bool */
+ protected $showSuggestion = true;
+
+ /** @var array Feature values */
+ protected $features = array();
/**
* Perform a full text search query and return a result set.
* If title searches are not supported or disabled, return null.
* STUB
*
- * @param string $term raw search term
+ * @param string $term Raw search term
* @return SearchResultSet|Status|null
*/
function searchText( $term ) {
@@ -70,7 +68,7 @@ class SearchEngine {
* If title searches are not supported or disabled, return null.
* STUB
*
- * @param string $term raw search term
+ * @param string $term Raw search term
* @return SearchResultSet|null
*/
function searchTitle( $term ) {
@@ -78,23 +76,12 @@ class SearchEngine {
}
/**
- * If this search backend can list/unlist redirects
- * @deprecated since 1.18 Call supports( 'list-redirects' );
- * @return bool
- */
- function acceptListRedirects() {
- wfDeprecated( __METHOD__, '1.18' );
- return $this->supports( 'list-redirects' );
- }
-
- /**
* @since 1.18
- * @param $feature String
- * @return Boolean
+ * @param string $feature
+ * @return bool
*/
public function supports( $feature ) {
switch ( $feature ) {
- case 'list-redirects':
case 'search-update':
return true;
case 'title-suffix-filter':
@@ -106,8 +93,8 @@ class SearchEngine {
/**
* Way to pass custom data for engines
* @since 1.18
- * @param $feature String
- * @param $data Mixed
+ * @param string $feature
+ * @param mixed $data
* @return bool
*/
public function setFeatureData( $feature, $data ) {
@@ -130,8 +117,11 @@ class SearchEngine {
}
/**
- * Transform search term in cases when parts of the query came as different GET params (when supported)
- * e.g. for prefix queries: search=test&prefix=Main_Page/Archive -> test prefix:Main Page/Archive
+ * Transform search term in cases when parts of the query came as different
+ * GET params (when supported), e.g. for prefix queries:
+ * search=test&prefix=Main_Page/Archive -> test prefix:Main Page/Archive
+ * @param string $term
+ * @return string
*/
function transformSearchTerm( $term ) {
return $term;
@@ -141,7 +131,7 @@ class SearchEngine {
* If an exact title match can be found, or a very slightly close match,
* return the title. If no match, returns NULL.
*
- * @param $searchterm String
+ * @param string $searchterm
* @return Title
*/
public static function getNearMatch( $searchterm ) {
@@ -155,7 +145,7 @@ class SearchEngine {
* Do a near match (see SearchEngine::getNearMatch) and wrap it into a
* SearchResultSet.
*
- * @param $searchterm string
+ * @param string $searchterm
* @return SearchResultSet
*/
public static function getNearMatchResultSet( $searchterm ) {
@@ -164,6 +154,7 @@ class SearchEngine {
/**
* Really find the title match.
+ * @param string $searchterm
* @return null|Title
*/
private static function getNearMatchInternal( $searchterm ) {
@@ -172,7 +163,10 @@ class SearchEngine {
$allSearchTerms = array( $searchterm );
if ( $wgContLang->hasVariants() ) {
- $allSearchTerms = array_merge( $allSearchTerms, $wgContLang->autoConvertToAllVariants( $searchterm ) );
+ $allSearchTerms = array_merge(
+ $allSearchTerms,
+ $wgContLang->autoConvertToAllVariants( $searchterm )
+ );
}
$titleResult = null;
@@ -286,8 +280,8 @@ class SearchEngine {
* Set the maximum number of results to return
* and how many to skip before returning the first.
*
- * @param $limit Integer
- * @param $offset Integer
+ * @param int $limit
+ * @param int $offset
*/
function setLimitOffset( $limit, $offset = 0 ) {
$this->limit = intval( $limit );
@@ -298,17 +292,28 @@ class SearchEngine {
* Set which namespaces the search should include.
* Give an array of namespace index numbers.
*
- * @param $namespaces Array
+ * @param array $namespaces
*/
function setNamespaces( $namespaces ) {
$this->namespaces = $namespaces;
}
/**
+ * Set whether the searcher should try to build a suggestion. Note: some searchers
+ * don't support building a suggestion in the first place and others don't respect
+ * this flag.
+ *
+ * @param bool $showSuggestion Should the searcher try to build suggestions
+ */
+ function setShowSuggestion( $showSuggestion ) {
+ $this->showSuggestion = $showSuggestion;
+ }
+
+ /**
* Parse some common prefixes: all (search everything)
* or namespace names
*
- * @param $query String
+ * @param string $query
* @return string
*/
function replacePrefixes( $query ) {
@@ -316,7 +321,6 @@ class SearchEngine {
$parsed = $query;
if ( strpos( $query, ':' ) === false ) { // nothing to do
- wfRunHooks( 'SearchEngineReplacePrefixesComplete', array( $this, $query, &$parsed ) );
return $parsed;
}
@@ -325,7 +329,7 @@ class SearchEngine {
$this->namespaces = null;
$parsed = substr( $query, strlen( $allkeyword ) );
} elseif ( strpos( $query, ':' ) !== false ) {
- $prefix = substr( $query, 0, strpos( $query, ':' ) );
+ $prefix = str_replace( ' ', '_', substr( $query, 0, strpos( $query, ':' ) ) );
$index = $wgContLang->getNsIndex( $prefix );
if ( $index !== false ) {
$this->namespaces = array( $index );
@@ -336,14 +340,12 @@ class SearchEngine {
$parsed = $query; // prefix was the whole query
}
- wfRunHooks( 'SearchEngineReplacePrefixesComplete', array( $this, $query, &$parsed ) );
-
return $parsed;
}
/**
* Make a list of searchable namespaces and their canonical names.
- * @return Array
+ * @return array
*/
public static function searchableNamespaces() {
global $wgContLang;
@@ -362,24 +364,12 @@ class SearchEngine {
* Extract default namespaces to search from the given user's
* settings, returning a list of index numbers.
*
- * @param $user User
- * @return Array
+ * @param user $user
+ * @return array
*/
public static function userNamespaces( $user ) {
- global $wgSearchEverythingOnlyLoggedIn;
-
- $searchableNamespaces = SearchEngine::searchableNamespaces();
-
- // get search everything preference, that can be set to be read for logged-in users
- // it overrides other options
- if ( !$wgSearchEverythingOnlyLoggedIn || $user->isLoggedIn() ) {
- if ( $user->getOption( 'searcheverything' ) ) {
- return array_keys( $searchableNamespaces );
- }
- }
-
$arr = array();
- foreach ( $searchableNamespaces as $ns => $name ) {
+ foreach ( SearchEngine::searchableNamespaces() as $ns => $name ) {
if ( $user->getOption( 'searchNs' . $ns ) ) {
$arr[] = $ns;
}
@@ -391,7 +381,7 @@ class SearchEngine {
/**
* Find snippet highlight settings for all users
*
- * @return Array contextlines, contextchars
+ * @return array Contextlines, contextchars
*/
public static function userHighlightPrefs() {
$contextlines = 2; // Hardcode this. Old defaults sucked. :)
@@ -402,7 +392,7 @@ class SearchEngine {
/**
* An array of namespaces indexes to be searched by default
*
- * @return Array
+ * @return array
*/
public static function defaultNamespaces() {
global $wgNamespacesToBeSearchedDefault;
@@ -414,7 +404,7 @@ class SearchEngine {
* Get a list of namespace names useful for showing in tooltips
* and preferences
*
- * @param $namespaces Array
+ * @param array $namespaces
* @return array
*/
public static function namespacesAsText( $namespaces ) {
@@ -430,31 +420,10 @@ class SearchEngine {
}
/**
- * Return the help namespaces to be shown on Special:Search
- *
- * @return Array
- */
- public static function helpNamespaces() {
- global $wgNamespacesToBeSearchedHelp;
-
- return array_keys( $wgNamespacesToBeSearchedHelp, true );
- }
-
- /**
- * Return a 'cleaned up' search string
- *
- * @param $text String
- * @return String
- */
- function filter( $text ) {
- $lc = $this->legalSearchChars();
- return trim( preg_replace( "/[^{$lc}]/", " ", $text ) );
- }
- /**
* Load up the appropriate search engine class for the currently
* active database backend, and return a configured instance.
*
- * @param String $type Type of search backend, if not the default
+ * @param string $type Type of search backend, if not the default
* @return SearchEngine
*/
public static function create( $type = null ) {
@@ -473,7 +442,6 @@ class SearchEngine {
}
$search = new $class( $dbr );
- $search->setLimitOffset( 0, 0 );
return $search;
}
@@ -485,11 +453,10 @@ class SearchEngine {
*/
public static function getSearchTypes() {
global $wgSearchType, $wgSearchTypeAlternatives;
- static $alternatives = null;
- if ( $alternatives === null ) {
- $alternatives = $wgSearchTypeAlternatives ?: array();
- array_unshift( $alternatives, $wgSearchType );
- }
+
+ $alternatives = $wgSearchTypeAlternatives ?: array();
+ array_unshift( $alternatives, $wgSearchType );
+
return $alternatives;
}
@@ -498,9 +465,9 @@ class SearchEngine {
* Title and text should be pre-processed.
* STUB
*
- * @param $id Integer
- * @param $title String
- * @param $text String
+ * @param int $id
+ * @param string $title
+ * @param string $text
*/
function update( $id, $title, $text ) {
// no-op
@@ -511,8 +478,8 @@ class SearchEngine {
* Title should be pre-processed.
* STUB
*
- * @param $id Integer
- * @param $title String
+ * @param int $id
+ * @param string $title
*/
function updateTitle( $id, $title ) {
// no-op
@@ -523,8 +490,8 @@ class SearchEngine {
* Title should be pre-processed.
* STUB
*
- * @param Integer $id Page id that was deleted
- * @param String $title Title of page that was deleted
+ * @param int $id Page id that was deleted
+ * @param string $title Title of page that was deleted
*/
function delete( $id, $title ) {
// no-op
@@ -533,10 +500,11 @@ class SearchEngine {
/**
* Get OpenSearch suggestion template
*
- * @return String
+ * @return string
*/
public static function getOpenSearchTemplate() {
global $wgOpenSearchTemplate, $wgCanonicalServer;
+
if ( $wgOpenSearchTemplate ) {
return $wgOpenSearchTemplate;
} else {
@@ -544,7 +512,9 @@ class SearchEngine {
if ( !$ns ) {
$ns = "0";
}
- return $wgCanonicalServer . wfScript( 'api' ) . '?action=opensearch&search={searchTerms}&namespace=' . $ns;
+
+ return $wgCanonicalServer . wfScript( 'api' )
+ . '?action=opensearch&search={searchTerms}&namespace=' . $ns;
}
}
@@ -575,940 +545,6 @@ class SearchEngine {
}
/**
- * @ingroup Search
- */
-class SearchResultSet {
- /**
- * Fetch an array of regular expression fragments for matching
- * the search terms as parsed by this engine in a text extract.
- * STUB
- *
- * @return Array
- */
- function termMatches() {
- return array();
- }
-
- function numRows() {
- return 0;
- }
-
- /**
- * Return true if results are included in this result set.
- * STUB
- *
- * @return Boolean
- */
- function hasResults() {
- return false;
- }
-
- /**
- * Some search modes return a total hit count for the query
- * in the entire article database. This may include pages
- * in namespaces that would not be matched on the given
- * settings.
- *
- * Return null if no total hits number is supported.
- *
- * @return Integer
- */
- function getTotalHits() {
- return null;
- }
-
- /**
- * Some search modes return a suggested alternate term if there are
- * no exact hits. Returns true if there is one on this set.
- *
- * @return Boolean
- */
- function hasSuggestion() {
- return false;
- }
-
- /**
- * @return String: suggested query, null if none
- */
- function getSuggestionQuery() {
- return null;
- }
-
- /**
- * @return String: HTML highlighted suggested query, '' if none
- */
- function getSuggestionSnippet() {
- return '';
- }
-
- /**
- * Return information about how and from where the results were fetched,
- * should be useful for diagnostics and debugging
- *
- * @return String
- */
- function getInfo() {
- return null;
- }
-
- /**
- * Return a result set of hits on other (multiple) wikis associated with this one
- *
- * @return SearchResultSet
- */
- function getInterwikiResults() {
- return null;
- }
-
- /**
- * Check if there are results on other wikis
- *
- * @return Boolean
- */
- function hasInterwikiResults() {
- return $this->getInterwikiResults() != null;
- }
-
- /**
- * Fetches next search result, or false.
- * STUB
- *
- * @return SearchResult
- */
- function next() {
- return false;
- }
-
- /**
- * Frees the result set, if applicable.
- */
- function free() {
- // ...
- }
-}
-
-/**
- * This class is used for different SQL-based search engines shipped with MediaWiki
- */
-class SqlSearchResultSet extends SearchResultSet {
-
- protected $mResultSet;
-
- function __construct( $resultSet, $terms ) {
- $this->mResultSet = $resultSet;
- $this->mTerms = $terms;
- }
-
- function termMatches() {
- return $this->mTerms;
- }
-
- function numRows() {
- if ( $this->mResultSet === false ) {
- return false;
- }
-
- return $this->mResultSet->numRows();
- }
-
- function next() {
- if ( $this->mResultSet === false ) {
- return false;
- }
-
- $row = $this->mResultSet->fetchObject();
- if ( $row === false ) {
- return false;
- }
-
- return SearchResult::newFromRow( $row );
- }
-
- function free() {
- if ( $this->mResultSet === false ) {
- return false;
- }
-
- $this->mResultSet->free();
- }
-}
-
-/**
- * @ingroup Search
- */
-class SearchResultTooMany {
- # # Some search engines may bail out if too many matches are found
-}
-
-/**
- * @todo FIXME: This class is horribly factored. It would probably be better to
- * have a useful base class to which you pass some standard information, then
- * let the fancy self-highlighters extend that.
- * @ingroup Search
- */
-class SearchResult {
-
- /**
- * @var Revision
- */
- var $mRevision = null;
- var $mImage = null;
-
- /**
- * @var Title
- */
- var $mTitle;
-
- /**
- * @var String
- */
- var $mText;
-
- /**
- * Return a new SearchResult and initializes it with a title.
- *
- * @param $title Title
- * @return SearchResult
- */
- public static function newFromTitle( $title ) {
- $result = new self();
- $result->initFromTitle( $title );
- return $result;
- }
- /**
- * Return a new SearchResult and initializes it with a row.
- *
- * @param $row object
- * @return SearchResult
- */
- public static function newFromRow( $row ) {
- $result = new self();
- $result->initFromRow( $row );
- return $result;
- }
-
- public function __construct( $row = null ) {
- if ( !is_null( $row ) ) {
- // Backwards compatibility with pre-1.17 callers
- $this->initFromRow( $row );
- }
- }
-
- /**
- * Initialize from a database row. Makes a Title and passes that to
- * initFromTitle.
- *
- * @param $row object
- */
- protected function initFromRow( $row ) {
- $this->initFromTitle( Title::makeTitle( $row->page_namespace, $row->page_title ) );
- }
-
- /**
- * Initialize from a Title and if possible initializes a corresponding
- * Revision and File.
- *
- * @param $title Title
- */
- protected function initFromTitle( $title ) {
- $this->mTitle = $title;
- if ( !is_null( $this->mTitle ) ) {
- $id = false;
- wfRunHooks( 'SearchResultInitFromTitle', array( $title, &$id ) );
- $this->mRevision = Revision::newFromTitle(
- $this->mTitle, $id, Revision::READ_NORMAL );
- if ( $this->mTitle->getNamespace() === NS_FILE ) {
- $this->mImage = wfFindFile( $this->mTitle );
- }
- }
- }
-
- /**
- * Check if this is result points to an invalid title
- *
- * @return Boolean
- */
- function isBrokenTitle() {
- if ( is_null( $this->mTitle ) ) {
- return true;
- }
- return false;
- }
-
- /**
- * Check if target page is missing, happens when index is out of date
- *
- * @return Boolean
- */
- function isMissingRevision() {
- return !$this->mRevision && !$this->mImage;
- }
-
- /**
- * @return Title
- */
- function getTitle() {
- return $this->mTitle;
- }
-
- /**
- * @return float|null if not supported
- */
- function getScore() {
- return null;
- }
-
- /**
- * Lazy initialization of article text from DB
- */
- protected function initText() {
- if ( !isset( $this->mText ) ) {
- if ( $this->mRevision != null ) {
- $this->mText = SearchEngine::create()
- ->getTextFromContent( $this->mTitle, $this->mRevision->getContent() );
- } else { // TODO: can we fetch raw wikitext for commons images?
- $this->mText = '';
- }
- }
- }
-
- /**
- * @param array $terms terms to highlight
- * @return String: highlighted text snippet, null (and not '') if not supported
- */
- function getTextSnippet( $terms ) {
- global $wgAdvancedSearchHighlighting;
- $this->initText();
-
- // TODO: make highliter take a content object. Make ContentHandler a factory for SearchHighliter.
- list( $contextlines, $contextchars ) = SearchEngine::userHighlightPrefs();
- $h = new SearchHighlighter();
- if ( $wgAdvancedSearchHighlighting ) {
- return $h->highlightText( $this->mText, $terms, $contextlines, $contextchars );
- } else {
- return $h->highlightSimple( $this->mText, $terms, $contextlines, $contextchars );
- }
- }
-
- /**
- * @param array $terms terms to highlight
- * @return String: highlighted title, '' if not supported
- */
- function getTitleSnippet( $terms ) {
- return '';
- }
-
- /**
- * @param array $terms terms to highlight
- * @return String: highlighted redirect name (redirect to this page), '' if none or not supported
- */
- function getRedirectSnippet( $terms ) {
- return '';
- }
-
- /**
- * @return Title object for the redirect to this page, null if none or not supported
- */
- function getRedirectTitle() {
- return null;
- }
-
- /**
- * @return string highlighted relevant section name, null if none or not supported
- */
- function getSectionSnippet() {
- return '';
- }
-
- /**
- * @return Title object (pagename+fragment) for the section, null if none or not supported
- */
- function getSectionTitle() {
- return null;
- }
-
- /**
- * @return String: timestamp
- */
- function getTimestamp() {
- if ( $this->mRevision ) {
- return $this->mRevision->getTimestamp();
- } elseif ( $this->mImage ) {
- return $this->mImage->getTimestamp();
- }
- return '';
- }
-
- /**
- * @return Integer: number of words
- */
- function getWordCount() {
- $this->initText();
- return str_word_count( $this->mText );
- }
-
- /**
- * @return Integer: size in bytes
- */
- function getByteSize() {
- $this->initText();
- return strlen( $this->mText );
- }
-
- /**
- * @return Boolean if hit has related articles
- */
- function hasRelated() {
- return false;
- }
-
- /**
- * @return String: interwiki prefix of the title (return iw even if title is broken)
- */
- function getInterwikiPrefix() {
- return '';
- }
-}
-/**
- * A SearchResultSet wrapper for SearchEngine::getNearMatch
- */
-class SearchNearMatchResultSet extends SearchResultSet {
- private $fetched = false;
- /**
- * @param $match mixed Title if matched, else null
- */
- public function __construct( $match ) {
- $this->result = $match;
- }
- public function hasResult() {
- return (bool)$this->result;
- }
- public function numRows() {
- return $this->hasResults() ? 1 : 0;
- }
- public function next() {
- if ( $this->fetched || !$this->result ) {
- return false;
- }
- $this->fetched = true;
- return SearchResult::newFromTitle( $this->result );
- }
-}
-
-/**
- * Highlight bits of wikitext
- *
- * @ingroup Search
- */
-class SearchHighlighter {
- var $mCleanWikitext = true;
-
- function __construct( $cleanupWikitext = true ) {
- $this->mCleanWikitext = $cleanupWikitext;
- }
-
- /**
- * Default implementation of wikitext highlighting
- *
- * @param $text String
- * @param array $terms terms to highlight (unescaped)
- * @param $contextlines Integer
- * @param $contextchars Integer
- * @return String
- */
- public function highlightText( $text, $terms, $contextlines, $contextchars ) {
- global $wgContLang;
- global $wgSearchHighlightBoundaries;
- $fname = __METHOD__;
-
- if ( $text == '' ) {
- return '';
- }
-
- // spli text into text + templates/links/tables
- $spat = "/(\\{\\{)|(\\[\\[[^\\]:]+:)|(\n\\{\\|)";
- // first capture group is for detecting nested templates/links/tables/references
- $endPatterns = array(
- 1 => '/(\{\{)|(\}\})/', // template
- 2 => '/(\[\[)|(\]\])/', // image
- 3 => "/(\n\\{\\|)|(\n\\|\\})/" ); // table
-
- // @todo FIXME: This should prolly be a hook or something
- if ( function_exists( 'wfCite' ) ) {
- $spat .= '|(<ref>)'; // references via cite extension
- $endPatterns[4] = '/(<ref>)|(<\/ref>)/';
- }
- $spat .= '/';
- $textExt = array(); // text extracts
- $otherExt = array(); // other extracts
- wfProfileIn( "$fname-split" );
- $start = 0;
- $textLen = strlen( $text );
- $count = 0; // sequence number to maintain ordering
- while ( $start < $textLen ) {
- // find start of template/image/table
- if ( preg_match( $spat, $text, $matches, PREG_OFFSET_CAPTURE, $start ) ) {
- $epat = '';
- foreach ( $matches as $key => $val ) {
- if ( $key > 0 && $val[1] != - 1 ) {
- if ( $key == 2 ) {
- // see if this is an image link
- $ns = substr( $val[0], 2, - 1 );
- if ( $wgContLang->getNsIndex( $ns ) != NS_FILE ) {
- break;
- }
-
- }
- $epat = $endPatterns[$key];
- $this->splitAndAdd( $textExt, $count, substr( $text, $start, $val[1] - $start ) );
- $start = $val[1];
- break;
- }
- }
- if ( $epat ) {
- // find end (and detect any nested elements)
- $level = 0;
- $offset = $start + 1;
- $found = false;
- while ( preg_match( $epat, $text, $endMatches, PREG_OFFSET_CAPTURE, $offset ) ) {
- if ( array_key_exists( 2, $endMatches ) ) {
- // found end
- if ( $level == 0 ) {
- $len = strlen( $endMatches[2][0] );
- $off = $endMatches[2][1];
- $this->splitAndAdd( $otherExt, $count,
- substr( $text, $start, $off + $len - $start ) );
- $start = $off + $len;
- $found = true;
- break;
- } else {
- // end of nested element
- $level -= 1;
- }
- } else {
- // nested
- $level += 1;
- }
- $offset = $endMatches[0][1] + strlen( $endMatches[0][0] );
- }
- if ( ! $found ) {
- // couldn't find appropriate closing tag, skip
- $this->splitAndAdd( $textExt, $count, substr( $text, $start, strlen( $matches[0][0] ) ) );
- $start += strlen( $matches[0][0] );
- }
- continue;
- }
- }
- // else: add as text extract
- $this->splitAndAdd( $textExt, $count, substr( $text, $start ) );
- break;
- }
-
- $all = $textExt + $otherExt; // these have disjunct key sets
-
- wfProfileOut( "$fname-split" );
-
- // prepare regexps
- foreach ( $terms as $index => $term ) {
- // manually do upper/lowercase stuff for utf-8 since PHP won't do it
- if ( preg_match( '/[\x80-\xff]/', $term ) ) {
- $terms[$index] = preg_replace_callback( '/./us', array( $this, 'caseCallback' ), $terms[$index] );
- } else {
- $terms[$index] = $term;
- }
- }
- $anyterm = implode( '|', $terms );
- $phrase = implode( "$wgSearchHighlightBoundaries+", $terms );
-
- // @todo FIXME: A hack to scale contextchars, a correct solution
- // would be to have contextchars actually be char and not byte
- // length, and do proper utf-8 substrings and lengths everywhere,
- // but PHP is making that very hard and unclean to implement :(
- $scale = strlen( $anyterm ) / mb_strlen( $anyterm );
- $contextchars = intval( $contextchars * $scale );
-
- $patPre = "(^|$wgSearchHighlightBoundaries)";
- $patPost = "($wgSearchHighlightBoundaries|$)";
-
- $pat1 = "/(" . $phrase . ")/ui";
- $pat2 = "/$patPre(" . $anyterm . ")$patPost/ui";
-
- wfProfileIn( "$fname-extract" );
-
- $left = $contextlines;
-
- $snippets = array();
- $offsets = array();
-
- // show beginning only if it contains all words
- $first = 0;
- $firstText = '';
- foreach ( $textExt as $index => $line ) {
- if ( strlen( $line ) > 0 && $line[0] != ';' && $line[0] != ':' ) {
- $firstText = $this->extract( $line, 0, $contextchars * $contextlines );
- $first = $index;
- break;
- }
- }
- if ( $firstText ) {
- $succ = true;
- // check if first text contains all terms
- foreach ( $terms as $term ) {
- if ( ! preg_match( "/$patPre" . $term . "$patPost/ui", $firstText ) ) {
- $succ = false;
- break;
- }
- }
- if ( $succ ) {
- $snippets[$first] = $firstText;
- $offsets[$first] = 0;
- }
- }
- if ( ! $snippets ) {
- // match whole query on text
- $this->process( $pat1, $textExt, $left, $contextchars, $snippets, $offsets );
- // match whole query on templates/tables/images
- $this->process( $pat1, $otherExt, $left, $contextchars, $snippets, $offsets );
- // match any words on text
- $this->process( $pat2, $textExt, $left, $contextchars, $snippets, $offsets );
- // match any words on templates/tables/images
- $this->process( $pat2, $otherExt, $left, $contextchars, $snippets, $offsets );
-
- ksort( $snippets );
- }
-
- // add extra chars to each snippet to make snippets constant size
- $extended = array();
- if ( count( $snippets ) == 0 ) {
- // couldn't find the target words, just show beginning of article
- if ( array_key_exists( $first, $all ) ) {
- $targetchars = $contextchars * $contextlines;
- $snippets[$first] = '';
- $offsets[$first] = 0;
- }
- } else {
- // if begin of the article contains the whole phrase, show only that !!
- if ( array_key_exists( $first, $snippets ) && preg_match( $pat1, $snippets[$first] )
- && $offsets[$first] < $contextchars * 2 ) {
- $snippets = array( $first => $snippets[$first] );
- }
-
- // calc by how much to extend existing snippets
- $targetchars = intval( ( $contextchars * $contextlines ) / count ( $snippets ) );
- }
-
- foreach ( $snippets as $index => $line ) {
- $extended[$index] = $line;
- $len = strlen( $line );
- if ( $len < $targetchars - 20 ) {
- // complete this line
- if ( $len < strlen( $all[$index] ) ) {
- $extended[$index] = $this->extract( $all[$index], $offsets[$index], $offsets[$index] + $targetchars, $offsets[$index] );
- $len = strlen( $extended[$index] );
- }
-
- // add more lines
- $add = $index + 1;
- while ( $len < $targetchars - 20
- && array_key_exists( $add, $all )
- && !array_key_exists( $add, $snippets ) ) {
- $offsets[$add] = 0;
- $tt = "\n" . $this->extract( $all[$add], 0, $targetchars - $len, $offsets[$add] );
- $extended[$add] = $tt;
- $len += strlen( $tt );
- $add++;
- }
- }
- }
-
- // $snippets = array_map( 'htmlspecialchars', $extended );
- $snippets = $extended;
- $last = - 1;
- $extract = '';
- foreach ( $snippets as $index => $line ) {
- if ( $last == - 1 ) {
- $extract .= $line; // first line
- } elseif ( $last + 1 == $index && $offsets[$last] + strlen( $snippets[$last] ) >= strlen( $all[$last] ) ) {
- $extract .= " " . $line; // continous lines
- } else {
- $extract .= '<b> ... </b>' . $line;
- }
-
- $last = $index;
- }
- if ( $extract ) {
- $extract .= '<b> ... </b>';
- }
-
- $processed = array();
- foreach ( $terms as $term ) {
- if ( ! isset( $processed[$term] ) ) {
- $pat3 = "/$patPre(" . $term . ")$patPost/ui"; // highlight word
- $extract = preg_replace( $pat3,
- "\\1<span class='searchmatch'>\\2</span>\\3", $extract );
- $processed[$term] = true;
- }
- }
-
- wfProfileOut( "$fname-extract" );
-
- return $extract;
- }
-
- /**
- * Split text into lines and add it to extracts array
- *
- * @param array $extracts index -> $line
- * @param $count Integer
- * @param $text String
- */
- function splitAndAdd( &$extracts, &$count, $text ) {
- $split = explode( "\n", $this->mCleanWikitext ? $this->removeWiki( $text ) : $text );
- foreach ( $split as $line ) {
- $tt = trim( $line );
- if ( $tt ) {
- $extracts[$count++] = $tt;
- }
- }
- }
-
- /**
- * Do manual case conversion for non-ascii chars
- *
- * @param $matches Array
- * @return string
- */
- function caseCallback( $matches ) {
- global $wgContLang;
- if ( strlen( $matches[0] ) > 1 ) {
- return '[' . $wgContLang->lc( $matches[0] ) . $wgContLang->uc( $matches[0] ) . ']';
- } else {
- return $matches[0];
- }
- }
-
- /**
- * Extract part of the text from start to end, but by
- * not chopping up words
- * @param $text String
- * @param $start Integer
- * @param $end Integer
- * @param $posStart Integer: (out) actual start position
- * @param $posEnd Integer: (out) actual end position
- * @return String
- */
- function extract( $text, $start, $end, &$posStart = null, &$posEnd = null ) {
- if ( $start != 0 ) {
- $start = $this->position( $text, $start, 1 );
- }
- if ( $end >= strlen( $text ) ) {
- $end = strlen( $text );
- } else {
- $end = $this->position( $text, $end );
- }
-
- if ( !is_null( $posStart ) ) {
- $posStart = $start;
- }
- if ( !is_null( $posEnd ) ) {
- $posEnd = $end;
- }
-
- if ( $end > $start ) {
- return substr( $text, $start, $end - $start );
- } else {
- return '';
- }
- }
-
- /**
- * Find a nonletter near a point (index) in the text
- *
- * @param $text String
- * @param $point Integer
- * @param $offset Integer: offset to found index
- * @return Integer: nearest nonletter index, or beginning of utf8 char if none
- */
- function position( $text, $point, $offset = 0 ) {
- $tolerance = 10;
- $s = max( 0, $point - $tolerance );
- $l = min( strlen( $text ), $point + $tolerance ) - $s;
- $m = array();
- if ( preg_match( '/[ ,.!?~!@#$%^&*\(\)+=\-\\\|\[\]"\'<>]/', substr( $text, $s, $l ), $m, PREG_OFFSET_CAPTURE ) ) {
- return $m[0][1] + $s + $offset;
- } else {
- // check if point is on a valid first UTF8 char
- $char = ord( $text[$point] );
- while ( $char >= 0x80 && $char < 0xc0 ) {
- // skip trailing bytes
- $point++;
- if ( $point >= strlen( $text ) ) {
- return strlen( $text );
- }
- $char = ord( $text[$point] );
- }
- return $point;
-
- }
- }
-
- /**
- * Search extracts for a pattern, and return snippets
- *
- * @param string $pattern regexp for matching lines
- * @param array $extracts extracts to search
- * @param $linesleft Integer: number of extracts to make
- * @param $contextchars Integer: length of snippet
- * @param array $out map for highlighted snippets
- * @param array $offsets map of starting points of snippets
- * @protected
- */
- function process( $pattern, $extracts, &$linesleft, &$contextchars, &$out, &$offsets ) {
- if ( $linesleft == 0 ) {
- return; // nothing to do
- }
- foreach ( $extracts as $index => $line ) {
- if ( array_key_exists( $index, $out ) ) {
- continue; // this line already highlighted
- }
-
- $m = array();
- if ( !preg_match( $pattern, $line, $m, PREG_OFFSET_CAPTURE ) ) {
- continue;
- }
-
- $offset = $m[0][1];
- $len = strlen( $m[0][0] );
- if ( $offset + $len < $contextchars ) {
- $begin = 0;
- } elseif ( $len > $contextchars ) {
- $begin = $offset;
- } else {
- $begin = $offset + intval( ( $len - $contextchars ) / 2 );
- }
-
- $end = $begin + $contextchars;
-
- $posBegin = $begin;
- // basic snippet from this line
- $out[$index] = $this->extract( $line, $begin, $end, $posBegin );
- $offsets[$index] = $posBegin;
- $linesleft--;
- if ( $linesleft == 0 ) {
- return;
- }
- }
- }
-
- /**
- * Basic wikitext removal
- * @protected
- * @return mixed
- */
- function removeWiki( $text ) {
- $fname = __METHOD__;
- wfProfileIn( $fname );
-
- // $text = preg_replace( "/'{2,5}/", "", $text );
- // $text = preg_replace( "/\[[a-z]+:\/\/[^ ]+ ([^]]+)\]/", "\\2", $text );
- // $text = preg_replace( "/\[\[([^]|]+)\]\]/", "\\1", $text );
- // $text = preg_replace( "/\[\[([^]]+\|)?([^|]]+)\]\]/", "\\2", $text );
- // $text = preg_replace( "/\\{\\|(.*?)\\|\\}/", "", $text );
- // $text = preg_replace( "/\\[\\[[A-Za-z_-]+:([^|]+?)\\]\\]/", "", $text );
- $text = preg_replace( "/\\{\\{([^|]+?)\\}\\}/", "", $text );
- $text = preg_replace( "/\\{\\{([^|]+\\|)(.*?)\\}\\}/", "\\2", $text );
- $text = preg_replace( "/\\[\\[([^|]+?)\\]\\]/", "\\1", $text );
- $text = preg_replace_callback( "/\\[\\[([^|]+\\|)(.*?)\\]\\]/", array( $this, 'linkReplace' ), $text );
- // $text = preg_replace("/\\[\\[([^|]+\\|)(.*?)\\]\\]/", "\\2", $text);
- $text = preg_replace( "/<\/?[^>]+>/", "", $text );
- $text = preg_replace( "/'''''/", "", $text );
- $text = preg_replace( "/('''|<\/?[iIuUbB]>)/", "", $text );
- $text = preg_replace( "/''/", "", $text );
-
- wfProfileOut( $fname );
- return $text;
- }
-
- /**
- * callback to replace [[target|caption]] kind of links, if
- * the target is category or image, leave it
- *
- * @param $matches Array
- */
- function linkReplace( $matches ) {
- $colon = strpos( $matches[1], ':' );
- if ( $colon === false ) {
- return $matches[2]; // replace with caption
- }
- global $wgContLang;
- $ns = substr( $matches[1], 0, $colon );
- $index = $wgContLang->getNsIndex( $ns );
- if ( $index !== false && ( $index == NS_FILE || $index == NS_CATEGORY ) ) {
- return $matches[0]; // return the whole thing
- } else {
- return $matches[2];
- }
- }
-
- /**
- * Simple & fast snippet extraction, but gives completely unrelevant
- * snippets
- *
- * @param $text String
- * @param $terms Array
- * @param $contextlines Integer
- * @param $contextchars Integer
- * @return String
- */
- public function highlightSimple( $text, $terms, $contextlines, $contextchars ) {
- global $wgContLang;
- $fname = __METHOD__;
-
- $lines = explode( "\n", $text );
-
- $terms = implode( '|', $terms );
- $max = intval( $contextchars ) + 1;
- $pat1 = "/(.*)($terms)(.{0,$max})/i";
-
- $lineno = 0;
-
- $extract = "";
- wfProfileIn( "$fname-extract" );
- foreach ( $lines as $line ) {
- if ( 0 == $contextlines ) {
- break;
- }
- ++$lineno;
- $m = array();
- if ( ! preg_match( $pat1, $line, $m ) ) {
- continue;
- }
- --$contextlines;
- // truncate function changes ... to relevant i18n message.
- $pre = $wgContLang->truncate( $m[1], - $contextchars, '...', false );
-
- if ( count( $m ) < 3 ) {
- $post = '';
- } else {
- $post = $wgContLang->truncate( $m[3], $contextchars, '...', false );
- }
-
- $found = $m[2];
-
- $line = htmlspecialchars( $pre . $found . $post );
- $pat2 = '/(' . $terms . ")/i";
- $line = preg_replace( $pat2, "<span class='searchmatch'>\\1</span>", $line );
-
- $extract .= "${line}\n";
- }
- wfProfileOut( "$fname-extract" );
-
- return $extract;
- }
-
-}
-
-/**
* Dummy class to be used when non-supported Database engine is present.
* @todo FIXME: Dummy class should probably try something at least mildly useful,
* such as a LIKE search through titles.
diff --git a/includes/search/SearchHighlighter.php b/includes/search/SearchHighlighter.php
new file mode 100644
index 00000000..c3c3a8f8
--- /dev/null
+++ b/includes/search/SearchHighlighter.php
@@ -0,0 +1,575 @@
+<?php
+/**
+ * Basic search engine highlighting
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Search
+ */
+
+/**
+ * Highlight bits of wikitext
+ *
+ * @ingroup Search
+ */
+class SearchHighlighter {
+ protected $mCleanWikitext = true;
+
+ function __construct( $cleanupWikitext = true ) {
+ $this->mCleanWikitext = $cleanupWikitext;
+ }
+
+ /**
+ * Default implementation of wikitext highlighting
+ *
+ * @param string $text
+ * @param array $terms Terms to highlight (unescaped)
+ * @param int $contextlines
+ * @param int $contextchars
+ * @return string
+ */
+ public function highlightText( $text, $terms, $contextlines, $contextchars ) {
+ global $wgContLang, $wgSearchHighlightBoundaries;
+
+ $fname = __METHOD__;
+
+ if ( $text == '' ) {
+ return '';
+ }
+
+ // spli text into text + templates/links/tables
+ $spat = "/(\\{\\{)|(\\[\\[[^\\]:]+:)|(\n\\{\\|)";
+ // first capture group is for detecting nested templates/links/tables/references
+ $endPatterns = array(
+ 1 => '/(\{\{)|(\}\})/', // template
+ 2 => '/(\[\[)|(\]\])/', // image
+ 3 => "/(\n\\{\\|)|(\n\\|\\})/" ); // table
+
+ // @todo FIXME: This should prolly be a hook or something
+ if ( function_exists( 'wfCite' ) ) {
+ $spat .= '|(<ref>)'; // references via cite extension
+ $endPatterns[4] = '/(<ref>)|(<\/ref>)/';
+ }
+ $spat .= '/';
+ $textExt = array(); // text extracts
+ $otherExt = array(); // other extracts
+ wfProfileIn( "$fname-split" );
+ $start = 0;
+ $textLen = strlen( $text );
+ $count = 0; // sequence number to maintain ordering
+ while ( $start < $textLen ) {
+ // find start of template/image/table
+ if ( preg_match( $spat, $text, $matches, PREG_OFFSET_CAPTURE, $start ) ) {
+ $epat = '';
+ foreach ( $matches as $key => $val ) {
+ if ( $key > 0 && $val[1] != - 1 ) {
+ if ( $key == 2 ) {
+ // see if this is an image link
+ $ns = substr( $val[0], 2, - 1 );
+ if ( $wgContLang->getNsIndex( $ns ) != NS_FILE ) {
+ break;
+ }
+
+ }
+ $epat = $endPatterns[$key];
+ $this->splitAndAdd( $textExt, $count, substr( $text, $start, $val[1] - $start ) );
+ $start = $val[1];
+ break;
+ }
+ }
+ if ( $epat ) {
+ // find end (and detect any nested elements)
+ $level = 0;
+ $offset = $start + 1;
+ $found = false;
+ while ( preg_match( $epat, $text, $endMatches, PREG_OFFSET_CAPTURE, $offset ) ) {
+ if ( array_key_exists( 2, $endMatches ) ) {
+ // found end
+ if ( $level == 0 ) {
+ $len = strlen( $endMatches[2][0] );
+ $off = $endMatches[2][1];
+ $this->splitAndAdd( $otherExt, $count,
+ substr( $text, $start, $off + $len - $start ) );
+ $start = $off + $len;
+ $found = true;
+ break;
+ } else {
+ // end of nested element
+ $level -= 1;
+ }
+ } else {
+ // nested
+ $level += 1;
+ }
+ $offset = $endMatches[0][1] + strlen( $endMatches[0][0] );
+ }
+ if ( !$found ) {
+ // couldn't find appropriate closing tag, skip
+ $this->splitAndAdd( $textExt, $count, substr( $text, $start, strlen( $matches[0][0] ) ) );
+ $start += strlen( $matches[0][0] );
+ }
+ continue;
+ }
+ }
+ // else: add as text extract
+ $this->splitAndAdd( $textExt, $count, substr( $text, $start ) );
+ break;
+ }
+
+ $all = $textExt + $otherExt; // these have disjunct key sets
+
+ wfProfileOut( "$fname-split" );
+
+ // prepare regexps
+ foreach ( $terms as $index => $term ) {
+ // manually do upper/lowercase stuff for utf-8 since PHP won't do it
+ if ( preg_match( '/[\x80-\xff]/', $term ) ) {
+ $terms[$index] = preg_replace_callback(
+ '/./us',
+ array( $this, 'caseCallback' ),
+ $terms[$index]
+ );
+ } else {
+ $terms[$index] = $term;
+ }
+ }
+ $anyterm = implode( '|', $terms );
+ $phrase = implode( "$wgSearchHighlightBoundaries+", $terms );
+
+ // @todo FIXME: A hack to scale contextchars, a correct solution
+ // would be to have contextchars actually be char and not byte
+ // length, and do proper utf-8 substrings and lengths everywhere,
+ // but PHP is making that very hard and unclean to implement :(
+ $scale = strlen( $anyterm ) / mb_strlen( $anyterm );
+ $contextchars = intval( $contextchars * $scale );
+
+ $patPre = "(^|$wgSearchHighlightBoundaries)";
+ $patPost = "($wgSearchHighlightBoundaries|$)";
+
+ $pat1 = "/(" . $phrase . ")/ui";
+ $pat2 = "/$patPre(" . $anyterm . ")$patPost/ui";
+
+ wfProfileIn( "$fname-extract" );
+
+ $left = $contextlines;
+
+ $snippets = array();
+ $offsets = array();
+
+ // show beginning only if it contains all words
+ $first = 0;
+ $firstText = '';
+ foreach ( $textExt as $index => $line ) {
+ if ( strlen( $line ) > 0 && $line[0] != ';' && $line[0] != ':' ) {
+ $firstText = $this->extract( $line, 0, $contextchars * $contextlines );
+ $first = $index;
+ break;
+ }
+ }
+ if ( $firstText ) {
+ $succ = true;
+ // check if first text contains all terms
+ foreach ( $terms as $term ) {
+ if ( !preg_match( "/$patPre" . $term . "$patPost/ui", $firstText ) ) {
+ $succ = false;
+ break;
+ }
+ }
+ if ( $succ ) {
+ $snippets[$first] = $firstText;
+ $offsets[$first] = 0;
+ }
+ }
+ if ( !$snippets ) {
+ // match whole query on text
+ $this->process( $pat1, $textExt, $left, $contextchars, $snippets, $offsets );
+ // match whole query on templates/tables/images
+ $this->process( $pat1, $otherExt, $left, $contextchars, $snippets, $offsets );
+ // match any words on text
+ $this->process( $pat2, $textExt, $left, $contextchars, $snippets, $offsets );
+ // match any words on templates/tables/images
+ $this->process( $pat2, $otherExt, $left, $contextchars, $snippets, $offsets );
+
+ ksort( $snippets );
+ }
+
+ // add extra chars to each snippet to make snippets constant size
+ $extended = array();
+ if ( count( $snippets ) == 0 ) {
+ // couldn't find the target words, just show beginning of article
+ if ( array_key_exists( $first, $all ) ) {
+ $targetchars = $contextchars * $contextlines;
+ $snippets[$first] = '';
+ $offsets[$first] = 0;
+ }
+ } else {
+ // if begin of the article contains the whole phrase, show only that !!
+ if ( array_key_exists( $first, $snippets ) && preg_match( $pat1, $snippets[$first] )
+ && $offsets[$first] < $contextchars * 2 ) {
+ $snippets = array( $first => $snippets[$first] );
+ }
+
+ // calc by how much to extend existing snippets
+ $targetchars = intval( ( $contextchars * $contextlines ) / count ( $snippets ) );
+ }
+
+ foreach ( $snippets as $index => $line ) {
+ $extended[$index] = $line;
+ $len = strlen( $line );
+ if ( $len < $targetchars - 20 ) {
+ // complete this line
+ if ( $len < strlen( $all[$index] ) ) {
+ $extended[$index] = $this->extract(
+ $all[$index],
+ $offsets[$index],
+ $offsets[$index] + $targetchars,
+ $offsets[$index]
+ );
+ $len = strlen( $extended[$index] );
+ }
+
+ // add more lines
+ $add = $index + 1;
+ while ( $len < $targetchars - 20
+ && array_key_exists( $add, $all )
+ && !array_key_exists( $add, $snippets ) ) {
+ $offsets[$add] = 0;
+ $tt = "\n" . $this->extract( $all[$add], 0, $targetchars - $len, $offsets[$add] );
+ $extended[$add] = $tt;
+ $len += strlen( $tt );
+ $add++;
+ }
+ }
+ }
+
+ // $snippets = array_map( 'htmlspecialchars', $extended );
+ $snippets = $extended;
+ $last = - 1;
+ $extract = '';
+ foreach ( $snippets as $index => $line ) {
+ if ( $last == - 1 ) {
+ $extract .= $line; // first line
+ } elseif ( $last + 1 == $index
+ && $offsets[$last] + strlen( $snippets[$last] ) >= strlen( $all[$last] )
+ ) {
+ $extract .= " " . $line; // continous lines
+ } else {
+ $extract .= '<b> ... </b>' . $line;
+ }
+
+ $last = $index;
+ }
+ if ( $extract ) {
+ $extract .= '<b> ... </b>';
+ }
+
+ $processed = array();
+ foreach ( $terms as $term ) {
+ if ( !isset( $processed[$term] ) ) {
+ $pat3 = "/$patPre(" . $term . ")$patPost/ui"; // highlight word
+ $extract = preg_replace( $pat3,
+ "\\1<span class='searchmatch'>\\2</span>\\3", $extract );
+ $processed[$term] = true;
+ }
+ }
+
+ wfProfileOut( "$fname-extract" );
+
+ return $extract;
+ }
+
+ /**
+ * Split text into lines and add it to extracts array
+ *
+ * @param array $extracts Index -> $line
+ * @param int $count
+ * @param string $text
+ */
+ function splitAndAdd( &$extracts, &$count, $text ) {
+ $split = explode( "\n", $this->mCleanWikitext ? $this->removeWiki( $text ) : $text );
+ foreach ( $split as $line ) {
+ $tt = trim( $line );
+ if ( $tt ) {
+ $extracts[$count++] = $tt;
+ }
+ }
+ }
+
+ /**
+ * Do manual case conversion for non-ascii chars
+ *
+ * @param array $matches
+ * @return string
+ */
+ function caseCallback( $matches ) {
+ global $wgContLang;
+ if ( strlen( $matches[0] ) > 1 ) {
+ return '[' . $wgContLang->lc( $matches[0] ) . $wgContLang->uc( $matches[0] ) . ']';
+ } else {
+ return $matches[0];
+ }
+ }
+
+ /**
+ * Extract part of the text from start to end, but by
+ * not chopping up words
+ * @param string $text
+ * @param int $start
+ * @param int $end
+ * @param int $posStart (out) actual start position
+ * @param int $posEnd (out) actual end position
+ * @return string
+ */
+ function extract( $text, $start, $end, &$posStart = null, &$posEnd = null ) {
+ if ( $start != 0 ) {
+ $start = $this->position( $text, $start, 1 );
+ }
+ if ( $end >= strlen( $text ) ) {
+ $end = strlen( $text );
+ } else {
+ $end = $this->position( $text, $end );
+ }
+
+ if ( !is_null( $posStart ) ) {
+ $posStart = $start;
+ }
+ if ( !is_null( $posEnd ) ) {
+ $posEnd = $end;
+ }
+
+ if ( $end > $start ) {
+ return substr( $text, $start, $end - $start );
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Find a nonletter near a point (index) in the text
+ *
+ * @param string $text
+ * @param int $point
+ * @param int $offset Offset to found index
+ * @return int Nearest nonletter index, or beginning of utf8 char if none
+ */
+ function position( $text, $point, $offset = 0 ) {
+ $tolerance = 10;
+ $s = max( 0, $point - $tolerance );
+ $l = min( strlen( $text ), $point + $tolerance ) - $s;
+ $m = array();
+
+ if ( preg_match(
+ '/[ ,.!?~!@#$%^&*\(\)+=\-\\\|\[\]"\'<>]/',
+ substr( $text, $s, $l ),
+ $m,
+ PREG_OFFSET_CAPTURE
+ ) ) {
+ return $m[0][1] + $s + $offset;
+ } else {
+ // check if point is on a valid first UTF8 char
+ $char = ord( $text[$point] );
+ while ( $char >= 0x80 && $char < 0xc0 ) {
+ // skip trailing bytes
+ $point++;
+ if ( $point >= strlen( $text ) ) {
+ return strlen( $text );
+ }
+ $char = ord( $text[$point] );
+ }
+
+ return $point;
+
+ }
+ }
+
+ /**
+ * Search extracts for a pattern, and return snippets
+ *
+ * @param string $pattern Regexp for matching lines
+ * @param array $extracts Extracts to search
+ * @param int $linesleft Number of extracts to make
+ * @param int $contextchars Length of snippet
+ * @param array $out Map for highlighted snippets
+ * @param array $offsets Map of starting points of snippets
+ * @protected
+ */
+ function process( $pattern, $extracts, &$linesleft, &$contextchars, &$out, &$offsets ) {
+ if ( $linesleft == 0 ) {
+ return; // nothing to do
+ }
+ foreach ( $extracts as $index => $line ) {
+ if ( array_key_exists( $index, $out ) ) {
+ continue; // this line already highlighted
+ }
+
+ $m = array();
+ if ( !preg_match( $pattern, $line, $m, PREG_OFFSET_CAPTURE ) ) {
+ continue;
+ }
+
+ $offset = $m[0][1];
+ $len = strlen( $m[0][0] );
+ if ( $offset + $len < $contextchars ) {
+ $begin = 0;
+ } elseif ( $len > $contextchars ) {
+ $begin = $offset;
+ } else {
+ $begin = $offset + intval( ( $len - $contextchars ) / 2 );
+ }
+
+ $end = $begin + $contextchars;
+
+ $posBegin = $begin;
+ // basic snippet from this line
+ $out[$index] = $this->extract( $line, $begin, $end, $posBegin );
+ $offsets[$index] = $posBegin;
+ $linesleft--;
+ if ( $linesleft == 0 ) {
+ return;
+ }
+ }
+ }
+
+ /**
+ * Basic wikitext removal
+ * @protected
+ * @param string $text
+ * @return mixed
+ */
+ function removeWiki( $text ) {
+ $fname = __METHOD__;
+ wfProfileIn( $fname );
+
+ // $text = preg_replace( "/'{2,5}/", "", $text );
+ // $text = preg_replace( "/\[[a-z]+:\/\/[^ ]+ ([^]]+)\]/", "\\2", $text );
+ // $text = preg_replace( "/\[\[([^]|]+)\]\]/", "\\1", $text );
+ // $text = preg_replace( "/\[\[([^]]+\|)?([^|]]+)\]\]/", "\\2", $text );
+ // $text = preg_replace( "/\\{\\|(.*?)\\|\\}/", "", $text );
+ // $text = preg_replace( "/\\[\\[[A-Za-z_-]+:([^|]+?)\\]\\]/", "", $text );
+ $text = preg_replace( "/\\{\\{([^|]+?)\\}\\}/", "", $text );
+ $text = preg_replace( "/\\{\\{([^|]+\\|)(.*?)\\}\\}/", "\\2", $text );
+ $text = preg_replace( "/\\[\\[([^|]+?)\\]\\]/", "\\1", $text );
+ $text = preg_replace_callback(
+ "/\\[\\[([^|]+\\|)(.*?)\\]\\]/",
+ array( $this, 'linkReplace' ),
+ $text
+ );
+ // $text = preg_replace("/\\[\\[([^|]+\\|)(.*?)\\]\\]/", "\\2", $text);
+ $text = preg_replace( "/<\/?[^>]+>/", "", $text );
+ $text = preg_replace( "/'''''/", "", $text );
+ $text = preg_replace( "/('''|<\/?[iIuUbB]>)/", "", $text );
+ $text = preg_replace( "/''/", "", $text );
+
+ wfProfileOut( $fname );
+ return $text;
+ }
+
+ /**
+ * callback to replace [[target|caption]] kind of links, if
+ * the target is category or image, leave it
+ *
+ * @param array $matches
+ * @return string
+ */
+ function linkReplace( $matches ) {
+ $colon = strpos( $matches[1], ':' );
+ if ( $colon === false ) {
+ return $matches[2]; // replace with caption
+ }
+ global $wgContLang;
+ $ns = substr( $matches[1], 0, $colon );
+ $index = $wgContLang->getNsIndex( $ns );
+ if ( $index !== false && ( $index == NS_FILE || $index == NS_CATEGORY ) ) {
+ return $matches[0]; // return the whole thing
+ } else {
+ return $matches[2];
+ }
+ }
+
+ /**
+ * Simple & fast snippet extraction, but gives completely unrelevant
+ * snippets
+ *
+ * @param string $text
+ * @param array $terms
+ * @param int $contextlines
+ * @param int $contextchars
+ * @return string
+ */
+ public function highlightSimple( $text, $terms, $contextlines, $contextchars ) {
+ global $wgContLang;
+ $fname = __METHOD__;
+
+ $lines = explode( "\n", $text );
+
+ $terms = implode( '|', $terms );
+ $max = intval( $contextchars ) + 1;
+ $pat1 = "/(.*)($terms)(.{0,$max})/i";
+
+ $lineno = 0;
+
+ $extract = "";
+ wfProfileIn( "$fname-extract" );
+ foreach ( $lines as $line ) {
+ if ( 0 == $contextlines ) {
+ break;
+ }
+ ++$lineno;
+ $m = array();
+ if ( !preg_match( $pat1, $line, $m ) ) {
+ continue;
+ }
+ --$contextlines;
+ // truncate function changes ... to relevant i18n message.
+ $pre = $wgContLang->truncate( $m[1], - $contextchars, '...', false );
+
+ if ( count( $m ) < 3 ) {
+ $post = '';
+ } else {
+ $post = $wgContLang->truncate( $m[3], $contextchars, '...', false );
+ }
+
+ $found = $m[2];
+
+ $line = htmlspecialchars( $pre . $found . $post );
+ $pat2 = '/(' . $terms . ")/i";
+ $line = preg_replace( $pat2, "<span class='searchmatch'>\\1</span>", $line );
+
+ $extract .= "${line}\n";
+ }
+ wfProfileOut( "$fname-extract" );
+
+ return $extract;
+ }
+
+ /**
+ * Returns the first few lines of the text
+ *
+ * @param string $text
+ * @param int $contextlines Max number of returned lines
+ * @param int $contextchars Average number of characters per line
+ * @return string
+ */
+ public function highlightNone( $text, $contextlines, $contextchars ) {
+ $match = array();
+ $text = ltrim( $text ) . "\n"; // make sure the preg_match may find the last line
+ $text = str_replace( "\n\n", "\n", $text ); // remove empty lines
+ preg_match( "/^(.*\n){0,$contextlines}/", $text, $match );
+ $text = htmlspecialchars( substr( trim( $match[0] ), 0, $contextlines * $contextchars ) ); // trim and limit to max number of chars
+ return str_replace( "\n", '<br>', $text );
+ }
+}
diff --git a/includes/search/SearchMssql.php b/includes/search/SearchMssql.php
index cbc1a7a7..0d7970de 100644
--- a/includes/search/SearchMssql.php
+++ b/includes/search/SearchMssql.php
@@ -25,58 +25,35 @@
* Search engine hook base class for Mssql (ConText).
* @ingroup Search
*/
-class SearchMssql extends SearchEngine {
-
- /**
- * Creates an instance of this class
- * @param $db DatabaseMssql: database object
- */
- function __construct( $db ) {
- parent::__construct( $db );
- }
-
+class SearchMssql extends SearchDatabase {
/**
* Perform a full text search query and return a result set.
*
- * @param string $term raw search term
- * @return MssqlSearchResultSet
+ * @param string $term Raw search term
+ * @return SqlSearchResultSet
* @access public
*/
function searchText( $term ) {
- $resultSet = $this->db->resultObject( $this->db->query( $this->getQuery( $this->filter( $term ), true ) ) );
- return new MssqlSearchResultSet( $resultSet, $this->searchTerms );
+ $resultSet = $this->db->query( $this->getQuery( $this->filter( $term ), true ) );
+ return new SqlSearchResultSet( $resultSet, $this->searchTerms );
}
/**
* Perform a title-only search query and return a result set.
*
- * @param string $term raw search term
- * @return MssqlSearchResultSet
+ * @param string $term Raw search term
+ * @return SqlSearchResultSet
* @access public
*/
function searchTitle( $term ) {
- $resultSet = $this->db->resultObject( $this->db->query( $this->getQuery( $this->filter( $term ), false ) ) );
- return new MssqlSearchResultSet( $resultSet, $this->searchTerms );
- }
-
- /**
- * Return a partial WHERE clause to exclude redirects, if so set
- *
- * @return String
- * @private
- */
- function queryRedirect() {
- if ( $this->showRedirects ) {
- return '';
- } else {
- return 'AND page_is_redirect=0';
- }
+ $resultSet = $this->db->query( $this->getQuery( $this->filter( $term ), false ) );
+ return new SqlSearchResultSet( $resultSet, $this->searchTerms );
}
/**
* Return a partial WHERE clause to limit the search to the given namespaces
*
- * @return String
+ * @return string
* @private
*/
function queryNamespaces() {
@@ -90,9 +67,9 @@ class SearchMssql extends SearchEngine {
/**
* Return a LIMIT clause to limit results on the query.
*
- * @param $sql string
+ * @param string $sql
*
- * @return String
+ * @return string
*/
function queryLimit( $sql ) {
return $this->db->limitResult( $sql, $this->limit, $this->offset );
@@ -102,7 +79,9 @@ class SearchMssql extends SearchEngine {
* Does not do anything for generic search engine
* subclasses may define this though
*
- * @return String
+ * @param string $filteredTerm
+ * @param bool $fulltext
+ * @return string
*/
function queryRanking( $filteredTerm, $fulltext ) {
return ' ORDER BY ftindex.[RANK] DESC'; // return ' ORDER BY score(1)';
@@ -112,13 +91,12 @@ class SearchMssql extends SearchEngine {
* Construct the full SQL query to do the search.
* The guts shoulds be constructed in queryMain()
*
- * @param $filteredTerm String
- * @param $fulltext Boolean
- * @return String
+ * @param string $filteredTerm
+ * @param bool $fulltext
+ * @return string
*/
function getQuery( $filteredTerm, $fulltext ) {
return $this->queryLimit( $this->queryMain( $filteredTerm, $fulltext ) . ' ' .
- $this->queryRedirect() . ' ' .
$this->queryNamespaces() . ' ' .
$this->queryRanking( $filteredTerm, $fulltext ) . ' ' );
}
@@ -126,7 +104,7 @@ class SearchMssql extends SearchEngine {
/**
* Picks which field to index on, depending on what type of query.
*
- * @param $fulltext Boolean
+ * @param bool $fulltext
* @return string
*/
function getIndexField( $fulltext ) {
@@ -136,9 +114,9 @@ class SearchMssql extends SearchEngine {
/**
* Get the base part of the search query.
*
- * @param $filteredTerm String
- * @param $fulltext Boolean
- * @return String
+ * @param string $filteredTerm
+ * @param bool $fulltext
+ * @return string
* @private
*/
function queryMain( $filteredTerm, $fulltext ) {
@@ -152,11 +130,13 @@ class SearchMssql extends SearchEngine {
}
/** @todo document
+ * @param string $filteredText
+ * @param bool $fulltext
* @return string
*/
function parseQuery( $filteredText, $fulltext ) {
global $wgContLang;
- $lc = SearchEngine::legalSearchChars();
+ $lc = $this->legalSearchChars();
$this->searchTerms = array();
# @todo FIXME: This doesn't handle parenthetical expressions.
@@ -180,18 +160,18 @@ class SearchMssql extends SearchEngine {
}
}
- $searchon = $this->db->strencode( join( ',', $q ) );
+ $searchon = $this->db->addQuotes( join( ',', $q ) );
$field = $this->getIndexField( $fulltext );
- return "$field, '$searchon'";
+ return "$field, $searchon";
}
/**
* Create or update the search index record for the given page.
* Title and text should be pre-processed.
*
- * @param $id Integer
- * @param $title String
- * @param $text String
+ * @param int $id
+ * @param string $title
+ * @param string $text
* @return bool|ResultWrapper
*/
function update( $id, $title, $text ) {
@@ -213,8 +193,8 @@ class SearchMssql extends SearchEngine {
* Update a search index record's title only.
* Title should be pre-processed.
*
- * @param $id Integer
- * @param $title String
+ * @param int $id
+ * @param string $title
* @return bool|ResultWrapper
*/
function updateTitle( $id, $title ) {
@@ -228,29 +208,3 @@ class SearchMssql extends SearchEngine {
return $this->db->query( $sql, 'SearchMssql::updateTitle' );
}
}
-
-/**
- * @ingroup Search
- */
-class MssqlSearchResultSet extends SearchResultSet {
- function __construct( $resultSet, $terms ) {
- $this->mResultSet = $resultSet;
- $this->mTerms = $terms;
- }
-
- function termMatches() {
- return $this->mTerms;
- }
-
- function numRows() {
- return $this->mResultSet->numRows();
- }
-
- function next() {
- $row = $this->mResultSet->fetchObject();
- if ( $row === false ) {
- return false;
- }
- return new SearchResult( $row );
- }
-}
diff --git a/includes/search/SearchMySQL.php b/includes/search/SearchMySQL.php
index b2bc1c26..78eba2d0 100644
--- a/includes/search/SearchMySQL.php
+++ b/includes/search/SearchMySQL.php
@@ -3,7 +3,7 @@
* MySQL search engine
*
* Copyright (C) 2004 Brion Vibber <brion@pobox.com>
- * http://www.mediawiki.org/
+ * https://www.mediawiki.org/
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -28,30 +28,24 @@
* Search engine hook for MySQL 4+
* @ingroup Search
*/
-class SearchMySQL extends SearchEngine {
- var $strictMatching = true;
- static $mMinSearchLength;
+class SearchMySQL extends SearchDatabase {
+ protected $strictMatching = true;
- /**
- * Creates an instance of this class
- * @param $db DatabaseMysql: database object
- */
- function __construct( $db ) {
- parent::__construct( $db );
- }
+ private static $mMinSearchLength;
/**
* Parse the user's query and transform it into an SQL fragment which will
* become part of a WHERE clause
*
- * @param $filteredText string
- * @param $fulltext string
+ * @param string $filteredText
+ * @param string $fulltext
*
* @return string
*/
function parseQuery( $filteredText, $fulltext ) {
global $wgContLang;
- $lc = SearchEngine::legalSearchChars(); // Minus format chars
+
+ $lc = $this->legalSearchChars(); // Minus format chars
$searchon = '';
$this->searchTerms = array();
@@ -60,7 +54,9 @@ class SearchMySQL extends SearchEngine {
if ( preg_match_all( '/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/',
$filteredText, $m, PREG_SET_ORDER ) ) {
foreach ( $m as $bits ) {
- @list( /* all */, $modifier, $term, $nonQuoted, $wildcard ) = $bits;
+ wfSuppressWarnings();
+ list( /* all */, $modifier, $term, $nonQuoted, $wildcard ) = $bits;
+ wfRestoreWarnings();
if ( $nonQuoted != '' ) {
$term = $nonQuoted;
@@ -129,9 +125,9 @@ class SearchMySQL extends SearchEngine {
wfDebug( __METHOD__ . ": Can't understand search query '{$filteredText}'\n" );
}
- $searchon = $this->db->strencode( $searchon );
+ $searchon = $this->db->addQuotes( $searchon );
$field = $this->getIndexField( $fulltext );
- return " MATCH($field) AGAINST('$searchon' IN BOOLEAN MODE) ";
+ return " MATCH($field) AGAINST($searchon IN BOOLEAN MODE) ";
}
function regexTerm( $string, $wildcard ) {
@@ -160,8 +156,8 @@ class SearchMySQL extends SearchEngine {
/**
* Perform a full text search query and return a result set.
*
- * @param string $term raw search term
- * @return MySQLSearchResultSet
+ * @param string $term Raw search term
+ * @return SqlSearchResultSet
*/
function searchText( $term ) {
return $this->searchInternal( $term, true );
@@ -170,16 +166,14 @@ class SearchMySQL extends SearchEngine {
/**
* Perform a title-only search query and return a result set.
*
- * @param string $term raw search term
- * @return MySQLSearchResultSet
+ * @param string $term Raw search term
+ * @return SqlSearchResultSet
*/
function searchTitle( $term ) {
return $this->searchInternal( $term, false );
}
protected function searchInternal( $term, $fulltext ) {
- global $wgCountTotalSearchHits;
-
// This seems out of place, why is this called with empty term?
if ( trim( $term ) === '' ) {
return null;
@@ -193,21 +187,19 @@ class SearchMySQL extends SearchEngine {
);
$total = null;
- if ( $wgCountTotalSearchHits ) {
- $query = $this->getCountQuery( $filteredTerm, $fulltext );
- $totalResult = $this->db->select(
- $query['tables'], $query['fields'], $query['conds'],
- __METHOD__, $query['options'], $query['joins']
- );
-
- $row = $totalResult->fetchObject();
- if ( $row ) {
- $total = intval( $row->c );
- }
- $totalResult->free();
+ $query = $this->getCountQuery( $filteredTerm, $fulltext );
+ $totalResult = $this->db->select(
+ $query['tables'], $query['fields'], $query['conds'],
+ __METHOD__, $query['options'], $query['joins']
+ );
+
+ $row = $totalResult->fetchObject();
+ if ( $row ) {
+ $total = intval( $row->c );
}
+ $totalResult->free();
- return new MySQLSearchResultSet( $resultSet, $this->searchTerms, $total );
+ return new SqlSearchResultSet( $resultSet, $this->searchTerms, $total );
}
public function supports( $feature ) {
@@ -221,14 +213,12 @@ class SearchMySQL extends SearchEngine {
/**
* Add special conditions
- * @param $query Array
+ * @param array $query
* @since 1.18
*/
protected function queryFeatures( &$query ) {
foreach ( $this->features as $feature => $value ) {
- if ( $feature === 'list-redirects' && !$value ) {
- $query['conds']['page_is_redirect'] = 0;
- } elseif ( $feature === 'title-suffix-filter' && $value ) {
+ if ( $feature === 'title-suffix-filter' && $value ) {
$query['conds'][] = 'page_title' . $this->db->buildLike( $this->db->anyString(), $value );
}
}
@@ -236,7 +226,7 @@ class SearchMySQL extends SearchEngine {
/**
* Add namespace conditions
- * @param $query Array
+ * @param array $query
* @since 1.18 (changed)
*/
function queryNamespaces( &$query ) {
@@ -250,7 +240,7 @@ class SearchMySQL extends SearchEngine {
/**
* Add limit options
- * @param $query Array
+ * @param array $query
* @since 1.18
*/
protected function limitResult( &$query ) {
@@ -261,9 +251,9 @@ class SearchMySQL extends SearchEngine {
/**
* Construct the SQL query to do the search.
* The guts shoulds be constructed in queryMain()
- * @param $filteredTerm String
- * @param $fulltext Boolean
- * @return Array
+ * @param string $filteredTerm
+ * @param bool $fulltext
+ * @return array
* @since 1.18 (changed)
*/
function getQuery( $filteredTerm, $fulltext ) {
@@ -285,8 +275,8 @@ class SearchMySQL extends SearchEngine {
/**
* Picks which field to index on, depending on what type of query.
- * @param $fulltext Boolean
- * @return String
+ * @param bool $fulltext
+ * @return string
*/
function getIndexField( $fulltext ) {
return $fulltext ? 'si_text' : 'si_title';
@@ -295,9 +285,9 @@ class SearchMySQL extends SearchEngine {
/**
* Get the base part of the search query.
*
- * @param &$query array Search query array
- * @param $filteredTerm String
- * @param $fulltext Boolean
+ * @param array &$query Search query array
+ * @param string $filteredTerm
+ * @param bool $fulltext
* @since 1.18 (changed)
*/
function queryMain( &$query, $filteredTerm, $fulltext ) {
@@ -313,6 +303,8 @@ class SearchMySQL extends SearchEngine {
/**
* @since 1.18 (changed)
+ * @param string $filteredTerm
+ * @param bool $fulltext
* @return array
*/
function getCountQuery( $filteredTerm, $fulltext ) {
@@ -336,9 +328,9 @@ class SearchMySQL extends SearchEngine {
* Create or update the search index record for the given page.
* Title and text should be pre-processed.
*
- * @param $id Integer
- * @param $title String
- * @param $text String
+ * @param int $id
+ * @param string $title
+ * @param string $text
*/
function update( $id, $title, $text ) {
$dbw = wfGetDB( DB_MASTER );
@@ -355,8 +347,8 @@ class SearchMySQL extends SearchEngine {
* Update a search index record's title only.
* Title should be pre-processed.
*
- * @param $id Integer
- * @param $title String
+ * @param int $id
+ * @param string $title
*/
function updateTitle( $id, $title ) {
$dbw = wfGetDB( DB_MASTER );
@@ -372,8 +364,8 @@ class SearchMySQL extends SearchEngine {
* Delete an indexed page
* Title should be pre-processed.
*
- * @param Integer $id Page id that was deleted
- * @param String $title Title of page that was deleted
+ * @param int $id Page id that was deleted
+ * @param string $title Title of page that was deleted
*/
function delete( $id, $title ) {
$dbw = wfGetDB( DB_MASTER );
@@ -384,6 +376,7 @@ class SearchMySQL extends SearchEngine {
/**
* Converts some characters for MySQL's indexing to grok it correctly,
* and pads short words to overcome limitations.
+ * @param string $string
* @return mixed|string
*/
function normalizeText( $string ) {
@@ -432,6 +425,7 @@ class SearchMySQL extends SearchEngine {
* Armor a case-folded UTF-8 string to get through MySQL's
* fulltext search without being mucked up by funny charset
* settings or anything else of the sort.
+ * @param array $matches
* @return string
*/
protected function stripForSearchCallback( $matches ) {
@@ -462,17 +456,3 @@ class SearchMySQL extends SearchEngine {
return self::$mMinSearchLength;
}
}
-
-/**
- * @ingroup Search
- */
-class MySQLSearchResultSet extends SqlSearchResultSet {
- function __construct( $resultSet, $terms, $totalHits = null ) {
- parent::__construct( $resultSet, $terms );
- $this->mTotalHits = $totalHits;
- }
-
- function getTotalHits() {
- return $this->mTotalHits;
- }
-}
diff --git a/includes/search/SearchOracle.php b/includes/search/SearchOracle.php
index a8479654..58211484 100644
--- a/includes/search/SearchOracle.php
+++ b/includes/search/SearchOracle.php
@@ -3,7 +3,7 @@
* Oracle search engine
*
* Copyright © 2004 Brion Vibber <brion@pobox.com>
- * http://www.mediawiki.org/
+ * https://www.mediawiki.org/
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -28,8 +28,7 @@
* Search engine hook base class for Oracle (ConText).
* @ingroup Search
*/
-class SearchOracle extends SearchEngine {
-
+class SearchOracle extends SearchDatabase {
private $reservedWords = array(
'ABOUT' => 1,
'ACCUM' => 1,
@@ -60,17 +59,9 @@ class SearchOracle extends SearchEngine {
);
/**
- * Creates an instance of this class
- * @param $db DatabasePostgres: database object
- */
- function __construct( $db ) {
- parent::__construct( $db );
- }
-
- /**
* Perform a full text search query and return a result set.
*
- * @param string $term raw search term
+ * @param string $term Raw search term
* @return SqlSearchResultSet
*/
function searchText( $term ) {
@@ -78,14 +69,14 @@ class SearchOracle extends SearchEngine {
return new SqlSearchResultSet( false, '' );
}
- $resultSet = $this->db->resultObject( $this->db->query( $this->getQuery( $this->filter( $term ), true ) ) );
+ $resultSet = $this->db->query( $this->getQuery( $this->filter( $term ), true ) );
return new SqlSearchResultSet( $resultSet, $this->searchTerms );
}
/**
* Perform a title-only search query and return a result set.
*
- * @param string $term raw search term
+ * @param string $term Raw search term
* @return SqlSearchResultSet
*/
function searchTitle( $term ) {
@@ -93,25 +84,13 @@ class SearchOracle extends SearchEngine {
return new SqlSearchResultSet( false, '' );
}
- $resultSet = $this->db->resultObject( $this->db->query( $this->getQuery( $this->filter( $term ), false ) ) );
- return new MySQLSearchResultSet( $resultSet, $this->searchTerms );
- }
-
- /**
- * Return a partial WHERE clause to exclude redirects, if so set
- * @return String
- */
- function queryRedirect() {
- if ( $this->showRedirects ) {
- return '';
- } else {
- return 'AND page_is_redirect=0';
- }
+ $resultSet = $this->db->query( $this->getQuery( $this->filter( $term ), false ) );
+ return new SqlSearchResultSet( $resultSet, $this->searchTerms );
}
/**
* Return a partial WHERE clause to limit the search to the given namespaces
- * @return String
+ * @return string
*/
function queryNamespaces() {
if ( is_null( $this->namespaces ) ) {
@@ -128,9 +107,9 @@ class SearchOracle extends SearchEngine {
/**
* Return a LIMIT clause to limit results on the query.
*
- * @param $sql string
+ * @param string $sql
*
- * @return String
+ * @return string
*/
function queryLimit( $sql ) {
return $this->db->limitResult( $sql, $this->limit, $this->offset );
@@ -140,7 +119,9 @@ class SearchOracle extends SearchEngine {
* Does not do anything for generic search engine
* subclasses may define this though
*
- * @return String
+ * @param string $filteredTerm
+ * @param bool $fulltext
+ * @return string
*/
function queryRanking( $filteredTerm, $fulltext ) {
return ' ORDER BY score(1)';
@@ -149,21 +130,20 @@ class SearchOracle extends SearchEngine {
/**
* Construct the full SQL query to do the search.
* The guts shoulds be constructed in queryMain()
- * @param $filteredTerm String
- * @param $fulltext Boolean
- * @return String
+ * @param string $filteredTerm
+ * @param bool $fulltext
+ * @return string
*/
function getQuery( $filteredTerm, $fulltext ) {
return $this->queryLimit( $this->queryMain( $filteredTerm, $fulltext ) . ' ' .
- $this->queryRedirect() . ' ' .
$this->queryNamespaces() . ' ' .
$this->queryRanking( $filteredTerm, $fulltext ) . ' ' );
}
/**
* Picks which field to index on, depending on what type of query.
- * @param $fulltext Boolean
- * @return String
+ * @param bool $fulltext
+ * @return string
*/
function getIndexField( $fulltext ) {
return $fulltext ? 'si_text' : 'si_title';
@@ -172,9 +152,9 @@ class SearchOracle extends SearchEngine {
/**
* Get the base part of the search query.
*
- * @param $filteredTerm String
- * @param $fulltext Boolean
- * @return String
+ * @param string $filteredTerm
+ * @param bool $fulltext
+ * @return string
*/
function queryMain( $filteredTerm, $fulltext ) {
$match = $this->parseQuery( $filteredTerm, $fulltext );
@@ -188,11 +168,13 @@ class SearchOracle extends SearchEngine {
/**
* Parse a user input search string, and return an SQL fragment to be used
* as part of a WHERE clause
+ * @param string $filteredText
+ * @param bool $fulltext
* @return string
*/
function parseQuery( $filteredText, $fulltext ) {
global $wgContLang;
- $lc = SearchEngine::legalSearchChars();
+ $lc = $this->legalSearchChars();
$this->searchTerms = array();
# @todo FIXME: This doesn't handle parenthetical expressions.
@@ -238,13 +220,14 @@ class SearchOracle extends SearchEngine {
$t = preg_replace( '/([-&|])/', '\\\\$1', $t );
return $t;
}
+
/**
* Create or update the search index record for the given page.
* Title and text should be pre-processed.
*
- * @param $id Integer
- * @param $title String
- * @param $text String
+ * @param int $id
+ * @param string $title
+ * @param string $text
*/
function update( $id, $title, $text ) {
$dbw = wfGetDB( DB_MASTER );
@@ -271,8 +254,8 @@ class SearchOracle extends SearchEngine {
* Update a search index record's title only.
* Title should be pre-processed.
*
- * @param $id Integer
- * @param $title String
+ * @param int $id
+ * @param string $title
*/
function updateTitle( $id, $title ) {
$dbw = wfGetDB( DB_MASTER );
diff --git a/includes/search/SearchPostgres.php b/includes/search/SearchPostgres.php
index 7f19ed13..59b0c31c 100644
--- a/includes/search/SearchPostgres.php
+++ b/includes/search/SearchPostgres.php
@@ -3,7 +3,7 @@
* PostgreSQL search engine
*
* Copyright © 2006-2007 Greg Sabino Mullane <greg@turnstep.com>
- * http://www.mediawiki.org/
+ * https://www.mediawiki.org/
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -28,56 +28,36 @@
* Search engine hook base class for Postgres
* @ingroup Search
*/
-class SearchPostgres extends SearchEngine {
-
- /**
- * @var DatabasePostgres
- */
- protected $db;
- /**
- * Creates an instance of this class
- * @param $db DatabaseSqlite: database object
- */
- function __construct( $db ) {
- parent::__construct( $db );
- }
-
+class SearchPostgres extends SearchDatabase {
/**
* Perform a full text search query via tsearch2 and return a result set.
* Currently searches a page's current title (page.page_title) and
* latest revision article text (pagecontent.old_text)
*
- * @param string $term raw search term
- * @return PostgresSearchResultSet
+ * @param string $term Raw search term
+ * @return SqlSearchResultSet
*/
function searchTitle( $term ) {
$q = $this->searchQuery( $term, 'titlevector', 'page_title' );
$olderror = error_reporting( E_ERROR );
- $resultSet = $this->db->resultObject( $this->db->query( $q, 'SearchPostgres', true ) );
+ $resultSet = $this->db->query( $q, 'SearchPostgres', true );
error_reporting( $olderror );
- if ( !$resultSet ) {
- // Needed for "Query requires full scan, GIN doesn't support it"
- return new SearchResultTooMany();
- }
- return new PostgresSearchResultSet( $resultSet, $this->searchTerms );
+ return new SqlSearchResultSet( $resultSet, $this->searchTerms );
}
function searchText( $term ) {
$q = $this->searchQuery( $term, 'textvector', 'old_text' );
$olderror = error_reporting( E_ERROR );
- $resultSet = $this->db->resultObject( $this->db->query( $q, 'SearchPostgres', true ) );
+ $resultSet = $this->db->query( $q, 'SearchPostgres', true );
error_reporting( $olderror );
- if ( !$resultSet ) {
- return new SearchResultTooMany();
- }
- return new PostgresSearchResultSet( $resultSet, $this->searchTerms );
+ return new SqlSearchResultSet( $resultSet, $this->searchTerms );
}
/**
* Transform the user's search string into a better form for tsearch2
* Returns an SQL fragment consisting of quoted text to search for.
*
- * @param $term string
+ * @param string $term
*
* @return string
*/
@@ -143,9 +123,9 @@ class SearchPostgres extends SearchEngine {
/**
* Construct the full SQL query to do the search.
- * @param $term String
- * @param $fulltext String
- * @param $colname
+ * @param string $term
+ * @param string $fulltext
+ * @param string $colname
* @return string
*/
function searchQuery( $term, $fulltext, $colname ) {
@@ -153,8 +133,8 @@ class SearchPostgres extends SearchEngine {
$searchstring = $this->parseQuery( $term );
## We need a separate query here so gin does not complain about empty searches
- $SQL = "SELECT to_tsquery($searchstring)";
- $res = $this->db->query( $SQL );
+ $sql = "SELECT to_tsquery($searchstring)";
+ $res = $this->db->query( $sql );
if ( !$res ) {
## TODO: Better output (example to catch: one 'two)
die( "Sorry, that was not a valid search string. Please go back and try again" );
@@ -162,6 +142,7 @@ class SearchPostgres extends SearchEngine {
$top = $res->fetchRow();
$top = $top[0];
+ $this->searchTerms = array();
if ( $top === "" ) { ## e.g. if only stopwords are used XXX return something better
$query = "SELECT page_id, page_namespace, page_title, 0 AS score " .
"FROM page p, revision r, pagecontent c WHERE p.page_latest = r.rev_id " .
@@ -181,11 +162,6 @@ class SearchPostgres extends SearchEngine {
"AND r.rev_text_id = c.old_id AND $fulltext @@ to_tsquery($searchstring)";
}
- ## Redirects
- if ( !$this->showRedirects ) {
- $query .= ' AND page_is_redirect = 0';
- }
-
## Namespaces - defaults to 0
if ( !is_null( $this->namespaces ) ) { // null -> search all
if ( count( $this->namespaces ) < 1 ) {
@@ -209,10 +185,10 @@ class SearchPostgres extends SearchEngine {
function update( $pageid, $title, $text ) {
## We don't want to index older revisions
- $SQL = "UPDATE pagecontent SET textvector = NULL WHERE old_id IN " .
+ $sql = "UPDATE pagecontent SET textvector = NULL WHERE textvector IS NOT NULL and old_id IN " .
"(SELECT rev_text_id FROM revision WHERE rev_page = " . intval( $pageid ) .
" ORDER BY rev_text_id DESC OFFSET 1)";
- $this->db->query( $SQL );
+ $this->db->query( $sql );
return true;
}
@@ -220,35 +196,4 @@ class SearchPostgres extends SearchEngine {
return true;
}
-} ## end of the SearchPostgres class
-
-/**
- * @ingroup Search
- */
-class PostgresSearchResult extends SearchResult {
- function __construct( $row ) {
- parent::__construct( $row );
- $this->score = $row->score;
- }
- function getScore() {
- return $this->score;
- }
-}
-
-/**
- * @ingroup Search
- */
-class PostgresSearchResultSet extends SqlSearchResultSet {
- function __construct( $resultSet, $terms ) {
- parent::__construct( $resultSet, $terms );
- }
-
- function next() {
- $row = $this->mResultSet->fetchObject();
- if ( $row === false ) {
- return false;
- } else {
- return new PostgresSearchResult( $row );
- }
- }
}
diff --git a/includes/search/SearchResult.php b/includes/search/SearchResult.php
new file mode 100644
index 00000000..aeaba8df
--- /dev/null
+++ b/includes/search/SearchResult.php
@@ -0,0 +1,237 @@
+<?php
+/**
+ * Search engine result
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Search
+ */
+
+/**
+ * @todo FIXME: This class is horribly factored. It would probably be better to
+ * have a useful base class to which you pass some standard information, then
+ * let the fancy self-highlighters extend that.
+ * @ingroup Search
+ */
+class SearchResult {
+
+ /**
+ * @var Revision
+ */
+ protected $mRevision = null;
+
+ /**
+ * @var File
+ */
+ protected $mImage = null;
+
+ /**
+ * @var Title
+ */
+ protected $mTitle;
+
+ /**
+ * @var string
+ */
+ protected $mText;
+
+ /**
+ * Return a new SearchResult and initializes it with a title.
+ *
+ * @param Title $title
+ * @return SearchResult
+ */
+ public static function newFromTitle( $title ) {
+ $result = new self();
+ $result->initFromTitle( $title );
+ return $result;
+ }
+
+ /**
+ * Initialize from a Title and if possible initializes a corresponding
+ * Revision and File.
+ *
+ * @param Title $title
+ */
+ protected function initFromTitle( $title ) {
+ $this->mTitle = $title;
+ if ( !is_null( $this->mTitle ) ) {
+ $id = false;
+ wfRunHooks( 'SearchResultInitFromTitle', array( $title, &$id ) );
+ $this->mRevision = Revision::newFromTitle(
+ $this->mTitle, $id, Revision::READ_NORMAL );
+ if ( $this->mTitle->getNamespace() === NS_FILE ) {
+ $this->mImage = wfFindFile( $this->mTitle );
+ }
+ }
+ }
+
+ /**
+ * Check if this is result points to an invalid title
+ *
+ * @return bool
+ */
+ function isBrokenTitle() {
+ return is_null( $this->mTitle );
+ }
+
+ /**
+ * Check if target page is missing, happens when index is out of date
+ *
+ * @return bool
+ */
+ function isMissingRevision() {
+ return !$this->mRevision && !$this->mImage;
+ }
+
+ /**
+ * @return Title
+ */
+ function getTitle() {
+ return $this->mTitle;
+ }
+
+ /**
+ * Get the file for this page, if one exists
+ * @return File|null
+ */
+ function getFile() {
+ return $this->mImage;
+ }
+
+ /**
+ * Lazy initialization of article text from DB
+ */
+ protected function initText() {
+ if ( !isset( $this->mText ) ) {
+ if ( $this->mRevision != null ) {
+ $this->mText = SearchEngine::create()
+ ->getTextFromContent( $this->mTitle, $this->mRevision->getContent() );
+ } else { // TODO: can we fetch raw wikitext for commons images?
+ $this->mText = '';
+ }
+ }
+ }
+
+ /**
+ * @param array $terms Terms to highlight
+ * @return string Highlighted text snippet, null (and not '') if not supported
+ */
+ function getTextSnippet( $terms ) {
+ global $wgAdvancedSearchHighlighting;
+ $this->initText();
+
+ // TODO: make highliter take a content object. Make ContentHandler a factory for SearchHighliter.
+ list( $contextlines, $contextchars ) = SearchEngine::userHighlightPrefs();
+
+ $h = new SearchHighlighter();
+ if ( count( $terms ) > 0 ) {
+ if ( $wgAdvancedSearchHighlighting ) {
+ return $h->highlightText( $this->mText, $terms, $contextlines, $contextchars );
+ } else {
+ return $h->highlightSimple( $this->mText, $terms, $contextlines, $contextchars );
+ }
+ } else {
+ return $h->highlightNone( $this->mText, $contextlines, $contextchars );
+ }
+ }
+
+ /**
+ * @return string Highlighted title, '' if not supported
+ */
+ function getTitleSnippet() {
+ return '';
+ }
+
+ /**
+ * @return string Highlighted redirect name (redirect to this page), '' if none or not supported
+ */
+ function getRedirectSnippet() {
+ return '';
+ }
+
+ /**
+ * @return Title Title object for the redirect to this page, null if none or not supported
+ */
+ function getRedirectTitle() {
+ return null;
+ }
+
+ /**
+ * @return string Highlighted relevant section name, null if none or not supported
+ */
+ function getSectionSnippet() {
+ return '';
+ }
+
+ /**
+ * @return Title Title object (pagename+fragment) for the section, null if none or not supported
+ */
+ function getSectionTitle() {
+ return null;
+ }
+
+ /**
+ * @return string Timestamp
+ */
+ function getTimestamp() {
+ if ( $this->mRevision ) {
+ return $this->mRevision->getTimestamp();
+ } elseif ( $this->mImage ) {
+ return $this->mImage->getTimestamp();
+ }
+ return '';
+ }
+
+ /**
+ * @return int Number of words
+ */
+ function getWordCount() {
+ $this->initText();
+ return str_word_count( $this->mText );
+ }
+
+ /**
+ * @return int Size in bytes
+ */
+ function getByteSize() {
+ $this->initText();
+ return strlen( $this->mText );
+ }
+
+ /**
+ * @return string Interwiki prefix of the title (return iw even if title is broken)
+ */
+ function getInterwikiPrefix() {
+ return '';
+ }
+
+ /**
+ * @return string Interwiki namespace of the title (since we likely can't resolve it locally)
+ */
+ function getInterwikiNamespaceText() {
+ return '';
+ }
+
+ /**
+ * Did this match file contents (eg: PDF/DJVU)?
+ * @return bool
+ */
+ function isFileMatch() {
+ return false;
+ }
+}
diff --git a/includes/search/SearchResultSet.php b/includes/search/SearchResultSet.php
new file mode 100644
index 00000000..406d322d
--- /dev/null
+++ b/includes/search/SearchResultSet.php
@@ -0,0 +1,211 @@
+<?php
+/**
+ * Search result sets
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Search
+ */
+
+/**
+ * @ingroup Search
+ */
+class SearchResultSet {
+ /**
+ * Fetch an array of regular expression fragments for matching
+ * the search terms as parsed by this engine in a text extract.
+ * STUB
+ *
+ * @return array
+ */
+ function termMatches() {
+ return array();
+ }
+
+ function numRows() {
+ return 0;
+ }
+
+ /**
+ * Some search modes return a total hit count for the query
+ * in the entire article database. This may include pages
+ * in namespaces that would not be matched on the given
+ * settings.
+ *
+ * Return null if no total hits number is supported.
+ *
+ * @return int
+ */
+ function getTotalHits() {
+ return null;
+ }
+
+ /**
+ * Some search modes return a suggested alternate term if there are
+ * no exact hits. Returns true if there is one on this set.
+ *
+ * @return bool
+ */
+ function hasSuggestion() {
+ return false;
+ }
+
+ /**
+ * @return string Suggested query, null if none
+ */
+ function getSuggestionQuery() {
+ return null;
+ }
+
+ /**
+ * @return string HTML highlighted suggested query, '' if none
+ */
+ function getSuggestionSnippet() {
+ return '';
+ }
+
+ /**
+ * Return a result set of hits on other (multiple) wikis associated with this one
+ *
+ * @return SearchResultSet
+ */
+ function getInterwikiResults() {
+ return null;
+ }
+
+ /**
+ * Check if there are results on other wikis
+ *
+ * @return bool
+ */
+ function hasInterwikiResults() {
+ return $this->getInterwikiResults() != null;
+ }
+
+ /**
+ * Fetches next search result, or false.
+ * STUB
+ *
+ * @return SearchResult
+ */
+ function next() {
+ return false;
+ }
+
+ /**
+ * Frees the result set, if applicable.
+ */
+ function free() {
+ // ...
+ }
+
+ /**
+ * Did the search contain search syntax? If so, Special:Search won't offer
+ * the user a link to a create a page named by the search string because the
+ * name would contain the search syntax.
+ * @return bool
+ */
+ public function searchContainedSyntax() {
+ return false;
+ }
+}
+
+/**
+ * This class is used for different SQL-based search engines shipped with MediaWiki
+ * @ingroup Search
+ */
+class SqlSearchResultSet extends SearchResultSet {
+ protected $resultSet;
+ protected $terms;
+ protected $totalHits;
+
+ function __construct( $resultSet, $terms, $total = null ) {
+ $this->resultSet = $resultSet;
+ $this->terms = $terms;
+ $this->totalHits = $total;
+ }
+
+ function termMatches() {
+ return $this->terms;
+ }
+
+ function numRows() {
+ if ( $this->resultSet === false ) {
+ return false;
+ }
+
+ return $this->resultSet->numRows();
+ }
+
+ function next() {
+ if ( $this->resultSet === false ) {
+ return false;
+ }
+
+ $row = $this->resultSet->fetchObject();
+ if ( $row === false ) {
+ return false;
+ }
+
+ return SearchResult::newFromTitle(
+ Title::makeTitle( $row->page_namespace, $row->page_title )
+ );
+ }
+
+ function free() {
+ if ( $this->resultSet === false ) {
+ return false;
+ }
+
+ $this->resultSet->free();
+ }
+
+ function getTotalHits() {
+ if ( !is_null( $this->totalHits ) ) {
+ return $this->totalHits;
+ } else {
+ // Special:Search expects a number here.
+ return $this->numRows();
+ }
+ }
+}
+
+/**
+ * A SearchResultSet wrapper for SearchEngine::getNearMatch
+ */
+class SearchNearMatchResultSet extends SearchResultSet {
+ private $fetched = false;
+
+ /**
+ * @param Title|null $match Title if matched, else null
+ */
+ public function __construct( $match ) {
+ $this->result = $match;
+ }
+
+ public function numRows() {
+ return $this->result ? 1 : 0;
+ }
+
+ public function next() {
+ if ( $this->fetched || !$this->result ) {
+ return false;
+ }
+ $this->fetched = true;
+ return SearchResult::newFromTitle( $this->result );
+ }
+}
diff --git a/includes/search/SearchSqlite.php b/includes/search/SearchSqlite.php
index 554181f6..eee69307 100644
--- a/includes/search/SearchSqlite.php
+++ b/includes/search/SearchSqlite.php
@@ -25,24 +25,10 @@
* Search engine hook for SQLite
* @ingroup Search
*/
-class SearchSqlite extends SearchEngine {
-
- /**
- * @var DatabaseSqlite
- */
- protected $db;
-
- /**
- * Creates an instance of this class
- * @param $db DatabaseSqlite: database object
- */
- function __construct( $db ) {
- parent::__construct( $db );
- }
-
+class SearchSqlite extends SearchDatabase {
/**
* Whether fulltext search is supported by current schema
- * @return Boolean
+ * @return bool
*/
function fulltextSearchSupported() {
return $this->db->checkForEnabledSearch();
@@ -52,11 +38,13 @@ class SearchSqlite extends SearchEngine {
* Parse the user's query and transform it into an SQL fragment which will
* become part of a WHERE clause
*
+ * @param string $filteredText
+ * @param bool $fulltext
* @return string
*/
function parseQuery( $filteredText, $fulltext ) {
global $wgContLang;
- $lc = SearchEngine::legalSearchChars(); // Minus format chars
+ $lc = $this->legalSearchChars(); // Minus format chars
$searchon = '';
$this->searchTerms = array();
@@ -64,7 +52,9 @@ class SearchSqlite extends SearchEngine {
if ( preg_match_all( '/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/',
$filteredText, $m, PREG_SET_ORDER ) ) {
foreach ( $m as $bits ) {
- @list( /* all */, $modifier, $term, $nonQuoted, $wildcard ) = $bits;
+ wfSuppressWarnings();
+ list( /* all */, $modifier, $term, $nonQuoted, $wildcard ) = $bits;
+ wfRestoreWarnings();
if ( $nonQuoted != '' ) {
$term = $nonQuoted;
@@ -127,9 +117,9 @@ class SearchSqlite extends SearchEngine {
wfDebug( __METHOD__ . ": Can't understand search query '{$filteredText}'\n" );
}
- $searchon = $this->db->strencode( $searchon );
+ $searchon = $this->db->addQuotes( $searchon );
$field = $this->getIndexField( $fulltext );
- return " $field MATCH '$searchon' ";
+ return " $field MATCH $searchon ";
}
function regexTerm( $string, $wildcard ) {
@@ -158,8 +148,8 @@ class SearchSqlite extends SearchEngine {
/**
* Perform a full text search query and return a result set.
*
- * @param string $term raw search term
- * @return SqliteSearchResultSet
+ * @param string $term Raw search term
+ * @return SqlSearchResultSet
*/
function searchText( $term ) {
return $this->searchInternal( $term, true );
@@ -168,15 +158,15 @@ class SearchSqlite extends SearchEngine {
/**
* Perform a title-only search query and return a result set.
*
- * @param string $term raw search term
- * @return SqliteSearchResultSet
+ * @param string $term Raw search term
+ * @return SqlSearchResultSet
*/
function searchTitle( $term ) {
return $this->searchInternal( $term, false );
}
protected function searchInternal( $term, $fulltext ) {
- global $wgCountTotalSearchHits, $wgContLang;
+ global $wgContLang;
if ( !$this->fulltextSearchSupported() ) {
return null;
@@ -186,33 +176,19 @@ class SearchSqlite extends SearchEngine {
$resultSet = $this->db->query( $this->getQuery( $filteredTerm, $fulltext ) );
$total = null;
- if ( $wgCountTotalSearchHits ) {
- $totalResult = $this->db->query( $this->getCountQuery( $filteredTerm, $fulltext ) );
- $row = $totalResult->fetchObject();
- if ( $row ) {
- $total = intval( $row->c );
- }
- $totalResult->free();
+ $totalResult = $this->db->query( $this->getCountQuery( $filteredTerm, $fulltext ) );
+ $row = $totalResult->fetchObject();
+ if ( $row ) {
+ $total = intval( $row->c );
}
+ $totalResult->free();
- return new SqliteSearchResultSet( $resultSet, $this->searchTerms, $total );
- }
-
- /**
- * Return a partial WHERE clause to exclude redirects, if so set
- * @return String
- */
- function queryRedirect() {
- if ( $this->showRedirects ) {
- return '';
- } else {
- return 'AND page_is_redirect=0';
- }
+ return new SqlSearchResultSet( $resultSet, $this->searchTerms, $total );
}
/**
* Return a partial WHERE clause to limit the search to the given namespaces
- * @return String
+ * @return string
*/
function queryNamespaces() {
if ( is_null( $this->namespaces ) ) {
@@ -228,8 +204,8 @@ class SearchSqlite extends SearchEngine {
/**
* Returns a query with limit for number of results set.
- * @param $sql String:
- * @return String
+ * @param string $sql
+ * @return string
*/
function limitResult( $sql ) {
return $this->db->limitResult( $sql, $this->limit, $this->offset );
@@ -238,22 +214,21 @@ class SearchSqlite extends SearchEngine {
/**
* Construct the full SQL query to do the search.
* The guts shoulds be constructed in queryMain()
- * @param $filteredTerm String
- * @param $fulltext Boolean
- * @return String
+ * @param string $filteredTerm
+ * @param bool $fulltext
+ * @return string
*/
function getQuery( $filteredTerm, $fulltext ) {
return $this->limitResult(
$this->queryMain( $filteredTerm, $fulltext ) . ' ' .
- $this->queryRedirect() . ' ' .
$this->queryNamespaces()
);
}
/**
* Picks which field to index on, depending on what type of query.
- * @param $fulltext Boolean
- * @return String
+ * @param bool $fulltext
+ * @return string
*/
function getIndexField( $fulltext ) {
return $fulltext ? 'si_text' : 'si_title';
@@ -262,9 +237,9 @@ class SearchSqlite extends SearchEngine {
/**
* Get the base part of the search query.
*
- * @param $filteredTerm String
- * @param $fulltext Boolean
- * @return String
+ * @param string $filteredTerm
+ * @param bool $fulltext
+ * @return string
*/
function queryMain( $filteredTerm, $fulltext ) {
$match = $this->parseQuery( $filteredTerm, $fulltext );
@@ -281,8 +256,7 @@ class SearchSqlite extends SearchEngine {
$searchindex = $this->db->tableName( 'searchindex' );
return "SELECT COUNT(*) AS c " .
"FROM $page,$searchindex " .
- "WHERE page_id=$searchindex.rowid AND $match" .
- $this->queryRedirect() . ' ' .
+ "WHERE page_id=$searchindex.rowid AND $match " .
$this->queryNamespaces();
}
@@ -290,9 +264,9 @@ class SearchSqlite extends SearchEngine {
* Create or update the search index record for the given page.
* Title and text should be pre-processed.
*
- * @param $id Integer
- * @param $title String
- * @param $text String
+ * @param int $id
+ * @param string $title
+ * @param string $text
*/
function update( $id, $title, $text ) {
if ( !$this->fulltextSearchSupported() ) {
@@ -316,8 +290,8 @@ class SearchSqlite extends SearchEngine {
* Update a search index record's title only.
* Title should be pre-processed.
*
- * @param $id Integer
- * @param $title String
+ * @param int $id
+ * @param string $title
*/
function updateTitle( $id, $title ) {
if ( !$this->fulltextSearchSupported() ) {
@@ -331,17 +305,3 @@ class SearchSqlite extends SearchEngine {
__METHOD__ );
}
}
-
-/**
- * @ingroup Search
- */
-class SqliteSearchResultSet extends SqlSearchResultSet {
- function __construct( $resultSet, $terms, $totalHits = null ) {
- parent::__construct( $resultSet, $terms );
- $this->mTotalHits = $totalHits;
- }
-
- function getTotalHits() {
- return $this->mTotalHits;
- }
-}
diff --git a/includes/search/SearchUpdate.php b/includes/search/SearchUpdate.php
deleted file mode 100644
index 82a413e9..00000000
--- a/includes/search/SearchUpdate.php
+++ /dev/null
@@ -1,185 +0,0 @@
-<?php
-/**
- * Search index updater
- *
- * See deferred.txt
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Search
- */
-
-/**
- * Database independant search index updater
- *
- * @ingroup Search
- */
-class SearchUpdate implements DeferrableUpdate {
- /**
- * Page id being updated
- * @var int
- */
- private $id = 0;
-
- /**
- * Title we're updating
- * @var Title
- */
- private $title;
-
- /**
- * Content of the page (not text)
- * @var Content|false
- */
- private $content;
-
- /**
- * Constructor
- *
- * @param int $id Page id to update
- * @param Title|string $title Title of page to update
- * @param Content|string|false $c Content of the page to update.
- * If a Content object, text will be gotten from it. String is for back-compat.
- * Passing false tells the backend to just update the title, not the content
- */
- public function __construct( $id, $title, $c = false ) {
- if ( is_string( $title ) ) {
- $nt = Title::newFromText( $title );
- } else {
- $nt = $title;
- }
-
- if ( $nt ) {
- $this->id = $id;
- // is_string() check is back-compat for ApprovedRevs
- if ( is_string( $c ) ) {
- $this->content = new TextContent( $c );
- } else {
- $this->content = $c ?: false;
- }
- $this->title = $nt;
- } else {
- wfDebug( "SearchUpdate object created with invalid title '$title'\n" );
- }
- }
-
- /**
- * Perform actual update for the entry
- */
- public function doUpdate() {
- global $wgDisableSearchUpdate;
-
- if ( $wgDisableSearchUpdate || !$this->id ) {
- return;
- }
-
- wfProfileIn( __METHOD__ );
-
- $page = WikiPage::newFromId( $this->id, WikiPage::READ_LATEST );
- $indexTitle = Title::indexTitle( $this->title->getNamespace(), $this->title->getText() );
-
- foreach ( SearchEngine::getSearchTypes() as $type ) {
- $search = SearchEngine::create( $type );
- if ( !$search->supports( 'search-update' ) ) {
- continue;
- }
-
- $normalTitle = $search->normalizeText( $indexTitle );
-
- if ( $page === null ) {
- $search->delete( $this->id, $normalTitle );
- continue;
- } elseif ( $this->content === false ) {
- $search->updateTitle( $this->id, $normalTitle );
- continue;
- }
-
- $text = $search->getTextFromContent( $this->title, $this->content );
- if ( !$search->textAlreadyUpdatedForIndex() ) {
- $text = self::updateText( $text );
- }
-
- # Perform the actual update
- $search->update( $this->id, $normalTitle, $search->normalizeText( $text ) );
- }
-
- wfProfileOut( __METHOD__ );
- }
-
- /**
- * Clean text for indexing. Only really suitable for indexing in databases.
- * If you're using a real search engine, you'll probably want to override
- * this behavior and do something nicer with the original wikitext.
- */
- public static function updateText( $text ) {
- global $wgContLang;
-
- # Language-specific strip/conversion
- $text = $wgContLang->normalizeForSearch( $text );
- $lc = SearchEngine::legalSearchChars() . '&#;';
-
- wfProfileIn( __METHOD__ . '-regexps' );
- $text = preg_replace( "/<\\/?\\s*[A-Za-z][^>]*?>/",
- ' ', $wgContLang->lc( " " . $text . " " ) ); # Strip HTML markup
- $text = preg_replace( "/(^|\\n)==\\s*([^\\n]+)\\s*==(\\s)/sD",
- "\\1\\2 \\2 \\2\\3", $text ); # Emphasize headings
-
- # Strip external URLs
- $uc = "A-Za-z0-9_\\/:.,~%\\-+&;#?!=()@\\x80-\\xFF";
- $protos = "http|https|ftp|mailto|news|gopher";
- $pat = "/(^|[^\\[])({$protos}):[{$uc}]+([^{$uc}]|$)/";
- $text = preg_replace( $pat, "\\1 \\3", $text );
-
- $p1 = "/([^\\[])\\[({$protos}):[{$uc}]+]/";
- $p2 = "/([^\\[])\\[({$protos}):[{$uc}]+\\s+([^\\]]+)]/";
- $text = preg_replace( $p1, "\\1 ", $text );
- $text = preg_replace( $p2, "\\1 \\3 ", $text );
-
- # Internal image links
- $pat2 = "/\\[\\[image:([{$uc}]+)\\.(gif|png|jpg|jpeg)([^{$uc}])/i";
- $text = preg_replace( $pat2, " \\1 \\3", $text );
-
- $text = preg_replace( "/([^{$lc}])([{$lc}]+)]]([a-z]+)/",
- "\\1\\2 \\2\\3", $text ); # Handle [[game]]s
-
- # Strip all remaining non-search characters
- $text = preg_replace( "/[^{$lc}]+/", " ", $text );
-
- # Handle 's, s'
- #
- # $text = preg_replace( "/([{$lc}]+)'s /", "\\1 \\1's ", $text );
- # $text = preg_replace( "/([{$lc}]+)s' /", "\\1s ", $text );
- #
- # These tail-anchored regexps are insanely slow. The worst case comes
- # when Japanese or Chinese text (ie, no word spacing) is written on
- # a wiki configured for Western UTF-8 mode. The Unicode characters are
- # expanded to hex codes and the "words" are very long paragraph-length
- # monstrosities. On a large page the above regexps may take over 20
- # seconds *each* on a 1GHz-level processor.
- #
- # Following are reversed versions which are consistently fast
- # (about 3 milliseconds on 1GHz-level processor).
- #
- $text = strrev( preg_replace( "/ s'([{$lc}]+)/", " s'\\1 \\1", strrev( $text ) ) );
- $text = strrev( preg_replace( "/ 's([{$lc}]+)/", " s\\1", strrev( $text ) ) );
-
- # Strip wiki '' and '''
- $text = preg_replace( "/''[']*/", " ", $text );
- wfProfileOut( __METHOD__ . '-regexps' );
- return $text;
- }
-}