Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 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: Download.php 304423 2010-10-15 13:36:46Z clockwerx $
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: 304423 $
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
     * Startup error
209
     *
210
     * @var    PEAR_Error
211
     * @access protected
212
     */
213
    var $_error = null;
214
    // }}}
215
 
216
    // {{{ constructor
217
    /**
218
     * Constructor
219
     *
220
     * Set supplied parameters.
221
     *
222
     * @access  public
223
     * @param   array   $params     associative array of parameters
224
     *  <strong>one of:</strong>
225
     *  <ul>
226
     *    <li>'file'               => path to file for download</li>
227
     *    <li>'data'               => raw data for download</li>
228
     *    <li>'resource'           => resource handle for download</li>
229
     *  </ul>
230
     *  <strong>and any of:</strong>
231
     *  <ul>
232
     *    <li>'cache'              => whether to allow cs caching</li>
233
     *    <li>'gzip'               => whether to gzip the download</li>
234
     *    <li>'lastmodified'       => unix timestamp</li>
235
     *    <li>'contenttype'        => content type of download</li>
236
     *    <li>'contentdisposition' => content disposition</li>
237
     *    <li>'buffersize'         => amount of bytes to buffer</li>
238
     *    <li>'throttledelay'      => amount of secs to sleep</li>
239
     *    <li>'cachecontrol'       => cache privacy and validity</li>
240
     *  </ul>
241
     *
242
     * 'Content-Disposition' is not HTTP compliant, but most browsers
243
     * follow this header, so it was borrowed from MIME standard.
244
     *
245
     * It looks like this:
246
     * "Content-Disposition: attachment; filename=example.tgz".
247
     *
248
     * @see HTTP_Download::setContentDisposition()
249
     */
250
    function HTTP_Download($params = array())
251
    {
252
        $this->HTTP = &new HTTP_Header;
253
        $this->_error = $this->setParams($params);
254
    }
255
    // }}}
256
 
257
    // {{{ public methods
258
    /**
259
     * Set parameters
260
     *
261
     * Set supplied parameters through its accessor methods.
262
     *
263
     * @access  public
264
     * @return  mixed   Returns true on success or PEAR_Error on failure.
265
     * @param   array   $params     associative array of parameters
266
     *
267
     * @see     HTTP_Download::HTTP_Download()
268
     */
269
    function setParams($params)
270
    {
271
        $error = $this->_getError();
272
        if ($error !== null) {
273
            return $error;
274
        }
275
        foreach((array) $params as $param => $value){
276
            $method = 'set'. $param;
277
 
278
            if (!method_exists($this, $method)) {
279
                return PEAR::raiseError(
280
                    "Method '$method' doesn't exist.",
281
                    HTTP_DOWNLOAD_E_INVALID_PARAM
282
                );
283
            }
284
 
285
            $e = call_user_func_array(array(&$this, $method), (array) $value);
286
 
287
            if (PEAR::isError($e)) {
288
                return $e;
289
            }
290
        }
291
        return true;
292
    }
293
 
294
    /**
295
     * Set path to file for download
296
     *
297
     * The Last-Modified header will be set to files filemtime(), actually.
298
     * Returns PEAR_Error (HTTP_DOWNLOAD_E_INVALID_FILE) if file doesn't exist.
299
     * Sends HTTP 404 or 403 status if $send_error is set to true.
300
     *
301
     * @access  public
302
     * @return  mixed   Returns true on success or PEAR_Error on failure.
303
     * @param   string  $file       path to file for download
304
     * @param   bool    $send_error whether to send HTTP/404 or 403 if
305
     *                              the file wasn't found or is not readable
306
     */
307
    function setFile($file, $send_error = true)
308
    {
309
        $error = $this->_getError();
310
        if ($error !== null) {
311
            return $error;
312
        }
313
        $file = realpath($file);
314
        if (!is_file($file)) {
315
            if ($send_error) {
316
                $this->HTTP->sendStatusCode(404);
317
            }
318
            return PEAR::raiseError(
319
                "File '$file' not found.",
320
                HTTP_DOWNLOAD_E_INVALID_FILE
321
            );
322
        }
323
        if (!is_readable($file)) {
324
            if ($send_error) {
325
                $this->HTTP->sendStatusCode(403);
326
            }
327
            return PEAR::raiseError(
328
                "Cannot read file '$file'.",
329
                HTTP_DOWNLOAD_E_INVALID_FILE
330
            );
331
        }
332
        $this->setLastModified(filemtime($file));
333
        $this->file = $file;
334
        $this->size = filesize($file);
335
        return true;
336
    }
337
 
338
    /**
339
     * Set data for download
340
     *
341
     * Set $data to null if you want to unset this.
342
     *
343
     * @access  public
344
     * @return  void
345
     * @param   $data   raw data to send
346
     */
347
    function setData($data = null)
348
    {
349
        $this->data = $data;
350
        $this->size = strlen($data);
351
    }
352
 
353
    /**
354
     * Set resource for download
355
     *
356
     * The resource handle supplied will be closed after sending the download.
357
     * Returns a PEAR_Error (HTTP_DOWNLOAD_E_INVALID_RESOURCE) if $handle
358
     * is no valid resource. Set $handle to null if you want to unset this.
359
     *
360
     * @access  public
361
     * @return  mixed   Returns true on success or PEAR_Error on failure.
362
     * @param   int     $handle     resource handle
363
     */
364
    function setResource($handle = null)
365
    {
366
        $error = $this->_getError();
367
        if ($error !== null) {
368
            return $error;
369
        }
370
        if (!isset($handle)) {
371
            $this->handle = null;
372
            $this->size = 0;
373
            return true;
374
        }
375
 
376
        if (is_resource($handle)) {
377
            $this->handle = $handle;
378
            $filestats    = fstat($handle);
379
            $this->size   = isset($filestats['size']) ? $filestats['size']
380
                                                      : -1;
381
            return true;
382
        }
383
 
384
        return PEAR::raiseError(
385
            "Handle '$handle' is no valid resource.",
386
            HTTP_DOWNLOAD_E_INVALID_RESOURCE
387
        );
388
    }
389
 
390
    /**
391
     * Whether to gzip the download
392
     *
393
     * Returns a PEAR_Error (HTTP_DOWNLOAD_E_NO_EXT_ZLIB)
394
     * if ext/zlib is not available/loadable.
395
     *
396
     * @access  public
397
     * @return  mixed   Returns true on success or PEAR_Error on failure.
398
     * @param   bool    $gzip   whether to gzip the download
399
     */
400
    function setGzip($gzip = false)
401
    {
402
        $error = $this->_getError();
403
        if ($error !== null) {
404
            return $error;
405
        }
406
        if ($gzip && !PEAR::loadExtension('zlib')){
407
            return PEAR::raiseError(
408
                'GZIP compression (ext/zlib) not available.',
409
                HTTP_DOWNLOAD_E_NO_EXT_ZLIB
410
            );
411
        }
412
        $this->gzip = (bool) $gzip;
413
        return true;
414
    }
415
 
416
    /**
417
     * Whether to allow caching
418
     *
419
     * If set to true (default) we'll send some headers that are commonly
420
     * used for caching purposes like ETag, Cache-Control and Last-Modified.
421
     *
422
     * If caching is disabled, we'll send the download no matter if it
423
     * would actually be cached at the client side.
424
     *
425
     * @access  public
426
     * @return  void
427
     * @param   bool    $cache  whether to allow caching
428
     */
429
    function setCache($cache = true)
430
    {
431
        $this->cache = (bool) $cache;
432
    }
433
 
434
    /**
435
     * Whether to allow proxies to cache
436
     *
437
     * If set to 'private' proxies shouldn't cache the response.
438
     * This setting defaults to 'public' and affects only cached responses.
439
     *
440
     * @access  public
441
     * @return  bool
442
     * @param   string  $cache  private or public
443
     * @param   int     $maxage maximum age of the client cache entry
444
     */
445
    function setCacheControl($cache = 'public', $maxage = 0)
446
    {
447
        switch ($cache = strToLower($cache))
448
        {
449
            case 'private':
450
            case 'public':
451
                $this->headers['Cache-Control'] =
452
                    $cache .', must-revalidate, max-age='. abs($maxage);
453
                return true;
454
            break;
455
        }
456
        return false;
457
    }
458
 
459
    /**
460
     * Set ETag
461
     *
462
     * Sets a user-defined ETag for cache-validation.  The ETag is usually
463
     * generated by HTTP_Download through its payload information.
464
     *
465
     * @access  public
466
     * @return  void
467
     * @param   string  $etag Entity tag used for strong cache validation.
468
     */
469
    function setETag($etag = null)
470
    {
471
        $this->etag = (string) $etag;
472
    }
473
 
474
    /**
475
     * Set Size of Buffer
476
     *
477
     * The amount of bytes specified as buffer size is the maximum amount
478
     * of data read at once from resources or files.  The default size is 2M
479
     * (2097152 bytes).  Be aware that if you enable gzip compression and
480
     * you set a very low buffer size that the actual file size may grow
481
     * due to added gzip headers for each sent chunk of the specified size.
482
     *
483
     * Returns PEAR_Error (HTTP_DOWNLOAD_E_INVALID_PARAM) if $size is not
484
     * greater than 0 bytes.
485
     *
486
     * @access  public
487
     * @return  mixed   Returns true on success or PEAR_Error on failure.
488
     * @param   int     $bytes Amount of bytes to use as buffer.
489
     */
490
    function setBufferSize($bytes = 2097152)
491
    {
492
        $error = $this->_getError();
493
        if ($error !== null) {
494
            return $error;
495
        }
496
        if (0 >= $bytes) {
497
            return PEAR::raiseError(
498
                'Buffer size must be greater than 0 bytes ('. $bytes .' given)',
499
                HTTP_DOWNLOAD_E_INVALID_PARAM);
500
        }
501
        $this->bufferSize = abs($bytes);
502
        return true;
503
    }
504
 
505
    /**
506
     * Set Throttle Delay
507
     *
508
     * Set the amount of seconds to sleep after each chunck that has been
509
     * sent.  One can implement some sort of throttle through adjusting the
510
     * buffer size and the throttle delay.  With the following settings
511
     * HTTP_Download will sleep a second after each 25 K of data sent.
512
     *
513
     * <code>
514
     *  Array(
515
     *      'throttledelay' => 1,
516
     *      'buffersize'    => 1024 * 25,
517
     *  )
518
     * </code>
519
     *
520
     * Just be aware that if gzipp'ing is enabled, decreasing the chunk size
521
     * too much leads to proportionally increased network traffic due to added
522
     * gzip header and bottom bytes around each chunk.
523
     *
524
     * @access  public
525
     * @return  void
526
     * @param   float   $seconds    Amount of seconds to sleep after each
527
     *                              chunk that has been sent.
528
     */
529
    function setThrottleDelay($seconds = 0)
530
    {
531
        $this->throttleDelay = abs($seconds) * 1000;
532
    }
533
 
534
    /**
535
     * Set "Last-Modified"
536
     *
537
     * This is usually determined by filemtime() in HTTP_Download::setFile()
538
     * If you set raw data for download with HTTP_Download::setData() and you
539
     * want do send an appropiate "Last-Modified" header, you should call this
540
     * method.
541
     *
542
     * @access  public
543
     * @return  void
544
     * @param   int     unix timestamp
545
     */
546
    function setLastModified($last_modified)
547
    {
548
        $this->lastModified = $this->headers['Last-Modified'] = (int) $last_modified;
549
    }
550
 
551
    /**
552
     * Set Content-Disposition header
553
     *
554
     * @see HTTP_Download::HTTP_Download
555
     *
556
     * @access  public
557
     * @return  void
558
     * @param   string  $disposition    whether to send the download
559
     *                                  inline or as attachment
560
     * @param   string  $file_name      the filename to display in
561
     *                                  the browser's download window
562
     *
563
     * <b>Example:</b>
564
     * <code>
565
     * $HTTP_Download->setContentDisposition(
566
     *   HTTP_DOWNLOAD_ATTACHMENT,
567
     *   'download.tgz'
568
     * );
569
     * </code>
570
     */
571
    function setContentDisposition( $disposition    = HTTP_DOWNLOAD_ATTACHMENT,
572
                                    $file_name      = null)
573
    {
574
        $cd = $disposition;
575
        if (isset($file_name)) {
576
            $cd .= '; filename="' . $file_name . '"';
577
        } elseif ($this->file) {
578
            $cd .= '; filename="' . basename($this->file) . '"';
579
        }
580
        $this->headers['Content-Disposition'] = $cd;
581
    }
582
 
583
    /**
584
     * Set content type of the download
585
     *
586
     * Default content type of the download will be 'application/x-octetstream'.
587
     * Returns PEAR_Error (HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE) if
588
     * $content_type doesn't seem to be valid.
589
     *
590
     * @access  public
591
     * @return  mixed   Returns true on success or PEAR_Error on failure.
592
     * @param   string  $content_type   content type of file for download
593
     */
594
    function setContentType($content_type = 'application/x-octetstream')
595
    {
596
        $error = $this->_getError();
597
        if ($error !== null) {
598
            return $error;
599
        }
600
        if (!preg_match('/^[a-z]+\w*\/[a-z]+[\w.;= -]*$/', $content_type)) {
601
            return PEAR::raiseError(
602
                "Invalid content type '$content_type' supplied.",
603
                HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE
604
            );
605
        }
606
        $this->headers['Content-Type'] = $content_type;
607
        return true;
608
    }
609
 
610
    /**
611
     * Guess content type of file
612
     *
613
     * First we try to use PEAR::MIME_Type, if installed, to detect the content
614
     * type, else we check if ext/mime_magic is loaded and properly configured.
615
     *
616
     * Returns PEAR_Error if:
617
     *      o if PEAR::MIME_Type failed to detect a proper content type
618
     *        (HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE)
619
     *      o ext/magic.mime is not installed, or not properly configured
620
     *        (HTTP_DOWNLOAD_E_NO_EXT_MMAGIC)
621
     *      o mime_content_type() couldn't guess content type or returned
622
     *        a content type considered to be bogus by setContentType()
623
     *        (HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE)
624
     *
625
     * @access  public
626
     * @return  mixed   Returns true on success or PEAR_Error on failure.
627
     */
628
    function guessContentType()
629
    {
630
        $error = $this->_getError();
631
        if ($error !== null) {
632
            return $error;
633
        }
634
        if (class_exists('MIME_Type') || @include_once 'MIME/Type.php') {
635
            if (PEAR::isError($mime_type = MIME_Type::autoDetect($this->file))) {
636
                return PEAR::raiseError($mime_type->getMessage(),
637
                    HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE);
638
            }
639
            return $this->setContentType($mime_type);
640
        }
641
        if (!function_exists('mime_content_type')) {
642
            return PEAR::raiseError(
643
                'This feature requires ext/mime_magic!',
644
                HTTP_DOWNLOAD_E_NO_EXT_MMAGIC
645
            );
646
        }
647
        if (!is_file(ini_get('mime_magic.magicfile'))) {
648
            return PEAR::raiseError(
649
                'ext/mime_magic is loaded but not properly configured!',
650
                HTTP_DOWNLOAD_E_NO_EXT_MMAGIC
651
            );
652
        }
653
        if (!$content_type = @mime_content_type($this->file)) {
654
            return PEAR::raiseError(
655
                'Couldn\'t guess content type with mime_content_type().',
656
                HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE
657
            );
658
        }
659
        return $this->setContentType($content_type);
660
    }
661
 
662
    /**
663
     * Send
664
     *
665
     * Returns PEAR_Error if:
666
     *   o HTTP headers were already sent (HTTP_DOWNLOAD_E_HEADERS_SENT)
667
     *   o HTTP Range was invalid (HTTP_DOWNLOAD_E_INVALID_REQUEST)
668
     *
669
     * @access  public
670
     * @return  mixed   Returns true on success or PEAR_Error on failure.
671
     * @param   bool    $autoSetContentDisposition Whether to set the
672
     *                  Content-Disposition header if it isn't already.
673
     */
674
    function send($autoSetContentDisposition = true)
675
    {
676
        $error = $this->_getError();
677
        if ($error !== null) {
678
            return $error;
679
        }
680
        if (headers_sent()) {
681
            return PEAR::raiseError(
682
                'Headers already sent.',
683
                HTTP_DOWNLOAD_E_HEADERS_SENT
684
            );
685
        }
686
 
687
        if (!ini_get('safe_mode')) {
688
            @set_time_limit(0);
689
        }
690
 
691
        if ($autoSetContentDisposition &&
692
            !isset($this->headers['Content-Disposition'])) {
693
            $this->setContentDisposition();
694
        }
695
 
696
        if ($this->cache) {
697
            $this->headers['ETag'] = $this->generateETag();
698
            if ($this->isCached()) {
699
                $this->HTTP->sendStatusCode(304);
700
                $this->sendHeaders();
701
                return true;
702
            }
703
        } else {
704
            unset($this->headers['Last-Modified']);
705
        }
706
 
707
        if (ob_get_level()) {
708
            while (@ob_end_clean());
709
        }
710
 
711
        if ($this->gzip) {
712
            @ob_start('ob_gzhandler');
713
        } else {
714
            ob_start();
715
        }
716
 
717
        $this->sentBytes = 0;
718
 
719
        // Known content length?
720
        $end = ($this->size >= 0) ? max($this->size - 1, 0) : '*';
721
 
722
        if ($end != '*' && $this->isRangeRequest()) {
723
             $chunks = $this->getChunks();
724
            if (empty($chunks)) {
725
                $this->HTTP->sendStatusCode(200);
726
                $chunks = array(array(0, $end));
727
 
728
            } elseif (PEAR::isError($chunks)) {
729
                ob_end_clean();
730
                $this->HTTP->sendStatusCode(416);
731
                return $chunks;
732
 
733
            } else {
734
                $this->HTTP->sendStatusCode(206);
735
            }
736
        } else {
737
            $this->HTTP->sendStatusCode(200);
738
            $chunks = array(array(0, $end));
739
            if (!$this->gzip && count(ob_list_handlers()) < 2 && $end != '*') {
740
                $this->headers['Content-Length'] = $this->size;
741
            }
742
        }
743
 
744
        $this->sendChunks($chunks);
745
 
746
        ob_end_flush();
747
        flush();
748
        return true;
749
    }
750
 
751
    /**
752
     * Static send
753
     *
754
     * @see     HTTP_Download::HTTP_Download()
755
     * @see     HTTP_Download::send()
756
     *
757
     * @static
758
     * @access  public
759
     * @return  mixed   Returns true on success or PEAR_Error on failure.
760
     * @param   array   $params     associative array of parameters
761
     * @param   bool    $guess      whether HTTP_Download::guessContentType()
762
     *                               should be called
763
     */
764
    function staticSend($params, $guess = false)
765
    {
766
        $d = &new HTTP_Download();
767
        $e = $d->setParams($params);
768
        if (PEAR::isError($e)) {
769
            return $e;
770
        }
771
        if ($guess) {
772
            $e = $d->guessContentType();
773
            if (PEAR::isError($e)) {
774
                return $e;
775
            }
776
        }
777
        return $d->send();
778
    }
779
 
780
    /**
781
     * Send a bunch of files or directories as an archive
782
     *
783
     * Example:
784
     * <code>
785
     *  require_once 'HTTP/Download.php';
786
     *  HTTP_Download::sendArchive(
787
     *      'myArchive.tgz',
788
     *      '/var/ftp/pub/mike',
789
     *      HTTP_DOWNLOAD_TGZ,
790
     *      '',
791
     *      '/var/ftp/pub'
792
     *  );
793
     * </code>
794
     *
795
     * @see         Archive_Tar::createModify()
796
     * @deprecated  use HTTP_Download_Archive::send()
797
     * @static
798
     * @access  public
799
     * @return  mixed   Returns true on success or PEAR_Error on failure.
800
     * @param   string  $name       name the sent archive should have
801
     * @param   mixed   $files      files/directories
802
     * @param   string  $type       archive type
803
     * @param   string  $add_path   path that should be prepended to the files
804
     * @param   string  $strip_path path that should be stripped from the files
805
     */
806
    function sendArchive(   $name,
807
                            $files,
808
                            $type       = HTTP_DOWNLOAD_TGZ,
809
                            $add_path   = '',
810
                            $strip_path = '')
811
    {
812
        require_once 'HTTP/Download/Archive.php';
813
        return HTTP_Download_Archive::send($name, $files, $type,
814
            $add_path, $strip_path);
815
    }
816
    // }}}
817
 
818
    // {{{ protected methods
819
    /**
820
     * Generate ETag
821
     *
822
     * @access  protected
823
     * @return  string
824
     */
825
    function generateETag()
826
    {
827
        if (!$this->etag) {
828
            if ($this->data) {
829
                $md5 = md5($this->data);
830
            } else {
831
                $mtime = time();
832
                $ino   = 0;
833
                $size  = mt_rand();
834
                extract(is_resource($this->handle) ? fstat($this->handle)
835
                                                   : stat($this->file));
836
                $md5 = md5($mtime .'='. $ino .'='. $size);
837
            }
838
            $this->etag = '"' . $md5 . '-' . crc32($md5) . '"';
839
        }
840
        return $this->etag;
841
    }
842
 
843
    /**
844
     * Send multiple chunks
845
     *
846
     * @access  protected
847
     * @return  mixed   Returns true on success or PEAR_Error on failure.
848
     * @param   array   $chunks
849
     */
850
    function sendChunks($chunks)
851
    {
852
        if (count($chunks) == 1) {
853
            return $this->sendChunk(current($chunks));
854
        }
855
 
856
        $bound = uniqid('HTTP_DOWNLOAD-', true);
857
        $cType = $this->headers['Content-Type'];
858
        $this->headers['Content-Type'] =
859
            'multipart/byteranges; boundary=' . $bound;
860
        $this->sendHeaders();
861
        foreach ($chunks as $chunk){
862
            $this->sendChunk($chunk, $cType, $bound);
863
        }
864
        #echo "\r\n--$bound--\r\n";
865
        return true;
866
    }
867
 
868
    /**
869
     * Send chunk of data
870
     *
871
     * @access  protected
872
     * @return  mixed   Returns true on success or PEAR_Error on failure.
873
     * @param   array   $chunk  start and end offset of the chunk to send
874
     * @param   string  $cType  actual content type
875
     * @param   string  $bound  boundary for multipart/byteranges
876
     */
877
    function sendChunk($chunk, $cType = null, $bound = null)
878
    {
879
        list($offset, $lastbyte) = $chunk;
880
        $length = ($lastbyte - $offset) + 1;
881
 
882
        $range = $offset . '-' . $lastbyte . '/'
883
                 . (($this->size >= 0) ? $this->size : '*');
884
 
885
        if (isset($cType, $bound)) {
886
            echo    "\r\n--$bound\r\n",
887
                    "Content-Type: $cType\r\n",
888
                    "Content-Range: bytes $range\r\n\r\n";
889
        } else {
890
            if ($lastbyte != '*' && $this->isRangeRequest()) {
891
                $this->headers['Content-Length'] = $length;
892
                $this->headers['Content-Range'] = 'bytes '. $range;
893
            }
894
            $this->sendHeaders();
895
        }
896
 
897
        if ($this->data) {
898
            while (($length -= $this->bufferSize) > 0) {
899
                $this->flush(substr($this->data, $offset, $this->bufferSize));
900
                $this->throttleDelay and $this->sleep();
901
                $offset += $this->bufferSize;
902
            }
903
            if ($length) {
904
                $this->flush(substr($this->data, $offset, $this->bufferSize + $length));
905
            }
906
        } else {
907
            if (!is_resource($this->handle)) {
908
                $this->handle = fopen($this->file, 'rb');
909
            }
910
            fseek($this->handle, $offset);
911
            if ($lastbyte == '*') {
912
                while (!feof($this->handle)) {
913
                    $this->flush(fread($this->handle, $this->bufferSize));
914
                    $this->throttleDelay and $this->sleep();
915
                }
916
            } else {
917
                while (($length -= $this->bufferSize) > 0) {
918
                    $this->flush(fread($this->handle, $this->bufferSize));
919
                    $this->throttleDelay and $this->sleep();
920
                }
921
                if ($length) {
922
                    $this->flush(fread($this->handle, $this->bufferSize + $length));
923
                }
924
             }
925
         }
926
         return true;
927
    }
928
 
929
    /**
930
     * Get chunks to send
931
     *
932
     * @access  protected
933
     * @return  array Chunk list or PEAR_Error on invalid range request
934
     * @link    http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35
935
     */
936
    function getChunks()
937
    {
938
        $end = ($this->size >= 0) ? max($this->size - 1, 0) : '*';
939
 
940
        // Trying to handle ranges on content with unknown length is too
941
        // big of a mess (impossible to determine if a range is valid)
942
        if ($end == '*') {
943
            return array();
944
        }
945
 
946
        $ranges = $this->getRanges();
947
        if (empty($ranges)) {
948
            return array();
949
        }
950
 
951
        $parts = array();
952
        $satisfiable = false;
953
        foreach (explode(',', $ranges) as $chunk){
954
            list($o, $e) = explode('-', trim($chunk));
955
 
956
            // If the last-byte-pos value is present, it MUST be greater than
957
            // or equal to the first-byte-pos in that byte-range-spec, or the
958
            // byte- range-spec is syntactically invalid. The recipient of a
959
            // byte-range- set that includes one or more syntactically invalid
960
            // byte-range-spec values MUST ignore the header field that
961
            // includes that byte-range- set.
962
            if ($e !== '' && $o !== '' && $e < $o) {
963
                return array();
964
            }
965
 
966
            // If the last-byte-pos value is absent, or if the value is
967
            // greater than or equal to the current length of the entity-body,
968
            // last-byte-pos is taken to be equal to one less than the current
969
            // length of the entity- body in bytes.
970
            if ($e === '' || $e > $end) {
971
                $e = $end;
972
            }
973
 
974
            // A suffix-byte-range-spec is used to specify the suffix of the
975
            // entity-body, of a length given by the suffix-length value. (That
976
            // is, this form specifies the last N bytes of an entity-body.) If
977
            // the entity is shorter than the specified suffix-length, the
978
            // entire entity-body is used.
979
            if ($o === '') {
980
                // If a syntactically valid byte-range-set includes at least
981
                // one suffix-byte-range-spec with a non-zero suffix-length,
982
                // then the byte-range-set is satisfiable.
983
                $satisfiable |= ($e != 0);
984
 
985
                $o = max($this->size - $e, 0);
986
                $e = $end;
987
 
988
            } elseif ($o <= $end) {
989
                // If a syntactically valid byte-range-set includes at least
990
                // one byte- range-spec whose first-byte-pos is less than the
991
                // current length of the entity-body, then the byte-range-set
992
                // is satisfiable.
993
                $satisfiable = true;
994
            } else {
995
                continue;
996
            }
997
 
998
            $parts[] = array($o, $e);
999
        }
1000
 
1001
        // If the byte-range-set is unsatisfiable, the server SHOULD return a
1002
        // response with a status of 416 (Requested range not satisfiable).
1003
        if (!$satisfiable) {
1004
            $error = PEAR::raiseError('Error processing range request',
1005
                                      HTTP_DOWNLOAD_E_INVALID_REQUEST);
1006
            return $error;
1007
        }
1008
        //$this->sortChunks($parts);
1009
        return $this->mergeChunks($parts);
1010
    }
1011
 
1012
    /**
1013
     * Sorts the ranges to be in ascending order
1014
     *
1015
     * @param array &$chunks ranges to sort
1016
     *
1017
     * @return void
1018
     * @access protected
1019
     * @static
1020
     * @author Philippe Jausions <jausions@php.net>
1021
     */
1022
    function sortChunks(&$chunks)
1023
    {
1024
        $sortFunc = create_function('$a,$b',
1025
            'if ($a[0] == $b[0]) {
1026
                if ($a[1] == $b[1]) {
1027
                    return 0;
1028
                }
1029
                return (($a[1] != "*" && $a[1] < $b[1])
1030
                        || $b[1] == "*") ? -1 : 1;
1031
             }
1032
 
1033
             return ($a[0] < $b[0]) ? -1 : 1;');
1034
 
1035
        usort($chunks, $sortFunc);
1036
    }
1037
 
1038
    /**
1039
     * Merges consecutive chunks to avoid overlaps
1040
     *
1041
     * @param array $chunks Ranges to merge
1042
     *
1043
     * @return array merged ranges
1044
     * @access protected
1045
     * @static
1046
     * @author Philippe Jausions <jausions@php.net>
1047
     */
1048
    function mergeChunks($chunks)
1049
    {
1050
        do {
1051
            $count = count($chunks);
1052
            $merged = array(current($chunks));
1053
            $j = 0;
1054
            for ($i = 1; $i < count($chunks); ++$i) {
1055
                list($o, $e) = $chunks[$i];
1056
                if ($merged[$j][1] == '*') {
1057
                    if ($merged[$j][0] <= $o) {
1058
                        continue;
1059
                    } elseif ($e == '*' || $merged[$j][0] <= $e) {
1060
                        $merged[$j][0] = min($merged[$j][0], $o);
1061
                    } else {
1062
                        $merged[++$j] = $chunks[$i];
1063
                    }
1064
                } elseif ($merged[$j][0] <= $o && $o <= $merged[$j][1]) {
1065
                    $merged[$j][1] = ($e == '*') ? '*' : max($e, $merged[$j][1]);
1066
                } elseif ($merged[$j][0] <= $e && $e <= $merged[$j][1]) {
1067
                    $merged[$j][0] = min($o, $merged[$j][0]);
1068
                } else {
1069
                    $merged[++$j] = $chunks[$i];
1070
                }
1071
            }
1072
            if ($count == count($merged)) {
1073
                break;
1074
            }
1075
            $chunks = $merged;
1076
        } while (true);
1077
        return $merged;
1078
    }
1079
 
1080
    /**
1081
     * Check if range is requested
1082
     *
1083
     * @access  protected
1084
     * @return  bool
1085
     */
1086
    function isRangeRequest()
1087
    {
1088
        if (!isset($_SERVER['HTTP_RANGE']) || !count($this->getRanges())) {
1089
            return false;
1090
        }
1091
        return $this->isValidRange();
1092
    }
1093
 
1094
    /**
1095
     * Get range request
1096
     *
1097
     * @access  protected
1098
     * @return  array
1099
     */
1100
    function getRanges()
1101
    {
1102
        return preg_match('/^bytes=((\d+-|\d+-\d+|-\d+)(, ?(\d+-|\d+-\d+|-\d+))*)$/',
1103
            @$_SERVER['HTTP_RANGE'], $matches) ? $matches[1] : array();
1104
    }
1105
 
1106
    /**
1107
     * Check if entity is cached
1108
     *
1109
     * @access  protected
1110
     * @return  bool
1111
     */
1112
    function isCached()
1113
    {
1114
        return (
1115
            (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) &&
1116
            $this->lastModified == strtotime(current($a = explode(
1117
                ';', $_SERVER['HTTP_IF_MODIFIED_SINCE'])))) ||
1118
            (isset($_SERVER['HTTP_IF_NONE_MATCH']) &&
1119
            $this->compareAsterisk('HTTP_IF_NONE_MATCH', $this->etag))
1120
        );
1121
    }
1122
 
1123
    /**
1124
     * Check if entity hasn't changed
1125
     *
1126
     * @access  protected
1127
     * @return  bool
1128
     */
1129
    function isValidRange()
1130
    {
1131
        if (isset($_SERVER['HTTP_IF_MATCH']) &&
1132
            !$this->compareAsterisk('HTTP_IF_MATCH', $this->etag)) {
1133
            return false;
1134
        }
1135
        if (isset($_SERVER['HTTP_IF_RANGE']) &&
1136
                  $_SERVER['HTTP_IF_RANGE'] !== $this->etag &&
1137
                  strtotime($_SERVER['HTTP_IF_RANGE']) !== $this->lastModified) {
1138
            return false;
1139
        }
1140
        if (isset($_SERVER['HTTP_IF_UNMODIFIED_SINCE'])) {
1141
            $lm = current($a = explode(';', $_SERVER['HTTP_IF_UNMODIFIED_SINCE']));
1142
            if (strtotime($lm) !== $this->lastModified) {
1143
                return false;
1144
            }
1145
        }
1146
        if (isset($_SERVER['HTTP_UNLESS_MODIFIED_SINCE'])) {
1147
            $lm = current($a = explode(';', $_SERVER['HTTP_UNLESS_MODIFIED_SINCE']));
1148
            if (strtotime($lm) !== $this->lastModified) {
1149
                return false;
1150
            }
1151
        }
1152
        return true;
1153
    }
1154
 
1155
    /**
1156
     * Compare against an asterisk or check for equality
1157
     *
1158
     * @access  protected
1159
     * @return  bool
1160
     * @param   string  key for the $_SERVER array
1161
     * @param   string  string to compare
1162
     */
1163
    function compareAsterisk($svar, $compare)
1164
    {
1165
        foreach (array_map('trim', explode(',', $_SERVER[$svar])) as $request) {
1166
            if ($request === '*' || $request === $compare) {
1167
                return true;
1168
            }
1169
        }
1170
        return false;
1171
    }
1172
 
1173
    /**
1174
     * Send HTTP headers
1175
     *
1176
     * @access  protected
1177
     * @return  void
1178
     */
1179
    function sendHeaders()
1180
    {
1181
        foreach ($this->headers as $header => $value) {
1182
            $this->HTTP->setHeader($header, $value);
1183
        }
1184
        $this->HTTP->sendHeaders();
1185
        /* NSAPI won't output anything if we did this */
1186
        if (strncasecmp(PHP_SAPI, 'nsapi', 5)) {
1187
            if (ob_get_level()) {
1188
                ob_flush();
1189
            }
1190
            flush();
1191
        }
1192
    }
1193
 
1194
    /**
1195
     * Flush
1196
     *
1197
     * @access  protected
1198
     * @return  void
1199
     * @param   string  $data
1200
     */
1201
    function flush($data = '')
1202
    {
1203
        if ($dlen = strlen($data)) {
1204
            $this->sentBytes += $dlen;
1205
            echo $data;
1206
        }
1207
        ob_flush();
1208
        flush();
1209
    }
1210
 
1211
    /**
1212
     * Sleep
1213
     *
1214
     * @access  protected
1215
     * @return  void
1216
     */
1217
    function sleep()
1218
    {
1219
        if (OS_WINDOWS) {
1220
            com_message_pump($this->throttleDelay);
1221
        } else {
1222
            usleep($this->throttleDelay * 1000);
1223
        }
1224
    }
1225
 
1226
    /**
1227
     * Returns and clears startup error
1228
     *
1229
     * @return NULL|PEAR_Error startup error if one exists
1230
     * @access protected
1231
     */
1232
    function _getError()
1233
    {
1234
        $error = null;
1235
        if (PEAR::isError($this->_error)) {
1236
            $error = $this->_error;
1237
            $this->_error = null;
1238
        }
1239
        return $error;
1240
    }
1241
    // }}}
1242
}
1243
?>