file.php 31.6 KB
Newer Older
1
2
3
4
5
<?php
/**
 *
 * @package    mahara
 * @subpackage core
6
 * @author     Martin Dougiamas <martin@moodle.com>
7
8
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later
 * @copyright  For copyright information on Mahara, please see the README file distributed with this software.
9
 * @copyright  (C) 2001-3001 Martin Dougiamas http://dougiamas.com
10
11
12
13
14
 *
 */

defined('INTERNAL') || die();

15
define('BYTESERVING_BOUNDARY', 'm1i2k3e40516'); //unique string constant
16

17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
 * Serves a file from dataroot.
 *
 * This function checks that the file is inside dataroot, but does not perform
 * any other checks. Authors using this function should make sure that their
 * scripts perform appropriate authentication.
 *
 * As an example: If the file is an artefact, you could ask for an artefact and
 * view ID, and check that the artefact is in the view and that the user can
 * view the view.
 *
 * @param string $path     The file to send. Must include the dataroot path.
 * @param string $filename The name of the file as the browser should use to
 *                         serve it.
31
 * @param string $mimetype Mime type to be sent in header
32
 * @param array  $options  Any options to use when serving the file. Currently
33
34
35
 *                         lifetime = 0 for no cache
 *                         forcedownload - force application rather than inline
 *                         overridecontenttype - send this instead of the mimetype
36
37
 *                         there are none.
 */
38
function serve_file($path, $filename, $mimetype, $options=array()) {
39
    $dataroot = realpath(get_config('dataroot'));
40
41
42
43
44
    $path = realpath($path);
    $options = array_merge(array(
        'lifetime' => 86400
    ), $options);

45
    if (!get_config('insecuredataroot') && substr($path, 0, strlen($dataroot)) != $dataroot) {
46
47
        throw new AccessDeniedException();
    }
48

49
50
51
    if (!file_exists($path)) {
        throw new NotFoundException();
    }
52

53
    session_write_close(); // unlock session during fileserving
54

55
56
    $lastmodified = filemtime($path);
    $filesize     = filesize($path);
57

58
59
60
61
62
    if ($mimetype == 'text/html'
        || $mimetype == 'text/xml'
        || $mimetype == 'application/xml'
        || $mimetype == 'application/xhtml+xml'
        || $mimetype == 'image/svg+xml') {
63
64
        if (isset($options['downloadurl']) && $filesize < 1024 * 1024) {
            display_cleaned_html(file_get_contents($path), $filename, $options);
65
            exit;
Richard Mansfield's avatar
Richard Mansfield committed
66
        }
67
68
        $options['forcedownload'] = true;
        $mimetype = 'application/octet-stream';
Richard Mansfield's avatar
Richard Mansfield committed
69
70
    }

71
    if (!$mimetype) {
Richard Mansfield's avatar
Richard Mansfield committed
72
73
74
        $mimetype = 'application/forcedownload';
    }

75
76
77
78
    if (ini_get('zlib.output_compression')) {
        ini_set('zlib.output_compression', 'Off');
    }

79
80
    // Try to disable automatic sid rewrite in cookieless mode
    @ini_set('session.use_trans_sid', 'false');
81
82
83

    header('Last-Modified: '. gmdate('D, d M Y H:i:s', $lastmodified) .' GMT');

84
85
    // @todo possibly need addslashes on the filename, but I'm unsure on exactly
    // how the browsers will handle it.
86
    if ($mimetype == 'application/forcedownload' || isset($options['forcedownload'])) {
87
88
89
90
        header('Content-Disposition: attachment; filename="' . $filename . '"');
    }
    else {
        header('Content-Disposition: inline; filename="' . $filename . '"');
91
    }
92
    header('X-Content-Type-Options: nosniff');
93

Hugh Davenport's avatar
Hugh Davenport committed
94
    if ($options['lifetime'] > 0 && !get_config('nocache')) {
95
96
97
        header('Cache-Control: max-age=' . $options['lifetime']);
        header('Expires: '. gmdate('D, d M Y H:i:s', time() + $options['lifetime']) .' GMT');
        header('Pragma: ');
98

Richard Mansfield's avatar
Richard Mansfield committed
99
        if ($mimetype != 'text/plain' && $mimetype != 'text/html' && !isset($fileoutput)) {
100
101
102
            @header('Accept-Ranges: bytes');

            if (!empty($_SERVER['HTTP_RANGE']) && strpos($_SERVER['HTTP_RANGE'],'bytes=') !== FALSE) {
103
                // Byteserving stuff - for Acrobat Reader and download accelerators
104
105
106
107
                // see: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35
                // inspired by: http://www.coneural.org/florian/papers/04_byteserving.php
                $ranges = false;
                if (preg_match_all('/(\d*)-(\d*)/', $_SERVER['HTTP_RANGE'], $ranges, PREG_SET_ORDER)) {
108
                    foreach ($ranges as $key => $value) {
109
                        if ($ranges[$key][1] == '') {
110
                            // Suffix case
111
112
                            $ranges[$key][1] = $filesize - $ranges[$key][2];
                            $ranges[$key][2] = $filesize - 1;
113
114
115
                        }
                        else if ($ranges[$key][2] == '' || $ranges[$key][2] > $filesize - 1) {
                            // Fix range length
116
117
118
                            $ranges[$key][2] = $filesize - 1;
                        }
                        if ($ranges[$key][2] != '' && $ranges[$key][2] < $ranges[$key][1]) {
119
                            // Invalid byte-range ==> ignore header
120
121
122
                            $ranges = false;
                            break;
                        }
123
124
125

                        // Prepare multipart header
                        $ranges[$key][0] =  "\r\n--" . BYTESERVING_BOUNDARY . "\r\nContent-Type: $mimetype\r\n";
126
127
128
129
130
131
132
133
134
135
                        $ranges[$key][0] .= "Content-Range: bytes {$ranges[$key][1]}-{$ranges[$key][2]}/$filesize\r\n\r\n";
                    }
                } else {
                    $ranges = false;
                }
                if ($ranges) {
                    byteserving_send_file($path, $mimetype, $ranges);
                }
            }
        }
136
137
138
        else {
            // Do not byteserve (disabled, strings, text and html files).
            header('Accept-Ranges: none');
139
        }
140
141
    }
    else { // Do not cache files in proxies and browsers
142
        if (is_https() === true) { //https sites - watch out for IE! KB812935 and KB316431
143
144
145
146
147
148
149
150
151
152
            header('Cache-Control: max-age=10');
            header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT');
            header('Pragma: ');
        }
        else { //normal http - prevent caching at all cost
            header('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0');
            header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT');
            header('Pragma: no-cache');
        }
        header('Accept-Ranges: none'); // Do not allow byteserving when caching disabled
153
154
    }

155
156
157
    if ($mimetype == 'text/plain') {
        // Add encoding
        header('Content-Type: Text/plain; charset=utf-8');
158
    }
159
    else {
160
161
162
163
164
165
        if (isset($options['overridecontenttype'])) {
            header('Content-Type: ' . $options['overridecontenttype']);
        }
        else {
            header('Content-Type: ' . $mimetype);
        }
166
    }
167
168
    header('Content-Length: ' . $filesize);
    while (@ob_end_flush()); //flush the buffers - save memory and disable sid rewrite
169
    readfile_chunked($path);
170
    perf_to_log();
171
    exit;
172
173
174
175
176
177
}

/**
 * Improves memory consumptions and works around buggy readfile() in PHP 5.0.4 (2MB readfile limit).
 */
function readfile_chunked($filename, $retbytes=true) {
178
    $chunksize = 1 * (1024 * 1024); // 1MB chunks - must be less than 2MB!
179
    $buffer = '';
180
    $cnt =0;
181
182
183
184
185
    $handle = fopen($filename, 'rb');
    if ($handle === false) {
        return false;
    }

186

187
    while (!feof($handle)) {
188
        @set_time_limit(60 * 60); //reset time limit to 60 min - should be enough for 1 MB chunk
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
        $buffer = fread($handle, $chunksize);
        echo $buffer;
        flush();
        if ($retbytes) {
            $cnt += strlen($buffer);
        }
    }
    $status = fclose($handle);
    if ($retbytes && $status) {
        return $cnt; // return num. bytes delivered like readfile() does.
    }
    return $status;
}

/**
 * Send requested byterange of file.
 */
function byteserving_send_file($filename, $mimetype, $ranges) {
207
    $chunksize = 1 * (1024 * 1024); // 1MB chunks - must be less than 2MB!
208
209
210
211
212
213
    $handle = fopen($filename, 'rb');
    if ($handle === false) {
        die;
    }
    if (count($ranges) == 1) { //only one range requested
        $length = $ranges[0][2] - $ranges[0][1] + 1;
214
215
216
217
        header('HTTP/1.1 206 Partial content');
        header('Content-Length: ' . $length);
        header('Content-Range: bytes ' . $ranges[0][1] . '-' . $ranges[0][2] . '/' . filesize($filename));
        header('Content-Type: ' . $mimetype);
218
219
220
221
        while (@ob_end_flush()); //flush the buffers - save memory and disable sid rewrite
        $buffer = '';
        fseek($handle, $ranges[0][1]);
        while (!feof($handle) && $length > 0) {
222
            @set_time_limit(60*60); //reset time limit to 60 min - should be enough for 1 MB chunk
223
224
225
226
227
228
            $buffer = fread($handle, ($chunksize < $length ? $chunksize : $length));
            echo $buffer;
            flush();
            $length -= strlen($buffer);
        }
        fclose($handle);
229
230
231
        exit;
    }
    else { // multiple ranges requested - not tested much
232
233
234
235
        $totallength = 0;
        foreach($ranges as $range) {
            $totallength += strlen($range[0]) + $range[2] - $range[1] + 1;
        }
236
237
238
239
        $totallength += strlen("\r\n--" . BYTESERVING_BOUNDARY . "--\r\n");
        header('HTTP/1.1 206 Partial content');
        header('Content-Length: ' . $totallength);
        header('Content-Type: multipart/byteranges; boundary=' . BYTESERVING_BOUNDARY);
240
241
242
243
244
245
246
247
        //TODO: check if "multipart/x-byteranges" is more compatible with current readers/browsers/servers
        while (@ob_end_flush()); //flush the buffers - save memory and disable sid rewrite
        foreach($ranges as $range) {
            $length = $range[2] - $range[1] + 1;
            echo $range[0];
            $buffer = '';
            fseek($handle, $range[1]);
            while (!feof($handle) && $length > 0) {
248
                @set_time_limit(60 * 60); //reset time limit to 60 min - should be enough for 1 MB chunk
249
250
251
252
253
254
                $buffer = fread($handle, ($chunksize < $length ? $chunksize : $length));
                echo $buffer;
                flush();
                $length -= strlen($buffer);
            }
        }
255
        echo "\r\n--" . BYTESERVING_BOUNDARY . "--\r\n";
256
        fclose($handle);
257
        exit;
258
259
260
    }
}

Richard Mansfield's avatar
Richard Mansfield committed
261

262
263
264
265
266
267
/**
 * Given a file path, guesses the mime type of the file using the
 * php functions finfo_file, mime_content_type, or looking for the
 * file extension in the artefact_file_mime_types table
 *
 * @param string $file The file to check
268
 * @param string $originalfilename The original name of the file (so we can check its extension)
269
270
 * @return string      The mime type of the file
 */
271
function file_mime_type($file, $originalfilename=false) {
272
273
    static $mimetypes = null;

274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
    // Try the filename extension in case it's a file that Mahara
    // cares about.  For now, use the artefact_file_mime_types table,
    // even though it's in a plugin and the description column doesn't
    // really contain filename extensions.
    if ($originalfilename) {
        $basename = $originalfilename;
    }
    else {
        $basename = basename($file);
    }
    if (strpos($basename, '.', 1)) {
        if (is_null($mimetypes)) {
            $mimetypes = get_records_assoc('artefact_file_mime_types', '', '', '', 'description,mimetype');
        }
        $ext = strtolower(array_pop(explode('.', $basename)));
        if (isset($mimetypes[$ext])) {
            return $mimetypes[$ext]->mimetype;
        }
    }

    // Try detecting it with the magic.mgc file
295
296
297
298
299
300
301
302
303
    if (get_config('pathtomagicdb') !== null) {
        // Manually specified magicdb path in config.php
        $magicfile = get_config('pathtomagicdb');
    }
    else {
        // Using one of the system presets (or if no matching system preset, this
        // will return false, indicating we shouldn't bother with fileinfo
        $magicfile = standard_magic_paths(get_config('defaultmagicdb'));
    }
304

305
    if ($magicfile !== false && class_exists('finfo') ) {
306
307
        if ($finfo = @new finfo(FILEINFO_MIME_TYPE, $magicfile)) {
            $type = @$finfo->file($file);
308
309
310
        }
    }
    else if (function_exists('mime_content_type')) {
311
312
313
        $type = mime_content_type($file);
    }

314
    if (!empty($type)) {
315
        return $type;
316
317
318
319
320
321
    }

    return 'application/octet-stream';
}


322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
/**
 * The standard locations we would expect the magicdb to be. The keys of the array returned
 * by this value, are the values stored in the config
 * @param int $key (optional)
 * @return multitype:string If a key is supplied, return the path matching that key. If no
 * key is supplied, return the full array of possible magic locations.
 */
function standard_magic_paths($key = 'fullarray') {
    static $standardmagicpaths = array(
        1=>'',
        2=>'/usr/share/misc/magic',
        3=>'/usr/share/misc/magic.mgc',
    );

    if ($key === 'fullarray') {
        return $standardmagicpaths;
    }

    if (array_key_exists($key, $standardmagicpaths)) {
        return $standardmagicpaths[$key];
    }
    else {
        return false;
    }
}


/**
 * Try a few different likely possibilities for the magicdb and see which of them returns
 * the correct response. Then store that configuration option for later use, in the config
 * setting 'defaultmagicdb'. Because this is a DB-settable setting, we don't store the file
 * path directly in it, but instead just store a key corresponding to a path specified in
 * standard_magic_paths().
 */
function update_magicdb_path() {
    // Determine where the server's "magic" db is\
    if (class_exists('finfo')) {
359
        $file = get_config('docroot') . 'theme/raw/static/images/powered_by_mahara.png';
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389

        $magicpathstotry = standard_magic_paths();
        $workingpath = false;
        foreach ($magicpathstotry as $i=>$magicfile) {
            $type = false;
            if (defined('FILEINFO_MIME_TYPE')) {
                if ($finfo = @new finfo(FILEINFO_MIME_TYPE, $magicfile)) {
                    $type = @$finfo->file($file);
                }
            }
            else if ($finfo = @new finfo(FILEINFO_MIME, $magicfile)) {
                if ($typecharset = @$finfo->file($file)) {
                    if ($bits = explode(';', $typecharset)) {
                        $type = $bits[0];
                    }
                }
            }
            if ($type == 'image/png') {
                $workingpath = $i;
                break;
            }
        }
        if (!$workingpath) {
            log_debug('Could not locate the path to your fileinfo magic db. Please set it manually using $cfg->pathtomagicdb.');
            $workingpath = 0;
        }
        set_config('defaultmagicdb', $workingpath);
    }
}

390
391
392
393
/**
 * Given a mimetype (perhaps returned by {@link get_mime_type}, returns whether
 * Mahara thinks it is a valid image file.
 *
394
395
 * Not all image types are valid for Mahara. Mahara supports JPEG, PNG, GIF
 * and BMP.
396
397
398
399
 *
 * @param string $type The mimetype to check
 * @return boolean     Whether the type is a valid image type for Mahara
 */
Richard Mansfield's avatar
Richard Mansfield committed
400
function is_image_mime_type($type) {
401
402
403
404
405
406
407
408
409
410
411
    $supported = array(
        'image/jpeg', 'image/jpg',
        'image/gif',
        'image/png'
    );
    if (extension_loaded('imagick')) {
        $supported = array_merge($supported, array(
            'image/bmp', 'image/x-bmp', 'image/ms-bmp', 'image/x-ms-bmp'
        ));
    }
    return in_array($type, $supported);
Richard Mansfield's avatar
Richard Mansfield committed
412
413
}

414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443

/**
 * Given an image type returned by getimagesize or exif_imagetype, returns whether
 * Mahara thinks it is a valid image type.
 *
 * Not all image types are valid for Mahara. Mahara supports JPEG, PNG, GIF
 * and BMP.
 *
 * @param string $type The type to check
 * @return boolean     Whether the type is a valid image type for Mahara
 */
function is_image_type($type) {
    $supported = array(IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG);
    if (extension_loaded('imagick')) {
        $supported[] = IMAGETYPE_BMP;
    }
    return $type && in_array($type, $supported);
}


/**
 * Given path to a file, returns whether Mahara thinks it is a valid image file.
 *
 * Not all image types are valid for Mahara. Mahara supports JPEG, PNG, GIF
 * and BMP.
 *
 * @param string $path The file to check
 * @return boolean     Whether the file is a valid image file for Mahara
 */
function is_image_file($path) {
444
445
    if (function_exists('exif_imagetype')) {
        // exif_imagetype is faster
446
447
448
        // surpressing errors because exif_imagetype spews "read error!" to the logs on small files
        // http://nz.php.net/manual/en/function.exif-imagetype.php#79283
        if (!$type = @exif_imagetype($path)) {
449
450
451
452
453
454
455
456
            return false;
        }
    }
    else {
        // getimagesize returns the same answer
        if (!list ($width, $height, $type) = getimagesize($path)) {
            return false;
        }
457
458
459
460
461
    }
    return is_image_type($type);
}


462
463
464
465
466
467
/**
 * Given a path under dataroot, an ID and a size, return the path to a file
 * matching all criteria.
 *
 * If the file with the ID exists but not of the correct size, this function
 * will make a copy that is resized to the correct size.
468
469
 *
 * @param string $path The base path in dataroot where the image is stored. For 
470
 *                     example, 'artefact/file/profileicons/' for profile 
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
 *                     icons
 * @param int $id      The ID of the image to return. Is typically the ID of an 
 *                     artefact
 * @param mixed $size  The size the image should be.
 *
 *                      As a two element hash with 'w' and 'h' keys:
 *                     - If 'w' and 'h' are not empty, the image will be 
 *                       exactly that size
 *                     - If just 'w' is not empty, the image will be that wide, 
 *                       and the height will be set to make the image scale 
 *                       correctly
 *                     - If just 'h' is not empty, the image will be that high, 
 *                       and the width will be set to make the image scale 
 *                       correctly
 *                     - If neither are set or the parameter is not set, the 
 *                       image will not be resized
 *
 *                     As a number, the path returned will have the largest side being 
 *                     the length specified.
 * @return string The path on disk where the appropriate file resides, or false 
 *                if an appropriate file could not be located or generated
492
 */
493
function get_dataroot_image_path($path, $id, $size=null) {
494
    global $THEME;
495
496
    $dataroot = get_config('dataroot');
    $imagepath = $dataroot . $path;
497
498
499
    if (substr($imagepath, -1) == '/') {
        $imagepath = substr($imagepath , 0, -1);
    }
500
501
502
503
504

    if (!is_dir($imagepath) || !is_readable($imagepath)) {
        return false;
    }

505
506
    // Work out the location of the original image
    $originalimage = $imagepath . '/originals/' . ($id % 256) . "/$id";
507

508
509
510
511
    // If the original has been deleted, then don't show any image, even a cached one. 
    // delete_image only deletes the original, not any cached ones, so we have 
    // to make sure the original is still around
    if (!is_readable($originalimage)) {
512
        return false;
513
    }
514
515
516
517
518

    if (!$size) {
        // No size has been asked for. Return the original
        return $originalimage;
    }
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
    else {
        // Check if the image is available in the size requested
        $sizestr = serialize($size);
        $md5     = md5("{$id}.{$sizestr}");

        $resizedimagedir = $imagepath . '/resized/';
        check_dir_exists($resizedimagedir);
        for ($i = 0; $i <= 2; $i++) {
           $resizedimagedir .= substr($md5, $i, 1) . '/';
            check_dir_exists($resizedimagedir);
        }
        $resizedimagefile = "{$resizedimagedir}{$md5}.$id";//.$sizestr";

        if (is_readable($resizedimagefile)) {
            return $resizedimagefile;
        }
Nigel McNie's avatar
Nigel McNie committed
535

536
537
        // Image is not available in this size. If there is a base image for
        // it, we can make one however.
538
        if (is_readable($originalimage) && filesize($originalimage)) {
539

540
541
            $imageinfo = getimagesize($originalimage);
            $originalmimetype = $imageinfo['mime'];
542
543
544

            // gd can eat a lot of memory shrinking large images, so use a placeholder image
            // here if necessary
545
546
547
548
549
550
551
552
            if (isset($imageinfo['bits'])) {
                $bits = $imageinfo['bits'];
            }
            else if ($imageinfo['mime'] == 'image/gif') {
                $bits = 8;
            }
            if (isset($imageinfo[0]) && isset($imageinfo[1]) && !empty($bits)) {
                $approxmem = $imageinfo[0] * $imageinfo[1] * ($bits / 8)
553
554
555
556
557
558
559
560
561
562
563
564
565
                    * (isset($imageinfo['channels']) ? $imageinfo['channels'] : 3);
            }
            if (empty($approxmem) || $approxmem > get_config('maximageresizememory')) {
                log_debug("Refusing to resize large image $originalimage $originalmimetype "
                    . $imageinfo[0] . 'x' .  $imageinfo[1] . ' ' . $imageinfo['bits'] . '-bit');
                $originalimage = $THEME->get_path('images/no_thumbnail.png');
                if (empty($originalimage) || !is_readable($originalimage)) {
                    return false;
                }
                $imageinfo = getimagesize($originalimage);
                $originalmimetype = $imageinfo['mime'];
            }

566
            $format = 'png';
567
            switch ($originalmimetype) {
Nigel McNie's avatar
Nigel McNie committed
568
                case 'image/jpeg':
569
                case 'image/jpg':
570
                    $format = 'jpeg';
Nigel McNie's avatar
Nigel McNie committed
571
                    $oldih = imagecreatefromjpeg($originalimage);
Nigel McNie's avatar
Nigel McNie committed
572
                    break;
Nigel McNie's avatar
Nigel McNie committed
573
574
                case 'image/png':
                    $oldih = imagecreatefrompng($originalimage);
Nigel McNie's avatar
Nigel McNie committed
575
                    break;
Nigel McNie's avatar
Nigel McNie committed
576
                case 'image/gif':
577
                    $format = 'gif';
Nigel McNie's avatar
Nigel McNie committed
578
                    $oldih = imagecreatefromgif($originalimage);
Nigel McNie's avatar
Nigel McNie committed
579
                    break;
580
581
582
583
                case 'image/bmp':
                case 'image/x-bmp':
                case 'image/ms-bmp':
                case 'image/x-ms-bmp':
584
                    if (!extension_loaded('imagick') || !class_exists('Imagick')) {
585
                        log_info('Bitmap image detected for resizing, but imagick extension is not available');
586
587
                        return false;
                    }
588

589
590
                    $ih = new Imagick($originalimage);
                    if (!$newdimensions = image_get_new_dimensions($ih->getImageWidth(), $ih->getImageHeight(), $size)) {
591
592
                        return false;
                    }
593
594
                    $ih->resizeImage($newdimensions['w'], $newdimensions['h'], imagick::FILTER_LANCZOS, 1);
                    if ($ih->writeImage($resizedimagefile)) {
595
596
597
                        return $resizedimagefile;
                    }
                    return false;
Nigel McNie's avatar
Nigel McNie committed
598
599
                default:
                    return false;
Nigel McNie's avatar
Nigel McNie committed
600
601
            }

Nigel McNie's avatar
Nigel McNie committed
602
603
604
            if (!$oldih) {
                return false;
            }
605

Nigel McNie's avatar
Nigel McNie committed
606
607
            $oldx = imagesx($oldih);
            $oldy = imagesy($oldih);
608

609
610
            if (!$newdimensions = image_get_new_dimensions($oldx, $oldy, $size)) {
                return false;
Nigel McNie's avatar
Nigel McNie committed
611
            }
612

613
            $newih = imagecreatetruecolor($newdimensions['w'], $newdimensions['h']);
614
615
616
617
618
619
620
621
622
623

            if ($originalmimetype == 'image/png' || $originalmimetype == 'image/gif') {
                // Create a new destination image which is completely 
                // transparent and turn off alpha blending for it, so that when 
                // the PNG source file is copied, the alpha channel is retained.
                // Thanks to http://alexle.net/archives/131

                $background = imagecolorallocate($newih, 0, 0, 0);
                imagecolortransparent($newih, $background);
                imagealphablending($newih, false);
624
                imagecopyresampled($newih, $oldih, 0, 0, 0, 0, $newdimensions['w'], $newdimensions['h'], $oldx, $oldy);
625
626
627
628
629
630
                imagesavealpha($newih, true);
            }
            else {
                // imagecopyresized is faster, but results in noticeably worse image quality. 
                // Given the images are resized only once each time they're 
                // made, I suggest you just leave the good quality one in place
631
632
                imagecopyresampled($newih, $oldih, 0, 0, 0, 0, $newdimensions['w'], $newdimensions['h'], $oldx, $oldy);
                //imagecopyresized($newih, $oldih, 0, 0, 0, 0, $newdimensions['w'], $newdimensions['h'], $oldx, $oldy);
633
634
            }

635
636
            $outputfunction = "image$format";
            $result = $outputfunction($newih, $resizedimagefile);
637
638
639
640
            if ($result) {
                return $resizedimagefile;
            }
        } // end attempting to build a resized image
641
642
643
644
645
    }

    // Image not available in any size
    return false;
}
646

647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
/**
 * Given the old dimensions of an image and a size object as obtained from 
 * get_imagesize_parameters(), calculates what the new size of the image should 
 * be
 *
 * @param int $oldx   The width of the image to calculate the new size for
 * @param int $oldy   The height of the image to calculate the new size for
 * @param mixed $size The size data
 * @return array      A hash with the new width and height, keyed by 'w' and 'h'
 */
function image_get_new_dimensions($oldx, $oldy, $size) {
    if (is_int($size)) {
        // If just a number (number is width AND height here)
        if ($oldy > $oldx) {
            $newy = $size;
            $newx = ($oldx * $newy) / $oldy;
        }
        else {
            $newx = $size;
            $newy = ($oldy * $newx) / $oldx;
        }
    }
    else if (isset($size['w']) && isset($size['h'])) {
        // If size explicitly X by Y
        $newx = $size['w'];
        $newy = $size['h'];
    }
    else if (isset($size['w'])) {
        // Else if just width
        $newx = $size['w'];
        $newy = ($oldy * $newx) / $oldx;
    }
    else if (isset($size['h'])) {
        // Else if just height
        $newy = $size['h'];
        $newx = ($oldx * $newy) / $oldy;
    }
684
685
    else if (isset($size['maxw']) && isset($size['maxh'])) {
        $scale = min(min($size['maxw'], $oldx) / $oldx, min($size['maxh'], $oldy) / $oldy);
686
687
        $newx = max(1, $oldx * $scale);
        $newy = max(1, $oldy * $scale);
688
    }
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
    else if (isset($size['maxw'])) {
        // Else if just maximum width
        if ($oldx > $size['maxw']) {
            $newx = $size['maxw'];
            $newy = ($oldy * $newx) / $oldx;
        }
        else {
            $newx = $oldx;
            $newy = $oldy;
        }
    }
    else if (isset($size['maxh'])) {
        // Else if just maximum height
        if ($oldy > $size['maxh']) {
            $newy = $size['maxh'];
            $newx = ($oldx * $newy) / $oldy;
        }
        else {
            $newx = $oldx;
            $newy = $oldy;
        }
    }
711
712
713
714
715
716
    else {
        return false;
    }
    return array('w' => $newx, 'h' => $newy);
}

717
/**
718
 * Deletes an image, excluding all resized versions of it, from dataroot.
719
720
721
722
 *
 * This function does not delete anything from anywhere else - it is your
 * responsibility to delete any database records.
 *
723
724
725
 * This function also does not try to delete all resized versions of this
 * image, as it would take a lot of effort to find them all.
 *
726
727
728
729
730
731
732
733
 * @param string $path The path in dataroot of the base directory where the
 *                     image resides.
 * @param int $id      The id of the image to delete.
 * @return boolean     Whether the image was deleted successfully.
 */
function delete_image($path, $id) {
    // Check that the image exists.
    $dataroot = get_config('dataroot');
734
    $imagepath = $dataroot . $path . '/originals';
735
736
737
738
739
740
741
742
743
744
745

    if (!is_dir($imagepath) || !is_readable($imagepath)) {
        return false;
    }

    $originalimage = $imagepath . '/' . ($id % 256) . "/$id";
    if (!is_readable($originalimage)) {
        return false;
    }

    unlink($originalimage);
746
747
    return true;
}
748

749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
/**
 * Delete a file, or a folder and its contents
 *
 * @author      Aidan Lister <aidan@php.net>
 * @version     1.0.3
 * @link        http://aidanlister.com/repos/v/function.rmdirr.php
 * @param       string   $dirname    Directory to delete
 * @return      bool     Returns TRUE on success, FALSE on failure
 */
function rmdirr($dirname)
{
    // Sanity check
    if (!file_exists($dirname)) {
        return false;
    }
 
    // Simple delete for a file
    if (is_file($dirname) || is_link($dirname)) {
        return unlink($dirname);
    }
 
    // Loop through the folder
    $dir = dir($dirname);
    while (false !== $entry = $dir->read()) {
        // Skip pointers
        if ($entry == '.' || $entry == '..') {
775
776
            continue;
        }
777
778
 
        // Recurse
779
        rmdirr($dirname . '/' . $entry);
780
    }
781
782
783
784
 
    // Clean up
    $dir->close();
    return rmdir($dirname);
785
786
}

787
788
789
790
791
792
/**
 * Copy a file, or recursively copy a folder and its contents
 *
 * @author      Aidan Lister <aidan@php.net>
 * @version     1.0.1
 * @link        http://aidanlister.com/repos/v/function.copyr.php
793
 * @license     Public Domain
794
795
796
797
798
799
 * @param       string   $source    Source path
 * @param       string   $dest      Destination path
 * @return      bool     Returns TRUE on success, FALSE on failure
 */
function copyr($source, $dest)
{
800
    $dest = trim($dest);
801
802
803
804
805
806
807
808
809
810
811
812
    // Check for symlinks
    if (is_link($source)) {
        return symlink(readlink($source), $dest);
    }

    // Simple copy for a file
    if (is_file($source)) {
        return copy($source, $dest);
    }

    // Make destination directory
    if (!is_dir($dest)) {
Hugh Davenport's avatar
Hugh Davenport committed
813
        mkdir($dest, get_config('directorypermissions'));
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
    }

    // Loop through the folder
    $dir = dir($source);
    while (false !== $entry = $dir->read()) {
        // Skip pointers
        if ($entry == '.' || $entry == '..') {
            continue;
        }

        // Deep copy directories
        copyr("$source/$entry", "$dest/$entry");
    }

    // Clean up
    $dir->close();
    return true;
}
832
833
834
835
836
837
838
839
840
841
842
843
844

function file_cleanup_old_cached_files() {
    global $THEME;
    $dirs = array('', '/profileicons');
    foreach (get_all_theme_objects() as $basename => $theme) {
        $dirs[] = '/profileicons/no_userphoto/' . $basename;
    }
    foreach ($dirs as $dir) {
        $basedir = get_config('dataroot') . 'artefact/file' . $dir . '/resized/';
        if (!check_dir_exists($basedir, false)) {
            continue;
        }

845
        $mintime = time() - (12 * 7 * 24 * 60 * 60); // delete caches older than 12 weeks
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883

        // Cached files are stored in a three tier md5sum layout
        // The actual files are stored in the third directory
        // This loops through all three directories, then checks the files for age
        // It cleans up any empty directories on the way down again

        $iter1 = new DirectoryIterator($basedir);
        foreach ($iter1 as $dir1) {
            if ($dir1->isDot()) continue;
            $dir1path = $dir1->getPath() . '/' . $dir1->getFilename();
            $iter2 = new DirectoryIterator($dir1path);
            foreach ($iter2 as $dir2) {
                if ($dir2->isDot()) continue;
                $dir2path = $dir2->getPath() . '/' . $dir2->getFilename();
                $iter3 = new DirectoryIterator($dir2path);
                foreach ($iter3 as $dir3) {
                    if ($dir3->isDot()) continue;
                    $dir3path = $dir3->getPath() . '/' . $dir3->getFilename();
                    $fileiter = new DirectoryIterator($dir3path);
                    foreach ($fileiter as $file) {
                        if ($file->isFile() && $file->getCTime() < $mintime) {
                            unlink($file->getPath() . '/' . $file->getFilename());
                        }
                    }
                    if (sizeof(scandir($dir3path)) <= 2) {   // first 2 entries are . and ..
                        rmdir($dir3path);
                    }
                }
                if (sizeof(scandir($dir2path)) <= 2) {   // first 2 entries are . and ..
                    rmdir($dir2path);
                }
            }
            if (sizeof(scandir($dir1path)) <= 2) {   // first 2 entries are . and ..
                rmdir($dir1path);
            }
        }
    }
}