Completed
Branch master (ecb46d)
by Tobias
01:39
created

ImageModal::assertImageModalNotSuppressed()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 7
cts 7
cp 1
rs 8.8571
c 0
b 0
f 0
cc 6
eloc 7
nc 4
nop 1
crap 6
1
<?php
2
/**
3
 * Contains the class for replacing image normal image display with a modal.
4
 *
5
 * @copyright (C) 2018, Tobias Oetterer, Paderborn University
6
 * @license       https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License, version 3 (or later)
7
 *
8
 * This file is part of the MediaWiki extension BootstrapComponents.
9
 * The BootstrapComponents extension is free software: you can redistribute it
10
 * and/or modify it under the terms of the GNU General Public License as published
11
 * by the Free Software Foundation, either version 3 of the License, or
12
 * (at your option) any later version.
13
 *
14
 * The BootstrapComponents extension 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
20
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
21
 *
22
 * @file
23
 * @ingroup       BootstrapComponents
24
 * @author        Tobias Oetterer
25
 */
26
27
namespace BootstrapComponents;
28
29
use \Linker;
30
use \Html;
31
use \MediaWiki\MediaWikiServices;
32
use \RequestContext;
33
use \Title;
34
35
/**
36
 * Class ImageModal
37
 *
38
 * @since 1.0
39
 */
40
class ImageModal implements NestableInterface {
41
42
	const CSS_CLASS_PREVENTING_MODAL = 'no-modal';
43
44
	/**
45
	 * The components listed here prevent the generation of an image modal.
46
	 * @var array
47
	 */
48
	const PARENTS_PREVENTING_MODAL = [ 'button', 'collapse ', 'image_modal', 'modal', 'popover', 'tooltip' ];
49
50
	/**
51
	 * @var \DummyLinker $dummyLinker
52
	 */
53
	private $dummyLinker;
54
55
	/**
56
	 * @var \File $file
57
	 */
58
	private $file;
59
60
	/**
61
	 * @var string $id
62
	 */
63
	private $id;
64
65
	/**
66
	 * @var NestingController $nestingController
67
	 */
68
	private $nestingController;
69
70
	/**
71
	 * @var NestableInterface|false $parentComponent
72
	 */
73
	private $parentComponent;
74
75
	/**
76
	 * @var ParserOutputHelper $parserOutputHelper
77
	 */
78
	private $parserOutputHelper;
79
80
	/**
81
	 * @var bool $disableSourceLink
82
	 */
83
	private $disableSourceLink;
84
85
	/**
86
	 * @var Title $title
87
	 */
88
	private $title;
89
90
	/**
91
	 * ImageModal constructor.
92
	 *
93
	 * @param \DummyLinker       $dummyLinker
94
	 * @param \Title             $title
95
	 * @param \File              $file
96
	 * @param NestingController  $nestingController
97
	 * @param ParserOutputHelper $parserOutputHelper DI for unit testing
98
	 *
99
	 * @throws \MWException cascading {@see \BootstrapComponents\ApplicationFactory} methods
100
	 */
101 23
	public function __construct( $dummyLinker, $title, $file, $nestingController = null, $parserOutputHelper = null ) {
102 23
		$this->file = $file;
103 23
		$this->dummyLinker = $dummyLinker;
104 23
		$this->title = $title;
105
106 23
		$this->nestingController = is_null( $nestingController )
107 23
			? ApplicationFactory::getInstance()->getNestingController()
108 1
			: $nestingController;
109 23
		$this->parserOutputHelper = is_null( $parserOutputHelper )
110 23
			? ApplicationFactory::getInstance()->getParserOutputHelper()
111 11
			: $parserOutputHelper ;
112
113 23
		$this->parentComponent = $this->getNestingController()->getCurrentElement();
114 23
		$this->id = $this->getNestingController()->generateUniqueId(
115 23
			$this->getComponentName()
116 23
		);
117 23
		$this->disableSourceLink = false;
118 23
	}
119
120
	/**
121
	 * @inheritdoc
122
	 */
123 23
	public function getComponentName() {
124 23
		return "image_modal";
125
	}
126
127
	/**
128
	 * @inheritdoc
129
	 */
130 15
	public function getId() {
131 15
		return $this->id;
132
	}
133
134
	/**
135
	 * @param array       $frameParams   Associative array of parameters external to the media handler.
136
	 *                                   Boolean parameters are indicated by presence or absence, the value is arbitrary and
137
	 *                                   will often be false.
138
	 *                                   thumbnail       If present, downscale and frame
139
	 *                                   manualthumb     Image name to use as a thumbnail, instead of automatic scaling
140
	 *                                   framed          Shows image in original size in a frame
141
	 *                                   frameless       Downscale but don't frame
142
	 *                                   upright         If present, tweak default sizes for portrait orientation
143
	 *                                   upright_factor  Fudge factor for "upright" tweak (default 0.75)
144
	 *                                   border          If present, show a border around the image
145
	 *                                   align           Horizontal alignment (left, right, center, none)
146
	 *                                   valign          Vertical alignment (baseline, sub, super, top, text-top, middle,
147
	 *                                                   bottom, text-bottom)
148
	 *                                   alt             Alternate text for image (i.e. alt attribute). Plain text.
149
	 *                                   class           HTML for image classes. Plain text.
150
	 *                                   caption         HTML for image caption.
151
	 *                                   link-url        URL to link to
152
	 *                                   link-title      Title object to link to
153
	 *                                   link-target     Value for the target attribute, only with link-url
154
	 *                                   no-link         Boolean, suppress description link
155
	 * @param array       $handlerParams Associative array of media handler parameters, to be passed
156
	 *                                   to transform(). Typical keys are "width" and "page".
157
	 * @param string|bool $time          Timestamp of the file, set as false for current
158
	 * @param string      $res           Final HTML output, used if this returns false
159
	 *
160
	 * @throws \MWException     cascading {@see \BootstrapComponents\NestingController::open}
161
	 * @throws \ConfigException cascading {@see \BootstrapComponents\ImageModal::generateTrigger}
162
	 *
163
	 * @return bool
164
	 */
165 22
	public function parse( &$frameParams, &$handlerParams, &$time, &$res ) {
0 ignored issues
show
Unused Code introduced by
The parameter $time is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
166 22
		if ( !$this->assertResponsibility( $this->getFile(), $frameParams ) ) {
167 10
			wfDebugLog( 'BootstrapComponents', 'Image modal relegating image rendering back to Linker.php.' );
168 10
			return true;
169
		}
170
171
		// it's on us, let's do some modal-ing
172 15
		$this->augmentParserOutput();
173 15
		$this->getNestingController()->open( $this );
174
175 15
		$sanitizedFrameParams = $this->sanitizeFrameParams( $frameParams );
176 15
		$handlerParams['page'] = isset( $handlerParams['page'] ) ? $handlerParams['page'] : false;
177
178 15
		$res = $this->turnParamsIntoModal( $sanitizedFrameParams, $handlerParams );
179
180 15
		$this->getNestingController()->close(
181 15
			$this->getId()
182 15
		);
183
184 15
		if ( $res === '' ) {
185
			// ImageModal::turnParamsIntoModal returns the empty string, when something went wrong
186 3
			return true;
187
		}
188 12
		return false;
189
	}
190
191
	/**
192
	 * After this, all bool params ( 'thumbnail', 'framed', 'frameless', 'border' ) are true, if they were present before, false otherwise and all
193
	 * string params are set (to the original value or the empty string).
194
	 *
195
	 * This method is public, because it is used in {@see \BootstrapComponents\Tests\ImageModalTest::doTestCompareTriggerWithOriginalThumb}
196
	 *
197
	 * @param array $frameParams
198
	 *
199
	 * @return array
200
	 */
201 15
	public function sanitizeFrameParams( $frameParams ) {
202 15
		foreach ( [ 'thumbnail', 'framed', 'frameless', 'border' ] as $boolField ) {
203 15
			$frameParams[$boolField] = isset( $frameParams[$boolField] );
204 15
		}
205 15
		foreach ( [ 'align', 'alt', 'caption', 'class', 'title', 'valign' ] as $stringField ) {
206 15
			$frameParams[$stringField] = !empty( $frameParams[$stringField] ) ? $frameParams[$stringField] : false;
207 15
		}
208 15
		$frameParams['caption'] = $this->preventModalInception( $frameParams['caption'] );
209 15
		$frameParams['title'] = $this->preventModalInception( $frameParams['title'] );
210 15
		return $frameParams;
211
	}
212
213
	/**
214
	 * Disables the source link in modal content.
215
	 */
216
	public function disableSourceLink() {
217
		$this->disableSourceLink = true;
218
	}
219
220
	/**
221
	 * Runs various tests, to see, if we delegate processing back to {@see \Linker::makeImageLink}
222
	 * After this, we can assume:
223
	 * * file is a {@see \File} and exists
224
	 * * there is no link param set (link-url, link-title, link-target, no-link)
225
	 * * file allows inline display (ref {@see \File::allowInlineDisplay})
226
	 * * we are not inside an image modal or an otherwise compromising component  (thanks to {@see ImageModal::getNestingController})
227
	 * * no magic word suppressing image modals is on the page
228
	 * * image does not have the "no-modal" class {@see ImageModal::CSS_CLASS_PREVENTING_MODAL}
229
	 *
230
	 * @param \File $file
231
	 * @param array $frameParams
232
	 *
233
	 * @return bool true, if all assertions hold, false if one fails (see above)
234
	 */
235 22
	protected function assertResponsibility( $file, $frameParams ) {
236 22
		if ( !$this->assertImageTagValid( $file, $frameParams ) ) {
237 9
			return false;
238
		}
239 20
		return $this->assertImageModalNotSuppressed( $frameParams );
240
	}
241
242
	/**
243
	 * @param \File $file
244
	 * @param array $sanitizedFrameParams
245
	 * @param array $handlerParams
246
	 *
247
	 * @return array bool|string bool (large image yes or no)
248
	 */
249 13
	protected function generateContent( $file, $sanitizedFrameParams, $handlerParams ) {
250
251
		/** @var \MediaTransformOutput $img $img */
252 13
		$img = $file->getUnscaledThumb(
253 13
			[ 'page' => $handlerParams['page'] ]
254 13
		);
255 13
		if ( !$img ) {
256 1
			return [ false, false ];
257
		}
258
		return [
259 12
			$this->buildContentImageString( $img, $sanitizedFrameParams ),
260 12
			$img->getWidth() > 600
261 12
		];
262
	}
263
264
	/**
265
	 * @return \DummyLinker
266
	 */
267
	/** @scrutinizer ignore-unused */
268
	protected function getDummyLinker() {
269
		return $this->dummyLinker;
270
	}
271
272
	/**
273
	 * @return \File
274
	 */
275 22
	protected function getFile() {
276 22
		return $this->file;
277
	}
278
279
	/**
280
	 * @return NestingController
281
	 */
282 23
	protected function getNestingController() {
283 23
		return $this->nestingController;
284
	}
285
286
	/**
287
	 * @return null|NestableInterface
288
	 */
289 20
	protected function getParentComponent() {
290 20
		return $this->parentComponent;
291
	}
292
293
	/**
294
	 * @return ParserOutputHelper
295
	 */
296 15
	protected function getParserOutputHelper() {
297 15
		return $this->parserOutputHelper;
298
	}
299
300
	/**
301
	 * @return Title
302
	 */
303 12
	protected function getTitle() {
304 12
		return $this->title;
305
	}
306
307
	/**
308
	 * @param $sanitizedFrameParams
309
	 * @param $handlerParams
310
	 *
311
	 * @throws \ConfigException
312
	 *
313
	 * @return string   rendered modal on success, empty string on failure.
314
	 */
315 15
	protected function turnParamsIntoModal( $sanitizedFrameParams, $handlerParams ) {
316 15
		$trigger = new ImageModalTrigger(
317 15
			$this->getId(),
318 15
			$this->getFile()
319 15
		);
320
321 15
		$triggerString = $trigger->generate( $sanitizedFrameParams, $handlerParams );
322
323 15
		if ( $triggerString === false ) {
324
			// something wrong with the trigger. Relegating back
325 2
			return '';
326
		}
327
328 13
		list ( $content, $largeDialog ) = $this->generateContent(
329 13
			$this->getFile(),
330 13
			$sanitizedFrameParams,
331
			$handlerParams
332 13
		);
333
334 13
		if ( $content === false ) {
335
			// could not create content image. Relegating back
336 1
			return '';
337
		}
338
339 12
		$modal = ApplicationFactory::getInstance()->getModalBuilder(
340 12
			$this->getId(),
341 12
			$triggerString,
342 12
			$content,
343 12
			$this->getParserOutputHelper()
344 12
		);
345 12
		$modal->setHeader(
346 12
			$this->getTitle()->getBaseText()
347 12
		);
348
349 12
		if ( !$this->disableSourceLink ) {
350 12
			$modal->setFooter(
351 12
				$this->generateButtonToSource(
352 12
					$this->getTitle(),
353
					$handlerParams
354 12
				)
355 12
			);
356 12
		};
357
358 12
		if ( $largeDialog ) {
359 8
			$modal->setDialogClass( 'modal-lg' );
360 8
		}
361
362 12
		return $modal->parse();
363
	}
364
365
	/**
366
	 * @param \File $file
367
	 * @param array $frameParams
368
	 *
369
	 * @return bool
370
	 */
371 22
	private function assertImageTagValid( $file, $frameParams ) {
372 22
		if ( !$file || !$file->exists() ) {
373 8
			return false;
374
		}
375 21
		if ( isset( $frameParams['link-url'] ) || isset( $frameParams['link-title'] )
376 21
			|| isset( $frameParams['link-target'] ) || isset( $frameParams['no-link'] )
377 21
		) {
378 2
			return false;
379
		}
380 21
		if ( !$file->allowInlineDisplay() ) {
381
			// let Linker.php handle these cases as well
382 1
			return false;
383
		}
384 20
		return true;
385
	}
386
387
	/**
388
	 * @param array $frameParams
389
	 *
390
	 * @return bool
391
	 */
392 20
	private function assertImageModalNotSuppressed( $frameParams ) {
393 20
		if ( $this->getParentComponent() && in_array( $this->getParentComponent()->getComponentName(), self::PARENTS_PREVENTING_MODAL ) ) {
394 5
			return false;
395
		}
396 16
		if ( isset( $frameParams['class'] ) && in_array( self::CSS_CLASS_PREVENTING_MODAL, explode( ' ', $frameParams['class'] ) ) ) {
397 1
			return false;
398
		}
399
		/** @see ParserOutputHelper::areImageModalsSuppressed as to why we need to use the global parser1 */
400 16
		$parser = $GLOBALS['wgParser'];
401
		// the is_null test has to be added because otherwise some unit tests will fail
402 16
		return is_null( $parser->getOutput() ) || !$parser->getOutput()->getExtensionData( 'bsc_no_image_modal' );
403
	}
404
405
	/**
406
	 * Performs all the mandatory actions on the parser output for the component class
407
	 *
408
	 * @throws \MWException cascading {@see \BootstrapComponents\ApplicationFactory::getComponentLibrary}
409
	 */
410 15
	private function augmentParserOutput() {
411 15
		$skin = $this->getParserOutputHelper()->getNameOfActiveSkin();
412 15
		$this->getParserOutputHelper()->loadBootstrapModules();
413 15
		$this->getParserOutputHelper()->addModules(
414 15
			ApplicationFactory::getInstance()->getComponentLibrary()->getModulesFor( 'modal', $skin )
415 15
		);
416 15
	}
417
418
	/**
419
	 * @param \MediaTransformOutput $img
420
	 * @param array                 $sanitizedFrameParams
421
	 *
422
	 * @return string
423
	 */
424 12
	private function buildContentImageString( $img, $sanitizedFrameParams ) {
425
		$imgParams = [
426 12
			'alt'       => $sanitizedFrameParams['alt'],
427 12
			'title'     => $sanitizedFrameParams['title'],
428 12
			'img-class' => trim( $sanitizedFrameParams['class'] . ' img-responsive' ),
429 12
		];
430 12
		$imgString = $img->toHtml( $imgParams );
431 12
		if ( $sanitizedFrameParams['caption'] ) {
432 3
			$imgString .= ' ' . Html::rawElement(
433 3
					'div',
434 3
					[ 'class' => 'modal-caption' ],
435 3
					$this->sanitizeCaption( $sanitizedFrameParams['caption'] )
436 3
				);
437 3
		}
438 12
		return $imgString;
439
	}
440
441
	/**
442
	 * @param Title $title
443
	 * @param array $handlerParams
444
	 *
445
	 * @return string
446
	 */
447 12
	private function generateButtonToSource( $title, $handlerParams ) {
448 12
		$url = $title->getLocalURL();
449 12
		if ( isset( $handlerParams['page'] ) ) {
450 12
			$url = wfAppendQuery( $url, [ 'page' => $handlerParams['page'] ] );
451 12
		}
452 12
		return Html::rawElement(
453 12
			'a',
454
			[
455 12
				'class' => 'btn btn-primary',
456 12
				'role'  => 'button',
457 12
				'href'  => $url,
458 12
			],
459 12
			wfMessage( 'bootstrap-components-image-modal-source-button' )->inContentLanguage()->text()
460 12
		);
461
	}
462
463
	/**
464
	 * We don't want a modal inside a modal. Unfortunately, the caption (and title) are parsed, before the modal is generated. So instead of
465
	 * building the modal from the outside, it is build from the inside. This method tries to detect this construct and removes any modal from
466
	 * the supplied text and replaces it with the image tag found inside the modal caption content.
467
	 *
468
	 * @param string $text
469
	 *
470
	 * @return string
471
	 */
472 15
	private function preventModalInception( $text ) {
473 15
		if ( preg_match(
474
			'~div class="modal-dialog.+div class="modal-content.+div class="modal-body.+'
475 15
			. '(<img[^>]*/>).+ class="modal-footer.+~Ds', $text, $matches ) ) {
476
			$text = $matches[1];
477
		}
478 15
		return $text;
479
	}
480
481
	/**
482
	 * @param string $caption
483
	 *
484
	 * @return string
485
	 */
486 3
	private function sanitizeCaption( $caption ) {
487 3
		return preg_replace( '/([^\n])\n([^\n])/m', '\1\2', $caption );
488
	}
489
}