Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F5962737
FullSearchResultWidget.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
15 KB
Referenced Files
None
Subscribers
None
FullSearchResultWidget.php
View Options
<?php
namespace
MediaWiki\Search\SearchWidgets
;
use
File
;
use
HtmlArmor
;
use
MediaTransformOutput
;
use
MediaWiki\Category\Category
;
use
MediaWiki\HookContainer\HookContainer
;
use
MediaWiki\HookContainer\HookRunner
;
use
MediaWiki\Html\Html
;
use
MediaWiki\Linker\LinkRenderer
;
use
MediaWiki\MainConfigNames
;
use
MediaWiki\Parser\Sanitizer
;
use
MediaWiki\Search\Entity\SearchResultThumbnail
;
use
MediaWiki\Search\SearchResultThumbnailProvider
;
use
MediaWiki\Specials\SpecialSearch
;
use
MediaWiki\Title\Title
;
use
MediaWiki\User\Options\UserOptionsManager
;
use
RepoGroup
;
use
SearchResult
;
use
ThumbnailImage
;
/**
* Renders a 'full' multi-line search result with metadata.
*
* The Title
* some *highlighted* *text* about the search result
* 5 KiB (651 words) - 12:40, 6 Aug 2016
*/
class
FullSearchResultWidget
implements
SearchResultWidget
{
public
const
THUMBNAIL_SIZE
=
90
;
/** @var SpecialSearch */
protected
$specialPage
;
/** @var LinkRenderer */
protected
$linkRenderer
;
/** @var HookRunner */
private
$hookRunner
;
/** @var RepoGroup */
private
$repoGroup
;
/** @var SearchResultThumbnailProvider */
private
$thumbnailProvider
;
/** @var string */
private
$thumbnailPlaceholderHtml
;
/** @var UserOptionsManager */
private
$userOptionsManager
;
public
function
__construct
(
SpecialSearch
$specialPage
,
LinkRenderer
$linkRenderer
,
HookContainer
$hookContainer
,
RepoGroup
$repoGroup
,
SearchResultThumbnailProvider
$thumbnailProvider
,
UserOptionsManager
$userOptionsManager
)
{
$this
->
specialPage
=
$specialPage
;
$this
->
linkRenderer
=
$linkRenderer
;
$this
->
hookRunner
=
new
HookRunner
(
$hookContainer
);
$this
->
repoGroup
=
$repoGroup
;
$this
->
thumbnailProvider
=
$thumbnailProvider
;
$this
->
userOptionsManager
=
$userOptionsManager
;
}
/**
* @param SearchResult $result The result to render
* @param int $position The result position, including offset
* @return string HTML
*/
public
function
render
(
SearchResult
$result
,
$position
)
{
// If the page doesn't *exist*... our search index is out of date.
// The least confusing at this point is to drop the result.
// You may get less results, but... on well. :P
if
(
$result
->
isBrokenTitle
()
||
$result
->
isMissingRevision
()
)
{
return
''
;
}
$link
=
$this
->
generateMainLinkHtml
(
$result
,
$position
);
// If page content is not readable, just return ths title.
// This is not quite safe, but better than showing excerpts from
// non-readable pages. Note that hiding the entry entirely would
// screw up paging (really?).
if
(
!
$this
->
specialPage
->
getAuthority
()->
definitelyCan
(
'read'
,
$result
->
getTitle
()
)
)
{
return
Html
::
rawElement
(
'li'
,
[],
$link
);
}
$redirect
=
$this
->
generateRedirectHtml
(
$result
);
$section
=
$this
->
generateSectionHtml
(
$result
);
$category
=
$this
->
generateCategoryHtml
(
$result
);
$date
=
htmlspecialchars
(
$this
->
specialPage
->
getLanguage
()->
userTimeAndDate
(
$result
->
getTimestamp
(),
$this
->
specialPage
->
getUser
()
)
);
[
$file
,
$desc
,
$thumb
]
=
$this
->
generateFileHtml
(
$result
);
$snippet
=
$result
->
getTextSnippet
();
if
(
$snippet
)
{
$snippetWithEllipsis
=
$snippet
.
$this
->
specialPage
->
msg
(
'ellipsis'
);
$extract
=
Html
::
rawElement
(
'div'
,
[
'class'
=>
'searchresult'
],
$snippetWithEllipsis
);
}
else
{
$extract
=
''
;
}
if
(
$result
->
getTitle
()
&&
$result
->
getTitle
()->
getNamespace
()
!==
NS_FILE
)
{
// If no file, then the description is about size
$desc
=
$this
->
generateSizeHtml
(
$result
);
// Let hooks do their own final construction if desired.
// FIXME: Not sure why this is only for results without thumbnails,
// but keeping it as-is for now to prevent breaking hook consumers.
$html
=
null
;
$score
=
''
;
$related
=
''
;
// TODO: remove this instanceof and always pass [], let implementors do the cast if
// they want to be SearchDatabase specific
$terms
=
$result
instanceof
\SqlSearchResult
?
$result
->
getTermMatches
()
:
[];
if
(
!
$this
->
hookRunner
->
onShowSearchHit
(
$this
->
specialPage
,
$result
,
$terms
,
$link
,
$redirect
,
$section
,
$extract
,
$score
,
// @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
$desc
,
$date
,
$related
,
$html
)
)
{
return
$html
;
}
}
// All the pieces have been collected. Now generate the final HTML
$joined
=
"{$link} {$redirect} {$category} {$section} {$file}"
;
$meta
=
$this
->
buildMeta
(
$desc
,
$date
);
// Text portion of the search result
$html
=
Html
::
rawElement
(
'div'
,
[
'class'
=>
'mw-search-result-heading'
],
$joined
);
$html
.=
$extract
.
' '
.
$meta
;
// If the result has a thumbnail, place it next to the text block
if
(
$thumb
)
{
$gridCells
=
Html
::
rawElement
(
'div'
,
[
'class'
=>
'searchResultImage-thumbnail'
],
$thumb
)
.
Html
::
rawElement
(
'div'
,
[
'class'
=>
'searchResultImage-text'
],
$html
);
$html
=
Html
::
rawElement
(
'div'
,
[
'class'
=>
'searchResultImage'
],
$gridCells
);
}
return
Html
::
rawElement
(
'li'
,
[
'class'
=>
[
'mw-search-result'
,
'mw-search-result-ns-'
.
$result
->
getTitle
()->
getNamespace
()
]
],
$html
);
}
/**
* Generates HTML for the primary call to action. It is
* typically the article title, but the search engine can
* return an exact snippet to use (typically the article
* title with highlighted words).
*
* @param SearchResult $result
* @param int $position
* @return string HTML
*/
protected
function
generateMainLinkHtml
(
SearchResult
$result
,
$position
)
{
$snippet
=
$result
->
getTitleSnippet
();
if
(
$snippet
===
''
)
{
$snippet
=
null
;
}
else
{
$snippet
=
new
HtmlArmor
(
$snippet
);
}
// clone to prevent hook from changing the title stored inside $result
$title
=
clone
$result
->
getTitle
();
$query
=
[];
$attributes
=
[
'data-serp-pos'
=>
$position
];
$this
->
hookRunner
->
onShowSearchHitTitle
(
$title
,
$snippet
,
$result
,
$result
instanceof
\SqlSearchResult
?
$result
->
getTermMatches
()
:
[],
// @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
$this
->
specialPage
,
$query
,
$attributes
);
$link
=
$this
->
linkRenderer
->
makeLink
(
$title
,
$snippet
,
$attributes
,
$query
);
return
$link
;
}
/**
* Generates an alternate title link, such as (redirect from <a>Foo</a>).
*
* @param string $msgKey i18n message used to wrap title
* @param Title|null $title The title to link to, or null to generate
* the message without a link. In that case $text must be non-null.
* @param string|null $text The text snippet to display, or null
* to use the title
* @return string HTML
*/
protected
function
generateAltTitleHtml
(
$msgKey
,
?
Title
$title
,
$text
)
{
$inner
=
$title
===
null
?
$text
:
$this
->
linkRenderer
->
makeLink
(
$title
,
$text
?
new
HtmlArmor
(
$text
)
:
null
);
return
"<span class='searchalttitle'>"
.
$this
->
specialPage
->
msg
(
$msgKey
)->
rawParams
(
$inner
)->
parse
()
.
"</span>"
;
}
/**
* @param SearchResult $result
* @return string HTML
*/
protected
function
generateRedirectHtml
(
SearchResult
$result
)
{
$title
=
$result
->
getRedirectTitle
();
return
$title
===
null
?
''
:
$this
->
generateAltTitleHtml
(
'search-redirect'
,
$title
,
$result
->
getRedirectSnippet
()
);
}
/**
* @param SearchResult $result
* @return string HTML
*/
protected
function
generateSectionHtml
(
SearchResult
$result
)
{
$title
=
$result
->
getSectionTitle
();
return
$title
===
null
?
''
:
$this
->
generateAltTitleHtml
(
'search-section'
,
$title
,
$result
->
getSectionSnippet
()
);
}
/**
* @param SearchResult $result
* @return string HTML
*/
protected
function
generateCategoryHtml
(
SearchResult
$result
)
{
$snippet
=
$result
->
getCategorySnippet
();
return
$snippet
?
$this
->
generateAltTitleHtml
(
'search-category'
,
null
,
$snippet
)
:
''
;
}
/**
* @param SearchResult $result
* @return string HTML
*/
protected
function
generateSizeHtml
(
SearchResult
$result
)
{
$title
=
$result
->
getTitle
();
if
(
$title
->
getNamespace
()
===
NS_CATEGORY
)
{
$cat
=
Category
::
newFromTitle
(
$title
);
return
$this
->
specialPage
->
msg
(
'search-result-category-size'
)
->
numParams
(
$cat
->
getMemberCount
(),
$cat
->
getSubcatCount
(),
$cat
->
getFileCount
()
)
->
escaped
();
// TODO: This is a bit odd...but requires changing the i18n message to fix
}
elseif
(
$result
->
getByteSize
()
!==
null
||
$result
->
getWordCount
()
>
0
)
{
return
$this
->
specialPage
->
msg
(
'search-result-size'
)
->
sizeParams
(
$result
->
getByteSize
()
)
->
numParams
(
$result
->
getWordCount
()
)
->
escaped
();
}
return
''
;
}
/**
* @param SearchResult $result
* @return array Three element array containing the main file html,
* a text description of the file, and finally the thumbnail html.
* If no thumbnail is available the second and third will be null.
*/
protected
function
generateFileHtml
(
SearchResult
$result
)
{
$title
=
$result
->
getTitle
();
// don't assume that result is a valid title; e.g. could be an interwiki link target
if
(
$title
===
null
||
!
$title
->
canExist
()
)
{
return
[
''
,
null
,
null
];
}
$html
=
''
;
if
(
$result
->
isFileMatch
()
)
{
$html
=
Html
::
rawElement
(
'span'
,
[
'class'
=>
'searchalttitle'
],
$this
->
specialPage
->
msg
(
'search-file-match'
)->
escaped
()
);
}
$allowExtraThumbsFromRequest
=
$this
->
specialPage
->
getRequest
()->
getVal
(
'search-thumbnail-extra-namespaces'
);
$allowExtraThumbsFromPreference
=
$this
->
userOptionsManager
->
getOption
(
$this
->
specialPage
->
getUser
(),
'search-thumbnail-extra-namespaces'
);
$allowExtraThumbs
=
(
bool
)(
$allowExtraThumbsFromRequest
??
$allowExtraThumbsFromPreference
);
if
(
!
$allowExtraThumbs
&&
$title
->
getNamespace
()
!==
NS_FILE
)
{
return
[
$html
,
null
,
null
];
}
$thumbnail
=
$this
->
getThumbnail
(
$result
,
self
::
THUMBNAIL_SIZE
);
$thumbnailName
=
$thumbnail
?
$thumbnail
->
getName
()
:
null
;
if
(
!
$thumbnailName
)
{
return
[
$html
,
null
,
$this
->
generateThumbnailHtml
(
$result
)
];
}
$img
=
$this
->
repoGroup
->
findFile
(
$thumbnailName
);
if
(
!
$img
)
{
return
[
$html
,
null
,
$this
->
generateThumbnailHtml
(
$result
)
];
}
// File::getShortDesc() is documented to return HTML, but many handlers used to incorrectly
// return plain text (T395834), so sanitize it in case the same bug is present in extensions.
$unsafeShortDesc
=
$img
->
getShortDesc
();
$shortDesc
=
Sanitizer
::
removeSomeTags
(
$unsafeShortDesc
);
return
[
$html
,
$this
->
specialPage
->
msg
(
'parentheses'
)->
rawParams
(
$shortDesc
)->
escaped
(),
$this
->
generateThumbnailHtml
(
$result
,
$thumbnail
)
];
}
/**
* @param SearchResult $result
* @param int $size
* @return SearchResultThumbnail|null
*/
private
function
getThumbnail
(
SearchResult
$result
,
int
$size
):
?
SearchResultThumbnail
{
$title
=
$result
->
getTitle
();
// don't assume that result is a valid title; e.g. could be an interwiki link target
if
(
$title
===
null
||
!
$title
->
canExist
()
)
{
return
null
;
}
$thumbnails
=
$this
->
thumbnailProvider
->
getThumbnails
(
[
$title
->
getArticleID
()
=>
$title
],
$size
);
return
$thumbnails
[
$title
->
getArticleID
()
]
??
null
;
}
/**
* @param SearchResult $result
* @param SearchResultThumbnail|null $thumbnail
* @return string|null
*/
private
function
generateThumbnailHtml
(
SearchResult
$result
,
?
SearchResultThumbnail
$thumbnail
=
null
):
?
string
{
$title
=
$result
->
getTitle
();
// don't assume that result is a valid title; e.g. could be an interwiki link target
if
(
$title
===
null
||
!
$title
->
canExist
()
)
{
return
null
;
}
$namespacesWithThumbnails
=
$this
->
specialPage
->
getConfig
()->
get
(
MainConfigNames
::
ThumbnailNamespaces
);
$showThumbnail
=
in_array
(
$title
->
getNamespace
(),
$namespacesWithThumbnails
);
if
(
!
$showThumbnail
)
{
return
null
;
}
$thumbnailName
=
$thumbnail
?
$thumbnail
->
getName
()
:
null
;
if
(
!
$thumbnail
||
!
$thumbnailName
)
{
return
$this
->
generateThumbnailPlaceholderHtml
();
}
$img
=
$this
->
repoGroup
->
findFile
(
$thumbnailName
);
if
(
!
$img
)
{
return
$this
->
generateThumbnailPlaceholderHtml
();
}
$thumb
=
$this
->
transformThumbnail
(
$img
,
$thumbnail
);
if
(
$thumb
)
{
if
(
$title
->
getNamespace
()
===
NS_FILE
)
{
// don't use a custom link, just use traditional thumbnail HTML
return
$thumb
->
toHtml
(
[
'desc-link'
=>
true
,
'loading'
=>
'lazy'
,
'alt'
=>
$this
->
specialPage
->
msg
(
'search-thumbnail-alt'
)->
params
(
$title
),
]
);
}
// thumbnails for non-file results should link to the relevant title
return
$thumb
->
toHtml
(
[
'desc-link'
=>
true
,
'custom-title-link'
=>
$title
,
'loading'
=>
'lazy'
,
'alt'
=>
$this
->
specialPage
->
msg
(
'search-thumbnail-alt'
)->
params
(
$title
),
]
);
}
return
$this
->
generateThumbnailPlaceholderHtml
();
}
/**
* @param File $img
* @param SearchResultThumbnail $thumbnail
* @return ThumbnailImage|MediaTransformOutput|bool False on failure
*/
private
function
transformThumbnail
(
File
$img
,
SearchResultThumbnail
$thumbnail
)
{
$optimalThumbnailWidth
=
$thumbnail
->
getWidth
();
// $thumb will have rescaled to fit within a <$size>x<$size> bounding
// box, but we want it to cover a full square (at the cost of losing
// some of the edges)
// instead of the largest side matching up with $size, we want the
// smallest size to match (or exceed) $size
$thumbnailMaxDimension
=
max
(
$thumbnail
->
getWidth
(),
$thumbnail
->
getHeight
()
);
$thumbnailMinDimension
=
min
(
$thumbnail
->
getWidth
(),
$thumbnail
->
getHeight
()
);
$rescaleCoefficient
=
$thumbnailMinDimension
?
$thumbnailMaxDimension
/
$thumbnailMinDimension
:
1
;
// we'll only deal with width from now on since conventions for
// standard sizes have formed around width; height will simply
// follow according to aspect ratio
$rescaledWidth
=
(
int
)
round
(
$rescaleCoefficient
*
$thumbnail
->
getWidth
()
);
// we'll also be looking at $wgThumbLimits to ensure that we pick
// from within the predefined list of sizes
// NOTE: only do this when there is a difference in the rescaled
// size vs the original thumbnail size - some media types are
// different and thumb limits don't matter (e.g. for audio, the
// player must remain at the size we want, regardless of whether or
// not it fits the thumb limits, which in this case are irrelevant)
if
(
$rescaledWidth
!==
$thumbnail
->
getWidth
()
)
{
$thumbLimits
=
$this
->
specialPage
->
getConfig
()->
get
(
MainConfigNames
::
ThumbLimits
);
$largerThumbLimits
=
array_filter
(
$thumbLimits
,
static
function
(
$limit
)
use
(
$rescaledWidth
)
{
return
$limit
>=
$rescaledWidth
;
}
);
$optimalThumbnailWidth
=
$largerThumbLimits
?
min
(
$largerThumbLimits
)
:
max
(
$thumbLimits
);
}
return
$img
->
transform
(
[
'width'
=>
$optimalThumbnailWidth
]
);
}
/**
* @return string
*/
private
function
generateThumbnailPlaceholderHtml
():
string
{
if
(
$this
->
thumbnailPlaceholderHtml
)
{
return
$this
->
thumbnailPlaceholderHtml
;
}
$path
=
MW_INSTALL_PATH
.
'/resources/lib/ooui/themes/wikimediaui/images/icons/imageLayoutFrameless.svg'
;
$this
->
thumbnailPlaceholderHtml
=
Html
::
rawElement
(
'div'
,
[
'class'
=>
'searchResultImage-thumbnail-placeholder'
,
'aria-hidden'
=>
'true'
,
],
file_get_contents
(
$path
)
);
return
$this
->
thumbnailPlaceholderHtml
;
}
/**
* @param string $desc HTML description of result, ex: size in bytes, or empty string
* @param string $date HTML representation of last edit date, or empty string
* @return string HTML A div combining $desc and $date with a separator in a <div>.
* If either is missing only one will be represented. If both are missing an empty
* string will be returned.
*/
protected
function
buildMeta
(
$desc
,
$date
)
{
if
(
$desc
&&
$date
)
{
$meta
=
"{$desc} - {$date}"
;
}
elseif
(
$desc
)
{
$meta
=
$desc
;
}
elseif
(
$date
)
{
$meta
=
$date
;
}
else
{
return
''
;
}
return
"<div class='mw-search-result-data'>{$meta}</div>"
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sat, Oct 3, 09:06 (1 w, 4 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
26/ac/ab7ca9af0e6621d48100e4fcfb69
Default Alt Text
FullSearchResultWidget.php (15 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment