1<?php
2
3namespace PageImages;
4
5use ApiBase;
6use ApiMain;
7use FauxRequest;
8use File;
9use IContextSource;
10use MediaWiki\MediaWikiServices;
11use OutputPage;
12use Skin;
13use Title;
14
15/**
16 * @license WTFPL
17 * @author Max Semenik
18 * @author Brad Jorsch
19 * @author Thiemo Kreuz
20 */
21class PageImages {
22	/**
23	 * @const value for free images
24	 */
25	public const LICENSE_FREE = 'free';
26
27	/**
28	 * @const value for images with any type of license
29	 */
30	public const LICENSE_ANY = 'any';
31
32	/**
33	 * Page property used to store the best page image information.
34	 * If the best image is the same as the best image with free license,
35	 * then nothing is stored under this property.
36	 * Note changing this value is not advised as it will invalidate all
37	 * existing page property names on a production instance
38	 * and cause them to be regenerated.
39	 * @see PageImages::PROP_NAME_FREE
40	 */
41	public const PROP_NAME = 'page_image';
42
43	/**
44	 * Page property used to store the best free page image information
45	 * Note changing this value is not advised as it will invalidate all
46	 * existing page property names on a production instance
47	 * and cause them to be regenerated.
48	 */
49	public const PROP_NAME_FREE = 'page_image_free';
50
51	/**
52	 * Get property name used in page_props table. When a page image
53	 * is stored it will be stored under this property name on the corresponding
54	 * article.
55	 *
56	 * @param bool $isFree Whether the image is a free-license image
57	 * @return string
58	 */
59	public static function getPropName( $isFree ) {
60		return $isFree ? self::PROP_NAME_FREE : self::PROP_NAME;
61	}
62
63	/**
64	 * Get property names used in page_props table
65	 *
66	 * If the license is free, then only the free property name will be returned,
67	 * otherwise both free and non-free property names will be returned. That's
68	 * because we save the image name only once if it's free and the best image.
69	 *
70	 * @param string $license either LICENSE_FREE or LICENSE_ANY,
71	 * specifying whether to return the non-free property name or not
72	 * @return string|array
73	 */
74	public static function getPropNames( $license ) {
75		if ( $license === self::LICENSE_FREE ) {
76			return self::getPropName( true );
77		}
78		return [ self::getPropName( true ), self::getPropName( false ) ];
79	}
80
81	/**
82	 * Returns page image for a given title
83	 *
84	 * @param Title $title Title to get page image for
85	 *
86	 * @return File|bool
87	 */
88	public static function getPageImage( Title $title ) {
89		// Do not query for special pages or other titles never in the database
90		if ( !$title->canExist() ) {
91			return false;
92		}
93
94		if ( $title->inNamespace( NS_FILE ) ) {
95			return MediaWikiServices::getInstance()->getRepoGroup()->findFile( $title );
96		}
97
98		if ( !$title->exists() ) {
99			// No page id to select from
100			return false;
101		}
102
103		$dbr = wfGetDB( DB_REPLICA );
104		$fileName = $dbr->selectField( 'page_props',
105			'pp_value',
106			[
107				'pp_page' => $title->getArticleID(),
108				'pp_propname' => [ self::PROP_NAME, self::PROP_NAME_FREE ]
109			],
110			__METHOD__,
111			[ 'ORDER BY' => 'pp_propname' ]
112		);
113
114		$file = false;
115		if ( $fileName ) {
116			$file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $fileName );
117		}
118
119		return $file;
120	}
121
122	/**
123	 * InfoAction hook handler, adds the page image to the info=action page
124	 *
125	 * @see https://www.mediawiki.org/wiki/Manual:Hooks/InfoAction
126	 *
127	 * @param IContextSource $context Context, used to extract the title of the page
128	 * @param array[] &$pageInfo Auxillary information about the page.
129	 */
130	public static function onInfoAction( IContextSource $context, &$pageInfo ) {
131		global $wgThumbLimits;
132
133		$imageFile = self::getPageImage( $context->getTitle() );
134		if ( !$imageFile ) {
135			// The page has no image
136			return;
137		}
138
139		$thumbSetting = $context->getUser()->getOption( 'thumbsize' );
140		$thumbSize = $wgThumbLimits[$thumbSetting];
141
142		$thumb = $imageFile->transform( [ 'width' => $thumbSize ] );
143		if ( !$thumb ) {
144			return;
145		}
146		$imageHtml = $thumb->toHtml(
147			[
148				'alt' => $imageFile->getTitle()->getText(),
149				'desc-link' => true,
150			]
151		);
152
153		$pageInfo['header-basic'][] = [
154			$context->msg( 'pageimages-info-label' ),
155			$imageHtml
156		];
157	}
158
159	/**
160	 * ApiOpenSearchSuggest hook handler, enhances ApiOpenSearch results with this extension's data
161	 *
162	 * @param array[] &$results Array of results to add page images too
163	 */
164	public static function onApiOpenSearchSuggest( array &$results ) {
165		global $wgPageImagesExpandOpenSearchXml;
166
167		if ( !$wgPageImagesExpandOpenSearchXml || !count( $results ) ) {
168			return;
169		}
170
171		$pageIds = array_keys( $results );
172		$data = self::getImages( $pageIds, 50 );
173		foreach ( $pageIds as $id ) {
174			if ( isset( $data[$id]['thumbnail'] ) ) {
175				$results[$id]['image'] = $data[$id]['thumbnail'];
176			} else {
177				$results[$id]['image'] = null;
178			}
179		}
180	}
181
182	/**
183	 * SpecialMobileEditWatchlist::images hook handler, adds images to mobile watchlist A-Z view
184	 *
185	 * @param IContextSource $context Context object. Ignored
186	 * @param array[] $watchlist Array of relevant pages on the watchlist, sorted by namespace
187	 * @param array[] &$images Array of images to populate
188	 */
189	public static function onSpecialMobileEditWatchlistImages(
190		IContextSource $context, array $watchlist, array &$images
191	) {
192		$ids = [];
193		foreach ( $watchlist as $ns => $pages ) {
194			foreach ( array_keys( $pages ) as $dbKey ) {
195				$title = Title::makeTitle( $ns, $dbKey );
196				// Getting page ID here is safe because SpecialEditWatchlist::getWatchlistInfo()
197				// uses LinkBatch
198				$id = $title->getArticleID();
199				if ( $id ) {
200					$ids[$id] = $dbKey;
201				}
202			}
203		}
204
205		$data = self::getImages( array_keys( $ids ) );
206		foreach ( $data as $id => $page ) {
207			if ( isset( $page['pageimage'] ) ) {
208				$images[ $page['ns'] ][ $ids[$id] ] = $page['pageimage'];
209			}
210		}
211	}
212
213	/**
214	 * Returns image information for pages with given ids
215	 *
216	 * @param int[] $pageIds
217	 * @param int $size
218	 *
219	 * @return array[]
220	 */
221	private static function getImages( array $pageIds, $size = 0 ) {
222		$ret = [];
223		foreach ( array_chunk( $pageIds, ApiBase::LIMIT_SML1 ) as $chunk ) {
224			$request = [
225				'action' => 'query',
226				'prop' => 'pageimages',
227				'piprop' => 'name',
228				'pageids' => implode( '|', $chunk ),
229				'pilimit' => 'max',
230			];
231
232			if ( $size ) {
233				$request['piprop'] = 'thumbnail';
234				$request['pithumbsize'] = $size;
235			}
236
237			$api = new ApiMain( new FauxRequest( $request ) );
238			$api->execute();
239
240			$ret += (array)$api->getResult()->getResultData(
241				[ 'query', 'pages' ], [ 'Strip' => 'base' ]
242			);
243		}
244		return $ret;
245	}
246
247	/**
248	 * @param OutputPage &$out The page being output.
249	 * @param Skin &$skin Skin object used to generate the page. Ignored
250	 */
251	public static function onBeforePageDisplay( OutputPage &$out, Skin &$skin ) {
252		$imageFile = self::getPageImage( $out->getContext()->getTitle() );
253		if ( !$imageFile ) {
254			return;
255		}
256
257		// See https://developers.facebook.com/docs/sharing/best-practices?locale=en_US#tags
258		$thumb = $imageFile->transform( [ 'width' => 1200 ] );
259		if ( !$thumb ) {
260			return;
261		}
262
263		$out->addMeta( 'og:image', wfExpandUrl( $thumb->getUrl(), PROTO_CANONICAL ) );
264	}
265
266}
267