Subversion Repositories Applications.bazar

Rev

Rev 468 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
468 mathias 1
<?php
2
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
3
 
4
/**
5
 * HTTP::Download
6
 *
7
 * PHP versions 4 and 5
8
 *
9
 * @category   HTTP
10
 * @package    HTTP_Download
11
 * @author     Michael Wallner <mike@php.net>
12
 * @copyright  2003-2005 Michael Wallner
13
 * @license    BSD, revised
14
 * @version    CVS: $Id$
15
 * @link       http://pear.php.net/package/HTTP_Download
16
 */
17
 
18
// {{{ includes
19
/**
20
 * Requires PEAR
21
 */
22
require_once 'PEAR.php';
23
 
24
/**
25
 * Requires HTTP_Header
26
 */
27
require_once 'HTTP/Header.php';
28
// }}}
29
 
30
// {{{ constants
31
/**#@+ Use with HTTP_Download::setContentDisposition() **/
32
/**
33
 * Send data as attachment
34
 */
35
define('HTTP_DOWNLOAD_ATTACHMENT', 'attachment');
36
/**
37
 * Send data inline
38
 */
39
define('HTTP_DOWNLOAD_INLINE', 'inline');
40
/**#@-**/
41
 
42
/**#@+ Use with HTTP_Download::sendArchive() **/
43
/**
44
 * Send as uncompressed tar archive
45
 */
46
define('HTTP_DOWNLOAD_TAR', 'TAR');
47
/**
48
 * Send as gzipped tar archive
49
 */
50
define('HTTP_DOWNLOAD_TGZ', 'TGZ');
51
/**
52
 * Send as bzip2 compressed tar archive
53
 */
54
define('HTTP_DOWNLOAD_BZ2', 'BZ2');
55
/**
56
 * Send as zip archive
57
 */
58
define('HTTP_DOWNLOAD_ZIP', 'ZIP');
59
/**#@-**/
60
 
61
/**#@+
62
 * Error constants
63
 */
64
define('HTTP_DOWNLOAD_E_HEADERS_SENT',          -1);
65
define('HTTP_DOWNLOAD_E_NO_EXT_ZLIB',           -2);
66
define('HTTP_DOWNLOAD_E_NO_EXT_MMAGIC',         -3);
67
define('HTTP_DOWNLOAD_E_INVALID_FILE',          -4);
68
define('HTTP_DOWNLOAD_E_INVALID_PARAM',         -5);
69
define('HTTP_DOWNLOAD_E_INVALID_RESOURCE',      -6);
70
define('HTTP_DOWNLOAD_E_INVALID_REQUEST',       -7);
71
define('HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE',  -8);
72
define('HTTP_DOWNLOAD_E_INVALID_ARCHIVE_TYPE',  -9);
73
/**#@-**/
74
// }}}
75
 
76
/**
77
 * Send HTTP Downloads/Responses.
78
 *
79
 * With this package you can handle (hidden) downloads.
80
 * It supports partial downloads, resuming and sending
81
 * raw data ie. from database BLOBs.
82
 *
83
 * <i>ATTENTION:</i>
84
 * You shouldn't use this package together with ob_gzhandler or
85
 * zlib.output_compression enabled in your php.ini, especially
86
 * if you want to send already gzipped data!
87
 *
88
 * @access   public
89
 * @version  $Revision$
90
 */
91
class HTTP_Download
92
{
93
    // {{{ protected member variables
94
    /**
95
     * Path to file for download
96
     *
97
     * @see     HTTP_Download::setFile()
98
     * @access  protected
99
     * @var     string
100
     */
101
    var $file = '';
102
 
103
    /**
104
     * Data for download
105
     *
106
     * @see     HTTP_Download::setData()
107
     * @access  protected
108
     * @var     string
109
     */
110
    var $data = null;
111
 
112
    /**
113
     * Resource handle for download
114
     *
115
     * @see     HTTP_Download::setResource()
116
     * @access  protected
117
     * @var     int
118
     */
119
    var $handle = null;
120
 
121
    /**
122
     * Whether to gzip the download
123
     *
124
     * @access  protected
125
     * @var     bool
126
     */
127
    var $gzip = false;
128
 
129
    /**
130
     * Whether to allow caching of the download on the clients side
131
     *
132
     * @access  protected
133
     * @var     bool
134
     */
135
    var $cache = true;
136
 
137
    /**
138
     * Size of download
139
     *
140
     * @access  protected
141
     * @var     int
142
     */
143
    var $size = 0;
144
 
145
    /**
146
     * Last modified
147
     *
148
     * @access  protected
149
     * @var     int
150
     */
151
    var $lastModified = 0;
152
 
153
    /**
154
     * HTTP headers
155
     *
156
     * @access  protected
157
     * @var     array
158
     */
159
    var $headers   = array(
160
        'Content-Type'  => 'application/x-octetstream',
161
        'Pragma'        => 'cache',
162
        'Cache-Control' => 'public, must-revalidate, max-age=0',
163
        'Accept-Ranges' => 'bytes',
164
        'X-Sent-By'     => 'PEAR::HTTP::Download'
165
    );
166
 
167
    /**
168
     * HTTP_Header
169
     *
170
     * @access  protected
171
     * @var     object
172
     */
173
    var $HTTP = null;
174
 
175
    /**
176
     * ETag
177
     *
178
     * @access  protected
179
     * @var     string
180
     */
181
    var $etag = '';
182
 
183
    /**
184
     * Buffer Size
185
     *
186
     * @access  protected
187
     * @var     int
188
     */
189
    var $bufferSize = 2097152;
190
 
191
    /**
192
     * Throttle Delay
193
     *
194
     * @access  protected
195
     * @var     float
196
     */
197
    var $throttleDelay = 0;
198
 
199
    /**
200
     * Sent Bytes
201
     *
202
     * @access  public
203
     * @var     int
204
     */
205
    var $sentBytes = 0;
206
    // }}}
207
 
208
    // {{{ constructor
209
    /**
210
     * Constructor
211
     *
212
     * Set supplied parameters.
213
     *
214
     * @access  public
215
     * @param   array   $params     associative array of parameters
216
     *
217
     *          <b>one of:</b>
218
     *                  o 'file'                => path to file for download
219
     *                  o 'data'                => raw data for download
220
     *                  o 'resource'            => resource handle for download
221
     * <br/>
222
     *          <b>and any of:</b>
223
     *                  o 'cache'               => whether to allow cs caching
224
     *                  o 'gzip'                => whether to gzip the download
225
     *                  o 'lastmodified'        => unix timestamp
226
     *                  o 'contenttype'         => content type of download
227
     *                  o 'contentdisposition'  => content disposition
228
     *                  o 'buffersize'          => amount of bytes to buffer
229
     *                  o 'throttledelay'       => amount of secs to sleep
230
     *                  o 'cachecontrol'        => cache privacy and validity
231
     *
232
     * <br />
233
     * 'Content-Disposition' is not HTTP compliant, but most browsers
234
     * follow this header, so it was borrowed from MIME standard.
235
     *
236
     * It looks like this: <br />
237
     * "Content-Disposition: attachment; filename=example.tgz".
238
     *
239
     * @see HTTP_Download::setContentDisposition()
240
     */
241
    function HTTP_Download($params = array())
242
    {
243
        $this->HTTP = &new HTTP_Header;
244
        $this->setParams($params);
245
    }
246
    // }}}
247
 
248
    // {{{ public methods
249
    /**
250
     * Set parameters
251
     *
252
     * Set supplied parameters through its accessor methods.
253
     *
254
     * @access  public
255
     * @return  mixed   Returns true on success or PEAR_Error on failure.
256
     * @param   array   $params     associative array of parameters
257
     *
258
     * @see     HTTP_Download::HTTP_Download()
259
     */
260
    function setParams($params)
261
    {
262
        foreach((array) $params as $param => $value){
263
            $method = 'set'. $param;
264
 
265
            if (!method_exists($this, $method)) {
266
                return PEAR::raiseError(
267
                    "Method '$method' doesn't exist.",
268
                    HTTP_DOWNLOAD_E_INVALID_PARAM
269
                );
270
            }
271
 
272
            $e = call_user_func_array(array(&$this, $method), (array) $value);
273
 
274
            if (PEAR::isError($e)) {
275
                return $e;
276
            }
277
        }
278
        return true;
279
    }
280
 
281
    /**
282
     * Set path to file for download
283
     *
284
     * The Last-Modified header will be set to files filemtime(), actually.
285
     * Returns PEAR_Error (HTTP_DOWNLOAD_E_INVALID_FILE) if file doesn't exist.
286
     * Sends HTTP 404 status if $send_404 is set to true.
287
     *
288
     * @access  public
289
     * @return  mixed   Returns true on success or PEAR_Error on failure.
290
     * @param   string  $file       path to file for download
291
     * @param   bool    $send_404   whether to send HTTP/404 if
292
     *                              the file wasn't found
293
     */
294
    function setFile($file, $send_404 = true)
295
    {
296
        $file = realpath($file);
297
        if (!is_file($file)) {
298
            if ($send_404) {
299
                $this->HTTP->sendStatusCode(404);
300
            }
301
            return PEAR::raiseError(
302
                "File '$file' not found.",
303
                HTTP_DOWNLOAD_E_INVALID_FILE
304
            );
305
        }
306
        $this->setLastModified(filemtime($file));
307
        $this->file = $file;
308
        $this->size = filesize($file);
309
        return true;
310
    }
311
 
312
    /**
313
     * Set data for download
314
     *
315
     * Set $data to null if you want to unset this.
316
     *
317
     * @access  public
318
     * @return  void
319
     * @param   $data   raw data to send
320
     */
321
    function setData($data = null)
322
    {
323
        $this->data = $data;
324
        $this->size = strlen($data);
325
    }
326
 
327
    /**
328
     * Set resource for download
329
     *
330
     * The resource handle supplied will be closed after sending the download.
331
     * Returns a PEAR_Error (HTTP_DOWNLOAD_E_INVALID_RESOURCE) if $handle
332
     * is no valid resource. Set $handle to null if you want to unset this.
333
     *
334
     * @access  public
335
     * @return  mixed   Returns true on success or PEAR_Error on failure.
336
     * @param   int     $handle     resource handle
337
     */
338
    function setResource($handle = null)
339
    {
340
        if (!isset($handle)) {
341
            $this->handle = null;
342
            $this->size = 0;
343
            return true;
344
        }
345
 
346
        if (is_resource($handle)) {
347
            $this->handle = $handle;
348
            $filestats    = fstat($handle);
349
            $this->size   = $filestats['size'];
350
            return true;
351
        }
352
 
353
        return PEAR::raiseError(
354
            "Handle '$handle' is no valid resource.",
355
            HTTP_DOWNLOAD_E_INVALID_RESOURCE
356
        );
357
    }
358
 
359
    /**
360
     * Whether to gzip the download
361
     *
362
     * Returns a PEAR_Error (HTTP_DOWNLOAD_E_NO_EXT_ZLIB)
363
     * if ext/zlib is not available/loadable.
364
     *
365
     * @access  public
366
     * @return  mixed   Returns true on success or PEAR_Error on failure.
367
     * @param   bool    $gzip   whether to gzip the download
368
     */
369
    function setGzip($gzip = false)
370
    {
371
        if ($gzip && !PEAR::loadExtension('zlib')){
372
            return PEAR::raiseError(
373
                'GZIP compression (ext/zlib) not available.',
374
                HTTP_DOWNLOAD_E_NO_EXT_ZLIB
375
            );
376
        }
377
        $this->gzip = (bool) $gzip;
378
        return true;
379
    }
380
 
381
    /**
382
     * Whether to allow caching
383
     *
384
     * If set to true (default) we'll send some headers that are commonly
385
     * used for caching purposes like ETag, Cache-Control and Last-Modified.
386
     *
387
     * If caching is disabled, we'll send the download no matter if it
388
     * would actually be cached at the client side.
389
     *
390
     * @access  public
391
     * @return  void
392
     * @param   bool    $cache  whether to allow caching
393
     */
394
    function setCache($cache = true)
395
    {
396
        $this->cache = (bool) $cache;
397
    }
398
 
399
    /**
400
     * Whether to allow proxies to cache
401
     *
402
     * If set to 'private' proxies shouldn't cache the response.
403
     * This setting defaults to 'public' and affects only cached responses.
404
     *
405
     * @access  public
406
     * @return  bool
407
     * @param   string  $cache  private or public
408
     * @param   int     $maxage maximum age of the client cache entry
409
     */
410
    function setCacheControl($cache = 'public', $maxage = 0)
411
    {
412
        switch ($cache = strToLower($cache))
413
        {
414
            case 'private':
415
            case 'public':
416
                $this->headers['Cache-Control'] =
417
                    $cache .', must-revalidate, max-age='. abs($maxage);
418
                return true;
419
            break;
420
        }
421
        return false;
422
    }
423
 
424
    /**
425
     * Set ETag
426
     *
427
     * Sets a user-defined ETag for cache-validation.  The ETag is usually
428
     * generated by HTTP_Download through its payload information.
429
     *
430
     * @access  public
431
     * @return  void
432
     * @param   string  $etag Entity tag used for strong cache validation.
433
     */
434
    function setETag($etag = null)
435
    {
436
        $this->etag = (string) $etag;
437
    }
438
 
439
    /**
440
     * Set Size of Buffer
441
     *
442
     * The amount of bytes specified as buffer size is the maximum amount
443
     * of data read at once from resources or files.  The default size is 2M
444
     * (2097152 bytes).  Be aware that if you enable gzip compression and
445
     * you set a very low buffer size that the actual file size may grow
446
     * due to added gzip headers for each sent chunk of the specified size.
447
     *
448
     * Returns PEAR_Error (HTTP_DOWNLOAD_E_INVALID_PARAM) if $size is not
449
     * greater than 0 bytes.
450
     *
451
     * @access  public
452
     * @return  mixed   Returns true on success or PEAR_Error on failure.
453
     * @param   int     $bytes Amount of bytes to use as buffer.
454
     */
455
    function setBufferSize($bytes = 2097152)
456
    {
457
        if (0 >= $bytes) {
458
            return PEAR::raiseError(
459
                'Buffer size must be greater than 0 bytes ('. $bytes .' given)',
460
                HTTP_DOWNLOAD_E_INVALID_PARAM);
461
        }
462
        $this->bufferSize = abs($bytes);
463
        return true;
464
    }
465
 
466
    /**
467
     * Set Throttle Delay
468
     *
469
     * Set the amount of seconds to sleep after each chunck that has been
470
     * sent.  One can implement some sort of throttle through adjusting the
471
     * buffer size and the throttle delay.  With the following settings
472
     * HTTP_Download will sleep a second after each 25 K of data sent.
473
     *
474
     * <code>
475
     *  Array(
476
     *      'throttledelay' => 1,
477
     *      'buffersize'    => 1024 * 25,
478
     *  )
479
     * </code>
480
     *
481
     * Just be aware that if gzipp'ing is enabled, decreasing the chunk size
482
     * too much leads to proportionally increased network traffic due to added
483
     * gzip header and bottom bytes around each chunk.
484
     *
485
     * @access  public
486
     * @return  void
487
     * @param   float   $seconds    Amount of seconds to sleep after each
488
     *                              chunk that has been sent.
489
     */
490
    function setThrottleDelay($seconds = 0)
491
    {
492
        $this->throttleDelay = abs($seconds) * 1000;
493
    }
494
 
495
    /**
496
     * Set "Last-Modified"
497
     *
498
     * This is usually determined by filemtime() in HTTP_Download::setFile()
499
     * If you set raw data for download with HTTP_Download::setData() and you
500
     * want do send an appropiate "Last-Modified" header, you should call this
501
     * method.
502
     *
503
     * @access  public
504
     * @return  void
505
     * @param   int     unix timestamp
506
     */
507
    function setLastModified($last_modified)
508
    {
509
        $this->lastModified = $this->headers['Last-Modified'] = (int) $last_modified;
510
    }
511
 
512
    /**
513
     * Set Content-Disposition header
514
     *
515
     * @see HTTP_Download::HTTP_Download
516
     *
517
     * @access  public
518
     * @return  void
519
     * @param   string  $disposition    whether to send the download
520
     *                                  inline or as attachment
521
     * @param   string  $file_name      the filename to display in
522
     *                                  the browser's download window
523
     *
524
     * <b>Example:</b>
525
     * <code>
526
     * $HTTP_Download->setContentDisposition(
527
     *   HTTP_DOWNLOAD_ATTACHMENT,
528
     *   'download.tgz'
529
     * );
530
     * </code>
531
     */
532
    function setContentDisposition( $disposition    = HTTP_DOWNLOAD_ATTACHMENT,
533
                                    $file_name      = null)
534
    {
535
        $cd = $disposition;
536
        if (isset($file_name)) {
537
            $cd .= '; filename="' . $file_name . '"';
538
        } elseif ($this->file) {
539
            $cd .= '; filename="' . basename($this->file) . '"';
540
        }
541
        $this->headers['Content-Disposition'] = $cd;
542
    }
543
 
544
    /**
545
     * Set content type of the download
546
     *
547
     * Default content type of the download will be 'application/x-octetstream'.
548
     * Returns PEAR_Error (HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE) if
549
     * $content_type doesn't seem to be valid.
550
     *
551
     * @access  public
552
     * @return  mixed   Returns true on success or PEAR_Error on failure.
553
     * @param   string  $content_type   content type of file for download
554
     */
555
    function setContentType($content_type = 'application/x-octetstream')
556
    {
557
        if (!preg_match('/^[a-z]+\w*\/[a-z]+[\w.;= -]*$/', $content_type)) {
558
            return PEAR::raiseError(
559
                "Invalid content type '$content_type' supplied.",
560
                HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE
561
            );
562
        }
563
        $this->headers['Content-Type'] = $content_type;
564
        return true;
565
    }
566
 
567
    /**
568
     * Guess content type of file
569
     *
570
     * First we try to use PEAR::MIME_Type, if installed, to detect the content
571
     * type, else we check if ext/mime_magic is loaded and properly configured.
572
     *
573
     * Returns PEAR_Error if:
574
     *      o if PEAR::MIME_Type failed to detect a proper content type
575
     *        (HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE)
576
     *      o ext/magic.mime is not installed, or not properly configured
577
     *        (HTTP_DOWNLOAD_E_NO_EXT_MMAGIC)
578
     *      o mime_content_type() couldn't guess content type or returned
579
     *        a content type considered to be bogus by setContentType()
580
     *        (HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE)
581
     *
582
     * @access  public
583
     * @return  mixed   Returns true on success or PEAR_Error on failure.
584
     */
585
    function guessContentType()
586
    {
587
        if (class_exists('MIME_Type') || @include_once 'MIME/Type.php') {
588
            if (PEAR::isError($mime_type = MIME_Type::autoDetect($this->file))) {
589
                return PEAR::raiseError($mime_type->getMessage(),
590
                    HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE);
591
            }
592
            return $this->setContentType($mime_type);
593
        }
594
        if (!function_exists('mime_content_type')) {
595
            return PEAR::raiseError(
596
                'This feature requires ext/mime_magic!',
597
                HTTP_DOWNLOAD_E_NO_EXT_MMAGIC
598
            );
599
        }
600
        if (!is_file(ini_get('mime_magic.magicfile'))) {
601
            return PEAR::raiseError(
602
                'ext/mime_magic is loaded but not properly configured!',
603
                HTTP_DOWNLOAD_E_NO_EXT_MMAGIC
604
            );
605
        }
606
        if (!$content_type = @mime_content_type($this->file)) {
607
            return PEAR::raiseError(
608
                'Couldn\'t guess content type with mime_content_type().',
609
                HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE
610
            );
611
        }
612
        return $this->setContentType($content_type);
613
    }
614
 
615
    /**
616
     * Send
617
     *
618
     * Returns PEAR_Error if:
619
     *   o HTTP headers were already sent (HTTP_DOWNLOAD_E_HEADERS_SENT)
620
     *   o HTTP Range was invalid (HTTP_DOWNLOAD_E_INVALID_REQUEST)
621
     *
622
     * @access  public
623
     * @return  mixed   Returns true on success or PEAR_Error on failure.
624
     * @param   bool    $autoSetContentDisposition Whether to set the
625
     *                  Content-Disposition header if it isn't already.
626
     */
627
    function send($autoSetContentDisposition = true)
628
    {
629
        if (headers_sent()) {
630
            return PEAR::raiseError(
631
                'Headers already sent.',
632
                HTTP_DOWNLOAD_E_HEADERS_SENT
633
            );
634
        }
635
 
636
        if (!ini_get('safe_mode')) {
637
            @set_time_limit(0);
638
        }
639
 
640
        if ($autoSetContentDisposition &&
641
            !isset($this->headers['Content-Disposition'])) {
642
            $this->setContentDisposition();
643
        }
644
 
645
        if ($this->cache) {
646
            $this->headers['ETag'] = $this->generateETag();
647
            if ($this->isCached()) {
648
                $this->HTTP->sendStatusCode(304);
649
                $this->sendHeaders();
650
                return true;
651
            }
652
        } else {
653
            unset($this->headers['Last-Modified']);
654
        }
655
 
656
        if (ob_get_level()) {
657
        	while (@ob_end_clean());
658
        }
659
 
660
        if ($this->gzip) {
661
            @ob_start('ob_gzhandler');
662
        } else {
663
            ob_start();
664
        }
665
 
666
        $this->sentBytes = 0;
667
 
668
        if ($this->isRangeRequest()) {
669
            $this->HTTP->sendStatusCode(206);
670
            $chunks = $this->getChunks();
671
        } else {
672
            $this->HTTP->sendStatusCode(200);
673
            $chunks = array(array(0, $this->size));
674
            if (!$this->gzip && count(ob_list_handlers()) < 2) {
675
                $this->headers['Content-Length'] = $this->size;
676
            }
677
        }
678
 
679
        if (PEAR::isError($e = $this->sendChunks($chunks))) {
680
            ob_end_clean();
681
            $this->HTTP->sendStatusCode(416);
682
            return $e;
683
        }
684
 
685
        ob_end_flush();
686
        flush();
687
        return true;
688
    }
689
 
690
    /**
691
     * Static send
692
     *
693
     * @see     HTTP_Download::HTTP_Download()
694
     * @see     HTTP_Download::send()
695
     *
696
     * @static
697
     * @access  public
698
     * @return  mixed   Returns true on success or PEAR_Error on failure.
699
     * @param   array   $params     associative array of parameters
700
     * @param   bool    $guess      whether HTTP_Download::guessContentType()
701
     *                               should be called
702
     */
703
    function staticSend($params, $guess = false)
704
    {
705
        $d = &new HTTP_Download();
706
        $e = $d->setParams($params);
707
        if (PEAR::isError($e)) {
708
            return $e;
709
        }
710
        if ($guess) {
711
            $e = $d->guessContentType();
712
            if (PEAR::isError($e)) {
713
                return $e;
714
            }
715
        }
716
        return $d->send();
717
    }
718
 
719
    /**
720
     * Send a bunch of files or directories as an archive
721
     *
722
     * Example:
723
     * <code>
724
     *  require_once 'HTTP/Download.php';
725
     *  HTTP_Download::sendArchive(
726
     *      'myArchive.tgz',
727
     *      '/var/ftp/pub/mike',
728
     *      HTTP_DOWNLOAD_TGZ,
729
     *      '',
730
     *      '/var/ftp/pub'
731
     *  );
732
     * </code>
733
     *
734
     * @see         Archive_Tar::createModify()
735
     * @deprecated  use HTTP_Download_Archive::send()
736
     * @static
737
     * @access  public
738
     * @return  mixed   Returns true on success or PEAR_Error on failure.
739
     * @param   string  $name       name the sent archive should have
740
     * @param   mixed   $files      files/directories
741
     * @param   string  $type       archive type
742
     * @param   string  $add_path   path that should be prepended to the files
743
     * @param   string  $strip_path path that should be stripped from the files
744
     */
745
    function sendArchive(   $name,
746
                            $files,
747
                            $type       = HTTP_DOWNLOAD_TGZ,
748
                            $add_path   = '',
749
                            $strip_path = '')
750
    {
751
        require_once 'HTTP/Download/Archive.php';
752
        return HTTP_Download_Archive::send($name, $files, $type,
753
            $add_path, $strip_path);
754
    }
755
    // }}}
756
 
757
    // {{{ protected methods
758
    /**
759
     * Generate ETag
760
     *
761
     * @access  protected
762
     * @return  string
763
     */
764
    function generateETag()
765
    {
766
        if (!$this->etag) {
767
            if ($this->data) {
768
                $md5 = md5($this->data);
769
            } else {
770
                $fst = is_resource($this->handle) ?
771
                    fstat($this->handle) : stat($this->file);
772
                $md5 = md5($fst['mtime'] .'='. $fst['ino'] .'='. $fst['size']);
773
            }
774
            $this->etag = '"' . $md5 . '-' . crc32($md5) . '"';
775
        }
776
        return $this->etag;
777
    }
778
 
779
    /**
780
     * Send multiple chunks
781
     *
782
     * @access  protected
783
     * @return  mixed   Returns true on success or PEAR_Error on failure.
784
     * @param   array   $chunks
785
     */
786
    function sendChunks($chunks)
787
    {
788
        if (count($chunks) == 1) {
789
            return $this->sendChunk(current($chunks));
790
        }
791
 
792
        $bound = uniqid('HTTP_DOWNLOAD-', true);
793
        $cType = $this->headers['Content-Type'];
794
        $this->headers['Content-Type'] =
795
            'multipart/byteranges; boundary=' . $bound;
796
        $this->sendHeaders();
797
        foreach ($chunks as $chunk){
798
            if (PEAR::isError($e = $this->sendChunk($chunk, $cType, $bound))) {
799
                return $e;
800
            }
801
        }
802
        #echo "\r\n--$bound--\r\n";
803
        return true;
804
    }
805
 
806
    /**
807
     * Send chunk of data
808
     *
809
     * @access  protected
810
     * @return  mixed   Returns true on success or PEAR_Error on failure.
811
     * @param   array   $chunk  start and end offset of the chunk to send
812
     * @param   string  $cType  actual content type
813
     * @param   string  $bound  boundary for multipart/byteranges
814
     */
815
    function sendChunk($chunk, $cType = null, $bound = null)
816
    {
817
        list($offset, $lastbyte) = $chunk;
818
        $length = ($lastbyte - $offset) + 1;
819
 
820
        if ($length < 1) {
821
            return PEAR::raiseError(
822
                "Error processing range request: $offset-$lastbyte/$length",
823
                HTTP_DOWNLOAD_E_INVALID_REQUEST
824
            );
825
        }
826
 
827
        $range = $offset . '-' . $lastbyte . '/' . $this->size;
828
 
829
        if (isset($cType, $bound)) {
830
            echo    "\r\n--$bound\r\n",
831
                    "Content-Type: $cType\r\n",
832
                    "Content-Range: bytes $range\r\n\r\n";
833
        } else {
834
            if ($this->isRangeRequest()) {
835
                $this->headers['Content-Length'] = $length;
836
                $this->headers['Content-Range'] = 'bytes '. $range;
837
            }
838
            $this->sendHeaders();
839
        }
840
 
841
        if ($this->data) {
842
            while (($length -= $this->bufferSize) > 0) {
843
                $this->flush(substr($this->data, $offset, $this->bufferSize));
844
                $this->throttleDelay and $this->sleep();
845
                $offset += $this->bufferSize;
846
            }
847
            if ($length) {
848
                $this->flush(substr($this->data, $offset, $this->bufferSize + $length));
849
            }
850
        } else {
851
            if (!is_resource($this->handle)) {
852
                $this->handle = fopen($this->file, 'rb');
853
            }
854
            fseek($this->handle, $offset);
855
            while (($length -= $this->bufferSize) > 0) {
856
                $this->flush(fread($this->handle, $this->bufferSize));
857
                $this->throttleDelay and $this->sleep();
858
            }
859
            if ($length) {
860
                $this->flush(fread($this->handle, $this->bufferSize + $length));
861
            }
862
        }
863
        return true;
864
    }
865
 
866
    /**
867
     * Get chunks to send
868
     *
869
     * @access  protected
870
     * @return  array
871
     */
872
    function getChunks()
873
    {
874
        $parts = array();
875
        foreach (explode(',', $this->getRanges()) as $chunk){
876
            list($o, $e) = explode('-', $chunk);
877
            if ($e >= $this->size || (empty($e) && $e !== 0 && $e !== '0')) {
878
                $e = $this->size - 1;
879
            }
880
            if (empty($o) && $o !== 0 && $o !== '0') {
881
                $o = $this->size - $e;
882
                $e = $this->size - 1;
883
            }
884
            $parts[] = array($o, $e);
885
        }
886
        return $parts;
887
    }
888
 
889
    /**
890
     * Check if range is requested
891
     *
892
     * @access  protected
893
     * @return  bool
894
     */
895
    function isRangeRequest()
896
    {
897
        if (!isset($_SERVER['HTTP_RANGE'])) {
898
            return false;
899
        }
900
        return $this->isValidRange();
901
    }
902
 
903
    /**
904
     * Get range request
905
     *
906
     * @access  protected
907
     * @return  array
908
     */
909
    function getRanges()
910
    {
911
        return preg_match('/^bytes=((\d*-\d*,? ?)+)$/',
912
            @$_SERVER['HTTP_RANGE'], $matches) ? $matches[1] : array();
913
    }
914
 
915
    /**
916
     * Check if entity is cached
917
     *
918
     * @access  protected
919
     * @return  bool
920
     */
921
    function isCached()
922
    {
923
        return (
924
            (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) &&
925
            $this->lastModified == strtotime(current($a = explode(
926
                ';', $_SERVER['HTTP_IF_MODIFIED_SINCE'])))) ||
927
            (isset($_SERVER['HTTP_IF_NONE_MATCH']) &&
928
            $this->compareAsterisk('HTTP_IF_NONE_MATCH', $this->etag))
929
        );
930
    }
931
 
932
    /**
933
     * Check if entity hasn't changed
934
     *
935
     * @access  protected
936
     * @return  bool
937
     */
938
    function isValidRange()
939
    {
940
        if (isset($_SERVER['HTTP_IF_MATCH']) &&
941
            !$this->compareAsterisk('HTTP_IF_MATCH', $this->etag)) {
942
            return false;
943
        }
944
        if (isset($_SERVER['HTTP_IF_RANGE']) &&
945
                  $_SERVER['HTTP_IF_RANGE'] !== $this->etag &&
946
                  strtotime($_SERVER['HTTP_IF_RANGE']) !== $this->lastModified) {
947
            return false;
948
        }
949
        if (isset($_SERVER['HTTP_IF_UNMODIFIED_SINCE'])) {
950
            $lm = current($a = explode(';', $_SERVER['HTTP_IF_UNMODIFIED_SINCE']));
951
            if (strtotime($lm) !== $this->lastModified) {
952
                return false;
953
            }
954
        }
955
        if (isset($_SERVER['HTTP_UNLESS_MODIFIED_SINCE'])) {
956
            $lm = current($a = explode(';', $_SERVER['HTTP_UNLESS_MODIFIED_SINCE']));
957
            if (strtotime($lm) !== $this->lastModified) {
958
                return false;
959
            }
960
        }
961
        return true;
962
    }
963
 
964
    /**
965
     * Compare against an asterisk or check for equality
966
     *
967
     * @access  protected
968
     * @return  bool
969
     * @param   string  key for the $_SERVER array
970
     * @param   string  string to compare
971
     */
972
    function compareAsterisk($svar, $compare)
973
    {
974
        foreach (array_map('trim', explode(',', $_SERVER[$svar])) as $request) {
975
            if ($request === '*' || $request === $compare) {
976
                return true;
977
            }
978
        }
979
        return false;
980
    }
981
 
982
    /**
983
     * Send HTTP headers
984
     *
985
     * @access  protected
986
     * @return  void
987
     */
988
    function sendHeaders()
989
    {
990
        foreach ($this->headers as $header => $value) {
991
            $this->HTTP->setHeader($header, $value);
992
        }
993
        $this->HTTP->sendHeaders();
994
        /* NSAPI won't output anything if we did this */
995
        if (strncasecmp(PHP_SAPI, 'nsapi', 5)) {
996
            ob_flush();
997
            flush();
998
        }
999
    }
1000
 
1001
    /**
1002
     * Flush
1003
     *
1004
     * @access  protected
1005
     * @return  void
1006
     * @param   string  $data
1007
     */
1008
    function flush($data = '')
1009
    {
1010
        if ($dlen = strlen($data)) {
1011
            $this->sentBytes += $dlen;
1012
            echo $data;
1013
        }
1014
        ob_flush();
1015
        flush();
1016
    }
1017
 
1018
    /**
1019
     * Sleep
1020
     *
1021
     * @access  protected
1022
     * @return  void
1023
     */
1024
    function sleep()
1025
    {
1026
        if (OS_WINDOWS) {
1027
            com_message_pump($this->throttleDelay);
1028
        } else {
1029
            usleep($this->throttleDelay * 1000);
1030
        }
1031
    }
1032
    // }}}
1033
}
1034
?>