'CacheHelper' => __DIR__ . '/includes/cache/CacheHelper.php',
'CacheTime' => __DIR__ . '/includes/parser/CacheTime.php',
'CachedAction' => __DIR__ . '/includes/actions/CachedAction.php',
+ 'CachedBagOStuff' => __DIR__ . '/includes/libs/objectcache/CachedBagOStuff.php',
'CachingSiteStore' => __DIR__ . '/includes/site/CachingSiteStore.php',
'CapsCleanup' => __DIR__ . '/maintenance/cleanupCaps.php',
'Category' => __DIR__ . '/includes/Category.php',
'SearchResult' => __DIR__ . '/includes/search/SearchResult.php',
'SearchResultSet' => __DIR__ . '/includes/search/SearchResultSet.php',
'SearchSqlite' => __DIR__ . '/includes/search/SearchSqlite.php',
+ 'SearchSuggestion' => __DIR__ . '/includes/search/SearchSuggestion.php',
+ 'SearchSuggestionSet' => __DIR__ . '/includes/search/SearchSuggestionSet.php',
'SearchUpdate' => __DIR__ . '/includes/deferred/SearchUpdate.php',
'SectionProfileCallback' => __DIR__ . '/includes/profiler/SectionProfiler.php',
'SectionProfiler' => __DIR__ . '/includes/profiler/SectionProfiler.php',
-# Protect against bug 28235
+# Protect against bug T30235
<IfModule rewrite_module>
RewriteEngine On
RewriteOptions inherit
/**
* Handles searching prefixes of titles and finding any page
* names that match. Used largely by the OpenSearch implementation.
+ * @deprecated Since 1.27, Use SearchEngine::prefixSearchSubpages or SearchEngine::completionSearch
*
* @ingroup Search
*/
* @param int $offset Number of items to skip
* @return array Array of Title objects
*/
- protected function defaultSearchBackend( $namespaces, $search, $limit, $offset ) {
+ public function defaultSearchBackend( $namespaces, $search, $limit, $offset ) {
$ns = array_shift( $namespaces ); // support only one namespace
- if ( in_array( NS_MAIN, $namespaces ) ) {
+ if ( is_null( $ns ) || in_array( NS_MAIN, $namespaces ) ) {
$ns = NS_MAIN; // if searching on many always default to main
}
- $t = Title::newFromText( $search, $ns );
+ if ( $ns == NS_SPECIAL ) {
+ return $this->specialSearch( $search, $limit, $offset );
+ }
+ $t = Title::newFromText( $search, $ns );
$prefix = $t ? $t->getDBkey() : '';
$dbr = wfGetDB( DB_SLAVE );
$res = $dbr->select( 'page',
/**
* Performs prefix search, returning Title objects
+ * @deprecated Since 1.27, Use SearchEngine::prefixSearchSubpages or SearchEngine::completionSearch
* @ingroup Search
*/
class TitlePrefixSearch extends PrefixSearch {
/**
* Performs prefix search, returning strings
+ * @deprecated Since 1.27, Use SearchEngine::prefixSearchSubpages or SearchEngine::completionSearch
* @ingroup Search
*/
class StringPrefixSearch extends PrefixSearch {
* @param array &$results Put results here. Keys have to be integers.
*/
protected function search( $search, $limit, $namespaces, $resolveRedir, &$results ) {
- // Find matching titles as Title objects
- $searcher = new TitlePrefixSearch;
- $titles = $searcher->searchWithVariants( $search, $limit, $namespaces );
+
+ $searchEngine = SearchEngine::create();
+ $searchEngine->setLimitOffset( $limit );
+ $searchEngine->setNamespaces( $namespaces );
+ $titles = $searchEngine->extractTitles( $searchEngine->completionSearchWithVariants( $search ) );
+
if ( !$titles ) {
return;
}
$namespaces = $params['namespace'];
$offset = $params['offset'];
- $searcher = new TitlePrefixSearch;
- $titles = $searcher->searchWithVariants( $search, $limit + 1, $namespaces, $offset );
+ $searchEngine = SearchEngine::create();
+ $searchEngine->setLimitOffset( $limit + 1, $offset );
+ $searchEngine->setNamespaces( $namespaces );
+ $titles = $searchEngine->extractTitles( $searchEngine->completionSearchWithVariants( $search ) );
+
if ( $resultPageSet ) {
$resultPageSet->setRedirectMergePolicy( function( array $current, array $new ) {
if ( !isset( $current['index'] ) || $new['index'] < $current['index'] ) {
const READ_VERIFIED = 2; // promise that caller can tell when keys are stale
/** Bitfield constants for set()/merge() */
const WRITE_SYNC = 1; // synchronously write to all locations for replicated stores
+ const WRITE_CACHE_ONLY = 2; // Only change state of the in-memory cache
public function __construct( array $params = array() ) {
if ( isset( $params['logger'] ) ) {
--- /dev/null
+<?php
+/**
+ * Wrapper around a BagOStuff that caches data in memory
+ *
+ * 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 Cache
+ */
+
+use Psr\Log\LoggerInterface;
+
+/**
+ * Wrapper around a BagOStuff that caches data in memory
+ *
+ * The differences between CachedBagOStuff and MultiWriteBagOStuff are:
+ * * CachedBagOStuff supports only one "backend".
+ * * There's a flag for writes to only go to the in-memory cache.
+ * * The in-memory cache is always updated.
+ * * Locks go to the backend cache (with MultiWriteBagOStuff, it would wind
+ * up going to the HashBagOStuff used for the in-memory cache).
+ *
+ * @ingroup Cache
+ */
+class CachedBagOStuff extends HashBagOStuff {
+ /** @var BagOStuff */
+ protected $backend;
+
+ /**
+ * @param BagOStuff $backend Permanent backend to use
+ * @param array $params Parameters for HashBagOStuff
+ */
+ function __construct( BagOStuff $backend, $params = array() ) {
+ $this->backend = $backend;
+ parent::__construct( $params );
+ }
+
+ protected function doGet( $key, $flags = 0 ) {
+ $ret = parent::doGet( $key, $flags );
+ if ( $ret === false ) {
+ $ret = $this->backend->doGet( $key, $flags );
+ if ( $ret !== false ) {
+ $this->set( $key, $ret, 0, self::WRITE_CACHE_ONLY );
+ }
+ }
+ return $ret;
+ }
+
+ public function set( $key, $value, $exptime = 0, $flags = 0 ) {
+ parent::set( $key, $value, $exptime, $flags );
+ if ( !( $flags & self::WRITE_CACHE_ONLY ) ) {
+ $this->backend->set( $key, $value, $exptime, $flags & ~self::WRITE_CACHE_ONLY );
+ }
+ return true;
+ }
+
+ public function delete( $key, $flags = 0 ) {
+ unset( $this->bag[$key] );
+ if ( !( $flags & self::WRITE_CACHE_ONLY ) ) {
+ $this->backend->delete( $key );
+ }
+
+ return true;
+ }
+
+ public function setLogger( LoggerInterface $logger ) {
+ parent::setLogger( $logger );
+ $this->backend->setLogger( $logger );
+ }
+
+ public function setDebug( $bool ) {
+ parent::setDebug( $bool );
+ $this->backend->setDebug( $bool );
+ }
+
+ public function lock( $key, $timeout = 6, $expiry = 6, $rclass = '' ) {
+ return $this->backend->lock( $key, $timeout, $expiry, $rclass );
+ }
+
+ public function unlock( $key ) {
+ return $this->backend->unlock( $key );
+ }
+
+ public function deleteObjectsExpiringBefore( $date, $progressCallback = false ) {
+ parent::deleteObjectsExpiringBefore( $date, $progressCallback );
+ return $this->backend->deleteObjectsExpiringBefore( $date, $progressCallback );
+ }
+
+ public function getLastError() {
+ return $this->backend->getLastError();
+ }
+
+ public function clearLastError() {
+ $this->backend->clearLastError();
+ }
+
+ public function modifySimpleRelayEvent( array $event ) {
+ return $this->backend->modifySimpleRelayEvent( $event );
+ }
+
+}
$inHeading = false;
// True if there are no more greater-than (>) signs right of $i
$noMoreGT = false;
- // Map of tag name => true if there are no more closing tags of given type right of $i
- $noMoreClosingTag = array();
// True to ignore all input up to the next <onlyinclude>
$findOnlyinclude = $enableOnlyinclude;
// Do a line-start run without outputting an LF character
} else {
$attrEnd = $tagEndPos;
// Find closing tag
- if (
- !isset( $noMoreClosingTag[$name] ) &&
- preg_match( "/<\/" . preg_quote( $name, '/' ) . "\s*>/i",
+ if ( preg_match( "/<\/" . preg_quote( $name, '/' ) . "\s*>/i",
$text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 )
) {
$inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 );
$i = $matches[0][1] + strlen( $matches[0][0] );
$close = '<close>' . htmlspecialchars( $matches[0][0] ) . '</close>';
} else {
- // No end tag -- don't match the tag, treat opening tag as literal and resume parsing.
- $i = $tagEndPos + 1;
- $accum .= htmlspecialchars( substr( $text, $tagStartPos, $tagEndPos + 1 - $tagStartPos ) );
- // Cache results, otherwise we have O(N^2) performance for input like <foo><foo><foo>...
- $noMoreClosingTag[$name] = true;
- continue;
+ // No end tag -- let it run out to the end of the text.
+ $inner = substr( $text, $tagEndPos + 1 );
+ $i = $lengthText;
+ $close = '';
}
}
// <includeonly> and <noinclude> just become <ignore> tags
$inHeading = false;
// True if there are no more greater-than (>) signs right of $i
$noMoreGT = false;
- // Map of tag name => true if there are no more closing tags of given type right of $i
- $noMoreClosingTag = array();
// True to ignore all input up to the next <onlyinclude>
$findOnlyinclude = $enableOnlyinclude;
// Do a line-start run without outputting an LF character
} else {
$attrEnd = $tagEndPos;
// Find closing tag
- if (
- !isset( $noMoreClosingTag[$name] ) &&
- preg_match( "/<\/" . preg_quote( $name, '/' ) . "\s*>/i",
+ if ( preg_match( "/<\/" . preg_quote( $name, '/' ) . "\s*>/i",
$text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 )
) {
$inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 );
$i = $matches[0][1] + strlen( $matches[0][0] );
$close = $matches[0][0];
} else {
- // No end tag -- don't match the tag, treat opening tag as literal and resume parsing.
- $i = $tagEndPos + 1;
- $accum->addLiteral( substr( $text, $tagStartPos, $tagEndPos + 1 - $tagStartPos ) );
- // Cache results, otherwise we have O(N^2) performance for input like <foo><foo><foo>...
- $noMoreClosingTag[$name] = true;
- continue;
+ // No end tag -- let it run out to the end of the text.
+ $inner = substr( $text, $tagEndPos + 1 );
+ $i = $lengthText;
+ $close = null;
}
}
// <includeonly> and <noinclude> just become <ignore> tags
* @param int[]|null $namespaces
*/
function setNamespaces( $namespaces ) {
+ if ( $namespaces ) {
+ // Filter namespaces to only keep valid ones
+ $validNs = $this->searchableNamespaces();
+ $namespaces = array_filter( $namespaces, function( $ns ) use( $validNs ) {
+ return $ns < 0 || isset( $validNs[$ns] );
+ } );
+ } else {
+ $namespaces = array();
+ }
$this->namespaces = $namespaces;
}
public function textAlreadyUpdatedForIndex() {
return false;
}
+
+ /**
+ * Makes search simple string if it was namespaced.
+ * Sets namespaces of the search to namespaces extracted from string.
+ * @param string $search
+ * @return $string Simplified search string
+ */
+ protected function normalizeNamespaces( $search ) {
+ // Find a Title which is not an interwiki and is in NS_MAIN
+ $title = Title::newFromText( $search );
+ $ns = $this->namespaces;
+ if ( $title && !$title->isExternal() ) {
+ $ns = array( $title->getNamespace() );
+ $search = $title->getText();
+ if ( $ns[0] == NS_MAIN ) {
+ $ns = $this->namespaces; // no explicit prefix, use default namespaces
+ Hooks::run( 'PrefixSearchExtractNamespace', array( &$ns, &$search ) );
+ }
+ } else {
+ $title = Title::newFromText( $search . 'Dummy' );
+ if ( $title && $title->getText() == 'Dummy'
+ && $title->getNamespace() != NS_MAIN
+ && !$title->isExternal() )
+ {
+ $ns = array( $title->getNamespace() );
+ $search = '';
+ } else {
+ Hooks::run( 'PrefixSearchExtractNamespace', array( &$ns, &$search ) );
+ }
+ }
+
+ $ns = array_map( function( $space ) {
+ return $space == NS_MEDIA ? NS_FILE : $space;
+ }, $ns );
+
+ $this->setNamespaces( $ns );
+ return $search;
+ }
+
+ /**
+ * Perform a completion search.
+ * Does not resolve namespaces and does not check variants.
+ * Search engine implementations may want to override this function.
+ * @param string $search
+ * @return SearchSuggestionSet
+ */
+ protected function completionSearchBackend( $search ) {
+ $results = array();
+
+ $search = trim( $search );
+
+ if ( !in_array( NS_SPECIAL, $this->namespaces ) && // We do not run hook on Special: search
+ !Hooks::run( 'PrefixSearchBackend',
+ array( $this->namespaces, $search, $this->limit, &$results, $this->offset )
+ ) ) {
+ // False means hook worked.
+ // FIXME: Yes, the API is weird. That's why it is going to be deprecated.
+
+ return SearchSuggestionSet::fromStrings( $results );
+ } else {
+ // Hook did not do the job, use default simple search
+ $results = $this->simplePrefixSearch( $search );
+ return SearchSuggestionSet::fromTitles( $results );
+ }
+ }
+
+ /**
+ * Perform a completion search.
+ * @param string $search
+ * @return SearchSuggestionSet
+ */
+ public function completionSearch( $search ) {
+ if ( trim( $search ) === '' ) {
+ return SearchSuggestionSet::emptySuggestionSet(); // Return empty result
+ }
+ $search = $this->normalizeNamespaces( $search );
+ return $this->processCompletionResults( $search, $this->completionSearchBackend( $search ) );
+ }
+
+ /**
+ * Perform a completion search with variants.
+ * @param string $search
+ * @return SearchSuggestionSet
+ */
+ public function completionSearchWithVariants( $search ) {
+ if ( trim( $search ) === '' ) {
+ return SearchSuggestionSet::emptySuggestionSet(); // Return empty result
+ }
+ $search = $this->normalizeNamespaces( $search );
+
+ $results = $this->completionSearchBackend( $search );
+ $fallbackLimit = $this->limit - $results->getSize();
+ if ( $fallbackLimit > 0 ) {
+ global $wgContLang;
+
+ $fallbackSearches = $wgContLang->autoConvertToAllVariants( $search );
+ $fallbackSearches = array_diff( array_unique( $fallbackSearches ), array( $search ) );
+
+ foreach ( $fallbackSearches as $fbs ) {
+ $this->setLimitOffset( $fallbackLimit );
+ $fallbackSearchResult = $this->completionSearch( $fbs );
+ $results->appendAll( $fallbackSearchResult );
+ $fallbackLimit -= count( $fallbackSearchResult );
+ if ( $fallbackLimit <= 0 ) {
+ break;
+ }
+ }
+ }
+ return $this->processCompletionResults( $search, $results );
+ }
+
+ /**
+ * Extract titles from completion results
+ * @param SearchSuggestionSet $completionResults
+ * @return Title[]
+ */
+ public function extractTitles( SearchSuggestionSet $completionResults ) {
+ return $completionResults->map( function( SearchSuggestion $sugg ) {
+ return $sugg->getSuggestedTitle();
+ } );
+ }
+
+ /**
+ * Process completion search results.
+ * Resolves the titles and rescores.
+ * @param SearchSuggestionSet $suggestions
+ * @return SearchSuggestionSet
+ */
+ protected function processCompletionResults( $search, SearchSuggestionSet $suggestions ) {
+ if ( $suggestions->getSize() == 0 ) {
+ // If we don't have anything, don't bother
+ return $suggestions;
+ }
+ $search = trim( $search );
+ // preload the titles with LinkBatch
+ $titles = $suggestions->map( function( SearchSuggestion $sugg ) {
+ return $sugg->getSuggestedTitle();
+ } );
+ $lb = new LinkBatch( $titles );
+ $lb->setCaller( __METHOD__ );
+ $lb->execute();
+
+ $results = $suggestions->map( function( SearchSuggestion $sugg ) {
+ return $sugg->getSuggestedTitle()->getPrefixedText();
+ } );
+
+ // Rescore results with an exact title match
+ $rescorer = new SearchExactMatchRescorer();
+ $rescoredResults = $rescorer->rescore( $search, $this->namespaces, $results, $this->limit );
+
+ if ( count( $rescoredResults ) > 0 ) {
+ $found = array_search( $rescoredResults[0], $results );
+ if ( $found === false ) {
+ // If the first result is not in the previous array it
+ // means that we found a new exact match
+ $exactMatch = SearchSuggestion::fromTitle( 0, Title::newFromText( $rescoredResults[0] ) );
+ $suggestions->prepend( $exactMatch );
+ $suggestions->shrink( $this->limit );
+ } else {
+ // if the first result is not the same we need to rescore
+ if ( $found > 0 ) {
+ $suggestions->rescore( $found );
+ }
+ }
+ }
+
+ return $suggestions;
+ }
+
+ /**
+ * Simple prefix search for subpages.
+ * @param string $search
+ * @return Title[]
+ */
+ public function defaultPrefixSearch( $search ) {
+ if ( trim( $search ) === '' ) {
+ return array();
+ }
+
+ $search = $this->normalizeNamespaces( $search );
+ return $this->simplePrefixSearch( $search );
+ }
+
+ /**
+ * Call out to simple search backend.
+ * Defaults to TitlePrefixSearch.
+ * @param string $search
+ * @return Title[]
+ */
+ protected function simplePrefixSearch( $search ) {
+ // Use default database prefix search
+ $backend = new TitlePrefixSearch;
+ return $backend->defaultSearchBackend( $this->namespaces, $search, $this->limit, $this->offset );
+ }
+
}
/**
--- /dev/null
+<?php
+
+/**
+ * Search suggestion
+ *
+ * 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
+ *
+ */
+
+/**
+ * A search suggestion
+ *
+ */
+class SearchSuggestion {
+ /**
+ * @var string the suggestion
+ */
+ private $text;
+
+ /**
+ * @var string the suggestion URL
+ */
+ private $url;
+
+ /**
+ * @var Title|null the suggested title
+ */
+ private $suggestedTitle;
+
+ /**
+ * NOTE: even if suggestedTitle is a redirect suggestedTitleID
+ * is the ID of the target page.
+ * @var int|null the suggested title ID
+ */
+ private $suggestedTitleID;
+
+ /**
+ * @var float|null The suggestion score
+ */
+ private $score;
+
+ /**
+ * Construct a new suggestion
+ * @param float $score the suggestion score
+ * @param string $text|null the suggestion text
+ * @param Title|null $suggestedTitle the suggested title
+ * @param int|null $suggestedTitleID the suggested title ID
+ */
+ public function __construct( $score, $text = null, Title $suggestedTitle = null,
+ $suggestedTitleID = null ) {
+ $this->score = $score;
+ $this->text = $text;
+ if ( $suggestedTitle ) {
+ $this->setSuggestedTitle( $suggestedTitle );
+ }
+ $this->suggestedTitleID = $suggestedTitleID;
+ }
+
+ /**
+ * The suggestion text
+ * @return string
+ */
+ public function getText() {
+ return $this->text;
+ }
+
+ /**
+ * Set the suggestion text.
+ * @param string $text
+ * @param bool $setTitle Should we also update the title?
+ */
+ public function setText( $text, $setTitle = true ) {
+ $this->text = $text;
+ if ( $setTitle && $text ) {
+ $this->setSuggestedTitle( Title::makeTitle( 0, $text ) );
+ }
+ }
+
+ /**
+ * Title object in the case this suggestion is based on a title.
+ * May return null if the suggestion is not a Title.
+ * @return Title|null
+ */
+ public function getSuggestedTitle() {
+ return $this->suggestedTitle;
+ }
+
+ /**
+ * Set the suggested title
+ * @param Title|null $title
+ */
+ public function setSuggestedTitle( Title $title = null ) {
+ $this->suggestedTitle = $title;
+ if ( $title !== null ) {
+ $this->url = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
+ }
+ }
+
+ /**
+ * Title ID in the case this suggestion is based on a title.
+ * May return null if the suggestion is not a Title.
+ * @return int|null
+ */
+ public function getSuggestedTitleID() {
+ return $this->suggestedTitleID;
+ }
+
+ /**
+ * Set the suggested title ID
+ * @param int|null $suggestedTitleID
+ */
+ public function setSuggestedTitleID( $suggestedTitleID = null ) {
+ $this->suggestedTitleID = $suggestedTitleID;
+ }
+
+ /**
+ * Suggestion score
+ * @return float Suggestion score
+ */
+ public function getScore() {
+ return $this->score;
+ }
+
+ /**
+ * Set the suggestion score
+ * @param float $score
+ */
+ public function setScore( $score ) {
+ $this->score = $score;
+ }
+
+ /**
+ * Suggestion URL, can be the link to the Title or maybe in the
+ * future a link to the search results for this search suggestion.
+ * @return string Suggestion URL
+ */
+ public function getURL() {
+ return $this->url;
+ }
+
+ /**
+ * Set the suggestion URL
+ * @param string $url
+ */
+ public function setURL( $url ) {
+ $this->url = $url;
+ }
+
+ /**
+ * Create suggestion from Title
+ * @param float $score Suggestions score
+ * @param Title $title
+ * @return SearchSuggestion
+ */
+ public static function fromTitle( $score, Title $title ) {
+ return new self( $score, $title->getPrefixedText(), $title, $title->getArticleID() );
+ }
+
+ /**
+ * Create suggestion from text
+ * Will also create a title if text if not empty.
+ * @param float $score Suggestions score
+ * @param string $text
+ * @return SearchSuggestion
+ */
+ public static function fromText( $score, $text ) {
+ $suggestion = new self( $score, $text );
+ if ( $text ) {
+ $suggestion->setSuggestedTitle( Title::makeTitle( 0, $text ) );
+ }
+ return $suggestion;
+ }
+
+}
--- /dev/null
+<?php
+
+/**
+ * Search suggestion 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
+ *
+ */
+
+/**
+ * A set of search suggestions.
+ * The set is always ordered by score, with the best match first.
+ */
+class SearchSuggestionSet {
+ /**
+ * @var SearchSuggestion[]
+ */
+ private $suggestions = array();
+
+ /**
+ *
+ * @var array
+ */
+ private $pageMap = array();
+
+ /**
+ * Builds a new set of suggestions.
+ *
+ * NOTE: the array should be sorted by score (higher is better),
+ * in descending order.
+ * SearchSuggestionSet will not try to re-order this input array.
+ * Providing an unsorted input array is a mistake and will lead to
+ * unexpected behaviors.
+ *
+ * @param SearchSuggestion[] $suggestions (must be sorted by score)
+ */
+ public function __construct( array $suggestions ) {
+ foreach ( $suggestions as $suggestion ) {
+ $pageID = $suggestion->getSuggestedTitleID();
+ if ( $pageID && empty( $this->pageMap[$pageID] ) ) {
+ $this->pageMap[$pageID] = true;
+ }
+ $this->suggestions[] = $suggestion;
+ }
+ }
+
+ /**
+ * Get the list of suggestions.
+ * @return SearchSuggestion[]
+ */
+ public function getSuggestions() {
+ return $this->suggestions;
+ }
+
+ /**
+ * Call array_map on the suggestions array
+ * @param callback $callback
+ * @return array
+ */
+ public function map( $callback ) {
+ return array_map( $callback, $this->suggestions );
+ }
+
+ /**
+ * Add a new suggestion at the end.
+ * If the score of the new suggestion is greater than the worst one,
+ * the new suggestion score will be updated (worst - 1).
+ *
+ * @param SearchSuggestion $suggestion
+ */
+ public function append( SearchSuggestion $suggestion ) {
+ $pageID = $suggestion->getSuggestedTitleID();
+ if ( $pageID && isset( $this->pageMap[$pageID] ) ) {
+ return;
+ }
+ if ( $this->getSize() > 0 && $suggestion->getScore() >= $this->getWorstScore() ) {
+ $suggestion->setScore( $this->getWorstScore() - 1 );
+ }
+ $this->suggestions[] = $suggestion;
+ if ( $pageID ) {
+ $this->pageMap[$pageID] = true;
+ }
+ }
+
+ /**
+ * Add suggestion set to the end of the current one.
+ * @param SearchSuggestionSet $set
+ */
+ public function appendAll( SearchSuggestionSet $set ) {
+ foreach ( $set->getSuggestions() as $sugg ) {
+ $this->append( $sugg );
+ }
+ }
+
+ /**
+ * Move the suggestion at index $key to the first position
+ */
+ public function rescore( $key ) {
+ $removed = array_splice( $this->suggestions, $key, 1 );
+ unset( $this->pageMap[$removed[0]->getSuggestedTitleID()] );
+ $this->prepend( $removed[0] );
+ }
+
+ /**
+ * Add a new suggestion at the top. If the new suggestion score
+ * is lower than the best one its score will be updated (best + 1)
+ * @param SearchSuggestion $suggestion
+ */
+ public function prepend( SearchSuggestion $suggestion ) {
+ $pageID = $suggestion->getSuggestedTitleID();
+ if ( $pageID && isset( $this->pageMap[$pageID] ) ) {
+ return;
+ }
+ if ( $this->getSize() > 0 && $suggestion->getScore() <= $this->getBestScore() ) {
+ $suggestion->setScore( $this->getBestScore() + 1 );
+ }
+ array_unshift( $this->suggestions, $suggestion );
+ if ( $pageID ) {
+ $this->pageMap[$pageID] = true;
+ }
+ }
+
+ /**
+ * @return float the best score in this suggestion set
+ */
+ public function getBestScore() {
+ if ( empty( $this->suggestions ) ) {
+ return 0;
+ }
+ return $this->suggestions[0]->getScore();
+ }
+
+ /**
+ * @return float the worst score in this set
+ */
+ public function getWorstScore() {
+ if ( empty( $this->suggestions ) ) {
+ return 0;
+ }
+ return end( $this->suggestions )->getScore();
+ }
+
+ /**
+ * @return int the number of suggestion in this set
+ */
+ public function getSize() {
+ return count( $this->suggestions );
+ }
+
+ /**
+ * Remove any extra elements in the suggestions set
+ * @param int $limit the max size of this set.
+ */
+ public function shrink( $limit ) {
+ if ( count( $this->suggestions ) > $limit ) {
+ $this->suggestions = array_slice( $this->suggestions, 0, $limit );
+ }
+ }
+
+ /**
+ * Builds a new set of suggestion based on a title array.
+ * Useful when using a backend that supports only Titles.
+ *
+ * NOTE: Suggestion scores will be generated.
+ *
+ * @param Title[] $titles
+ * @return SearchSuggestionSet
+ */
+ public static function fromTitles( array $titles ) {
+ $score = count( $titles );
+ $suggestions = array_map( function( $title ) use ( &$score ) {
+ return SearchSuggestion::fromTitle( $score--, $title );
+ }, $titles );
+ return new SearchSuggestionSet( $suggestions );
+ }
+
+ /**
+ * Builds a new set of suggestion based on a string array.
+ *
+ * NOTE: Suggestion scores will be generated.
+ *
+ * @param string[] $titles
+ * @return SearchSuggestionSet
+ */
+ public static function fromStrings( array $titles ) {
+ $score = count( $titles );
+ $suggestions = array_map( function( $title ) use ( &$score ) {
+ return SearchSuggestion::fromText( $score--, $title );
+ }, $titles );
+ return new SearchSuggestionSet( $suggestions );
+ }
+
+
+ /**
+ * @return SearchSuggestionSet an empty suggestion set
+ */
+ public static function emptySuggestionSet() {
+ return new SearchSuggestionSet( array() );
+ }
+}
namespace MediaWiki\Session;
-use BagOStuff;
+use CachedBagOStuff;
use Psr\Log\LoggerInterface;
use User;
use WebRequest;
/** @var string Used to detect subarray modifications */
private $dataHash = null;
- /** @var BagOStuff */
- private $tempStore;
- /** @var BagOStuff */
- private $permStore;
+ /** @var CachedBagOStuff */
+ private $store;
/** @var LoggerInterface */
private $logger;
/**
* @param SessionId $id Session ID object
* @param SessionInfo $info Session info to populate from
- * @param BagOStuff $tempStore In-process data store
- * @param BagOStuff $permstore Backend data store for persisted sessions
+ * @param CachedBagOStuff $store Backend data store
* @param LoggerInterface $logger
* @param int $lifetime Session data lifetime in seconds
*/
public function __construct(
- SessionId $id, SessionInfo $info, BagOStuff $tempStore, BagOStuff $permStore,
- LoggerInterface $logger, $lifetime
+ SessionId $id, SessionInfo $info, CachedBagOStuff $store, LoggerInterface $logger, $lifetime
) {
$phpSessionHandling = \RequestContext::getMain()->getConfig()->get( 'PHPSessionHandling' );
$this->usePhpSessionHandling = $phpSessionHandling !== 'disable';
$this->id = $id;
$this->user = $info->getUserInfo() ? $info->getUserInfo()->getUser() : new User;
- $this->tempStore = $tempStore;
- $this->permStore = $permStore;
+ $this->store = $store;
$this->logger = $logger;
$this->lifetime = $lifetime;
$this->provider = $info->getProvider();
$this->forceHTTPS = $info->forceHTTPS();
$this->providerMetadata = $info->getProviderMetadata();
- $key = wfMemcKey( 'MWSession', (string)$this->id );
- $blob = $tempStore->get( $key );
- if ( $blob === false ) {
- $blob = $permStore->get( $key );
- if ( $blob !== false ) {
- $tempStore->set( $key, $blob );
- }
- }
+ $blob = $store->get( wfMemcKey( 'MWSession', (string)$this->id ) );
if ( !is_array( $blob ) ||
!isset( $blob['metadata'] ) || !is_array( $blob['metadata'] ) ||
!isset( $blob['data'] ) || !is_array( $blob['data'] )
$this->autosave();
// Delete the data for the old session ID now
- $this->tempStore->delete( wfMemcKey( 'MWSession', $oldId ) );
- $this->permStore->delete( wfMemcKey( 'MWSession', $oldId ) );
+ $this->store->delete( wfMemcKey( 'MWSession', $oldId ) );
}
}
}
}
- $this->tempStore->set(
+ $this->store->set(
wfMemcKey( 'MWSession', (string)$this->id ),
array(
'data' => $this->data,
'metadata' => $metadata,
),
- $metadata['expires']
+ $metadata['expires'],
+ $this->persist ? 0 : CachedBagOStuff::WRITE_CACHE_ONLY
);
- if ( $this->persist ) {
- $this->permStore->set(
- wfMemcKey( 'MWSession', (string)$this->id ),
- array(
- 'data' => $this->data,
- 'metadata' => $metadata,
- ),
- $metadata['expires']
- );
- }
$this->metaDirty = false;
$this->dataDirty = false;
use Psr\Log\LoggerInterface;
use BagOStuff;
+use CachedBagOStuff;
use Config;
use FauxRequest;
use Language;
/** @var Config */
private $config;
- /** @var BagOStuff|null */
- private $tempStore;
-
- /** @var BagOStuff|null */
- private $permStore;
+ /** @var CachedBagOStuff|null */
+ private $store;
/** @var SessionProvider[] */
private $sessionProviders = null;
$this->setLogger( \MediaWiki\Logger\LoggerFactory::getInstance( 'session' ) );
}
- $this->tempStore = new \HashBagOStuff;
if ( isset( $options['store'] ) ) {
if ( !$options['store'] instanceof BagOStuff ) {
throw new \InvalidArgumentException(
'$options[\'store\'] must be an instance of BagOStuff'
);
}
- $this->permStore = $options['store'];
+ $store = $options['store'];
} else {
- $this->permStore = \ObjectCache::getInstance( $this->config->get( 'SessionCacheType' ) );
- $this->permStore->setLogger( $this->logger );
+ $store = \ObjectCache::getInstance( $this->config->get( 'SessionCacheType' ) );
+ $store->setLogger( $this->logger );
}
+ $this->store = $store instanceof CachedBagOStuff ? $store : new CachedBagOStuff( $store );
register_shutdown_function( array( $this, 'shutdown' ) );
}
// Test this here to provide a better log message for the common case
// of "no such ID"
$key = wfMemcKey( 'MWSession', $id );
- $existing = $this->tempStore->get( $key );
- if ( $existing === false ) {
- $existing = $this->permStore->get( $key );
- if ( $existing !== false ) {
- $this->tempStore->set( $key, $existing );
- }
- }
- if ( is_array( $existing ) ) {
+ if ( is_array( $this->store->get( $key ) ) ) {
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, array( 'id' => $id, 'idIsSafe' => true ) );
if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
$session = $this->getSessionFromInfo( $info, $request );
}
$key = wfMemcKey( 'MWSession', $id );
- $existing = $this->tempStore->get( $key );
- if ( $existing === false ) {
- $existing = $this->permStore->get( $key );
- if ( $existing !== false ) {
- $this->tempStore->set( $key, $existing );
- }
- }
- if ( is_array( $existing ) ) {
+ if ( is_array( $this->store->get( $key ) ) ) {
throw new \InvalidArgumentException( 'Session ID already exists' );
}
}
*/
private function loadSessionInfoFromStore( SessionInfo &$info, WebRequest $request ) {
$key = wfMemcKey( 'MWSession', $info->getId() );
- $blob = $this->tempStore->get( $key );
- if ( $blob === false ) {
- $blob = $this->permStore->get( $key );
- if ( $blob !== false ) {
- $this->tempStore->set( $key, $blob );
- }
- }
+ $blob = $this->store->get( $key );
$newParams = array();
// Sanity check: blob must be an array, if it's saved at all
if ( !is_array( $blob ) ) {
$this->logger->warning( "Session $info: Bad data" );
- $this->tempStore->delete( $key );
- $this->permStore->delete( $key );
+ $this->store->delete( $key );
return false;
}
!isset( $blob['metadata'] ) || !is_array( $blob['metadata'] )
) {
$this->logger->warning( "Session $info: Bad data structure" );
- $this->tempStore->delete( $key );
- $this->permStore->delete( $key );
+ $this->store->delete( $key );
return false;
}
!array_key_exists( 'provider', $metadata )
) {
$this->logger->warning( "Session $info: Bad metadata" );
- $this->tempStore->delete( $key );
- $this->permStore->delete( $key );
+ $this->store->delete( $key );
return false;
}
$newParams['provider'] = $provider = $this->getProvider( $metadata['provider'] );
if ( !$provider ) {
$this->logger->warning( "Session $info: Unknown provider, " . $metadata['provider'] );
- $this->tempStore->delete( $key );
- $this->permStore->delete( $key );
+ $this->store->delete( $key );
return false;
}
} elseif ( $metadata['provider'] !== (string)$provider ) {
$backend = new SessionBackend(
$this->allSessionIds[$id],
$info,
- $this->tempStore,
- $this->permStore,
+ $this->store,
$this->logger,
$this->config->get( 'ObjectCacheSessionExpiry' )
);
do {
$id = wfBaseConvert( \MWCryptRand::generateHex( 40 ), 16, 32, 32 );
$key = wfMemcKey( 'MWSession', $id );
- } while ( isset( $this->allSessionIds[$id] ) ||
- is_array( $this->tempStore->get( $key ) ) || is_array( $this->permStore->get( $key ) )
- );
+ } while ( isset( $this->allSessionIds[$id] ) || is_array( $this->store->get( $key ) ) );
return $id;
}
* @param PHPSessionHandler $handler
*/
public function setupPHPSessionHandler( PHPSessionHandler $handler ) {
- $handler->setManager( $this, $this->permStore, $this->logger );
+ $handler->setManager( $this, $this->store, $this->logger );
}
/**
return array();
}
+ /**
+ * Perform a regular substring search for prefixSearchSubpages
+ * @param string $search Prefix to search for
+ * @param int $limit Maximum number of results to return (usually 10)
+ * @param int $offset Number of results to skip (usually 0)
+ * @return string[] Matching subpages
+ */
+ protected function prefixSearchString( $search, $limit, $offset ) {
+ $title = Title::newFromText( $search );
+ if ( !$title || !$title->canExist() ) {
+ // No prefix suggestion in special and media namespace
+ return array();
+ }
+
+ $search = SearchEngine::create();
+ $search->setLimitOffset( $limit, $offset );
+ $search->setNamespaces( array() );
+ $result = $search->defaultPrefixSearch( $search );
+ return array_map( function( Title $t ) {
+ return $t->getPrefixedText();
+ }, $result );
+ }
+
/**
* Helper function for implementations of prefixSearchSubpages() that
* filter the values in memory (as opposed to making a query).
* @return string[] Matching subpages
*/
public function prefixSearchSubpages( $search, $limit, $offset ) {
- $title = Title::newFromText( $search );
- if ( !$title || !$title->canExist() ) {
- // No prefix suggestion in special and media namespace
- return array();
- }
- // Autocomplete subpage the same as a normal search
- $prefixSearcher = new StringPrefixSearch;
- $result = $prefixSearcher->search( $search, $limit, array(), $offset );
- return $result;
+ return $this->prefixSearchString( $search, $limit, $offset );
}
protected function getGroupName() {
* @return string[] Matching subpages
*/
public function prefixSearchSubpages( $search, $limit, $offset ) {
- $title = Title::newFromText( $search );
- if ( !$title || !$title->canExist() ) {
- // No prefix suggestion in special and media namespace
- return array();
- }
- // Autocomplete subpage the same as a normal search
- $prefixSearcher = new StringPrefixSearch;
- $result = $prefixSearcher->search( $search, $limit, array(), $offset );
- return $result;
+ return $this->prefixSearchString( $search, $limit, $offset );
}
protected function getGroupName() {
// No prefix suggestion outside of file namespace
return array();
}
+ $search = SearchEngine::create();
+ $search->setLimitOffset( $limit, $offset );
// Autocomplete subpage the same as a normal search, but just for files
- $prefixSearcher = new TitlePrefixSearch;
- $result = $prefixSearcher->search( $search, $limit, array( NS_FILE ), $offset );
+ $search->setNamespaces( array( NS_FILE ) );
+ $result = $search->defaultPrefixSearch( $search );
return array_map( function ( Title $t ) {
// Remove namespace in search suggestion
* @return string[] Matching subpages
*/
public function prefixSearchSubpages( $search, $limit, $offset ) {
- $title = Title::newFromText( $search );
- if ( !$title || !$title->canExist() ) {
- // No prefix suggestion in special and media namespace
- return array();
- }
- // Autocomplete subpage the same as a normal search
- $prefixSearcher = new StringPrefixSearch;
- $result = $prefixSearcher->search( $search, $limit, array(), $offset );
- return $result;
+ return $this->prefixSearchString( $search, $limit, $offset );
}
protected function getGroupName() {
* @return string[] Matching subpages
*/
public function prefixSearchSubpages( $search, $limit, $offset ) {
- $title = Title::newFromText( $search );
- if ( !$title || !$title->canExist() ) {
- // No prefix suggestion in special and media namespace
- return array();
- }
- // Autocomplete subpage the same as a normal search
- $prefixSearcher = new StringPrefixSearch;
- $result = $prefixSearcher->search( $search, $limit, array(), $offset );
- return $result;
+ return $this->prefixSearchString( $search, $limit, $offset );
}
protected function getGroupName() {
* @return string[] Matching subpages
*/
public function prefixSearchSubpages( $search, $limit, $offset ) {
- $title = Title::newFromText( $search );
- if ( !$title || !$title->canExist() ) {
- // No prefix suggestion in special and media namespace
- return array();
- }
- // Autocomplete subpage the same as a normal search
- $prefixSearcher = new StringPrefixSearch;
- $result = $prefixSearcher->search( $search, $limit, array(), $offset );
- return $result;
+ return $this->prefixSearchString( $search, $limit, $offset );
}
protected function getGroupName() {
* @return string[] Matching subpages
*/
public function prefixSearchSubpages( $search, $limit, $offset ) {
- $title = Title::newFromText( $search );
- if ( !$title || !$title->canExist() ) {
- // No prefix suggestion in special and media namespace
- return array();
- }
- // Autocomplete subpage the same as a normal search
- $prefixSearcher = new StringPrefixSearch;
- $result = $prefixSearcher->search( $search, $limit, array(), $offset );
- return $result;
+ return $this->prefixSearchString( $search, $limit, $offset );
}
}
* @return string[] Matching subpages
*/
public function prefixSearchSubpages( $search, $limit, $offset ) {
- $title = Title::newFromText( $search );
- if ( !$title || !$title->canExist() ) {
- // No prefix suggestion in special and media namespace
- return array();
- }
- // Autocomplete subpage the same as a normal search
- $prefixSearcher = new StringPrefixSearch;
- $result = $prefixSearcher->search( $search, $limit, array(), $offset );
- return $result;
+ return $this->prefixSearchString( $search, $limit, $offset );
}
protected function getGroupName() {
* @return string[] Matching subpages
*/
public function prefixSearchSubpages( $search, $limit, $offset ) {
- $title = Title::newFromText( $search );
- if ( !$title || !$title->canExist() ) {
- // No prefix suggestion in special and media namespace
- return array();
- }
- // Autocomplete subpage the same as a normal search
- $prefixSearcher = new StringPrefixSearch;
- $result = $prefixSearcher->search( $search, $limit, array(), $offset );
- return $result;
+ return $this->prefixSearchString( $search, $limit, $offset );
}
protected function getGroupName() {
</p>
!! end
+## PHP parser discards the "<pre " string
!! test
Handle broken pre-like tags (bug 64025)
!! options
<table><pre </table>
!! html/php
<pre>x</pre>
-<table><pre </table>
+<table><pre></pre></table>
-!! html/php+tidy
-<pre>
-x
-</pre>
-<p><pre</p>
!! html/parsoid
<pre about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"a":{"<pre":null},"sa":{"<pre":""},"stx":"html","pi":[[{"k":"1","spc":["","","",""]}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"<pre <pre>x</pre>"}},"i":0}}]}'>x</pre>
!! wikitext
<includeonly>
!! html
-<p><includeonly>
-</p>
!! end
## We used to, but no longer wt2wt this test since the default serializer
--- /dev/null
+<?php
+
+/**
+ * @group BagOStuff
+ */
+class CachedBagOStuffTest extends PHPUnit_Framework_TestCase {
+
+ public function testGetFromBackend() {
+ $backend = new HashBagOStuff;
+ $cache = new CachedBagOStuff( $backend );
+
+ $backend->set( 'foo', 'bar' );
+ $this->assertEquals( 'bar', $cache->get( 'foo' ) );
+
+ $backend->set( 'foo', 'baz' );
+ $this->assertEquals( 'bar', $cache->get( 'foo' ), 'cached' );
+ }
+
+ public function testSetAndDelete() {
+ $backend = new HashBagOStuff;
+ $cache = new CachedBagOStuff( $backend );
+
+ for ( $i = 0; $i < 10; $i++ ) {
+ $cache->set( "key$i", 1 );
+ $this->assertEquals( 1, $cache->get( "key$i" ) );
+ $this->assertEquals( 1, $backend->get( "key$i" ) );
+ $cache->delete( "key$i" );
+ $this->assertEquals( false, $cache->get( "key$i" ) );
+ $this->assertEquals( false, $backend->get( "key$i" ) );
+ }
+ }
+
+ public function testWriteCacheOnly() {
+ $backend = new HashBagOStuff;
+ $cache = new CachedBagOStuff( $backend );
+
+ $cache->set( 'foo', 'bar', 0, CachedBagOStuff::WRITE_CACHE_ONLY );
+ $this->assertEquals( 'bar', $cache->get( 'foo' ) );
+ $this->assertFalse( $backend->get( 'foo' ) );
+
+ $cache->set( 'foo', 'old' );
+ $this->assertEquals( 'old', $cache->get( 'foo' ) );
+ $this->assertEquals( 'old', $backend->get( 'foo' ) );
+
+ $cache->set( 'foo', 'new', 0, CachedBagOStuff::WRITE_CACHE_ONLY );
+ $this->assertEquals( 'new', $cache->get( 'foo' ) );
+ $this->assertEquals( 'old', $backend->get( 'foo' ) );
+
+ $cache->delete( 'foo', CachedBagOStuff::WRITE_CACHE_ONLY );
+ $this->assertEquals( 'old', $cache->get( 'foo' ) ); // Reloaded from backend
+ }
+}
array( "<noinclude> Foo bar </noinclude>", "<root><ignore><noinclude></ignore> Foo bar <ignore></noinclude></ignore></root>" ),
array( "<noinclude>\n{{Foo}}\n</noinclude>", "<root><ignore><noinclude></ignore>\n<template lineStart=\"1\"><title>Foo</title></template>\n<ignore></noinclude></ignore></root>" ),
array( "<noinclude>\n{{Foo}}\n</noinclude>\n", "<root><ignore><noinclude></ignore>\n<template lineStart=\"1\"><title>Foo</title></template>\n<ignore></noinclude></ignore>\n</root>" ),
- array( "<gallery>foo bar", "<root><gallery>foo bar</root>" ),
+ array( "<gallery>foo bar", "<root><ext><name>gallery</name><attr></attr><inner>foo bar</inner></ext></root>" ),
array( "<{{foo}}>", "<root><<template><title>foo</title></template>></root>" ),
array( "<{{{foo}}}>", "<root><<tplarg><title>foo</title></tplarg>></root>" ),
array( "<gallery></gallery</gallery>", "<root><ext><name>gallery</name><attr></attr><inner></gallery</inner><close></gallery></close></ext></root>" ),
--- /dev/null
+<?php
+/**
+ * @group Search
+ * @group Database
+ */
+class SearchEnginePrefixTest extends MediaWikiLangTestCase {
+
+ /**
+ * @var SearchEngine
+ */
+ private $search;
+
+ public function addDBData() {
+ if ( !$this->isWikitextNS( NS_MAIN ) ) {
+ // tests are skipped if NS_MAIN is not wikitext
+ return;
+ }
+
+ $this->insertPage( 'Sandbox' );
+ $this->insertPage( 'Bar' );
+ $this->insertPage( 'Example' );
+ $this->insertPage( 'Example Bar' );
+ $this->insertPage( 'Example Foo' );
+ $this->insertPage( 'Example Foo/Bar' );
+ $this->insertPage( 'Example/Baz' );
+ $this->insertPage( 'Redirect test', '#REDIRECT [[Redirect Test]]' );
+ $this->insertPage( 'Redirect Test' );
+ $this->insertPage( 'Redirect Test Worse Result' );
+ $this->insertPage( 'Redirect test2', '#REDIRECT [[Redirect Test2]]' );
+ $this->insertPage( 'Redirect TEST2', '#REDIRECT [[Redirect Test2]]' );
+ $this->insertPage( 'Redirect Test2' );
+ $this->insertPage( 'Redirect Test2 Worse Result' );
+
+ $this->insertPage( 'Talk:Sandbox' );
+ $this->insertPage( 'Talk:Example' );
+
+ $this->insertPage( 'User:Example' );
+ }
+
+ protected function setUp() {
+ parent::setUp();
+
+ if ( !$this->isWikitextNS( NS_MAIN ) ) {
+ $this->markTestSkipped( 'Main namespace does not support wikitext.' );
+ }
+
+ // Avoid special pages from extensions interferring with the tests
+ $this->setMwGlobals( 'wgSpecialPages', array() );
+ $this->search = SearchEngine::create();
+ $this->search->setNamespaces( array() );
+ }
+
+ protected function searchProvision( Array $results = null ) {
+ if ( $results === null ) {
+ $this->setMwGlobals( 'wgHooks', array() );
+ } else {
+ $this->setMwGlobals( 'wgHooks', array(
+ 'PrefixSearchBackend' => array(
+ function ( $namespaces, $search, $limit, &$srchres ) use ( $results ) {
+ $srchres = $results;
+ return false;
+ }
+ ),
+ ) );
+ }
+ }
+
+ public static function provideSearch() {
+ return array(
+ array( array(
+ 'Empty string',
+ 'query' => '',
+ 'results' => array(),
+ ) ),
+ array( array(
+ 'Main namespace with title prefix',
+ 'query' => 'Ex',
+ 'results' => array(
+ 'Example',
+ 'Example/Baz',
+ 'Example Bar',
+ ),
+ // Third result when testing offset
+ 'offsetresult' => array(
+ 'Example Foo',
+ ),
+ ) ),
+ array( array(
+ 'Talk namespace prefix',
+ 'query' => 'Talk:',
+ 'results' => array(
+ 'Talk:Example',
+ 'Talk:Sandbox',
+ ),
+ ) ),
+ array( array(
+ 'User namespace prefix',
+ 'query' => 'User:',
+ 'results' => array(
+ 'User:Example',
+ ),
+ ) ),
+ array( array(
+ 'Special namespace prefix',
+ 'query' => 'Special:',
+ 'results' => array(
+ 'Special:ActiveUsers',
+ 'Special:AllMessages',
+ 'Special:AllMyFiles',
+ ),
+ // Third result when testing offset
+ 'offsetresult' => array(
+ 'Special:AllMyUploads',
+ ),
+ ) ),
+ array( array(
+ 'Special namespace with prefix',
+ 'query' => 'Special:Un',
+ 'results' => array(
+ 'Special:Unblock',
+ 'Special:UncategorizedCategories',
+ 'Special:UncategorizedFiles',
+ ),
+ // Third result when testing offset
+ 'offsetresult' => array(
+ 'Special:UncategorizedImages',
+ ),
+ ) ),
+ array( array(
+ 'Special page name',
+ 'query' => 'Special:EditWatchlist',
+ 'results' => array(
+ 'Special:EditWatchlist',
+ ),
+ ) ),
+ array( array(
+ 'Special page subpages',
+ 'query' => 'Special:EditWatchlist/',
+ 'results' => array(
+ 'Special:EditWatchlist/clear',
+ 'Special:EditWatchlist/raw',
+ ),
+ ) ),
+ array( array(
+ 'Special page subpages with prefix',
+ 'query' => 'Special:EditWatchlist/cl',
+ 'results' => array(
+ 'Special:EditWatchlist/clear',
+ ),
+ ) ),
+ );
+ }
+
+ /**
+ * @dataProvider provideSearch
+ * @covers SearchEngine::defaultPrefixSearch
+ */
+ public function testSearch( Array $case ) {
+ $this->search->setLimitOffset( 3 );
+ $results = $this->search->defaultPrefixSearch( $case['query'] );
+ $results = array_map( function( Title $t ) {
+ return $t->getPrefixedText();
+ }, $results );
+ $this->assertEquals(
+ $case['results'],
+ $results,
+ $case[0]
+ );
+ }
+
+ /**
+ * @dataProvider provideSearch
+ * @covers SearchEngine::defaultPrefixSearch
+ */
+ public function testSearchWithOffset( Array $case ) {
+ $this->search->setLimitOffset( 3, 1 );
+ $results = $this->search->defaultPrefixSearch( $case['query'] );
+ $results = array_map( function( Title $t ) {
+ return $t->getPrefixedText();
+ }, $results );
+
+ // We don't expect the first result when offsetting
+ array_shift( $case['results'] );
+ // And sometimes we expect a different last result
+ $expected = isset( $case['offsetresult'] ) ?
+ array_merge( $case['results'], $case['offsetresult'] ) :
+ $case['results'];
+
+ $this->assertEquals(
+ $expected,
+ $results,
+ $case[0]
+ );
+ }
+
+ public static function provideSearchBackend() {
+ return array(
+ array( array(
+ 'Simple case',
+ 'provision' => array(
+ 'Bar',
+ 'Barcelona',
+ 'Barbara',
+ ),
+ 'query' => 'Bar',
+ 'results' => array(
+ 'Bar',
+ 'Barcelona',
+ 'Barbara',
+ ),
+ ) ),
+ array( array(
+ 'Exact match not on top (bug 70958)',
+ 'provision' => array(
+ 'Barcelona',
+ 'Bar',
+ 'Barbara',
+ ),
+ 'query' => 'Bar',
+ 'results' => array(
+ 'Bar',
+ 'Barcelona',
+ 'Barbara',
+ ),
+ ) ),
+ array( array(
+ 'Exact match missing (bug 70958)',
+ 'provision' => array(
+ 'Barcelona',
+ 'Barbara',
+ 'Bart',
+ ),
+ 'query' => 'Bar',
+ 'results' => array(
+ 'Bar',
+ 'Barcelona',
+ 'Barbara',
+ ),
+ ) ),
+ array( array(
+ 'Exact match missing and not existing',
+ 'provision' => array(
+ 'Exile',
+ 'Exist',
+ 'External',
+ ),
+ 'query' => 'Ex',
+ 'results' => array(
+ 'Exile',
+ 'Exist',
+ 'External',
+ ),
+ ) ),
+ array( array(
+ "Exact match shouldn't override already found match if " .
+ "exact is redirect and found isn't",
+ 'provision' => array(
+ // Target of the exact match is low in the list
+ 'Redirect Test Worse Result',
+ 'Redirect Test',
+ ),
+ 'query' => 'redirect test',
+ 'results' => array(
+ // Redirect target is pulled up and exact match isn't added
+ 'Redirect Test',
+ 'Redirect Test Worse Result',
+ ),
+ ) ),
+ array( array(
+ "Exact match shouldn't override already found match if " .
+ "both exact match and found match are redirect",
+ 'provision' => array(
+ // Another redirect to the same target as the exact match
+ // is low in the list
+ 'Redirect Test2 Worse Result',
+ 'Redirect test2',
+ ),
+ 'query' => 'redirect TEST2',
+ 'results' => array(
+ // Found redirect is pulled to the top and exact match isn't
+ // added
+ 'Redirect test2',
+ 'Redirect Test2 Worse Result',
+ ),
+ ) ),
+ array( array(
+ "Exact match should override any already found matches that " .
+ "are redirects to it",
+ 'provision' => array(
+ // Another redirect to the same target as the exact match
+ // is low in the list
+ 'Redirect Test Worse Result',
+ 'Redirect test',
+ ),
+ 'query' => 'Redirect Test',
+ 'results' => array(
+ // Found redirect is pulled to the top and exact match isn't
+ // added
+ 'Redirect Test',
+ 'Redirect Test Worse Result',
+ 'Redirect test',
+ ),
+ ) ),
+ );
+ }
+
+ /**
+ * @dataProvider provideSearchBackend
+ * @covers PrefixSearch::searchBackend
+ */
+ public function testSearchBackend( Array $case ) {
+ $search = $stub = $this->getMockBuilder( 'SearchEngine' )
+ ->setMethods( array( 'completionSearchBackend' ) )->getMock();
+
+ $return = SearchSuggestionSet::fromStrings( $case['provision'] );
+
+ $search->expects( $this->any() )
+ ->method( 'completionSearchBackend' )
+ ->will( $this->returnValue( $return ) );
+
+ $search->setLimitOffset( 3 );
+ $results = $search->completionSearch( $case['query'] );
+
+ $results = $results->map( function( SearchSuggestion $s ) {
+ return $s->getText();
+ } );
+
+ $this->assertEquals(
+ $case['results'],
+ $results,
+ $case[0]
+ );
+ }
+}
--- /dev/null
+<?php
+
+/**
+ * Test for filter utilities.
+ *
+ * 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
+ */
+
+class SearchSuggestionSetTest extends \PHPUnit_Framework_TestCase {
+ /**
+ * Test that adding a new suggestion at the end
+ * will keep proper score ordering
+ */
+ public function testAppend() {
+ $set = SearchSuggestionSet::emptySuggestionSet();
+ $this->assertEquals( 0, $set->getSize() );
+ $set->append( new SearchSuggestion( 3 ) );
+ $this->assertEquals( 3, $set->getWorstScore() );
+ $this->assertEquals( 3, $set->getBestScore() );
+
+ $suggestion = new SearchSuggestion( 4 );
+ $set->append( $suggestion );
+ $this->assertEquals( 2, $set->getWorstScore() );
+ $this->assertEquals( 3, $set->getBestScore() );
+ $this->assertEquals( 2, $suggestion->getScore() );
+
+ $suggestion = new SearchSuggestion( 2 );
+ $set->append( $suggestion );
+ $this->assertEquals( 1, $set->getWorstScore() );
+ $this->assertEquals( 3, $set->getBestScore() );
+ $this->assertEquals( 1, $suggestion->getScore() );
+
+ $scores = $set->map( function( $s ) {
+ return $s->getScore();
+ } );
+ $sorted = $scores;
+ asort( $sorted );
+ $this->assertEquals( $sorted, $scores );
+ }
+
+ /**
+ * Test that adding a new best suggestion will keep proper score
+ * ordering
+ */
+ public function testInsertBest() {
+ $set = SearchSuggestionSet::emptySuggestionSet();
+ $this->assertEquals( 0, $set->getSize() );
+ $set->prepend( new SearchSuggestion( 3 ) );
+ $this->assertEquals( 3, $set->getWorstScore() );
+ $this->assertEquals( 3, $set->getBestScore() );
+
+ $suggestion = new SearchSuggestion( 4 );
+ $set->prepend( $suggestion );
+ $this->assertEquals( 3, $set->getWorstScore() );
+ $this->assertEquals( 4, $set->getBestScore() );
+ $this->assertEquals( 4, $suggestion->getScore() );
+
+ $suggestion = new SearchSuggestion( 0 );
+ $set->prepend( $suggestion );
+ $this->assertEquals( 3, $set->getWorstScore() );
+ $this->assertEquals( 5, $set->getBestScore() );
+ $this->assertEquals( 5, $suggestion->getScore() );
+
+ $suggestion = new SearchSuggestion( 2 );
+ $set->prepend( $suggestion );
+ $this->assertEquals( 3, $set->getWorstScore() );
+ $this->assertEquals( 6, $set->getBestScore() );
+ $this->assertEquals( 6, $suggestion->getScore() );
+
+ $scores = $set->map( function( $s ) {
+ return $s->getScore();
+ } );
+ $sorted = $scores;
+ asort( $sorted );
+ $this->assertEquals( $sorted, $scores );
+ }
+
+ public function testShrink() {
+ $set = SearchSuggestionSet::emptySuggestionSet();
+ for ( $i = 0; $i < 100; $i++ ) {
+ $set->append( new SearchSuggestion( 0 ) );
+ }
+ $set->shrink( 10 );
+ $this->assertEquals( 10, $set->getSize() );
+
+ $set->shrink( 0 );
+ $this->assertEquals( 0, $set->getSize() );
+ }
+
+ // TODO: test for fromTitles
+}
$provider->setManager( SessionManager::singleton() );
$sessionId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
- $store = new \HashBagOStuff();
+ $store = new TestBagOStuff();
$user = User::newFromName( 'UTSysop' );
$anon = new User;
'idIsSafe' => true,
) ),
$store,
- $store,
new \Psr\Log\NullLogger(),
10
);
'persisted' => true,
'idIsSafe' => true,
) ),
- new \EmptyBagOStuff(),
- new \EmptyBagOStuff(),
+ new TestBagOStuff(),
new \Psr\Log\NullLogger(),
10
);
$provider->setManager( SessionManager::singleton() );
$sessionId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
- $store = new \HashBagOStuff();
+ $store = new TestBagOStuff();
$user = User::newFromName( 'UTSysop' );
$anon = new User;
'idIsSafe' => true,
) ),
$store,
- $store,
new \Psr\Log\NullLogger(),
10
);
'userInfo' => UserInfo::newFromUser( $user, true ),
'idIsSafe' => true,
) ),
- new \EmptyBagOStuff(),
- new \EmptyBagOStuff(),
+ new TestBagOStuff(),
new \Psr\Log\NullLogger(),
10
);
ini_set( 'session.use_cookies', 1 );
ini_set( 'session.use_trans_sid', 1 );
- $store = new \HashBagOStuff();
+ $store = new TestBagOStuff();
$logger = new \TestLogger();
$manager = new SessionManager( array(
'store' => $store,
'wgObjectCacheSessionExpiry' => 2,
) );
- $store = new \HashBagOStuff();
+ $store = new TestBagOStuff();
$logger = new \TestLogger( true, function ( $m ) {
return preg_match( '/^SessionBackend a{32} /', $m ) ? null : $m;
} );
$this->assertSame( $expect, $_SESSION );
}
+ // Test expiry
+ session_write_close();
+ ini_set( 'session.gc_divisor', 1 );
+ ini_set( 'session.gc_probability', 1 );
+ sleep( 3 );
+ session_start();
+ $this->assertSame( array(), $_SESSION );
+
// Re-fill the session, then test that session_destroy() works.
$_SESSION['AuthenticationSessionTest'] = $rand;
session_write_close();
) );
$id = new SessionId( $info->getId() );
- $backend = new SessionBackend( $id, $info, $this->store, $this->store, $logger, 10 );
+ $backend = new SessionBackend( $id, $info, $this->store, $logger, 10 );
$priv = \TestingAccessWrapper::newFromObject( $backend );
$priv->persist = false;
$priv->requests = array( 100 => new \FauxRequest() );
$id = new SessionId( $info->getId() );
$logger = new \Psr\Log\NullLogger();
try {
- new SessionBackend( $id, $info, $this->store, $this->store, $logger, 10 );
+ new SessionBackend( $id, $info, $this->store, $logger, 10 );
$this->fail( 'Expected exception not thrown' );
} catch ( \InvalidArgumentException $ex ) {
$this->assertSame(
) );
$id = new SessionId( $info->getId() );
try {
- new SessionBackend( $id, $info, $this->store, $this->store, $logger, 10 );
+ new SessionBackend( $id, $info, $this->store, $logger, 10 );
$this->fail( 'Expected exception not thrown' );
} catch ( \InvalidArgumentException $ex ) {
$this->assertSame( 'Cannot create session without a provider', $ex->getMessage() );
) );
$id = new SessionId( '!' . $info->getId() );
try {
- new SessionBackend( $id, $info, $this->store, $this->store, $logger, 10 );
+ new SessionBackend( $id, $info, $this->store, $logger, 10 );
$this->fail( 'Expected exception not thrown' );
} catch ( \InvalidArgumentException $ex ) {
$this->assertSame(
'idIsSafe' => true,
) );
$id = new SessionId( $info->getId() );
- $backend = new SessionBackend( $id, $info, $this->store, $this->store, $logger, 10 );
+ $backend = new SessionBackend( $id, $info, $this->store, $logger, 10 );
$this->assertSame( self::SESSIONID, $backend->getId() );
$this->assertSame( $id, $backend->getSessionId() );
$this->assertSame( $this->provider, $backend->getProvider() );
'idIsSafe' => true,
) );
$id = new SessionId( $info->getId() );
- $backend = new SessionBackend( $id, $info, $this->store, $this->store, $logger, 10 );
+ $backend = new SessionBackend( $id, $info, $this->store, $logger, 10 );
$this->assertSame( self::SESSIONID, $backend->getId() );
$this->assertSame( $id, $backend->getSessionId() );
$this->assertSame( $this->provider, $backend->getProvider() );
$this->assertInternalType( 'array', $metadata );
$this->assertArrayHasKey( '???', $metadata );
$this->assertSame( '!!!', $metadata['???'] );
+ $this->assertFalse( $this->store->getSessionFromBackend( self::SESSIONID ),
+ 'making sure it didn\'t save to backend' );
// Persistent, not dirty
$this->provider = $neverProvider;
$this->assertInternalType( 'array', $metadata );
$this->assertArrayHasKey( '???', $metadata );
$this->assertSame( '!!!', $metadata['???'] );
+ $this->assertNotSame( false, $this->store->getSessionFromBackend( self::SESSIONID ),
+ 'making sure it did save to backend' );
$this->provider = $this->getMock( 'DummySessionProvider', array( 'persistSession' ) );
$this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' );
$this->assertInternalType( 'array', $metadata );
$this->assertArrayHasKey( '???', $metadata );
$this->assertSame( '!!!', $metadata['???'] );
+ $this->assertNotSame( false, $this->store->getSessionFromBackend( self::SESSIONID ),
+ 'making sure it did save to backend' );
$this->provider = $this->getMock( 'DummySessionProvider', array( 'persistSession' ) );
$this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' );
$this->assertInternalType( 'array', $metadata );
$this->assertArrayHasKey( '???', $metadata );
$this->assertSame( '!!!', $metadata['???'] );
+ $this->assertNotSame( false, $this->store->getSessionFromBackend( self::SESSIONID ),
+ 'making sure it did save to backend' );
// Not marked dirty, but dirty data
$this->provider = $neverProvider;
$this->assertInternalType( 'array', $metadata );
$this->assertArrayHasKey( '???', $metadata );
$this->assertSame( '!!!', $metadata['???'] );
+ $this->assertNotSame( false, $this->store->getSessionFromBackend( self::SESSIONID ),
+ 'making sure it did save to backend' );
// Bad hook
$this->provider = null;
$manager = \TestingAccessWrapper::newFromObject( $this->getManager() );
$this->assertSame( $this->config, $manager->config );
$this->assertSame( $this->logger, $manager->logger );
- $this->assertSame( $this->store, $manager->permStore );
+ $this->assertSame( $this->store, $manager->store );
$manager = \TestingAccessWrapper::newFromObject( new SessionManager() );
$this->assertSame( \RequestContext::getMain()->getConfig(), $manager->config );
$manager = \TestingAccessWrapper::newFromObject( new SessionManager( array(
'config' => $this->config,
) ) );
- $this->assertSame( \ObjectCache::$instances['testSessionStore'], $manager->permStore );
+ $this->assertSame( \ObjectCache::$instances['testSessionStore'], $manager->store );
foreach ( array(
'config' => '$options[\'config\'] must be an instance of Config',
public function testGetSessionById() {
$manager = $this->getManager();
-
- // Disable the in-process cache so our $this->store->setSession() takes effect.
- \TestingAccessWrapper::newFromObject( $manager )->tempStore = new \EmptyBagOStuff;
-
try {
$manager->getSessionById( 'bad' );
$this->fail( 'Expected exception not thrown' );
$that = $this;
- \ObjectCache::$instances[__METHOD__] = new \HashBagOStuff();
+ \ObjectCache::$instances[__METHOD__] = new TestBagOStuff();
$this->setMwGlobals( array( 'wgMainCacheType' => __METHOD__ ) );
$this->stashMwGlobals( array( 'wgGroupPermissions' ) );
$manager->setLogger( $logger );
$request = new \FauxRequest();
- // Disable the in-process cache so our $this->store->setSession() takes effect.
- \TestingAccessWrapper::newFromObject( $manager )->tempStore = new \EmptyBagOStuff;
-
// TestingAccessWrapper can't handle methods with reference arguments, sigh.
$rClass = new \ReflectionClass( $manager );
$rMethod = $rClass->getMethod( 'loadSessionInfoFromStore' );
/**
* BagOStuff with utility functions for MediaWiki\\Session\\* testing
*/
-class TestBagOStuff extends \HashBagOStuff {
+class TestBagOStuff extends \CachedBagOStuff {
+
+ public function __construct() {
+ parent::__construct( new \HashBagOStuff );
+ }
/**
* @param string $id Session ID
return $this->get( wfMemcKey( 'MWSession', $id ) );
}
+ /**
+ * @param string $id Session ID
+ * @return mixed
+ */
+ public function getSessionFromBackend( $id ) {
+ return $this->backend->get( wfMemcKey( 'MWSession', $id ) );
+ }
+
/**
* @param string $id Session ID
*/