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