]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - extensions/PdfHandler/PdfHandler_body.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / extensions / PdfHandler / PdfHandler_body.php
1 <?php
2 /**
3  * Copyright © 2007 Martin Seidel (Xarax) <jodeldi@gmx.de>
4  *
5  * Inspired by djvuhandler from Tim Starling
6  * Modified and written by Xarax
7  *
8  * This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation; either version 2 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License along
19  * with this program; if not, write to the Free Software Foundation, Inc.,
20  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21  * http://www.gnu.org/copyleft/gpl.html
22  */
23
24 class PdfHandler extends ImageHandler {
25         public static $messages = [
26                 'main' => 'pdf-file-page-warning',
27                 'header' => 'pdf-file-page-warning-header',
28                 'info' => 'pdf-file-page-warning-info',
29                 'footer' => 'pdf-file-page-warning-footer',
30         ];
31
32         /**
33          * @return bool
34          */
35         function isEnabled() {
36                 global $wgPdfProcessor, $wgPdfPostProcessor, $wgPdfInfo;
37
38                 if ( !isset( $wgPdfProcessor ) || !isset( $wgPdfPostProcessor ) || !isset( $wgPdfInfo ) ) {
39                         wfDebug( "PdfHandler is disabled, please set the following\n" );
40                         wfDebug( "variables in LocalSettings.php:\n" );
41                         wfDebug( "\$wgPdfProcessor, \$wgPdfPostProcessor, \$wgPdfInfo\n" );
42                         return false;
43                 }
44                 return true;
45         }
46
47         /**
48          * @param $file
49          * @return bool
50          */
51         function mustRender( $file ) {
52                 return true;
53         }
54
55         /**
56          * @param $file
57          * @return bool
58          */
59         function isMultiPage( $file ) {
60                 return true;
61         }
62
63         /**
64          * @param $name
65          * @param $value
66          * @return bool
67          */
68         function validateParam( $name, $value ) {
69                 if ( $name === 'page' && trim( $value ) !== (string)intval( $value ) ) {
70                         // Extra junk on the end of page, probably actually a caption
71                         // e.g. [[File:Foo.pdf|thumb|Page 3 of the document shows foo]]
72                         return false;
73                 }
74                 if ( in_array( $name, [ 'width', 'height', 'page' ] ) ) {
75                         return ( $value > 0 );
76                 }
77                 return false;
78         }
79
80         /**
81          * @param $params array
82          * @return bool|string
83          */
84         function makeParamString( $params ) {
85                 $page = isset( $params['page'] ) ? $params['page'] : 1;
86                 if ( !isset( $params['width'] ) ) {
87                         return false;
88                 }
89                 return "page{$page}-{$params['width']}px";
90         }
91
92         /**
93          * @param $str string
94          * @return array|bool
95          */
96         function parseParamString( $str ) {
97                 $m = false;
98
99                 if ( preg_match( '/^page(\d+)-(\d+)px$/', $str, $m ) ) {
100                         return [ 'width' => $m[2], 'page' => $m[1] ];
101                 }
102
103                 return false;
104         }
105
106         /**
107          * @param $params array
108          * @return array
109          */
110         function getScriptParams( $params ) {
111                 return [
112                         'width' => $params['width'],
113                         'page' => $params['page'],
114                 ];
115         }
116
117         /**
118          * @return array
119          */
120         function getParamMap() {
121                 return [
122                         'img_width' => 'width',
123                         'img_page' => 'page',
124                 ];
125         }
126
127         /**
128          * @param $width
129          * @param $height
130          * @param $msg
131          * @return MediaTransformError
132          */
133         protected function doThumbError( $width, $height, $msg ) {
134                 return new MediaTransformError( 'thumbnail_error',
135                         $width, $height, wfMessage( $msg )->inContentLanguage()->text() );
136         }
137
138         /**
139          * @param $image File
140          * @param $dstPath string
141          * @param $dstUrl string
142          * @param $params array
143          * @param $flags int
144          * @return MediaTransformError|MediaTransformOutput|ThumbnailImage|TransformParameterError
145          */
146         function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
147                 global $wgPdfProcessor, $wgPdfPostProcessor, $wgPdfHandlerDpi, $wgPdfHandlerJpegQuality;
148
149                 if ( !$this->normaliseParams( $image, $params ) ) {
150                         return new TransformParameterError( $params );
151                 }
152
153                 $width = (int)$params['width'];
154                 $height = (int)$params['height'];
155                 $page = (int)$params['page'];
156
157                 if ( $page > $this->pageCount( $image ) ) {
158                         return $this->doThumbError( $width, $height, 'pdf_page_error' );
159                 }
160
161                 if ( $flags & self::TRANSFORM_LATER ) {
162                         return new ThumbnailImage( $image, $dstUrl, $width, $height, false, $page );
163                 }
164
165                 if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
166                         return $this->doThumbError( $width, $height, 'thumbnail_dest_directory' );
167                 }
168
169                 // Thumbnail extraction is very inefficient for large files.
170                 // Provide a way to pool count limit the number of downloaders.
171                 if ( $image->getSize() >= 1e7 ) { // 10MB
172                         $work = new PoolCounterWorkViaCallback( 'GetLocalFileCopy', sha1( $image->getName() ),
173                                 [
174                                         'doWork' => function () use ( $image ) {
175                                                 return $image->getLocalRefPath();
176                                         }
177                                 ]
178                         );
179                         $srcPath = $work->execute();
180                 } else {
181                         $srcPath = $image->getLocalRefPath();
182                 }
183
184                 if ( $srcPath === false ) { // could not download original
185                         return $this->doThumbError( $width, $height, 'filemissing' );
186                 }
187
188                 $cmd = '(' . wfEscapeShellArg(
189                         $wgPdfProcessor,
190                         "-sDEVICE=jpeg",
191                         "-sOutputFile=-",
192                         "-dFirstPage={$page}",
193                         "-dLastPage={$page}",
194                         "-dSAFER",
195                         "-r{$wgPdfHandlerDpi}",
196                         "-dBATCH",
197                         "-dNOPAUSE",
198                         "-q",
199                         $srcPath
200                 );
201                 $cmd .= " | " . wfEscapeShellArg(
202                         $wgPdfPostProcessor,
203                         "-depth",
204                         "8",
205                         "-quality",
206                         $wgPdfHandlerJpegQuality,
207                         "-resize",
208                         $width,
209                         "-",
210                         $dstPath
211                 );
212                 $cmd .= ")";
213
214                 wfDebug( __METHOD__ . ": $cmd\n" );
215                 $retval = '';
216                 $err = wfShellExecWithStderr( $cmd, $retval );
217
218                 $removed = $this->removeBadFile( $dstPath, $retval );
219
220                 if ( $retval != 0 || $removed ) {
221                         wfDebugLog( 'thumbnail',
222                                 sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"',
223                                 wfHostname(), $retval, trim( $err ), $cmd ) );
224                         return new MediaTransformError( 'thumbnail_error', $width, $height, $err );
225                 } else {
226                         return new ThumbnailImage( $image, $dstUrl, $width, $height, $dstPath, $page );
227                 }
228         }
229
230         /**
231          * @param $image File
232          * @param $path string
233          * @return PdfImage
234          */
235         function getPdfImage( $image, $path ) {
236                 if ( !$image ) {
237                         $pdfimg = new PdfImage( $path );
238                 } elseif ( !isset( $image->pdfImage ) ) {
239                         $pdfimg = $image->pdfImage = new PdfImage( $path );
240                 } else {
241                         $pdfimg = $image->pdfImage;
242                 }
243
244                 return $pdfimg;
245         }
246
247         /**
248          * @param $image File
249          * @return bool
250          */
251         function getMetaArray( $image ) {
252                 if ( isset( $image->pdfMetaArray ) ) {
253                         return $image->pdfMetaArray;
254                 }
255
256                 $metadata = $image->getMetadata();
257
258                 if ( !$this->isMetadataValid( $image, $metadata ) ) {
259                         wfDebug( "Pdf metadata is invalid or missing, should have been fixed in upgradeRow\n" );
260                         return false;
261                 }
262
263                 $work = new PoolCounterWorkViaCallback(
264                         'PdfHandler-unserialize-metadata',
265                         $image->getName(),
266                         [
267                                 'doWork' => function () use ( $image, $metadata ) {
268                                         wfSuppressWarnings();
269                                         $image->pdfMetaArray = unserialize( $metadata );
270                                         wfRestoreWarnings();
271                                 },
272                         ]
273                 );
274                 $work->execute();
275
276                 return $image->pdfMetaArray;
277         }
278
279         /**
280          * @param $image File
281          * @param $path string
282          * @return array|bool
283          */
284         function getImageSize( $image, $path ) {
285                 return $this->getPdfImage( $image, $path )->getImageSize();
286         }
287
288         /**
289          * @param $ext
290          * @param $mime string
291          * @param $params null
292          * @return array
293          */
294         function getThumbType( $ext, $mime, $params = null ) {
295                 global $wgPdfOutputExtension;
296                 static $mime;
297
298                 if ( !isset( $mime ) ) {
299                         $magic = MimeMagic::singleton();
300                         $mime = $magic->guessTypesForExtension( $wgPdfOutputExtension );
301                 }
302                 return [ $wgPdfOutputExtension, $mime ];
303         }
304
305         /**
306          * @param $image File
307          * @param $path string
308          * @return string
309          */
310         function getMetadata( $image, $path ) {
311                 return serialize( $this->getPdfImage( $image, $path )->retrieveMetaData() );
312         }
313
314         /**
315          * @param $image File
316          * @param $metadata string
317          * @return bool
318          */
319         function isMetadataValid( $image, $metadata ) {
320                 if ( !$metadata || $metadata === serialize( [] ) ) {
321                         return self::METADATA_BAD;
322                 } elseif ( strpos( $metadata, 'mergedMetadata' ) === false ) {
323                         return self::METADATA_COMPATIBLE;
324                 }
325                 return self::METADATA_GOOD;
326         }
327
328         /**
329          * @param $image File
330          * @param bool|IContextSource $context Context to use (optional)
331          * @return bool|array
332          */
333         function formatMetadata( $image, $context = false ) {
334                 $meta = $image->getMetadata();
335
336                 if ( !$meta ) {
337                         return false;
338                 }
339                 wfSuppressWarnings();
340                 $meta = unserialize( $meta );
341                 wfRestoreWarnings();
342
343                 if ( !isset( $meta['mergedMetadata'] )
344                         || !is_array( $meta['mergedMetadata'] )
345                         || count( $meta['mergedMetadata'] ) < 1
346                 ) {
347                         return false;
348                 }
349
350                 // Inherited from MediaHandler.
351                 return $this->formatMetadataHelper( $meta['mergedMetadata'], $context );
352         }
353
354         /**
355          * @param File $image
356          * @return bool|int
357          */
358         function pageCount( File $image ) {
359                 $info = $this->getDimensionInfo( $image );
360
361                 return $info ? $info['pageCount'] : false;
362         }
363
364         /**
365          * @param $image File
366          * @param $page int
367          * @return array|bool
368          */
369         function getPageDimensions( File $image, $page ) {
370                 $index = $page; // MW starts pages at 1, as they are stored here
371
372                 $info = $this->getDimensionInfo( $image );
373                 if ( $info && isset( $info['dimensionsByPage'][$index] ) ) {
374                         return $info['dimensionsByPage'][$index];
375                 }
376
377                 return false;
378         }
379
380         protected function getDimensionInfo( File $file ) {
381                 $cache = ObjectCache::getMainWANInstance();
382                 return $cache->getWithSetCallback(
383                         $cache->makeKey( 'file-pdf', 'dimensions', $file->getSha1() ),
384                         $cache::TTL_INDEFINITE,
385                         function () use ( $file ) {
386                                 $data = $this->getMetaArray( $file );
387                                 if ( !$data || !isset( $data['Pages'] ) ) {
388                                         return false;
389                                 }
390                                 unset( $data['text'] ); // lower peak RAM
391
392                                 $dimsByPage = [];
393                                 $count = intval( $data['Pages'] );
394                                 for ( $i = 1; $i <= $count; $i++ ) {
395                                         $dimsByPage[$i] = PdfImage::getPageSize( $data, $i );
396                                 }
397
398                                 return [ 'pageCount' => $count, 'dimensionsByPage' => $dimsByPage ];
399                         },
400                         [ 'pcTTL' => $cache::TTL_INDEFINITE ]
401                 );
402         }
403
404         /**
405          * @param $image File
406          * @param $page int
407          * @return bool
408          */
409         function getPageText( File $image, $page ) {
410                 $data = $this->getMetaArray( $image );
411                 if ( !$data || !isset( $data['text'] ) || !isset( $data['text'][$page - 1] ) ) {
412                         return false;
413                 }
414                 return $data['text'][$page - 1];
415         }
416
417         /**
418          * Adds a warning about PDFs being potentially dangerous to the file
419          * page. Multiple messages with this base will be used.
420          * @param File $file
421          * @return array
422          */
423         function getWarningConfig( $file ) {
424                 return [
425                         'messages' => self::$messages,
426                         'link' => '//www.mediawiki.org/wiki/Special:MyLanguage/Help:Security/PDF_files',
427                         'module' => 'pdfhandler.messages',
428                 ];
429         }
430
431         /**
432          * Register a module with the warning messages in it.
433          * @param &$resourceLoader ResourceLoader
434          */
435         static function registerWarningModule( &$resourceLoader ) {
436                 $resourceLoader->register( 'pdfhandler.messages', [
437                         'messages' => array_values( self::$messages ),
438                 ] );
439         }
440 }