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