1<?php 2 3use MediaWiki\Linker\LinkRenderer; 4use MediaWiki\MediaWikiServices; 5 6/** 7 * Image gallery. 8 * 9 * This program is free software; you can redistribute it and/or modify 10 * it under the terms of the GNU General Public License as published by 11 * the Free Software Foundation; either version 2 of the License, or 12 * (at your option) any later version. 13 * 14 * This program is distributed in the hope that it will be useful, 15 * but WITHOUT ANY WARRANTY; without even the implied warranty of 16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 * GNU General Public License for more details. 18 * 19 * You should have received a copy of the GNU General Public License along 20 * with this program; if not, write to the Free Software Foundation, Inc., 21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 22 * http://www.gnu.org/copyleft/gpl.html 23 * 24 * @file 25 */ 26 27class TraditionalImageGallery extends ImageGalleryBase { 28 /** 29 * Return a HTML representation of the image gallery 30 * 31 * For each image in the gallery, display 32 * - a thumbnail 33 * - the image name 34 * - the additional text provided when adding the image 35 * - the size of the image 36 * 37 * @return string 38 */ 39 public function toHTML() { 40 $resolveFilesViaParser = $this->mParser instanceof Parser; 41 if ( $resolveFilesViaParser ) { 42 $out = $this->mParser->getOutput(); 43 $repoGroup = null; 44 $linkRenderer = $this->mParser->getLinkRenderer(); 45 $badFileLookup = $this->mParser->getBadFileLookup(); 46 } else { 47 $out = $this->getOutput(); 48 $services = MediaWikiServices::getInstance(); 49 $repoGroup = $services->getRepoGroup(); 50 $linkRenderer = $services->getLinkRenderer(); 51 $badFileLookup = $services->getBadFileLookup(); 52 } 53 54 if ( $this->mPerRow > 0 ) { 55 $maxwidth = $this->mPerRow * ( $this->mWidths + $this->getAllPadding() ); 56 $oldStyle = $this->mAttribs['style'] ?? ''; 57 $this->mAttribs['style'] = "max-width: {$maxwidth}px;" . $oldStyle; 58 } 59 60 $attribs = Sanitizer::mergeAttributes( 61 [ 'class' => 'gallery mw-gallery-' . $this->mMode ], $this->mAttribs ); 62 63 $out->addModules( $this->getModules() ); 64 $out->addModuleStyles( 'mediawiki.page.gallery.styles' ); 65 $output = Xml::openElement( 'ul', $attribs ); 66 if ( $this->mCaption ) { 67 $output .= "\n\t<li class='gallerycaption'>{$this->mCaption}</li>"; 68 } 69 70 if ( $this->mShowFilename ) { 71 // Preload LinkCache info for when generating links 72 // of the filename below 73 $linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory(); 74 $lb = $linkBatchFactory->newLinkBatch(); 75 foreach ( $this->mImages as [ $title, /* see below */ ] ) { 76 $lb->addObj( $title ); 77 } 78 $lb->execute(); 79 } 80 81 $lang = $this->getRenderLang(); 82 # Output each image... 83 foreach ( $this->mImages as [ $nt, $text, $alt, $link, $handlerOpts, $loading ] ) { 84 // "text" means "caption" here 85 /** @var Title $nt */ 86 87 $descQuery = false; 88 if ( $nt->getNamespace() === NS_FILE ) { 89 # Get the file... 90 if ( $resolveFilesViaParser ) { 91 # Give extensions a chance to select the file revision for us 92 $options = []; 93 Hooks::runner()->onBeforeParserFetchFileAndTitle( 94 $this->mParser, $nt, $options, $descQuery ); 95 # Fetch and register the file (file title may be different via hooks) 96 list( $img, $nt ) = $this->mParser->fetchFileAndTitle( $nt, $options ); 97 } else { 98 $img = $repoGroup->findFile( $nt ); 99 } 100 } else { 101 $img = false; 102 } 103 104 $params = $this->getThumbParams( $img ); 105 $transformOptions = $params + $handlerOpts; 106 107 $thumb = false; 108 109 if ( !$img ) { 110 # We're dealing with a non-image, spit out the name and be done with it. 111 $thumbhtml = "\n\t\t\t" . '<div class="thumb" style="height: ' 112 . ( $this->getThumbPadding() + $this->mHeights ) . 'px;">' 113 . htmlspecialchars( $nt->getText() ) . '</div>'; 114 115 if ( $resolveFilesViaParser ) { 116 $this->mParser->addTrackingCategory( 'broken-file-category' ); 117 } 118 } elseif ( $this->mHideBadImages && 119 $badFileLookup->isBadFile( $nt->getDBkey(), $this->getContextTitle() ) 120 ) { 121 # The image is blacklisted, just show it as a text link. 122 $thumbhtml = "\n\t\t\t" . '<div class="thumb" style="height: ' . 123 ( $this->getThumbPadding() + $this->mHeights ) . 'px;">' . 124 $linkRenderer->makeKnownLink( $nt, $nt->getText() ) . 125 '</div>'; 126 } else { 127 $thumb = $img->transform( $transformOptions ); 128 if ( !$thumb ) { 129 # Error generating thumbnail. 130 $thumbhtml = "\n\t\t\t" . '<div class="thumb" style="height: ' 131 . ( $this->getThumbPadding() + $this->mHeights ) . 'px;">' 132 . htmlspecialchars( $img->getLastError() ) . '</div>'; 133 } else { 134 /** @var MediaTransformOutput $thumb */ 135 $vpad = $this->getVPad( $this->mHeights, $thumb->getHeight() ); 136 137 $imageParameters = [ 138 'desc-link' => true, 139 'desc-query' => $descQuery, 140 'alt' => $alt, 141 'custom-url-link' => $link 142 ]; 143 144 // In the absence of both alt text and caption, fall back on 145 // providing screen readers with the filename as alt text 146 if ( $alt == '' && $text == '' ) { 147 $imageParameters['alt'] = $nt->getText(); 148 } 149 150 if ( $loading === ImageGalleryBase::LOADING_LAZY ) { 151 $imageParameters['loading'] = 'lazy'; 152 } 153 154 $this->adjustImageParameters( $thumb, $imageParameters ); 155 156 Linker::processResponsiveImages( $img, $thumb, $transformOptions ); 157 158 # Set both fixed width and min-height. 159 $thumbhtml = "\n\t\t\t" 160 . '<div class="thumb" style="width: ' 161 . $this->getThumbDivWidth( $thumb->getWidth() ) . 'px;">' 162 # Auto-margin centering for block-level elements. Needed 163 # now that we have video handlers since they may emit block- 164 # level elements as opposed to simple <img> tags. ref 165 # http://css-discuss.incutio.com/?page=CenteringBlockElement 166 . '<div style="margin:' . $vpad . 'px auto;">' 167 . $thumb->toHtml( $imageParameters ) . '</div></div>'; 168 169 // Call parser transform hook 170 /** @var MediaHandler $handler */ 171 $handler = $img->getHandler(); 172 if ( $resolveFilesViaParser && $handler ) { 173 $handler->parserTransformHook( $this->mParser, $img ); 174 } 175 } 176 } 177 178 $meta = []; 179 if ( $img ) { 180 if ( $this->mShowDimensions ) { 181 $meta[] = $img->getDimensionsString(); 182 } 183 if ( $this->mShowBytes ) { 184 $meta[] = htmlspecialchars( $lang->formatSize( $img->getSize() ) ); 185 } 186 } elseif ( $this->mShowDimensions || $this->mShowBytes ) { 187 $meta[] = $this->msg( 'filemissing' )->escaped(); 188 } 189 $meta = $lang->semicolonList( $meta ); 190 if ( $meta ) { 191 $meta .= "<br />\n"; 192 } 193 194 $textlink = $this->mShowFilename ? 195 $this->getCaptionHtml( $nt, $lang, $linkRenderer ) : 196 ''; 197 198 $galleryText = $textlink . $text . $meta; 199 $galleryText = $this->wrapGalleryText( $galleryText, $thumb ); 200 201 $gbWidth = $this->getGBWidth( $thumb ) . 'px'; 202 if ( $this->getGBWidthOverwrite( $thumb ) ) { 203 $gbWidth = $this->getGBWidthOverwrite( $thumb ); 204 } 205 # Weird double wrapping (the extra div inside the li) needed due to FF2 bug 206 # Can be safely removed if FF2 falls completely out of existence 207 $output .= "\n\t\t" . '<li class="gallerybox" style="width: ' 208 . $gbWidth . '">' 209 . '<div style="width: ' . $gbWidth . '">' 210 . $thumbhtml 211 . $galleryText 212 . "\n\t\t</div></li>"; 213 } 214 $output .= "\n</ul>"; 215 216 return $output; 217 } 218 219 /** 220 * @param Title $nt 221 * @param Language $lang 222 * @param LinkRenderer $linkRenderer 223 * @return string HTML 224 */ 225 protected function getCaptionHtml( Title $nt, Language $lang, LinkRenderer $linkRenderer ) { 226 // Preloaded into LinkCache in toHTML 227 return $linkRenderer->makeKnownLink( 228 $nt, 229 is_int( $this->getCaptionLength() ) ? 230 $lang->truncateForVisual( $nt->getText(), $this->getCaptionLength() ) : 231 $nt->getText(), 232 [ 233 'class' => 'galleryfilename' . 234 ( $this->getCaptionLength() === true ? ' galleryfilename-truncate' : '' ) 235 ] 236 ) . "\n"; 237 } 238 239 /** 240 * Add the wrapper html around the thumb's caption 241 * 242 * @param string $galleryText The caption 243 * @param MediaTransformOutput|bool $thumb The thumb this caption is for 244 * or false for bad image. 245 * @return string 246 */ 247 protected function wrapGalleryText( $galleryText, $thumb ) { 248 # ATTENTION: The newline after <div class="gallerytext"> is needed to 249 # accommodate htmltidy which in version 4.8.6 generated crackpot html in 250 # its absence, see: https://phabricator.wikimedia.org/T3765 251 # -Ævar 252 253 return "\n\t\t\t" . '<div class="gallerytext">' . "\n" 254 . $galleryText 255 . "\n\t\t\t</div>"; 256 } 257 258 /** 259 * How much padding the thumb has between the image and the inner div 260 * that contains the border. This is for both vertical and horizontal 261 * padding. (However, it is cut in half in the vertical direction). 262 * @return int 263 */ 264 protected function getThumbPadding() { 265 return 30; 266 } 267 268 /** 269 * @note GB stands for gallerybox (as in the <li class="gallerybox"> element) 270 * 271 * @return int 272 */ 273 protected function getGBPadding() { 274 return 5; 275 } 276 277 /** 278 * Get how much extra space the borders around the image takes up. 279 * 280 * For this mode, it is 2px borders on each side + 2px implied padding on 281 * each side from the stylesheet, giving us 2*2+2*2 = 8. 282 * @return int 283 */ 284 protected function getGBBorders() { 285 return 8; 286 } 287 288 /** 289 * Length (in characters) to truncate filename to in caption when using "showfilename" (if int). 290 * A value of 'true' will truncate the filename to one line using CSS, while 291 * 'false' will disable truncating. 292 * 293 * @return int|bool 294 */ 295 protected function getCaptionLength() { 296 return $this->mCaptionLength; 297 } 298 299 /** 300 * Get total padding. 301 * 302 * @return int Number of pixels of whitespace surrounding the thumbnail. 303 */ 304 protected function getAllPadding() { 305 return $this->getThumbPadding() + $this->getGBPadding() + $this->getGBBorders(); 306 } 307 308 /** 309 * Get vertical padding for a thumbnail 310 * 311 * Generally this is the total height minus how high the thumb is. 312 * 313 * @param int $boxHeight How high we want the box to be. 314 * @param int $thumbHeight How high the thumbnail is. 315 * @return int Vertical padding to add on each side. 316 */ 317 protected function getVPad( $boxHeight, $thumbHeight ) { 318 return ( $this->getThumbPadding() + $boxHeight - $thumbHeight ) / 2; 319 } 320 321 /** 322 * Get the transform parameters for a thumbnail. 323 * 324 * @param File $img The file in question. May be false for invalid image 325 * @return array 326 */ 327 protected function getThumbParams( $img ) { 328 return [ 329 'width' => $this->mWidths, 330 'height' => $this->mHeights 331 ]; 332 } 333 334 /** 335 * Get the width of the inner div that contains the thumbnail in 336 * question. This is the div with the class of "thumb". 337 * 338 * @param int $thumbWidth The width of the thumbnail. 339 * @return int Width of inner thumb div. 340 */ 341 protected function getThumbDivWidth( $thumbWidth ) { 342 return $this->mWidths + $this->getThumbPadding(); 343 } 344 345 /** 346 * Computed width of gallerybox <li>. 347 * 348 * Generally is the width of the image, plus padding on image 349 * plus padding on gallerybox. 350 * 351 * @note Important: parameter will be false if no thumb used. 352 * @param MediaTransformOutput|bool $thumb MediaTransformObject object or false. 353 * @return int Width of gallerybox element 354 */ 355 protected function getGBWidth( $thumb ) { 356 return $this->mWidths + $this->getThumbPadding() + $this->getGBPadding(); 357 } 358 359 /** 360 * Allows overwriting the computed width of the gallerybox <li> with a string, 361 * like '100%'. 362 * 363 * Generally is the width of the image, plus padding on image 364 * plus padding on gallerybox. 365 * 366 * @note Important: parameter will be false if no thumb used. 367 * @param MediaTransformOutput|bool $thumb MediaTransformObject object or false. 368 * @return bool|string Ignored if false. 369 */ 370 protected function getGBWidthOverwrite( $thumb ) { 371 return false; 372 } 373 374 /** 375 * Get a list of modules to include in the page. 376 * 377 * Primarily intended for subclasses. 378 * 379 * @return array Modules to include 380 */ 381 protected function getModules() { 382 return []; 383 } 384 385 /** 386 * Adjust the image parameters for a thumbnail. 387 * 388 * Used by a subclass to insert extra high resolution images. 389 * @param MediaTransformOutput $thumb The thumbnail 390 * @param array &$imageParameters Array of options 391 */ 392 protected function adjustImageParameters( $thumb, &$imageParameters ) { 393 } 394} 395