]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/media/MediaTransformOutput.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / includes / media / MediaTransformOutput.php
1 <?php
2 /**
3  * Base class for the output of file transformation methods.
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  * http://www.gnu.org/copyleft/gpl.html
19  *
20  * @file
21  * @ingroup Media
22  */
23
24 /**
25  * Base class for the output of MediaHandler::doTransform() and File::transform().
26  *
27  * @ingroup Media
28  */
29 abstract class MediaTransformOutput {
30         /** @var array Associative array mapping optional supplementary image files
31          *  from pixel density (eg 1.5 or 2) to additional URLs.
32          */
33         public $responsiveUrls = [];
34
35         /** @var File */
36         protected $file;
37
38         /** @var int Image width */
39         protected $width;
40
41         /** @var int Image height */
42         protected $height;
43
44         /** @var string URL path to the thumb */
45         protected $url;
46
47         /** @var bool|string */
48         protected $page;
49
50         /** @var bool|string Filesystem path to the thumb  */
51         protected $path;
52
53         /** @var bool|string Language code, false if not set */
54         protected $lang;
55
56         /** @var bool|string Permanent storage path  */
57         protected $storagePath = false;
58
59         /**
60          * @return int Width of the output box
61          */
62         public function getWidth() {
63                 return $this->width;
64         }
65
66         /**
67          * @return int Height of the output box
68          */
69         public function getHeight() {
70                 return $this->height;
71         }
72
73         /**
74          * @return File
75          */
76         public function getFile() {
77                 return $this->file;
78         }
79
80         /**
81          * Get the final extension of the thumbnail.
82          * Returns false for scripted transformations.
83          * @return string|bool
84          */
85         public function getExtension() {
86                 return $this->path ? FileBackend::extensionFromPath( $this->path ) : false;
87         }
88
89         /**
90          * @return string|bool The thumbnail URL
91          */
92         public function getUrl() {
93                 return $this->url;
94         }
95
96         /**
97          * @return string|bool The permanent thumbnail storage path
98          */
99         public function getStoragePath() {
100                 return $this->storagePath;
101         }
102
103         /**
104          * @param string $storagePath The permanent storage path
105          * @return void
106          */
107         public function setStoragePath( $storagePath ) {
108                 $this->storagePath = $storagePath;
109                 if ( $this->path === false ) {
110                         $this->path = $storagePath;
111                 }
112         }
113
114         /**
115          * Fetch HTML for this transform output
116          *
117          * @param array $options Associative array of options. Boolean options
118          *     should be indicated with a value of true for true, and false or
119          *     absent for false.
120          *
121          *     alt          Alternate text or caption
122          *     desc-link    Boolean, show a description link
123          *     file-link    Boolean, show a file download link
124          *     custom-url-link    Custom URL to link to
125          *     custom-title-link  Custom Title object to link to
126          *     valign       vertical-align property, if the output is an inline element
127          *     img-class    Class applied to the "<img>" tag, if there is such a tag
128          *
129          * For images, desc-link and file-link are implemented as a click-through. For
130          * sounds and videos, they may be displayed in other ways.
131          *
132          * @return string
133          */
134         abstract public function toHtml( $options = [] );
135
136         /**
137          * This will be overridden to return true in error classes
138          * @return bool
139          */
140         public function isError() {
141                 return false;
142         }
143
144         /**
145          * Check if an output thumbnail file actually exists.
146          *
147          * This will return false if there was an error, the
148          * thumbnail is to be handled client-side only, or if
149          * transformation was deferred via TRANSFORM_LATER.
150          * This file may exist as a new file in /tmp, a file
151          * in permanent storage, or even refer to the original.
152          *
153          * @return bool
154          */
155         public function hasFile() {
156                 // If TRANSFORM_LATER, $this->path will be false.
157                 // Note: a null path means "use the source file".
158                 return ( !$this->isError() && ( $this->path || $this->path === null ) );
159         }
160
161         /**
162          * Check if the output thumbnail is the same as the source.
163          * This can occur if the requested width was bigger than the source.
164          *
165          * @return bool
166          */
167         public function fileIsSource() {
168                 return ( !$this->isError() && $this->path === null );
169         }
170
171         /**
172          * Get the path of a file system copy of the thumbnail.
173          * Callers should never write to this path.
174          *
175          * @return string|bool Returns false if there isn't one
176          */
177         public function getLocalCopyPath() {
178                 if ( $this->isError() ) {
179                         return false;
180                 } elseif ( $this->path === null ) {
181                         return $this->file->getLocalRefPath(); // assume thumb was not scaled
182                 } elseif ( FileBackend::isStoragePath( $this->path ) ) {
183                         $be = $this->file->getRepo()->getBackend();
184                         // The temp file will be process cached by FileBackend
185                         $fsFile = $be->getLocalReference( [ 'src' => $this->path ] );
186
187                         return $fsFile ? $fsFile->getPath() : false;
188                 } else {
189                         return $this->path; // may return false
190                 }
191         }
192
193         /**
194          * Stream the file if there were no errors
195          *
196          * @param array $headers Additional HTTP headers to send on success
197          * @return Status
198          * @since 1.27
199          */
200         public function streamFileWithStatus( $headers = [] ) {
201                 if ( !$this->path ) {
202                         return Status::newFatal( 'backend-fail-stream', '<no path>' );
203                 } elseif ( FileBackend::isStoragePath( $this->path ) ) {
204                         $be = $this->file->getRepo()->getBackend();
205                         return $be->streamFile( [ 'src' => $this->path, 'headers' => $headers ] );
206                 } else { // FS-file
207                         $success = StreamFile::stream( $this->getLocalCopyPath(), $headers );
208                         return $success ? Status::newGood() : Status::newFatal( 'backend-fail-stream', $this->path );
209                 }
210         }
211
212         /**
213          * Stream the file if there were no errors
214          *
215          * @deprecated since 1.26, use streamFileWithStatus
216          * @param array $headers Additional HTTP headers to send on success
217          * @return bool Success
218          */
219         public function streamFile( $headers = [] ) {
220                 $this->streamFileWithStatus( $headers )->isOK();
221         }
222
223         /**
224          * Wrap some XHTML text in an anchor tag with the given attributes
225          *
226          * @param array $linkAttribs
227          * @param string $contents
228          * @return string
229          */
230         protected function linkWrap( $linkAttribs, $contents ) {
231                 if ( $linkAttribs ) {
232                         return Xml::tags( 'a', $linkAttribs, $contents );
233                 } else {
234                         return $contents;
235                 }
236         }
237
238         /**
239          * @param string $title
240          * @param string|array $params Query parameters to add
241          * @return array
242          */
243         public function getDescLinkAttribs( $title = null, $params = [] ) {
244                 if ( is_array( $params ) ) {
245                         $query = $params;
246                 } else {
247                         $query = [];
248                 }
249                 if ( $this->page && $this->page !== 1 ) {
250                         $query['page'] = $this->page;
251                 }
252                 if ( $this->lang ) {
253                         $query['lang'] = $this->lang;
254                 }
255
256                 if ( is_string( $params ) && $params !== '' ) {
257                         $query = $params . '&' . wfArrayToCgi( $query );
258                 }
259
260                 $attribs = [
261                         'href' => $this->file->getTitle()->getLocalURL( $query ),
262                         'class' => 'image',
263                 ];
264                 if ( $title ) {
265                         $attribs['title'] = $title;
266                 }
267
268                 return $attribs;
269         }
270 }
271
272 /**
273  * Media transform output for images
274  *
275  * @ingroup Media
276  */
277 class ThumbnailImage extends MediaTransformOutput {
278         /**
279          * Get a thumbnail object from a file and parameters.
280          * If $path is set to null, the output file is treated as a source copy.
281          * If $path is set to false, no output file will be created.
282          * $parameters should include, as a minimum, (file) 'width' and 'height'.
283          * It may also include a 'page' parameter for multipage files.
284          *
285          * @param File $file
286          * @param string $url URL path to the thumb
287          * @param string|bool $path Filesystem path to the thumb
288          * @param array $parameters Associative array of parameters
289          */
290         function __construct( $file, $url, $path = false, $parameters = [] ) {
291                 # Previous parameters:
292                 #   $file, $url, $width, $height, $path = false, $page = false
293
294                 $defaults = [
295                         'page' => false,
296                         'lang' => false
297                 ];
298
299                 if ( is_array( $parameters ) ) {
300                         $actualParams = $parameters + $defaults;
301                 } else {
302                         # Using old format, should convert. Later a warning could be added here.
303                         $numArgs = func_num_args();
304                         $actualParams = [
305                                 'width' => $path,
306                                 'height' => $parameters,
307                                 'page' => ( $numArgs > 5 ) ? func_get_arg( 5 ) : false
308                         ] + $defaults;
309                         $path = ( $numArgs > 4 ) ? func_get_arg( 4 ) : false;
310                 }
311
312                 $this->file = $file;
313                 $this->url = $url;
314                 $this->path = $path;
315
316                 # These should be integers when they get here.
317                 # If not, there's a bug somewhere.  But let's at
318                 # least produce valid HTML code regardless.
319                 $this->width = round( $actualParams['width'] );
320                 $this->height = round( $actualParams['height'] );
321
322                 $this->page = $actualParams['page'];
323                 $this->lang = $actualParams['lang'];
324         }
325
326         /**
327          * Return HTML <img ... /> tag for the thumbnail, will include
328          * width and height attributes and a blank alt text (as required).
329          *
330          * @param array $options Associative array of options. Boolean options
331          *     should be indicated with a value of true for true, and false or
332          *     absent for false.
333          *
334          *     alt          HTML alt attribute
335          *     title        HTML title attribute
336          *     desc-link    Boolean, show a description link
337          *     file-link    Boolean, show a file download link
338          *     valign       vertical-align property, if the output is an inline element
339          *     img-class    Class applied to the \<img\> tag, if there is such a tag
340          *     desc-query   String, description link query params
341          *     override-width     Override width attribute. Should generally not set
342          *     override-height    Override height attribute. Should generally not set
343          *     no-dimensions      Boolean, skip width and height attributes (useful if
344          *                        set in CSS)
345          *     custom-url-link    Custom URL to link to
346          *     custom-title-link  Custom Title object to link to
347          *     custom target-link Value of the target attribute, for custom-target-link
348          *     parser-extlink-*   Attributes added by parser for external links:
349          *          parser-extlink-rel: add rel="nofollow"
350          *          parser-extlink-target: link target, but overridden by custom-target-link
351          *
352          * For images, desc-link and file-link are implemented as a click-through. For
353          * sounds and videos, they may be displayed in other ways.
354          *
355          * @throws MWException
356          * @return string
357          */
358         function toHtml( $options = [] ) {
359                 if ( count( func_get_args() ) == 2 ) {
360                         throw new MWException( __METHOD__ . ' called in the old style' );
361                 }
362
363                 $alt = isset( $options['alt'] ) ? $options['alt'] : '';
364
365                 $query = isset( $options['desc-query'] ) ? $options['desc-query'] : '';
366
367                 $attribs = [
368                         'alt' => $alt,
369                         'src' => $this->url,
370                 ];
371
372                 if ( !empty( $options['custom-url-link'] ) ) {
373                         $linkAttribs = [ 'href' => $options['custom-url-link'] ];
374                         if ( !empty( $options['title'] ) ) {
375                                 $linkAttribs['title'] = $options['title'];
376                         }
377                         if ( !empty( $options['custom-target-link'] ) ) {
378                                 $linkAttribs['target'] = $options['custom-target-link'];
379                         } elseif ( !empty( $options['parser-extlink-target'] ) ) {
380                                 $linkAttribs['target'] = $options['parser-extlink-target'];
381                         }
382                         if ( !empty( $options['parser-extlink-rel'] ) ) {
383                                 $linkAttribs['rel'] = $options['parser-extlink-rel'];
384                         }
385                 } elseif ( !empty( $options['custom-title-link'] ) ) {
386                         /** @var Title $title */
387                         $title = $options['custom-title-link'];
388                         $linkAttribs = [
389                                 'href' => $title->getLinkURL(),
390                                 'title' => empty( $options['title'] ) ? $title->getFullText() : $options['title']
391                         ];
392                 } elseif ( !empty( $options['desc-link'] ) ) {
393                         $linkAttribs = $this->getDescLinkAttribs(
394                                 empty( $options['title'] ) ? null : $options['title'],
395                                 $query
396                         );
397                 } elseif ( !empty( $options['file-link'] ) ) {
398                         $linkAttribs = [ 'href' => $this->file->getUrl() ];
399                 } else {
400                         $linkAttribs = false;
401                         if ( !empty( $options['title'] ) ) {
402                                 $attribs['title'] = $options['title'];
403                         }
404                 }
405
406                 if ( empty( $options['no-dimensions'] ) ) {
407                         $attribs['width'] = $this->width;
408                         $attribs['height'] = $this->height;
409                 }
410                 if ( !empty( $options['valign'] ) ) {
411                         $attribs['style'] = "vertical-align: {$options['valign']}";
412                 }
413                 if ( !empty( $options['img-class'] ) ) {
414                         $attribs['class'] = $options['img-class'];
415                 }
416                 if ( isset( $options['override-height'] ) ) {
417                         $attribs['height'] = $options['override-height'];
418                 }
419                 if ( isset( $options['override-width'] ) ) {
420                         $attribs['width'] = $options['override-width'];
421                 }
422
423                 // Additional densities for responsive images, if specified.
424                 // If any of these urls is the same as src url, it'll be excluded.
425                 $responsiveUrls = array_diff( $this->responsiveUrls, [ $this->url ] );
426                 if ( !empty( $responsiveUrls ) ) {
427                         $attribs['srcset'] = Html::srcSet( $responsiveUrls );
428                 }
429
430                 Hooks::run( 'ThumbnailBeforeProduceHTML', [ $this, &$attribs, &$linkAttribs ] );
431
432                 return $this->linkWrap( $linkAttribs, Xml::element( 'img', $attribs ) );
433         }
434 }
435
436 /**
437  * Basic media transform error class
438  *
439  * @ingroup Media
440  */
441 class MediaTransformError extends MediaTransformOutput {
442         /** @var Message */
443         private $msg;
444
445         function __construct( $msg, $width, $height /*, ... */ ) {
446                 $args = array_slice( func_get_args(), 3 );
447                 $this->msg = wfMessage( $msg )->params( $args );
448                 $this->width = intval( $width );
449                 $this->height = intval( $height );
450                 $this->url = false;
451                 $this->path = false;
452         }
453
454         function toHtml( $options = [] ) {
455                 return "<div class=\"MediaTransformError\" style=\"" .
456                         "width: {$this->width}px; height: {$this->height}px; display:inline-block;\">" .
457                         $this->getHtmlMsg() .
458                         "</div>";
459         }
460
461         function toText() {
462                 return $this->msg->text();
463         }
464
465         function getHtmlMsg() {
466                 return $this->msg->escaped();
467         }
468
469         function getMsg() {
470                 return $this->msg;
471         }
472
473         function isError() {
474                 return true;
475         }
476
477         function getHttpStatusCode() {
478                 return 500;
479         }
480 }
481
482 /**
483  * Shortcut class for parameter validation errors
484  *
485  * @ingroup Media
486  */
487 class TransformParameterError extends MediaTransformError {
488         function __construct( $params ) {
489                 parent::__construct( 'thumbnail_error',
490                         max( isset( $params['width'] ) ? $params['width'] : 0, 120 ),
491                         max( isset( $params['height'] ) ? $params['height'] : 0, 120 ),
492                         wfMessage( 'thumbnail_invalid_params' )
493                 );
494         }
495
496         function getHttpStatusCode() {
497                 return 400;
498         }
499 }
500
501 /**
502  * Shortcut class for parameter file size errors
503  *
504  * @ingroup Media
505  * @since 1.25
506  */
507 class TransformTooBigImageAreaError extends MediaTransformError {
508         function __construct( $params, $maxImageArea ) {
509                 $msg = wfMessage( 'thumbnail_toobigimagearea' );
510                 $msg->rawParams(
511                         $msg->getLanguage()->formatComputingNumbers( $maxImageArea, 1000, "size-$1pixel" )
512                 );
513
514                 parent::__construct( 'thumbnail_error',
515                         max( isset( $params['width'] ) ? $params['width'] : 0, 120 ),
516                         max( isset( $params['height'] ) ? $params['height'] : 0, 120 ),
517                         $msg
518                 );
519         }
520
521         function getHttpStatusCode() {
522                 return 400;
523         }
524 }