Torrent::offset()   B
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 24
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 24
rs 8.9714
cc 3
eloc 17
nc 2
nop 0
1
<?php
2
3
namespace League\Torrent;
4
5
use League\Torrent\Helper\Decoder;
6
use League\Torrent\Helper\Encoder;
7
use League\Torrent\Helper\FileSystem;
8
9
/**
10
 * Class Torrent
11
 *
12
 * @package League\Torrent
13
 */
14
class Torrent
15
{
16
    /**
17
     * Optional comment
18
     *
19
     * @var string
20
     */
21
    public $comment;
22
23
    /**
24
     *
25
     * @var string
26
     */
27
    public $announce;
28
29
    /**
30
     *
31
     * @var string
32
     */
33
    protected $createdBy;
34
35
    /**
36
     *
37
     * @var int
38
     */
39
    protected $creationDate;
40
41
    /**
42
     * Info about the file(s) in the torrent
43
     *
44
     * @var array
45
     */
46
    public $info;
47
48
    /**
49
     * @param null $data
50
     * @param array $meta
51
     * @param int $piece_length
52
     *
53
     * @throws Exception
54
     */
55
    public function __construct($data = null, $meta = array(), $piece_length = 256)
56
    {
57
        if (is_null($data)) {
58
            return false;
0 ignored issues
show
Bug introduced by
Constructors do not have meaningful return values, anything that is returned from here is discarded. Are you sure this is correct?
Loading history...
59
        }
60
        if ($piece_length < 32 || $piece_length > 4096) {
61
            throw new Exception('Invalid piece lenth, must be between 32 and 4096');
62
        }
63
        if ($this->build($data, $piece_length * 1024)) {
64
            $this->touch();
65
        } else {
66
            $meta = array_merge($meta, Decoder::decode($data));
67
        }
68
        foreach ($meta as $key => $value) {
69
            $this->{$key} = $value;
70
        }
71
    }
72
73
    public static function createFromTorrentFile($filename, $meta = array())
74
    {
75
        return self::setMeta(new self(), file_get_contents($filename), $meta);
76
    }
77
78
    public static function createFromUrl($url, $meta = array())
79
    {
80
        if (!FileSystem::url_exists($url)) {
81
            throw new \InvalidArgumentException('Url is not valud');
82
        }
83
84
        return self::setMeta(new self(), FileSystem::downloadTorrent($url), $meta);
85
    }
86
87
    public static function createFromFilesList(array $list, $meta = array())
88
    {
89
        $instance = new self;
90
        if ($instance->build($list, 256 * 1024)) {
91
            $instance->touch();
92
        }
93
94
        return self::setMeta($instance, '', $meta);
95
    }
96
97
    public static function setMeta($instance, $data = '', $meta = array())
98
    {
99
        if (strlen($data)) {
100
            $meta = array_merge($meta, (array) Decoder::decodeData($data));
101
        }
102
103
        foreach ($meta as $key => $value) {
104
            $instance->{$key} = $value;
105
        }
106
107
        return $instance;
108
    }
109
110
    /**
111
     * @return string
112
     */
113
    public function __toString()
114
    {
115
        return Encoder::encode(get_object_vars($this));
116
    }
117
118
    /**
119
     * @param null $announce
120
     *
121
     * @return array|mixed|null|string
122
     */
123
    public function announce($announce = null)
124
    {
125
        if (is_null($announce)) {
126
            return !isset($this->{'announce-list'}) ?
127
                isset($this->announce) ? $this->announce : null :
128
                $this->{'announce-list'};
129
        }
130
        $this->touch();
131
        if (is_string($announce) && isset($this->announce)) {
132
            return $this->{'announce-list'} = self::announce_list(isset($this->{'announce-list'}) ? $this->{'announce-list'} : $this->announce,
133
                $announce);
0 ignored issues
show
Documentation introduced by
$announce is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
134
        }
135
        unset($this->{'announce-list'});
136
        if (is_array($announce) || is_object($announce)) {
137
            if (($this->announce = self::first_announce($announce)) && count($announce) > 1) {
138
                return $this->{'announce-list'} = self::announce_list($announce);
139
            } else {
140
                return $this->announce;
141
            }
142
        }
143
        if (!isset($this->announce) && $announce) {
144
            return $this->announce = (string) $announce;
145
        }
146
        unset($this->announce);
147
    }
148
149
    /**
150
     *
151
     * @return null|string
152
     */
153
    public function getComment()
154
    {
155
        return $this->comment;
156
    }
157
158
    /**
159
     * @param null $comment
160
     *
161
     */
162
    public function setComment($comment)
163
    {
164
        $this->touch($this->comment = (string) $comment);
165
    }
166
167
    /**
168
     *
169
     * @return string|null
170
     */
171
    public function getName()
172
    {
173
        return isset($this->info['name']) ? $this->info['name'] : null;
174
    }
175
176
    /**
177
     * @param string $name
178
     *
179
     * @return null|string
180
     */
181
    public function setName($name)
182
    {
183
        $this->touch($this->info['name'] = (string) $name);
184
    }
185
186
    /**
187
     * @param null $private
188
     *
189
     * @return bool|null
190
     */
191
    public function isPrivate($private = null)
192
    {
193
        return is_null($private) ?
194
            !empty($this->info['private']) :
195
            $this->touch($this->info['private'] = $private ? 1 : 0);
196
    }
197
198
    /**
199
     * @param null $urls
200
     *
201
     * @return null
202
     */
203
    public function urlList($urls = null)
204
    {
205
        return is_null($urls) ?
206
            isset($this->{'url-list'}) ? $this->{'url-list'} : null :
207
            $this->touch($this->{'url-list'} = is_string($urls) ? $urls : (array) $urls);
208
    }
209
210
    /**
211
     * @param null $urls
212
     *
213
     * @return null
214
     */
215
    public function httpSeeds($urls = null)
216
    {
217
        return is_null($urls) ?
218
            isset($this->httpseeds) ? $this->httpseeds : null :
0 ignored issues
show
Bug introduced by
The property httpseeds does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
219
            $this->touch($this->httpseeds = (array) $urls);
220
    }
221
222
    /**
223
     * @return null
224
     */
225
    public function piece_length()
226
    {
227
        return isset($this->info['piece length']) ?
228
            $this->info['piece length'] :
229
            null;
230
    }
231
232
    /**
233
     * @return null|string
234
     */
235
    public function hash_info()
236
    {
237
        return isset($this->info) ? sha1(Encoder::encode($this->info)) : null;
238
    }
239
240
    /**
241
     *
242
     * @return array
243
     */
244
    public function content()
245
    {
246
        $files = array();
247
        if ($this->isOneFile()) {
248
            $files[$this->info['name']] = $this->info['length'];
249
250
        } else {
251
            foreach ((array) $this->info['files'] as $file) {
252
                $files[FileSystem::path($file['path'], $this->info['name'])] = $file['length'];
253
            }
254
        }
255
256
        return $files;
257
    }
258
259
260
    private function isOneFile()
261
    {
262
        return isset($this->info['length'], $this->info['name']);
263
    }
264
265
    /**
266
     * @return array
267
     */
268
    public function offset()
269
    {
270
        $files = array();
271
        $size = 0;
272
        if ($this->isOneFile()) {
273
            $files[$this->info['name']] = array(
274
                'startpiece' => 0,
275
                'offset' => 0,
276
                'size' => $this->info['length'],
277
                'endpiece' => floor($this->info['length'] / $this->info['piece length'])
278
            );
279
        } else {
280
            foreach ((array) $this->info['files'] as $file) {
281
                $files[FileSystem::path($file['path'], $this->info['name'])] = array(
282
                    'startpiece' => floor($size / $this->info['piece length']),
283
                    'offset' => fmod($size, $this->info['piece length']),
284
                    'size' => $size += $file['length'],
285
                    'endpiece' => floor($size / $this->info['piece length'])
286
                );
287
            }
288
        }
289
290
        return $files;
291
    }
292
293
    /**
294
     *
295
     * @return int
296
     */
297
    public function getSize()
298
    {
299
        $size = 0;
300
301
        if ($this->isOneFile()) {
302
            return $this->info['length'];
303
        } else {
304
            foreach ((array) $this->info['files'] as $file) {
305
                $size += $file['length'];
306
            }
307
        }
308
309
        return $size;
310
    }
311
312
313
    /**
314
     * @param null $announce
315
     * @param null $hash_info
316
     * @param int $timeout
317
     *
318
     * @return array|bool
319
     *
320
     * @throws Exception
321
     */
322
    public function scrape($announce = null, $hash_info = null, $timeout = FileSystem::timeout)
323
    {
324
        $packed_hash = urlencode(pack('H*', $hash_info ? $hash_info : $this->hash_info()));
325
        $handles = $scrape = array();
326
        if (!function_exists('curl_multi_init')) {
327
            throw new Exception('Install CURL with "curl_multi_init" enabled');
328
        }
329
        $curl = curl_multi_init();
330
        foreach ((array) ($announce ? $announce : $this->announce()) as $tier) {
331
            foreach ((array) $tier as $tracker) {
332
                $tracker = str_ireplace(array('udp://', '/announce', ':80/'), array(
333
                    'http://',
334
                    '/scrape',
335
                    '/'
336
                ), $tracker);
337
                if (isset($handles[$tracker])) {
338
                    continue;
339
                }
340
                $handles[$tracker] = curl_init($tracker . '?info_hash=' . $packed_hash);
341
                curl_setopt($handles[$tracker], CURLOPT_RETURNTRANSFER, true);
342
                curl_setopt($handles[$tracker], CURLOPT_TIMEOUT, $timeout);
343
                curl_multi_add_handle($curl, $handles[$tracker]);
344
            }
345
        }
346
        do {
347
            while (($state = curl_multi_exec($curl, $running)) == CURLM_CALL_MULTI_PERFORM) {
348
                ;
349
            }
350
            if ($state != CURLM_OK) {
351
                continue;
352
            }
353
            while ($done = curl_multi_info_read($curl)) {
354
                $info = curl_getinfo($done['handle']);
355
                $tracker = explode('?', $info['url'], 2);
356
                $tracker = array_shift($tracker);
357
                if (empty($info['http_code'])) {
358
                    $scrape[$tracker] = 'Tracker request timeout (' . $timeout . 's)';
359
                    continue;
360
                } elseif ($info['http_code'] != 200) {
361
                    $scrape[$tracker] = 'Tracker request failed (' . $info['http_code'] . ' code)';
362
                    continue;
363
                }
364
                $data = curl_multi_getcontent($done['handle']);
365
                $stats = Decoder::decodeData($data);
366
                curl_multi_remove_handle($curl, $done['handle']);
367
                $scrape[$tracker] = empty($stats['files']) ?
368
                    'Empty scrape data' :
369
                    array_shift($stats['files']) + (empty($stats['flags']) ? array() : $stats['flags']);
370
            }
371
        } while ($running);
372
        curl_multi_close($curl);
373
        return $scrape;
374
    }
375
376
    /**
377
     * @param string $filename
378
     *
379
     * @return string
380
     */
381
    public function saveToFile($filename = null)
382
    {
383
        return file_put_contents($filename, Encoder::encode(get_object_vars($this)));
384
    }
385
386
    /**
387
     *
388
     * @return string
389
     */
390
    public function save()
391
    {
392
        return file_put_contents($this->info['name'] . '.torrent', Encoder::encode(get_object_vars($this)));
393
    }
394
395
    /**
396
     * @param bool $html
397
     *
398
     * @return string
399
     */
400
    public function magnet($html = true)
401
    {
402
        $ampersand = $html ? '&amp;' : '&';
403
        return sprintf(
404
            'magnet:?xt=urn:btih:%2$s%1$sdn=%3$s%1$sxl=%4$d%1$str=%5$s',
405
            $ampersand,
406
            $this->hash_info(),
407
            urlencode($this->getName()),
408
            $this->getSize(),
409
            implode($ampersand . 'tr=', FileSystem::untier($this->announce()))
410
        );
411
    }
412
413
414
    /**
415
     * @param $data
416
     * @param integer $piece_length
417
     *
418
     * @return array|bool
419
     */
420
    protected function build($data, $piece_length)
421
    {
422
        if (is_null($data)) {
423
            return false;
424
        } elseif (is_array($data) && FileSystem::is_list($data)) {
425
            return $this->info = $this->files($data, $piece_length);
426
        } elseif (is_dir($data)) {
427
            return $this->info = $this->folder($data, $piece_length);
428
        } elseif (
429
            (is_file($data) && !FileSystem::isTorrent(file_get_contents($data)))
430
            || (FileSystem::url_exists($data) && !FileSystem::isTorrent(FileSystem::downloadTorrent($data)))
431
        ) {
432
            return $this->info = $this->file($data, $piece_length);
433
        }
434
435
        return false;
436
    }
437
438
    /**
439
     * @param null $void
440
     *
441
     * @return null
442
     */
443
    protected function touch($void = null)
444
    {
445
        $this->createdBy = 'League\Torrent Class - http://github.com/iGusev/torrent';
446
        $this->creationDate = time();
447
        return $void;
448
    }
449
450
    /**
451
     * @param $announce
452
     * @param array $merge
453
     *
454
     * @return array
455
     */
456
    protected static function announce_list($announce, $merge = array())
457
    {
458
        return array_map(create_function('$a', 'return (array) $a;'), array_merge((array) $announce, (array) $merge));
459
    }
460
461
    /**
462
     * @param $announce
463
     *
464
     * @return array|mixed
465
     */
466
    protected static function first_announce($announce)
467
    {
468
        while (is_array($announce)) {
469
            $announce = reset($announce);
470
        }
471
        return $announce;
472
    }
473
474
    /**
475
     * @param $handle
476
     * @param $piece_length
477
     * @param bool $last
478
     *
479
     * @return bool|string
480
     *
481
     * @throws Exception
482
     */
483
    private function pieces($handle, $piece_length, $last = true)
484
    {
485
        static $piece, $length;
486
        if (empty($length)) {
487
            $length = $piece_length;
488
        }
489
        $pieces = null;
490
        while (!feof($handle)) {
491
            if (($length = strlen($piece .= fread($handle, $length))) == $piece_length) {
492
                $pieces .= FileSystem::pack($piece);
493
            } elseif (($length = $piece_length - $length) < 0) {
494
                throw new Exception('Invalid piece length!');
495
            }
496
        }
497
        fclose($handle);
498
        return $pieces . ($last && $piece ? FileSystem::pack($piece) : null);
499
    }
500
501
    /**
502
     * @param $file
503
     * @param $piece_length
504
     *
505
     * @return array|bool
506
     */
507
    protected function file($file, $piece_length)
508
    {
509
510
        if (FileSystem::is_url($file)) {
511
            $this->urlList($file);
512
        }
513
        $path = explode(DIRECTORY_SEPARATOR, $file);
514
        return array(
515
            'length' => FileSystem::filesize($file),
516
            'name' => end($path),
517
            'piece length' => $piece_length,
518
            'pieces' => $this->pieces(self::fopen($file), $piece_length)
519
        );
520
    }
521
522
    /**
523
     * @param $files
524
     * @param $piece_length
525
     *
526
     * @return array
527
     *
528
     * @throws Exception
529
     */
530
    protected function files($files, $piece_length)
531
    {
532
        if (!FileSystem::is_url(current($files))) {
533
            $files = array_map('realpath', $files);
534
        }
535
        sort($files);
536
        usort($files,
537
            create_function('$a,$b', 'return strrpos($a,DIRECTORY_SEPARATOR)-strrpos($b,DIRECTORY_SEPARATOR);'));
538
        $first = current($files);
539
        $root = dirname($first);
540
        if ($url = FileSystem::is_url($first)) {
541
            $this->urlList(dirname($root) . DIRECTORY_SEPARATOR);
542
        }
543
        $path = explode(DIRECTORY_SEPARATOR, dirname($url ? $first : realpath($first)));
544
        $pieces = null;
545
        $info_files = array();
546
        $count = count($files) - 1;
547
        foreach ($files as $i => $file) {
548
            if ($path != array_intersect_assoc($file_path = explode(DIRECTORY_SEPARATOR, $file), $path)) {
549
                throw new Exception('Files must be in the same folder: "' . $file . '" discarded');
550
            }
551
            $pieces .= $this->pieces(self::fopen($file), $piece_length, $count == $i);
552
            $info_files[] = array(
553
                'length' => FileSystem::filesize($file),
554
                'path' => array_values(array_diff($file_path, $path))
555
            );
556
        }
557
        return array(
558
            'files' => $info_files,
559
            'name' => end($path),
560
            'piece length' => $piece_length,
561
            'pieces' => $pieces
562
        );
563
    }
564
565
    /**
566
     * @param $dir
567
     * @param $piece_length
568
     *
569
     * @return array
570
     */
571
    protected function folder($dir, $piece_length)
572
    {
573
        return $this->files(FileSystem::scandir($dir), $piece_length);
574
    }
575
576
    /**
577
     * @param $filename
578
     *
579
     * @return bool|resource
580
     *
581
     * @throws Exception
582
     */
583
    public static function fopen($filename)
584
    {
585
        if (!$handle = fopen($filename, 'r')) {
586
            throw new Exception('Failed to open file: "' . $filename . '"');
587
        }
588
589
        return $handle;
590
    }
591
592
593
}
594