Advertisement
atmoner

Php class torrent Bdecode et Bencode

Sep 28th, 2011
337
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 29.83 KB | None | 0 0
  1. <?php
  2. /**
  3.  * Torrent
  4.  *
  5.  * PHP version 5.2+ (with cURL extention enabled)
  6.  *
  7.  * LICENSE: This source file is subject to version 3 of the GNU GPL
  8.  * that is available through the world-wide-web at the following URI:
  9.  * http://www.gnu.org/licenses/gpl.html. If you did not receive a copy of
  10.  * the GNU GPL License and are unable to obtain it through the web, please
  11.  * send a note to adrien.gibrat@gmail.com so I can mail you a copy.
  12.  *
  13.  * 1) Features:
  14.  * - Decode torrent file or data from local file and distant url
  15.  * - Build torrent from source folder/file(s) or distant url
  16.  * - Super easy usage & syntax
  17.  * - Silent Exception error system
  18.  *
  19.  * 2) Usage example
  20.  * <code>
  21. require_once 'Torrent.php';
  22.  
  23. // create torrent
  24. $torrent = new Torrent( './path-to-file-or-folder', 'http://torrent.tracker/annonce' );
  25. if ( ! $error = $torrent->error() ) // error method return the last error message
  26.     $torrent->save('test.torrent'); // save to disk
  27. else
  28.     echo '<br>DEBUG: ',$error;
  29.  
  30. // print torrent infos
  31. $torrent = new Torrent( './test.torrent' );
  32. echo '<pre>private: ', $torrent->is_private() ? 'yes' : 'no',
  33.      '<br>annonce: ';
  34. var_dump( $torrent->announce() );
  35. echo '<br>name: ', $torrent->name(),
  36.      '<br>comment: ', $torrent->comment(),
  37.      '<br>piece_length: ', $torrent->piece_length(),
  38.      '<br>size: ', $torrent->size( 2 ),
  39.      '<br>hash info: ', $torrent->hash_info(),
  40.      '<br>stats: ';
  41. var_dump( $torrent->scrape() );
  42. echo '<br>content: ';
  43. var_dump( $torrent->content() );
  44. echo '<br>source: ',
  45.      $torrent;
  46.  
  47. // modify torrent
  48. $torrent->announce('http://alternate-torrent.tracker/annonce'); // add a tracker
  49. $torrent->announce(false); // reset announce trackers
  50. $torrent->announce(array('http://torrent.tracker/annonce', 'http://alternate-torrent.tracker/annonce')); // set tracker(s), it also works with a 'one tracker' array...
  51. $torrent->announce(array(array('http://torrent.tracker/annonce', 'http://alternate-torrent.tracker/annonce'), 'http://another-torrent.tracker/annonce')); // set tiered trackers
  52. $torrent->comment('hello world');
  53. $torrent->name('test torrent');
  54. $torrent->is_private(true);
  55. $torrent->httpseeds('http://file-hosting.domain/path/'); // Bittornado implementation
  56. $torrent->url_list(array('http://file-hosting.domain/path/','http://another-file-hosting.domain/path/')); // GetRight implementation
  57.  
  58. // print errors
  59. if ( $errors = $torrent->errors() ) // errors method return the error stack
  60.     var_dump( '<br>DEBUG: ', $errors );
  61.  
  62. // send to user
  63. //$torrent->send();
  64.  * </code>
  65.  *
  66.  * @author   Adrien Gibrat <adrien.gibrat@gmail.com>
  67.  * @tester   Jeong, official tester ;) Thanks for your precious feedback
  68.  * @copyleft 2010 - Just use it!
  69.  * @license  http://www.gnu.org/licenses/gpl.html GNU General Public License version 3
  70.  * @version  Release: 1.2 (6 july 2010)
  71.  */
  72. class Torrent {
  73.    
  74.     /**
  75.     * @const float Default http timeout
  76.     */
  77.     const timeout = 30;
  78.  
  79.     /**
  80.     * @var array List of error occured
  81.     */
  82.     static protected $errors = array();
  83.  
  84.     /** Read and decode torrent file/data OR build a torrent from source folder/file(s)
  85.      * Supported signatures:
  86.      * - Torrent(); // get an instance (usefull to scrape and check errors)
  87.      * - Torrent( string $torrent ); // analyse a torrent file
  88.      * - Torrent( string $torrent, string $announce );
  89.      * - Torrent( string $torrent, array $meta );
  90.      * - Torrent( string $file_or_folder ); // create a torrent file
  91.      * - Torrent( string $file_or_folder, string $announce_url, [int $piece_length] );
  92.      * - Torrent( string $file_or_folder, array $meta, [int $piece_length] );
  93.      * - Torrent( array $files_list );
  94.      * - Torrent( array $files_list, string $announce_url, [int $piece_length] );
  95.      * - Torrent( array $files_list, array $meta, [int $piece_length] );
  96.      * @param string|array torrent to read or source folder/file(s) (optional, to get an instance)
  97.      * @param string|array announce url or meta informations (optional)
  98.      * @param int piece length (optional)
  99.      */
  100.     public function __construct ( $data = null, $meta = array(), $piece_length = 256 ) {
  101.         if ( is_null( $data ) )
  102.             return false;
  103.         if ( $piece_length < 32 || $piece_length > 4096 )
  104.             return self::set_error( new Exception( 'Invalid piece lenth, must be between 32 and 4096' ) );
  105.         if ( is_string( $meta ) )
  106.             $meta = array( 'announce' => $meta );
  107.         if ( $this->build( $data, $piece_length * 1024 ) )
  108.             $this->touch();
  109.         else
  110.             $meta = array_merge( $meta, $this->decode( $data ) );
  111.         foreach( $meta as $key => $value )
  112.             $this->{$key} = $value;
  113.     }
  114.  
  115.     /** Convert the current Torrent instance in torrent format
  116.      * @return string encoded torrent data
  117.      */
  118.     public function __toString() {
  119.         return $this->encode( $this );
  120.     }
  121.  
  122.     /** Return last error message
  123.      * @return string|boolean last error message or false if none
  124.      */
  125.     public function error() {
  126.         return empty( self::$errors ) ?
  127.             false :
  128.             self::$errors[0]->getMessage();
  129.     }
  130.  
  131.     /** Return Errors
  132.      * @return array|boolean error list or false if none
  133.      */
  134.     public function errors() {
  135.         return empty( self::$errors ) ?
  136.             false :
  137.             self::$errors;
  138.     }
  139.  
  140.     /**** Getters and setters ****/
  141.  
  142.     /** Getter and setter of torrent announce url / list
  143.      * If the argument is a string, announce url is added to announce list (or set as announce if announce is not set)
  144.      * If the argument is an array/object, set announce url (with first url) and list (if array has more than one url), tiered list supported
  145.      * If the argument is false announce url & list are unset
  146.      * @param null|false|string|array announce url / list, reset all if false (optional, if omitted it's a getter)
  147.      * @return string|array|null announce url / list or null if not set
  148.      */
  149.     public function announce ( $announce = null ) {
  150.         if ( is_null( $announce ) )
  151.             return ! isset( $this->{'announce-list'} ) ?
  152.                 isset( $this->announce ) ? $this->announce : null :
  153.                 $this->{'announce-list'};
  154.         $this->touch();
  155.         if ( is_string( $announce ) && isset( $this->announce ) )
  156.             return $this->{'announce-list'} = self::announce_list( isset( $this->{'announce-list'} ) ? $this->{'announce-list'} : $this->announce, $announce );
  157.         unset( $this->{'announce-list'} );
  158.         if ( is_array( $announce ) || is_object( $announce ) )
  159.             if ( ( $this->announce = self::first_announce( $announce ) ) && count( $announce ) > 1 )
  160.                 return $this->{'announce-list'} = self::announce_list( $announce );
  161.             else
  162.                 return $this->announce;
  163.         if ( ! isset( $this->announce ) && $announce )
  164.             return $this->announce = (string) $announce;
  165.         unset( $this->announce );
  166.     }
  167.  
  168.     /** Getter and setter of torrent comment
  169.      * @param null|string comment (optional, if omitted it's a getter)
  170.      * @return string|null comment or null if not set
  171.      */
  172.     public function comment ( $comment = null ) {
  173.         return is_null( $comment ) ?
  174.             isset( $this->comment ) ? $this->comment : null :
  175.             $this->touch( $this->comment = (string) $comment );
  176.     }
  177.  
  178.     /** Getter and setter of torrent name
  179.      * @param null|string name (optional, if omitted it's a getter)
  180.      * @return string|null name or null if not set
  181.      */
  182.     public function name ( $name = null ) {
  183.         return is_null( $name ) ?
  184.             isset( $this->info['name'] ) ? $this->info['name'] : null :
  185.             $this->touch( $this->info['name'] = (string) $name );
  186.     }
  187.  
  188.     /** Getter and setter of private flag
  189.      * @param null|boolean is private or not (optional, if omitted it's a getter)
  190.      * @return boolean private flag
  191.      */
  192.     public function is_private ( $private = null ) {
  193.         return is_null( $private ) ?
  194.             ! empty( $this->info['private'] ) :
  195.             $this->touch( $this->info['private'] = $private ? 1 : 0 );
  196.     }
  197.  
  198.     /** Getter and setter of webseed(s) url list ( GetRight implementation )
  199.      * @param null|string|array webseed or webseeds mirror list (optional, if omitted it's a getter)
  200.      * @return string|array|null webseed(s) or null if not set
  201.      */
  202.     public function url_list ( $urls = null ) {
  203.         return is_null( $urls ) ?
  204.             isset( $this->{'url-list'} ) ? $this->{'url-list'} : null :
  205.             $this->touch( $this->{'url-list'} = is_string( $urls) ? $urls : (array) $urls );
  206.     }
  207.  
  208.     /** Getter and setter of httpseed(s) url list ( Bittornado implementation )
  209.      * @param null|string|array httpseed or httpseeds mirror list (optional, if omitted it's a getter)
  210.      * @return array|null httpseed(s) or null if not set
  211.      */
  212.     public function httpseeds ( $urls = null ) {
  213.         return is_null( $urls ) ?
  214.             isset( $this->httpseeds ) ? $this->httpseeds : null :
  215.             $this->touch( $this->httpseeds = (array) $urls );
  216.     }
  217.  
  218.     /**** Analyze BitTorrent ****/
  219.  
  220.     /** Get piece length
  221.      * @return integer piece length or null if not set
  222.      */
  223.     public function piece_length () {
  224.         return isset( $this->info['piece length'] ) ?
  225.             $this->info['piece length'] :
  226.             null;
  227.     }
  228.  
  229.     /** Compute hash info
  230.      * @return string hash info or null if info not set
  231.      */
  232.     public function hash_info () {
  233.         return isset( $this->info ) ?
  234.             sha1( self::encode( $this->info ) ) :
  235.             null;
  236.     }
  237.  
  238.     /** List torrent content
  239.      * @param integer|null size precision (optional, if omitted returns sizes in bytes)
  240.      * @return array file(s) and size(s) list, files as keys and sizes as values
  241.      */
  242.     public function content ( $precision = null ) {
  243.         $files = array();
  244.         if ( isset( $this->info['files'] ) && is_array( $this->info['files'] ) )
  245.             foreach ( $this->info['files'] as $file )
  246.                 $files[self::path( $file['path'], $this->info['name'] )] = $precision ?
  247.                     self::format( $file['length'], $precision ) :
  248.                     $file['length'];
  249.         elseif ( isset( $this->info['name'] ) )
  250.                 $files[$this->info['name']] = $precision ?
  251.                     self::format( $this->info['length'], $precision ) :
  252.                     $this->info['length'];
  253.         return $files;
  254.     }
  255.  
  256.     /** List torrent content pieces and offset(s)
  257.      * @return array file(s) and pieces/offset(s) list, file(s) as keys and pieces/offset(s) as values
  258.      */
  259.     public function offset () {
  260.         $files = array();
  261.         $size = 0;
  262.         if ( isset( $this->info['files'] ) && is_array( $this->info['files'] ) )
  263.             foreach ( $this->info['files'] as $file )
  264.                 $files[self::path( $file['path'], $this->info['name'] )] = array(
  265.                     'startpiece'    => floor( $size / $this->info['piece length'] ),
  266.                     'offset'    => fmod( $size, $this->info['piece length'] ),
  267.                     'size'      => $size += $file['length'],
  268.                     'endpiece'  => floor( $size / $this->info['piece length'] )
  269.                 );
  270.         elseif ( isset( $this->info['name'] ) )
  271.                 $files[$this->info['name']] = array(
  272.                     'startpiece'    => 0,
  273.                     'offset'    => 0,
  274.                     'size'      => $this->info['length'],
  275.                     'endpiece'  => floor( $this->info['length'] / $this->info['piece length'] )
  276.                 );
  277.         return $files;
  278.     }
  279.  
  280.     /** Sum torrent content size
  281.      * @param integer|null size precision (optional, if omitted returns size in bytes)
  282.      * @return integer|string file(s) size
  283.      */
  284.     public function size ( $precision = null ) {
  285.         $size = 0;
  286.         if ( isset( $this->info['files'] ) && is_array( $this->info['files'] ) )
  287.             foreach ( $this->info['files'] as $file )
  288.                 $size += $file['length'];
  289.         elseif ( isset( $this->info['name'] ) )
  290.                 $size = $this->info['length'];
  291.         return is_null( $precision ) ?
  292.             $size :
  293.             self::format( $size, $precision );
  294.     }
  295.  
  296.     /** Request torrent statistics from scrape page USING CURL!!
  297.      * @param string|array announce or scrape page url (optional, to request an alternative tracker BUT requirered for static call)
  298.      * @param string torrent hash info (optional, requirered ONLY for static call)
  299.      * @param float read timeout in seconds (optional, default to self::timeout 30s)
  300.      * @return array tracker torrent statistics
  301.      */
  302.     /* static */ public function scrape ( $announce = null, $hash_info = null, $timeout = self::timeout ) {
  303.         $packed_hash = urlencode( pack('H*', $hash_info ? $hash_info : $this->hash_info() ) );
  304.         $handles = $scrape = array();
  305.         if ( ! function_exists( 'curl_multi_init' ) )
  306.             return self::set_error( new Exception( 'Install CURL with "curl_multi_init" enabled' ) );
  307.         $curl = curl_multi_init();
  308.         foreach ( (array) ($announce ? $announce : $this->announce()) as $tier )
  309.             foreach ( (array) $tier as $tracker ) {
  310.                 $tracker = str_ireplace( array( 'udp://', '/announce', ':80/' ), array( 'http://', '/scrape', '/' ), $tracker );
  311.                 if ( isset( $handles[$tracker] ) )
  312.                     continue;
  313.                 $handles[$tracker] = curl_init( $tracker . '?info_hash=' . $packed_hash );
  314.                 curl_setopt( $handles[$tracker], CURLOPT_RETURNTRANSFER, true );
  315.                 curl_setopt( $handles[$tracker], CURLOPT_TIMEOUT, $timeout );
  316.                 curl_multi_add_handle( $curl, $handles[$tracker] );
  317.             }
  318.         do {
  319.             while ( ( $state = curl_multi_exec( $curl, $running ) ) == CURLM_CALL_MULTI_PERFORM );
  320.             if( $state != CURLM_OK )
  321.                 continue;
  322.             while ( $done = curl_multi_info_read( $curl ) ) {
  323.                 $info = curl_getinfo( $done['handle'] );
  324.                 $tracker = array_shift( explode( '?', $info['url'], 2 ) );
  325.                 if ( empty( $info['http_code'] ) )
  326.                     continue $scrape[$tracker] = self::set_error( new Exception( 'Tracker request timeout (' . $timeout . 's)' ), true );
  327.                 elseif ( $info['http_code'] != 200 )
  328.                     continue $scrape[$tracker] = self::set_error( new Exception( 'Tracker request failed (' . $info['http_code'] . ' code)' ), true );
  329.                 $stats = self::decode_data( curl_multi_getcontent( $done['handle']  ) );
  330.                 curl_multi_remove_handle( $curl, $done['handle'] );
  331.                 $scrape[$tracker] = empty( $stats['files'] ) ?
  332.                     self::set_error( new Exception( 'Empty scrape data' ), true ) :
  333.                     array_shift( $stats['files'] ) + ( empty( $stats['flags'] ) ? array() : $stats['flags'] );
  334.             }
  335.         } while ( $running );
  336.         curl_multi_close( $curl );
  337.         return $scrape;
  338.     }
  339.  
  340.     /**** Save and Send ****/
  341.  
  342.     /** Save torrent file to disk
  343.      * @param null|string name of the file (optional)
  344.      * @return boolean file has been saved or not
  345.      */
  346.     public function save ( $filename = null ) {
  347.         return file_put_contents( is_null( $filename ) ? $this->info['name'] . '.torrent' : $filename, $this->encode( $this ) );
  348.     }
  349.  
  350.     /** Send torrent file to client
  351.      * @param null|string name of the file (optional)
  352.      * @return void script exit
  353.      */
  354.     public function send ( $filename = null ) {
  355.         $data = $this->encode( $this );
  356.         header( 'Content-type: application/x-bittorrent' );
  357.         header( 'Content-Length: ' . strlen( $data ) );
  358.         header( 'Content-Disposition: attachment; filename="' . ( is_null( $filename ) ? $this->info['name'] . '.torrent' : $filename ) . '"' );
  359.         exit( $data );
  360.     }
  361.  
  362.     /**** Encode BitTorrent ****/
  363.  
  364.     /** Encode torrent data
  365.      * @param mixed data to encode
  366.      * @return string torrent encoded data
  367.      */
  368.     static public function encode ( $mixed ) {
  369.         switch ( gettype( $mixed ) ) {
  370.             case 'integer':
  371.             case 'double':
  372.                 return self::encode_integer( $mixed );
  373.             case 'object':
  374.                 $mixed = (array) $mixed; //Thanks to W-Shadow: http://w-shadow.com/blog/2008/11/11/parse-edit-and-create-torrent-files-with-php/
  375.             case 'array':
  376.                 return self::encode_array( $mixed );
  377.             default:
  378.                 return self::encode_string( (string) $mixed );
  379.         }
  380.     }
  381.  
  382.     /** Encode torrent string
  383.      * @param string string to encode
  384.      * @return string encoded string
  385.      */
  386.     static private function encode_string ( $string ) {
  387.         return strlen( $string ) . ':' . $string;
  388.     }
  389.  
  390.     /** Encode torrent integer
  391.      * @param integer integer to encode
  392.      * @return string encoded integer
  393.      */
  394.     static private function encode_integer ( $integer ) {
  395.         return 'i' . $integer . 'e';
  396.     }
  397.  
  398.     /** Encode torrent dictionary or list
  399.      * @param array array to encode
  400.      * @return string encoded dictionary or list
  401.      */
  402.     static private function encode_array ( $array ) {
  403.         if ( self::is_list( $array ) ) {
  404.             $return = 'l';
  405.             foreach ( $array as $value )
  406.                 $return .= self::encode( $value );
  407.         } else {
  408.             ksort( $array, SORT_STRING );
  409.             $return = 'd';
  410.             foreach ( $array as $key => $value )
  411.                 $return .= self::encode( strval( $key ) ) . self::encode( $value );
  412.         }
  413.         return $return . 'e';
  414.     }
  415.  
  416.     /**** Decode BitTorrent ****/
  417.  
  418.     /** Decode torrent data or file
  419.      * @param string data or file path to decode
  420.      * @return array decoded torrent data
  421.      */
  422.     static protected function decode ( $string ) {
  423.         $data = is_file( $string ) || self::url_exists( $string ) ?
  424.             self::file_get_contents( $string ) :
  425.             $string;
  426.         return (array) self::decode_data( $data );
  427.     }
  428.  
  429.     /** Decode torrent data
  430.      * @param string data to decode
  431.      * @return array decoded torrent data
  432.      */
  433.     static private function decode_data ( & $data ) {
  434.         switch( self::char( $data ) ) {
  435.         case 'i':
  436.             $data = substr( $data, 1 );
  437.             return self::decode_integer( $data );
  438.         case 'l':
  439.             $data = substr( $data, 1 );
  440.             return self::decode_list( $data );
  441.         case 'd':
  442.             $data = substr( $data, 1 );
  443.             return self::decode_dictionary( $data );
  444.         default:
  445.             return self::decode_string( $data );
  446.         }
  447.     }
  448.  
  449.     /** Decode torrent dictionary
  450.      * @param string data to decode
  451.      * @return array decoded dictionary
  452.      */
  453.     static private function decode_dictionary ( & $data ) {
  454.         $dictionary = array();
  455.         $previous = null;
  456.         while ( ( $char = self::char( $data ) ) != 'e' ) {
  457.             if ( $char === false )
  458.                 return self::set_error( new Exception( 'Unterminated dictionary' ) );
  459.             if ( ! ctype_digit( $char ) )
  460.                 return self::set_error( new Exception( 'Invalid dictionary key' ) );
  461.             $key = self::decode_string( $data );
  462.             if ( isset( $dictionary[$key] ) )
  463.                 return self::set_error( new Exception( 'Duplicate dictionary key' ) );
  464.             if ( $key < $previous )
  465.                 return self::set_error( new Exception( 'Missorted dictionary key' ) );
  466.             $dictionary[$key] = self::decode_data( $data );
  467.             $previous = $key;
  468.         }
  469.         $data = substr( $data, 1 );
  470.         return $dictionary;
  471.     }
  472.  
  473.     /** Decode torrent list
  474.      * @param string data to decode
  475.      * @return array decoded list
  476.      */
  477.     static private function decode_list ( & $data ) {
  478.         $list = array();
  479.         while ( ( $char = self::char( $data ) ) != 'e' ) {
  480.             if ( $char === false )
  481.                 return self::set_error( new Exception( 'Unterminated list' ) );
  482.             $list[] = self::decode_data( $data );
  483.         }
  484.         $data = substr( $data, 1 );
  485.         return $list;
  486.     }
  487.  
  488.     /** Decode torrent string
  489.      * @param string data to decode
  490.      * @return string decoded string
  491.      */
  492.     static private function decode_string ( & $data ) {
  493.         if ( self::char( $data ) === '0' && substr( $data, 1, 1 ) != ':' )
  494.             self::set_error( new Exception( 'Invalid string length, leading zero' ) );
  495.         if ( ! $colon = @strpos( $data, ':' ) )
  496.             return self::set_error( new Exception( 'Invalid string length, colon not found' ) );
  497.         $length = intval( substr( $data, 0, $colon ) );
  498.         if ( $length + $colon + 1 > strlen( $data ) )
  499.             return self::set_error( new Exception( 'Invalid string, input too short for string length' ) );
  500.         $string = substr( $data, $colon + 1, $length );
  501.         $data = substr( $data, $colon + $length + 1 );
  502.         return $string;
  503.     }
  504.  
  505.     /** Decode torrent integer
  506.      * @param string data to decode
  507.      * @return integer decoded integer
  508.      */
  509.     static private function decode_integer ( & $data ) {
  510.         $start = 0;
  511.         $end    = strpos( $data, 'e');
  512.         if ( $end === 0 )
  513.             self::set_error( new Exception( 'Empty integer' ) );
  514.         if ( self::char( $data ) == '-' )
  515.             $start++;
  516.         if ( substr( $data, $start, 1 ) == '0' && ( $start != 0 || $end > $start + 1 ) )
  517.             self::set_error( new Exception( 'Leading zero in integer' ) );
  518.         if ( ! ctype_digit( substr( $data, $start, $end ) ) )
  519.             self::set_error( new Exception( 'Non-digit characters in integer' ) );
  520.         $integer = substr( $data, 0, $end );
  521.         $data = substr( $data, $end + 1 );
  522.         return $integer + 0;
  523.     }
  524.  
  525.     /**** Internal Helpers ****/
  526.  
  527.     /** Build torrent info
  528.      * @param string|array source folder/file(s) path
  529.      * @param integer piece length
  530.      * @return array|boolean torrent info or false if data isn't folder/file(s)
  531.      */
  532.     protected function build ( $data, $piece_length ) {
  533.         if ( is_null( $data ) )
  534.             return false;
  535.         elseif ( is_array( $data ) && self::is_list( $data ) )
  536.             return $this->info = $this->files( $data, $piece_length );
  537.         elseif ( is_dir( $data ) )
  538.             return $this->info = $this->folder( $data, $piece_length );
  539.         elseif ( ( is_file( $data ) || self::url_exists( $data ) ) && ! self::is_torrent( $data ) )
  540.             return $this->info = $this->file( $data, $piece_length );
  541.         else
  542.             return false;
  543.     }
  544.  
  545.     /** Set torrent creator and creation date
  546.      * @param any param
  547.      * @return any param
  548.      */
  549.     protected function touch ( $void = null ) {
  550.         $this->{'created by'}       = 'Torrent PHP Class - Adrien Gibrat';
  551.         $this->{'creation date'}    = time();
  552.         return $void;
  553.     }
  554.  
  555.     /** Add an error to errors stack
  556.      * @param Exception error to add
  557.      * @param boolean return error message or not (optional, default to false)
  558.      * @return boolean|string return false or error message if requested
  559.      */
  560.     static protected function set_error ( $exception, $message = false ) {
  561.         return ( array_unshift( self::$errors,  $exception ) && $message ) ? $exception->getMessage() : false;
  562.     }
  563.  
  564.     /** Build announce list
  565.      * @param string|array announce url / list
  566.      * @param string|array announce url / list to add (optionnal)
  567.      * @return array announce list (array of arrays)
  568.      */
  569.     static protected function announce_list( $announce, $merge = array() ) {
  570.         return array_map( create_function( '$a', 'return (array) $a;' ), array_merge( (array) $announce, (array) $merge ) );
  571.     }
  572.  
  573.     /** Get the first announce url in a list
  574.      * @param array announce list (array of arrays if tiered trackers)
  575.      * @return string first announce url
  576.      */
  577.     static protected function first_announce( $announce ) {
  578.         while ( is_array( $announce ) )
  579.             $announce = reset( $announce );
  580.         return $announce;
  581.     }
  582.  
  583.     /** Helper to pack data hash
  584.      * @param string data
  585.      * @return string packed data hash
  586.      */
  587.     static protected function pack ( & $data ) {
  588.         return pack('H*', sha1( $data ) ) . ( $data = null );
  589.     }
  590.  
  591.     /** Helper to build file path
  592.      * @param array file path
  593.      * @param string base folder
  594.      * @return string real file path
  595.      */
  596.     static protected function path ( $path, $folder ) {
  597.         array_unshift( $path, $folder );
  598.         return join( DIRECTORY_SEPARATOR, $path );
  599.     }
  600.  
  601.     /** Helper to test if an array is a list
  602.      * @param array array to test
  603.      * @return boolean is the array a list or not
  604.      */
  605.     static protected function is_list ( $array ) {
  606.         foreach ( array_keys( $array ) as $key )
  607.             if ( ! is_int( $key ) )
  608.                 return false;
  609.         return true;
  610.     }
  611.  
  612.     /** Build pieces depending on piece length from a file handler
  613.      * @param ressource file handle
  614.      * @param integer piece length
  615.      * @param boolean is last piece
  616.      * @return string pieces
  617.      */
  618.     private function pieces ( $handle, $piece_length, $last = true ) {
  619.         static $piece, $length;
  620.         if ( empty( $length ) )
  621.             $length = $piece_length;
  622.         $pieces = null;
  623.         while ( ! feof( $handle ) ) {
  624.             if ( ( $length = strlen( $piece .= fread( $handle, $length ) ) ) == $piece_length )
  625.                 $pieces .= self::pack( $piece );
  626.             elseif ( ( $length = $piece_length - $length ) < 0 )
  627.                 return self::set_error( new Exception( 'Invalid piece length!' ) );
  628.         }
  629.         fclose( $handle );
  630.         return $pieces . ( $last && $piece ? self::pack( $piece ) : null);
  631.     }
  632.  
  633.     /** Build torrent info from single file
  634.      * @param string file path
  635.      * @param integer piece length
  636.      * @return array torrent info
  637.      */
  638.     private function file ( $file, $piece_length ) {
  639.         if ( ! $handle = self::fopen( $file, $size = self::filesize( $file ) ) )
  640.             return self::set_error( new Exception( 'Failed to open file: "' . $file . '"' ) );
  641.         return array(
  642.             'length'    => $size,
  643.             'name'      => end( explode( DIRECTORY_SEPARATOR, $file ) ),
  644.             'piece length'  => $piece_length,
  645.             'pieces'    => $this->pieces( $handle, $piece_length )
  646.         );
  647.     }
  648.  
  649.     /** Build torrent info from files
  650.      * @param array file list
  651.      * @param integer piece length
  652.      * @return array torrent info
  653.      */
  654.     private function files ( $files, $piece_length ) {
  655.         $files = array_map( 'realpath', $files );
  656.         sort( $files );
  657.         usort( $files, create_function( '$a,$b', 'return strrpos($a,DIRECTORY_SEPARATOR)-strrpos($b,DIRECTORY_SEPARATOR);' ) );
  658.         $path   = explode( DIRECTORY_SEPARATOR, dirname( realpath( current( $files ) ) ) );
  659.         $pieces = null; $info_files = array(); $count = count($files) - 1;
  660.         foreach ( $files as $i => $file ) {
  661.             if ( $path != array_intersect_assoc( $file_path = explode( DIRECTORY_SEPARATOR, $file ), $path ) )
  662.                 continue self::set_error( new Exception( 'Files must be in the same folder: "' . $file . '" discarded' ) );
  663.             if ( ! $handle = self::fopen( $file, $filesize = self::filesize( $file ) ) )
  664.                 continue self::set_error( new Exception( 'Failed to open file: "' . $file . '" discarded' ) );
  665.             $pieces .= $this->pieces( $handle, $piece_length, $count == $i );
  666.             $info_files[] = array(
  667.                 'length'    => $filesize,
  668.                 'path'      => array_diff( $file_path, $path )
  669.             );
  670.         }
  671.         return array(
  672.             'files'     => $info_files,
  673.             'name'      => end( $path ),
  674.             'piece length'  => $piece_length,
  675.             'pieces'    => $pieces
  676.         );
  677.  
  678.     }
  679.  
  680.     /** Build torrent info from folder content
  681.      * @param string folder path
  682.      * @param integer piece length
  683.      * @return array torrent info
  684.      */
  685.     private function folder ( $dir, $piece_length ) {
  686.         return $this->files( self::scandir( $dir ), $piece_length );
  687.     }
  688.  
  689.     /** Helper to return the first char of encoded data
  690.      * @param string encoded data
  691.      * @return string|boolean first char of encoded data or false if empty data
  692.      */
  693.     static private function char ( $data ) {
  694.         return empty( $data ) ?
  695.             false :
  696.             substr( $data, 0, 1 );
  697.     }
  698.  
  699.     /**** Public Helpers ****/
  700.  
  701.     /** Helper to format size in bytes to human readable
  702.      * @param integer size in bytes
  703.      * @param integer precision after coma
  704.      * @return string formated size in appropriate unit
  705.      */
  706.     static public function format ( $size, $precision = 2 ) {
  707.         $units = array ('octets', 'Ko', 'Mo', 'Go', 'To');
  708.         while( ( $next = next( $units ) ) && $size > 1024 )
  709.             $size /= 1024;
  710.         return round( $size, $precision ) . ' ' . ( $next ? prev( $units ) : end( $units ) );
  711.     }
  712.  
  713.     /** Helper to return filesize (even bigger than 2Gb -linux only- and distant files size)
  714.      * @param string file path
  715.      * @return double|boolean filesize or false if error
  716.      */
  717.     static public function filesize ( $file ) {
  718.         if ( is_file( $file ) )
  719.             return (double) sprintf( '%u', @filesize( $file ) );
  720.         else if ( $content_length = preg_grep( $pattern = '#^Content-Length:\s+(\d+)$#i', (array) @get_headers( $file ) ) )
  721.             return (int) preg_replace( $pattern, '$1', reset( $content_length ) );
  722.     }
  723.  
  724.     /** Helper to open file to read (even bigger than 2Gb, linux only)
  725.      * @param string file path
  726.      * @param integer|double file size (optional)
  727.      * @return ressource|boolean file handle or false if error
  728.      */
  729.     static public function fopen ( $file, $size = null ) {
  730.         if ( ( is_null( $size ) ? self::filesize( $file ) : $size ) <= 2 * pow( 1024, 3 ) )
  731.             return fopen( $file, 'r' );
  732.         elseif ( PHP_OS != 'Linux' )
  733.             return self::set_error( new Exception( 'File size is greater than 2GB. This is only supported under Linux' ) );
  734.         elseif ( ! is_readable( $file ) )
  735.             return false;
  736.         else
  737.             return popen( 'cat ' . escapeshellarg( realpath( $file ) ), 'r' );
  738.     }
  739.  
  740.     /** Helper to scan directories files and sub directories recursivly
  741.      * @param string directory path
  742.      * @return array directory content list
  743.      */
  744.     static public function scandir ( $dir ) {
  745.         $paths = array();
  746.         foreach ( scandir( $dir ) as $item )
  747.                 if ( $item != '.' && $item != '..' )
  748.                     if ( is_dir( $path = realpath( $dir . DIRECTORY_SEPARATOR . $item ) ) )
  749.                         $paths = array_merge( self::scandir( $path ), $paths );
  750.                     else
  751.                         $paths[] = $path;
  752.         return $paths;
  753.     }
  754.  
  755.     /** Helper to check if url exists
  756.      * @param string url to check
  757.      * @return boolean does the url exist or not
  758.      */
  759.     static public function url_exists ( $url ) {
  760.         return preg_match( '#^http(s)?://[a-z0-9-]+(.[a-z0-9-]+)*(:[0-9]+)?(/.*)?$#i', $url ) ?
  761.             (bool) preg_grep( '#^HTTP/.*\s(200|304)\s#', (array) @get_headers( $url ) ) :
  762.             false;
  763.     }
  764.  
  765.     /** Helper to check if a file is a torrent
  766.      * @param string file location
  767.      * @param float http timeout (optional, default to self::timeout 30s)
  768.      * @return boolean is the file a torrent or not
  769.      */
  770.     static public function is_torrent ( $file, $timeout = self::timeout ) {
  771.         return self::file_get_contents( $file, $timeout, 0, 11 ) === 'd8:announce';
  772.     }
  773.  
  774.     /** Helper to get (distant) file content
  775.      * @param string file location
  776.      * @param float http timeout (optional, default to self::timeout 30s)
  777.      * @param integer starting offset (optional, default to null)
  778.      * @param integer content length (optional, default to null)
  779.      * @return string|boolean file content or false if error
  780.      */
  781.     static public function file_get_contents ( $file, $timeout = self::timeout, $offset = null, $length = null ) {
  782.         if ( is_file( $file ) || ini_get( 'allow_url_fopen' ) ) {
  783.             $context = ! is_file( $file ) && $timeout ?
  784.                 stream_context_create( array( 'http' => array( 'timeout' => $timeout ) ) ) :
  785.                 null;
  786.             return ! is_null( $offset ) ? $length ?
  787.                 @file_get_contents( $file, false, $context, $offset, $length ) :
  788.                 @file_get_contents( $file, false, $context, $offset ) :
  789.                 @file_get_contents( $file, false, $context );
  790.         } elseif ( ! function_exists( 'curl_init' ) )
  791.             return self::set_error( new Exception( 'Install CURL or enable "allow_url_fopen"' ) );
  792.         $handle = curl_init( $file );
  793.         if ( $timeout )
  794.             curl_setopt( $handle, CURLOPT_TIMEOUT, $timeout );
  795.         if ( $offset || $length )
  796.             curl_setopt( $handle, CURLOPT_RANGE, $offset . '-' . ( $length ? $offset + $length -1 : null ) );
  797.         curl_setopt( $handle, CURLOPT_RETURNTRANSFER, 1 );
  798.         $content = curl_exec( $handle );
  799.         $size = curl_getinfo( $handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD );
  800.         curl_close( $handle );
  801.         return ( $offset && $size == -1 ) || ( $length && $length != $size ) ? $length ?
  802.             substr( $content, $offset, $length) :
  803.             substr( $content, $offset) :
  804.             $content;
  805.     }
  806.  
  807. }
  808.  
  809. ?>
  810.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement