2012-02-29 10:50:36 +00:00
|
|
|
<?php
|
|
|
|
|
2020-04-19 22:41:19 +00:00
|
|
|
namespace PageImages;
|
|
|
|
|
|
|
|
use ApiBase;
|
|
|
|
use ApiMain;
|
|
|
|
use File;
|
2022-11-09 12:07:06 +00:00
|
|
|
use MapCacheLRU;
|
2022-06-15 16:08:43 +00:00
|
|
|
use MediaWiki\Api\Hook\ApiOpenSearchSuggestHook;
|
2022-11-09 12:07:06 +00:00
|
|
|
use MediaWiki\Cache\CacheKeyHelper;
|
2024-04-10 18:42:02 +00:00
|
|
|
use MediaWiki\Config\Config;
|
2024-06-09 17:03:17 +00:00
|
|
|
use MediaWiki\Context\IContextSource;
|
2022-06-15 16:08:43 +00:00
|
|
|
use MediaWiki\Hook\InfoActionHook;
|
2024-04-10 18:42:02 +00:00
|
|
|
use MediaWiki\MainConfigNames;
|
2020-04-04 15:34:58 +00:00
|
|
|
use MediaWiki\MediaWikiServices;
|
2024-06-09 17:03:17 +00:00
|
|
|
use MediaWiki\Output\Hook\BeforePageDisplayHook;
|
2024-01-05 21:37:40 +00:00
|
|
|
use MediaWiki\Output\OutputPage;
|
2023-08-19 23:54:45 +00:00
|
|
|
use MediaWiki\Request\FauxRequest;
|
2023-08-19 04:18:19 +00:00
|
|
|
use MediaWiki\Title\Title;
|
2023-11-29 12:39:39 +00:00
|
|
|
use MediaWiki\User\Options\UserOptionsLookup;
|
2023-10-15 13:39:15 +00:00
|
|
|
use RepoGroup;
|
2020-04-19 22:41:19 +00:00
|
|
|
use Skin;
|
2023-10-15 18:48:50 +00:00
|
|
|
use Wikimedia\Rdbms\IConnectionProvider;
|
2020-04-04 15:34:58 +00:00
|
|
|
|
2015-11-16 14:59:34 +00:00
|
|
|
/**
|
2018-05-25 04:42:29 +00:00
|
|
|
* @license WTFPL
|
2015-11-17 08:54:24 +00:00
|
|
|
* @author Max Semenik
|
2015-11-16 14:59:34 +00:00
|
|
|
* @author Brad Jorsch
|
2017-11-24 07:33:49 +00:00
|
|
|
* @author Thiemo Kreuz
|
2015-11-16 14:59:34 +00:00
|
|
|
*/
|
2022-06-15 16:08:43 +00:00
|
|
|
class PageImages implements
|
|
|
|
ApiOpenSearchSuggestHook,
|
|
|
|
BeforePageDisplayHook,
|
|
|
|
InfoActionHook
|
|
|
|
{
|
2020-04-20 13:34:12 +00:00
|
|
|
/**
|
|
|
|
* @const value for free images
|
|
|
|
*/
|
2020-05-19 23:27:08 +00:00
|
|
|
public const LICENSE_FREE = 'free';
|
2020-04-20 13:34:12 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @const value for images with any type of license
|
|
|
|
*/
|
2020-05-19 23:27:08 +00:00
|
|
|
public const LICENSE_ANY = 'any';
|
2015-11-16 14:59:34 +00:00
|
|
|
|
2013-11-04 22:00:38 +00:00
|
|
|
/**
|
2016-11-10 00:02:00 +00:00
|
|
|
* Page property used to store the best page image information.
|
|
|
|
* If the best image is the same as the best image with free license,
|
|
|
|
* then nothing is stored under this property.
|
2017-01-23 21:53:05 +00:00
|
|
|
* Note changing this value is not advised as it will invalidate all
|
|
|
|
* existing page property names on a production instance
|
|
|
|
* and cause them to be regenerated.
|
2016-11-10 00:02:00 +00:00
|
|
|
* @see PageImages::PROP_NAME_FREE
|
2013-11-04 22:00:38 +00:00
|
|
|
*/
|
2020-05-19 23:27:08 +00:00
|
|
|
public const PROP_NAME = 'page_image';
|
2017-01-31 12:59:16 +00:00
|
|
|
|
2016-11-10 00:02:00 +00:00
|
|
|
/**
|
|
|
|
* Page property used to store the best free page image information
|
2017-01-23 21:53:05 +00:00
|
|
|
* Note changing this value is not advised as it will invalidate all
|
|
|
|
* existing page property names on a production instance
|
|
|
|
* and cause them to be regenerated.
|
2016-11-10 00:02:00 +00:00
|
|
|
*/
|
2020-05-19 23:27:08 +00:00
|
|
|
public const PROP_NAME_FREE = 'page_image_free';
|
2016-11-10 00:02:00 +00:00
|
|
|
|
2024-04-10 18:42:02 +00:00
|
|
|
/** @var Config */
|
|
|
|
private $config;
|
|
|
|
|
2023-10-15 18:48:50 +00:00
|
|
|
/** @var IConnectionProvider */
|
|
|
|
private $dbProvider;
|
|
|
|
|
2023-10-15 13:39:15 +00:00
|
|
|
/** @var RepoGroup */
|
|
|
|
private $repoGroup;
|
|
|
|
|
2022-09-02 22:15:57 +00:00
|
|
|
/** @var UserOptionsLookup */
|
|
|
|
private $userOptionsLookup;
|
|
|
|
|
2022-11-09 12:07:06 +00:00
|
|
|
/** @var MapCacheLRU */
|
|
|
|
private static $cache = null;
|
|
|
|
|
2023-10-15 13:29:43 +00:00
|
|
|
/**
|
|
|
|
* @return PageImages
|
|
|
|
*/
|
2023-10-16 06:00:36 +00:00
|
|
|
private static function factory(): self {
|
2023-10-15 13:39:15 +00:00
|
|
|
$services = MediaWikiServices::getInstance();
|
2023-10-15 13:29:43 +00:00
|
|
|
return new self(
|
2024-04-10 18:42:02 +00:00
|
|
|
$services->getMainConfig(),
|
2023-10-15 18:48:50 +00:00
|
|
|
$services->getDBLoadBalancerFactory(),
|
2023-10-15 13:39:15 +00:00
|
|
|
$services->getRepoGroup(),
|
|
|
|
$services->getUserOptionsLookup()
|
2023-10-15 13:29:43 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-09-02 22:15:57 +00:00
|
|
|
/**
|
2024-04-10 18:42:02 +00:00
|
|
|
* @param Config $config
|
2023-10-15 18:48:50 +00:00
|
|
|
* @param IConnectionProvider $dbProvider
|
2023-10-15 13:39:15 +00:00
|
|
|
* @param RepoGroup $repoGroup
|
2022-09-02 22:15:57 +00:00
|
|
|
* @param UserOptionsLookup $userOptionsLookup
|
|
|
|
*/
|
2023-10-15 13:39:15 +00:00
|
|
|
public function __construct(
|
2024-04-10 18:42:02 +00:00
|
|
|
Config $config,
|
2023-10-15 18:48:50 +00:00
|
|
|
IConnectionProvider $dbProvider,
|
2023-10-15 13:39:15 +00:00
|
|
|
RepoGroup $repoGroup,
|
|
|
|
UserOptionsLookup $userOptionsLookup
|
|
|
|
) {
|
2024-04-10 18:42:02 +00:00
|
|
|
$this->config = $config;
|
2023-10-15 18:48:50 +00:00
|
|
|
$this->dbProvider = $dbProvider;
|
2023-10-15 13:39:15 +00:00
|
|
|
$this->repoGroup = $repoGroup;
|
2022-09-02 22:15:57 +00:00
|
|
|
$this->userOptionsLookup = $userOptionsLookup;
|
|
|
|
}
|
|
|
|
|
2016-11-10 00:02:00 +00:00
|
|
|
/**
|
2017-01-23 21:53:05 +00:00
|
|
|
* Get property name used in page_props table. When a page image
|
|
|
|
* is stored it will be stored under this property name on the corresponding
|
|
|
|
* article.
|
2016-11-10 00:02:00 +00:00
|
|
|
*
|
|
|
|
* @param bool $isFree Whether the image is a free-license image
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public static function getPropName( $isFree ) {
|
|
|
|
return $isFree ? self::PROP_NAME_FREE : self::PROP_NAME;
|
|
|
|
}
|
2013-11-04 22:00:38 +00:00
|
|
|
|
2020-04-20 13:34:12 +00:00
|
|
|
/**
|
|
|
|
* Get property names used in page_props table
|
|
|
|
*
|
|
|
|
* If the license is free, then only the free property name will be returned,
|
|
|
|
* otherwise both free and non-free property names will be returned. That's
|
|
|
|
* because we save the image name only once if it's free and the best image.
|
|
|
|
*
|
|
|
|
* @param string $license either LICENSE_FREE or LICENSE_ANY,
|
|
|
|
* specifying whether to return the non-free property name or not
|
|
|
|
* @return string|array
|
|
|
|
*/
|
|
|
|
public static function getPropNames( $license ) {
|
|
|
|
if ( $license === self::LICENSE_FREE ) {
|
|
|
|
return self::getPropName( true );
|
|
|
|
}
|
|
|
|
return [ self::getPropName( true ), self::getPropName( false ) ];
|
|
|
|
}
|
|
|
|
|
2013-11-04 22:00:38 +00:00
|
|
|
/**
|
2022-11-17 17:10:27 +00:00
|
|
|
* Return page image for a given title
|
2013-11-04 22:00:38 +00:00
|
|
|
*
|
2017-09-01 04:54:58 +00:00
|
|
|
* @param Title $title Title to get page image for
|
2023-10-16 06:00:36 +00:00
|
|
|
* @return File|false
|
2013-11-04 22:00:38 +00:00
|
|
|
*/
|
|
|
|
public static function getPageImage( Title $title ) {
|
2023-10-16 06:00:36 +00:00
|
|
|
// Cast any cacheable null to false
|
|
|
|
return self::factory()->getPageImageInternal( $title ) ?? false;
|
2023-10-15 13:29:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return page image for a given title
|
|
|
|
*
|
|
|
|
* @param Title $title Title to get page image for
|
2023-10-16 06:00:36 +00:00
|
|
|
* @return File|null
|
2023-10-15 13:29:43 +00:00
|
|
|
*/
|
2023-10-16 06:00:36 +00:00
|
|
|
public function getPageImageInternal( Title $title ): ?File {
|
2022-11-17 17:10:27 +00:00
|
|
|
self::$cache ??= new MapCacheLRU( 100 );
|
|
|
|
|
|
|
|
$file = self::$cache->getWithSetCallback(
|
|
|
|
CacheKeyHelper::getKeyForPage( $title ),
|
2024-03-11 19:13:21 +00:00
|
|
|
fn () => $this->fetchPageImage( $title )
|
2022-11-17 17:10:27 +00:00
|
|
|
);
|
|
|
|
|
2023-10-16 06:00:36 +00:00
|
|
|
// Cast false to null
|
|
|
|
return $file ?: null;
|
2022-11-17 17:10:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param Title $title Title to get page image for
|
2023-10-16 06:00:36 +00:00
|
|
|
* @return File|null|false
|
2022-11-17 17:10:27 +00:00
|
|
|
*/
|
2023-10-15 13:29:43 +00:00
|
|
|
private function fetchPageImage( Title $title ) {
|
2019-10-31 19:38:11 +00:00
|
|
|
if ( !$title->canExist() ) {
|
2022-11-17 17:10:27 +00:00
|
|
|
// Optimization: Do not query for special pages or other titles never in the database
|
2019-10-31 19:38:11 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-08-15 12:31:18 +00:00
|
|
|
if ( $title->inNamespace( NS_FILE ) ) {
|
2023-10-15 13:39:15 +00:00
|
|
|
return $this->repoGroup->findFile( $title );
|
2019-08-15 12:31:18 +00:00
|
|
|
}
|
|
|
|
|
2022-11-17 17:10:27 +00:00
|
|
|
$pageId = $title->getArticleID();
|
|
|
|
if ( !$pageId ) {
|
2019-10-31 19:38:11 +00:00
|
|
|
// No page id to select from
|
2022-11-17 17:10:27 +00:00
|
|
|
// Allow caching, cast null to false later
|
|
|
|
return null;
|
2019-10-31 19:38:11 +00:00
|
|
|
}
|
|
|
|
|
2023-10-15 18:48:50 +00:00
|
|
|
$dbr = $this->dbProvider->getReplicaDatabase();
|
2024-04-21 10:41:53 +00:00
|
|
|
$fileName = $dbr->newSelectQueryBuilder()
|
|
|
|
->select( 'pp_value' )
|
|
|
|
->from( 'page_props' )
|
|
|
|
->where( [
|
2022-11-17 17:10:27 +00:00
|
|
|
'pp_page' => $pageId,
|
2017-05-30 19:49:44 +00:00
|
|
|
'pp_propname' => [ self::PROP_NAME, self::PROP_NAME_FREE ]
|
2024-04-21 10:41:53 +00:00
|
|
|
] )
|
|
|
|
->orderBy( 'pp_propname' )
|
|
|
|
->caller( __METHOD__ )
|
|
|
|
->fetchField();
|
2022-11-17 17:10:27 +00:00
|
|
|
if ( !$fileName ) {
|
2023-10-16 06:00:36 +00:00
|
|
|
// Return not found without caching.
|
2022-11-17 17:10:27 +00:00
|
|
|
return false;
|
2013-11-04 22:00:38 +00:00
|
|
|
}
|
2015-11-16 14:59:34 +00:00
|
|
|
|
2023-10-15 13:39:15 +00:00
|
|
|
return $this->repoGroup->findFile( $fileName );
|
2013-11-04 22:00:38 +00:00
|
|
|
}
|
|
|
|
|
2014-05-02 06:29:28 +00:00
|
|
|
/**
|
|
|
|
* InfoAction hook handler, adds the page image to the info=action page
|
2015-10-26 10:41:13 +00:00
|
|
|
*
|
2014-05-02 06:29:28 +00:00
|
|
|
* @see https://www.mediawiki.org/wiki/Manual:Hooks/InfoAction
|
2015-10-26 10:41:13 +00:00
|
|
|
*
|
2017-12-06 21:02:05 +00:00
|
|
|
* @param IContextSource $context Context, used to extract the title of the page
|
|
|
|
* @param array[] &$pageInfo Auxillary information about the page.
|
2014-05-02 06:29:28 +00:00
|
|
|
*/
|
2022-06-15 16:08:43 +00:00
|
|
|
public function onInfoAction( $context, &$pageInfo ) {
|
2023-10-15 13:29:43 +00:00
|
|
|
$imageFile = $this->getPageImageInternal( $context->getTitle() );
|
2014-06-18 19:58:12 +00:00
|
|
|
if ( !$imageFile ) {
|
2014-05-02 06:29:28 +00:00
|
|
|
// The page has no image
|
2016-03-07 08:47:51 +00:00
|
|
|
return;
|
2014-05-02 06:29:28 +00:00
|
|
|
}
|
|
|
|
|
2022-09-02 22:15:57 +00:00
|
|
|
$thumbSetting = $this->userOptionsLookup->getOption( $context->getUser(), 'thumbsize' );
|
2024-04-10 18:42:02 +00:00
|
|
|
$thumbSize = $this->config->get( MainConfigNames::ThumbLimits )[$thumbSetting];
|
2014-05-02 06:29:28 +00:00
|
|
|
|
2016-11-21 23:29:28 +00:00
|
|
|
$thumb = $imageFile->transform( [ 'width' => $thumbSize ] );
|
2014-06-18 19:58:12 +00:00
|
|
|
if ( !$thumb ) {
|
2016-03-07 08:47:51 +00:00
|
|
|
return;
|
2014-06-18 19:58:12 +00:00
|
|
|
}
|
|
|
|
$imageHtml = $thumb->toHtml(
|
2016-11-21 23:29:28 +00:00
|
|
|
[
|
2014-05-02 06:29:28 +00:00
|
|
|
'alt' => $imageFile->getTitle()->getText(),
|
|
|
|
'desc-link' => true,
|
2016-11-21 23:29:28 +00:00
|
|
|
]
|
2014-05-02 06:29:28 +00:00
|
|
|
);
|
|
|
|
|
2016-11-21 23:29:28 +00:00
|
|
|
$pageInfo['header-basic'][] = [
|
2014-05-02 06:29:28 +00:00
|
|
|
$context->msg( 'pageimages-info-label' ),
|
|
|
|
$imageHtml
|
2016-11-21 23:29:28 +00:00
|
|
|
];
|
2014-05-02 06:29:28 +00:00
|
|
|
}
|
|
|
|
|
2012-05-08 21:42:07 +00:00
|
|
|
/**
|
2014-11-05 22:18:30 +00:00
|
|
|
* ApiOpenSearchSuggest hook handler, enhances ApiOpenSearch results with this extension's data
|
2015-10-26 10:41:13 +00:00
|
|
|
*
|
2017-12-06 21:02:05 +00:00
|
|
|
* @param array[] &$results Array of results to add page images too
|
2012-05-08 21:42:07 +00:00
|
|
|
*/
|
2022-06-15 16:08:43 +00:00
|
|
|
public function onApiOpenSearchSuggest( &$results ) {
|
2024-04-10 18:42:02 +00:00
|
|
|
if ( !$this->config->get( 'PageImagesExpandOpenSearchXml' ) || !count( $results ) ) {
|
2016-03-07 08:47:51 +00:00
|
|
|
return;
|
2012-03-08 14:01:00 +00:00
|
|
|
}
|
2015-10-26 10:41:13 +00:00
|
|
|
|
2012-03-08 14:01:00 +00:00
|
|
|
$pageIds = array_keys( $results );
|
2014-09-19 18:41:48 +00:00
|
|
|
$data = self::getImages( $pageIds, 50 );
|
2012-03-08 14:01:00 +00:00
|
|
|
foreach ( $pageIds as $id ) {
|
2014-09-19 18:41:48 +00:00
|
|
|
if ( isset( $data[$id]['thumbnail'] ) ) {
|
|
|
|
$results[$id]['image'] = $data[$id]['thumbnail'];
|
2012-03-08 14:01:00 +00:00
|
|
|
} else {
|
|
|
|
$results[$id]['image'] = null;
|
|
|
|
}
|
|
|
|
}
|
2012-05-08 21:42:07 +00:00
|
|
|
}
|
|
|
|
|
2014-09-19 18:41:48 +00:00
|
|
|
/**
|
|
|
|
* Returns image information for pages with given ids
|
|
|
|
*
|
2015-10-26 10:41:13 +00:00
|
|
|
* @param int[] $pageIds
|
|
|
|
* @param int $size
|
2015-11-16 14:59:34 +00:00
|
|
|
*
|
2015-10-26 10:41:13 +00:00
|
|
|
* @return array[]
|
2014-09-19 18:41:48 +00:00
|
|
|
*/
|
2023-08-15 08:03:09 +00:00
|
|
|
public static function getImages( array $pageIds, $size = 0 ) {
|
2017-05-26 21:23:42 +00:00
|
|
|
$ret = [];
|
|
|
|
foreach ( array_chunk( $pageIds, ApiBase::LIMIT_SML1 ) as $chunk ) {
|
|
|
|
$request = [
|
|
|
|
'action' => 'query',
|
|
|
|
'prop' => 'pageimages',
|
|
|
|
'piprop' => 'name',
|
|
|
|
'pageids' => implode( '|', $chunk ),
|
|
|
|
'pilimit' => 'max',
|
|
|
|
];
|
|
|
|
|
|
|
|
if ( $size ) {
|
|
|
|
$request['piprop'] = 'thumbnail';
|
|
|
|
$request['pithumbsize'] = $size;
|
|
|
|
}
|
2015-11-16 14:59:34 +00:00
|
|
|
|
2017-05-26 21:23:42 +00:00
|
|
|
$api = new ApiMain( new FauxRequest( $request ) );
|
|
|
|
$api->execute();
|
2015-11-16 14:59:34 +00:00
|
|
|
|
2018-04-25 20:01:49 +00:00
|
|
|
$ret += (array)$api->getResult()->getResultData(
|
|
|
|
[ 'query', 'pages' ], [ 'Strip' => 'base' ]
|
|
|
|
);
|
2014-09-19 18:41:48 +00:00
|
|
|
}
|
2017-05-26 21:23:42 +00:00
|
|
|
return $ret;
|
2014-09-19 18:41:48 +00:00
|
|
|
}
|
|
|
|
|
2017-01-31 12:59:16 +00:00
|
|
|
/**
|
2022-06-15 16:08:43 +00:00
|
|
|
* @param OutputPage $out The page being output.
|
|
|
|
* @param Skin $skin Skin object used to generate the page. Ignored
|
2017-01-31 12:59:16 +00:00
|
|
|
*/
|
2022-06-15 16:08:43 +00:00
|
|
|
public function onBeforePageDisplay( $out, $skin ): void {
|
2022-07-28 15:27:15 +00:00
|
|
|
if ( !$out->getConfig()->get( 'PageImagesOpenGraph' ) ) {
|
|
|
|
return;
|
|
|
|
}
|
2023-10-15 13:29:43 +00:00
|
|
|
$imageFile = $this->getPageImageInternal( $out->getContext()->getTitle() );
|
2017-01-28 15:43:55 +00:00
|
|
|
if ( !$imageFile ) {
|
2021-06-18 03:16:36 +00:00
|
|
|
$fallback = $out->getConfig()->get( 'PageImagesOpenGraphFallbackImage' );
|
|
|
|
if ( $fallback ) {
|
|
|
|
$out->addMeta( 'og:image', wfExpandUrl( $fallback, PROTO_CANONICAL ) );
|
|
|
|
}
|
2017-01-28 15:43:55 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-10-26 10:49:57 +00:00
|
|
|
// Open Graph protocol -- https://ogp.me/
|
|
|
|
// Multiple images are supported according to https://ogp.me/#array
|
|
|
|
// See https://developers.facebook.com/docs/sharing/best-practices?locale=en_US#images
|
|
|
|
// See T282065: WhatsApp expects an image <300kB
|
|
|
|
foreach ( [ 1200, 800, 640 ] as $width ) {
|
|
|
|
$thumb = $imageFile->transform( [ 'width' => $width ] );
|
|
|
|
if ( !$thumb ) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
$out->addMeta( 'og:image', wfExpandUrl( $thumb->getUrl(), PROTO_CANONICAL ) );
|
|
|
|
$out->addMeta( 'og:image:width', strval( $thumb->getWidth() ) );
|
|
|
|
$out->addMeta( 'og:image:height', strval( $thumb->getHeight() ) );
|
2017-01-28 15:43:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-02-29 10:50:36 +00:00
|
|
|
}
|