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