Completed
Push — master ( 330236...0b9b69 )
by Angus
02:41
created

Base_Roku_Site_Model::doCustomUpdate()   C

Complexity

Conditions 9
Paths 3

Size

Total Lines 53
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

Changes 0
Metric Value
cc 9
eloc 36
nc 3
nop 0
dl 0
loc 53
rs 6.8963
c 0
b 0
f 0
ccs 0
cts 33
cp 0
crap 90

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php declare(strict_types=1); defined('BASEPATH') OR exit('No direct script access allowed');
2
3
/**
4
 * Class Tracker_Sites_Model
5
 */
6
class Tracker_Sites_Model extends CI_Model {
7 127
	public function __construct() {
8 127
		parent::__construct();
9 127
	}
10
11
	public function __get($name) {
12
		//TODO: Is this a good idea? There wasn't a good consensus on if this is good practice or not..
13
		//      It's probably a minor speed reduction, but that isn't much of an issue.
14
		//      An alternate solution would simply have a function which generates a PHP file with code to load each model. Similar to: https://github.com/shish/shimmie2/blob/834bc740a4eeef751f546979e6400fd089db64f8/core/util.inc.php#L1422
15
		$validClasses = [
16
			'Base_Site_Model',
17
			'Base_FoolSlide_Site_Model',
18
			'Base_myMangaReaderCMS_Site_Model',
19
			'Base_GlossyBright_Site_Model',
20
			'Base_Roku_Site_Model'
21
		];
22
		if(!class_exists($name) || !(in_array(get_parent_class($name), $validClasses))) {
23
			return get_instance()->{$name};
24
		} else {
25
			$this->loadSite($name);
26
			return $this->{$name};
27
		}
28
	}
29
30
	private function loadSite(string $siteName) : void {
31
		$this->{$siteName} = new $siteName();
32
	}
33
}
34
35
abstract class Base_Site_Model extends CI_Model {
36
	public $site          = '';
37
	public $titleFormat   = '//';
38
	public $chapterFormat = '//';
39
	public $hasCloudFlare = FALSE;
40
	public $userAgent     = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2824.0 Safari/537.36';
41
42
	public $baseURL = '';
43
44
	/**
45
	 * 0: No custom updater.
46
	 * 1: Uses following page.
47
	 * 2: Uses latest releases page.
48
	 */
49
	public $customType = 0;
50
51 16
	public function __construct() {
52 16
		parent::__construct();
53
54 16
		$this->load->database();
55
56 16
		$this->site = get_class($this);
57 16
	}
58
59
	/**
60
	 * Generates URL to the title page of the requested series.
61
	 *
62
	 * NOTE: In some cases, we are required to store more data in the title_string than is needed to generate the URL. (Namely as the title_string is our unique identifier for that series)
63
	 *       When storing additional data, we use ':--:' as a delimiter to separate the data. Make sure to handle this as needed.
64
	 *
65
	 * Example:
66
	 *    return "http://mangafox.me/manga/{$title_url}/";
67
	 *
68
	 * Example (with extra data):
69
	 *    $title_parts = explode(':--:', title_url);
70
	 *    return "https://bato.to/comic/_/comics/-r".$title_parts[0];
71
	 *
72
	 * @param string $title_url
73
	 * @return string
74
	 */
75
	abstract public function getFullTitleURL(string $title_url) : string;
76
77
	/**
78
	 * Generates chapter data from given $title_url and $chapter.
79
	 *
80
	 * Chapter must be in a (v[0-9]+/)?c[0-9]+(\..+)? format.
81
	 *
82
	 * NOTE: In some cases, we are required to store the chapter number, and the segment required to generate the chapter URL separately.
83
	 *       Much like when generating the title URL, we use ':--:' as a delimiter to separate the data. Make sure to handle this as needed.
84
	 *
85
	 * Example:
86
	 *     return [
87
	 *        'url'    => $this->getFullTitleURL($title_url).'/'.$chapter,
88
	 *        'number' => "c{$chapter}"
89
	 *    ];
90
	 *
91
	 * @param string $title_url
92
	 * @param string $chapter
93
	 * @return array [url, number]
94
	 */
95
	abstract public function getChapterData(string $title_url, string $chapter) : array;
96
97
	/**
98
	 * Used to get the latest chapter of given $title_url.
99
	 *
100
	 * This <should> utilize both get_content and parseTitleDataDOM functions when possible, as these can both reduce a lot of the code required to set this up.
101
	 *
102
	 * $titleData params must be set accordingly:
103
	 * * `title` should always be used with html_entity_decode.
104
	 * * `latest_chapter` must match $this->chapterFormat.
105
	 * * `last_updated` should always be in date("Y-m-d H:i:s") format.
106
	 * * `followed` should never be set within via getTitleData, with the exception of via a array_merge with doCustomFollow.
107
	 *
108
	 * $firstGet is set to true when the series is first added to the DB, and is used to follow the series on given site (if possible).
109
	 *
110
	 * @param string $title_url
111
	 * @param bool   $firstGet
112
	 * @return array|null [title,latest_chapter,last_updated,followed?]
113
	 */
114
	abstract public function getTitleData(string $title_url, bool $firstGet = FALSE) : ?array;
115
116
	/**
117
	 * Validates given $title_url against titleFormat.
118
	 *
119
	 * Failure to match against titleFormat will stop the series from being added to the DB.
120
	 *
121
	 * @param string $title_url
122
	 * @return bool
123
	 */
124 2
	final public function isValidTitleURL(string $title_url) : bool {
125 2
		$success = (bool) preg_match($this->titleFormat, $title_url);
126 2
		if(!$success) log_message('error', "Invalid Title URL ({$this->site}): {$title_url}");
127 2
		return $success;
128
	}
129
130
	/**
131
	 * Validates given $chapter against chapterFormat.
132
	 *
133
	 * Failure to match against chapterFormat will stop the chapter being updated.
134
	 *
135
	 * @param string $chapter
136
	 * @return bool
137
	 */
138 2
	final public function isValidChapter(string $chapter) : bool {
139 2
		$success = (bool) preg_match($this->chapterFormat, $chapter);
140 2
		if(!$success) log_message('error', "Invalid Chapter ({$this->site}): {$chapter}");
141 2
		return $success;
142
	}
143
144
	/**
145
	 * Used by getTitleData (& similar functions) to get the requested page data.
146
	 *
147
	 * @param string $url
148
	 * @param string $cookie_string
149
	 * @param string $cookiejar_path
150
	 * @param bool   $follow_redirect
151
	 * @param bool   $isPost
152
	 * @param array  $postFields
153
	 *
154
	 * @return array|bool
155
	 */
156
	final protected function get_content(string $url, string $cookie_string = "", string $cookiejar_path = "", bool $follow_redirect = FALSE, bool $isPost = FALSE, array $postFields = []) {
157
		$refresh = TRUE; //For sites that have CloudFlare, we want to loop get_content again.
158
		$loops   = 0;
159
		while($refresh && $loops < 2) {
160
			$refresh = FALSE;
161
			$loops++;
162
163
			$ch = curl_init();
164
			curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
165
			curl_setopt($ch, CURLOPT_ENCODING , "gzip");
166
			//curl_setopt($ch, CURLOPT_VERBOSE, 1);
167
			curl_setopt($ch, CURLOPT_HEADER, 1);
168
169
			if($follow_redirect)        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
170
171
			if($cookies = $this->cache->get("cloudflare_{$this->site}")) {
172
				$cookie_string .= "; {$cookies}";
173
			}
174
175
			if(!empty($cookie_string))  curl_setopt($ch, CURLOPT_COOKIE, $cookie_string);
176
			if(!empty($cookiejar_path)) curl_setopt($ch, CURLOPT_COOKIEFILE, $cookiejar_path);
177
178
			//Some sites check the useragent for stuff, use a pre-defined user-agent to avoid stuff.
179
			curl_setopt($ch, CURLOPT_USERAGENT, $this->userAgent);
180
181
			//NOTE: This is required for SSL URLs for now. Without it we tend to get error code 60.
182
			curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); //FIXME: This isn't safe, but it allows us to grab SSL URLs
183
184
			curl_setopt($ch, CURLOPT_URL, $url);
185
186
			if($isPost) {
187
				curl_setopt($ch,CURLOPT_POST, count($postFields));
188
				curl_setopt($ch,CURLOPT_POSTFIELDS, http_build_query($postFields));
189
			}
190
191
			$response = curl_exec($ch);
192
193
			$this->Tracker->admin->incrementRequests();
194
195
			if($response === FALSE) {
196
				log_message('error', "curl failed with error: ".curl_errno($ch)." | ".curl_error($ch));
197
				//FIXME: We don't always account for FALSE return
198
				return FALSE;
199
			}
200
201
			$status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
202
			$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
203
			$headers     = http_parse_headers(substr($response, 0, $header_size));
204
			$body        = substr($response, $header_size);
205
			curl_close($ch);
206
207
			if($status_code === 503) $refresh = $this->handleCloudFlare($url, $body);
208
		}
209
210
		return [
211
			'headers'     => $headers,
0 ignored issues
show
Bug introduced by
The variable $headers does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
212
			'status_code' => $status_code,
0 ignored issues
show
Bug introduced by
The variable $status_code does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
213
			'body'        => $body
0 ignored issues
show
Bug introduced by
The variable $body does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
214
		];
215
	}
216
217
	final private function handleCloudFlare(string $url, string $body) : bool {
218
		$refresh = FALSE;
219
220
		if((strpos($body, 'DDoS protection by Cloudflare') !== FALSE) || (strpos($body, '<input type="hidden" id="jschl-answer" name="jschl_answer"/>') !== FALSE)) {
221
			//print "Cloudflare detected? Grabbing Cookies.\n";
222
			if(!$this->hasCloudFlare) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
223
				//TODO: Site appears to have enabled CloudFlare, disable it and contact admin.
224
				//      We'll continue to bypass CloudFlare as this may occur in a loop.
225
			}
226
227
			$urlData = [
228
				'url'        => $url,
229
				'user_agent' => $this->userAgent
230
			];
231
			//TODO: shell_exec seems bad since the URLs "could" be user inputted? Better way of doing this?
232
			$result = shell_exec('python '.APPPATH.'../_scripts/get_cloudflare_cookie.py '.escapeshellarg(json_encode($urlData)));
233
			$cookieData = json_decode($result, TRUE);
234
235
			$this->cache->save("cloudflare_{$this->site}", $cookieData['cookies'],  31536000 /* 1 year, or until we renew it */);
236
			log_message('debug', "Saving CloudFlare Cookies for {$this->site}");
237
238
			$refresh = TRUE;
239
		} else {
0 ignored issues
show
Unused Code introduced by
This else statement is empty and can be removed.

This check looks for the else branches of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These else branches can be removed.

if (rand(1, 6) > 3) {
print "Check failed";
} else {
    //print "Check succeeded";
}

could be turned into

if (rand(1, 6) > 3) {
    print "Check failed";
}

This is much more concise to read.

Loading history...
240
			//Either site doesn't have CloudFlare or we have bypassed it. Either is good!
241
		}
242
		return $refresh;
243
	}
244
245
	/**
246
	 * Used by getTitleData to get the title, latest_chapter & last_updated data from the data returned by get_content.
247
	 *
248
	 * parseTitleDataDOM checks if the data returned by get_content is valid via a few simple checks.
249
	 * * If the request was actually successful, had a valid status code & data wasn't empty. We also do an additional check on an optional $failure_string param, which will throw a failure if it's matched.
250
	 *
251
	 * Data is cleaned by cleanTitleDataDOM prior to being passed to DOMDocument.
252
	 *
253
	 * All $node_* params must be XPath to the requested node, and must only return 1 result. Anything else will throw a failure.
254
	 *
255
	 * @param array  $content
256
	 * @param string $title_url
257
	 * @param string $node_title_string
258
	 * @param string $node_row_string
259
	 * @param string $node_latest_string
260
	 * @param string $node_chapter_string
261
	 * @param string $failure_string
262
	 * @return DOMElement[]|false [nodes_title,nodes_chapter,nodes_latest]
263
	 */
264
	final protected function parseTitleDataDOM(
265
		$content, string $title_url,
266
		string $node_title_string, string $node_row_string,
267
		string $node_latest_string, string $node_chapter_string,
268
		string $failure_string = "") {
269
270
		if(!is_array($content)) {
271
			log_message('error', "{$this->site} : {$title_url} | Failed to grab URL (See above curl error)");
272
		} else {
273
			list('headers' => $headers, 'status_code' => $status_code, 'body' => $data) = $content;
0 ignored issues
show
Unused Code introduced by
The assignment to $headers is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
274
275
			if(!($status_code >= 200 && $status_code < 300)) {
276
				log_message('error', "{$this->site} : {$title_url} | Bad Status Code ({$status_code})");
277
			} else if(empty($data)) {
278
				log_message('error', "{$this->site} : {$title_url} | Data is empty? (Status code: {$status_code})");
279
			} else if($failure_string !== "" && strpos($data, $failure_string) !== FALSE) {
280
				log_message('error', "{$this->site} : {$title_url} | Failure string matched");
281
			} else {
282
				$data = $this->cleanTitleDataDOM($data); //This allows us to clean the DOM prior to parsing. It's faster to grab the only part we need THEN parse it.
283
284
				$dom = new DOMDocument();
285
				libxml_use_internal_errors(TRUE);
286
				$dom->loadHTML('<?xml encoding="utf-8" ?>' . $data);
287
				libxml_use_internal_errors(FALSE);
288
289
				$xpath = new DOMXPath($dom);
290
				$nodes_title = $xpath->query($node_title_string);
291
				$nodes_row   = $xpath->query($node_row_string);
292
				if($nodes_title->length === 1 && $nodes_row->length === 1) {
293
					$firstRow      = $nodes_row->item(0);
294
					$nodes_latest  = $xpath->query($node_latest_string,  $firstRow);
295
296
					if($node_chapter_string !== '') {
297
						$nodes_chapter = $xpath->query($node_chapter_string, $firstRow);
298
					} else {
299
						$nodes_chapter = $nodes_row;
300
					}
301
302
					if($nodes_latest->length === 1 && $nodes_chapter->length === 1) {
303
						return [
304
							'nodes_title'   => $nodes_title->item(0),
305
							'nodes_latest'  => $nodes_latest->item(0),
306
							'nodes_chapter' => $nodes_chapter->item(0)
307
						];
308
					} else {
309
						log_message('error', "{$this->site} : {$title_url} | Invalid amount of nodes (LATEST: {$nodes_latest->length} | CHAPTER: {$nodes_chapter->length})");
310
					}
311
				} else {
312
					log_message('error', "{$this->site} : {$title_url} | Invalid amount of nodes (TITLE: {$nodes_title->length} | ROW: {$nodes_row->length})");
313
				}
314
			}
315
		}
316
317
		return FALSE;
318
	}
319
320
	/**
321
	 * Used by parseTitleDataDOM to clean the data prior to passing it to DOMDocument & DOMXPath.
322
	 * This is mostly done as an (assumed) speed improvement due to the reduced amount of DOM to parse, or simply just making it easier to parse with XPath.
323
	 *
324
	 * @param string $data
325
	 * @return string
326
	 */
327
	public function cleanTitleDataDOM(string $data) : string {
328
		return $data;
329
	}
330
331
	/**
332
	 * Used to follow a series on given site if supported.
333
	 *
334
	 * This is called by getTitleData if $firstGet is true (which occurs when the series is first being added to the DB).
335
	 *
336
	 * Most of the actual following is done by handleCustomFollow.
337
	 *
338
	 * @param string $data
339
	 * @param array  $extra
340
	 * @return array
341
	 */
342
	final public function doCustomFollow(string $data = "", array $extra = []) : array {
343
		$titleData = [];
344
		$this->handleCustomFollow(function($content, $id, closure $successCallback = NULL) use(&$titleData) {
345
			if(is_array($content)) {
346
				if(array_key_exists('status_code', $content)) {
347
					$statusCode = $content['status_code'];
348
					if($statusCode === 200) {
349
						$isCallable = is_callable($successCallback);
350
						if(($isCallable && $successCallback($content['body'])) || !$isCallable) {
351
							$titleData['followed'] = 'Y';
352
353
							log_message('info', "doCustomFollow succeeded for {$id}");
354
						} else {
355
							log_message('error', "doCustomFollow failed (Invalid response?) for {$id}");
356
						}
357
					} else {
358
						log_message('error', "doCustomFollow failed (Invalid status code ({$statusCode})) for {$id}");
359
					}
360
				} else {
361
					log_message('error', "doCustomFollow failed (Missing status code?) for {$id}");
362
				}
363
			} else {
364
				log_message('error', "doCustomFollow failed (Failed request) for {$id}");
365
			}
366
		}, $data, $extra);
367
		return $titleData;
368
	}
369
370
	/**
371
	 * Used by doCustomFollow to handle following series on sites.
372
	 *
373
	 * Uses get_content to get data.
374
	 *
375
	 * $callback must return ($content, $id, closure $successCallback = NULL).
376
	 * * $content is simply just the get_content data.
377
	 * * $id is the dbID. This should be passed by the $extra arr.
378
	 * * $successCallback is an optional success check to make sure the series was properly followed.
379
	 *
380
	 * @param callable $callback
381
	 * @param string   $data
382
	 * @param array    $extra
383
	 */
384
	public function handleCustomFollow(callable $callback, string $data = "", array $extra = []) {}
0 ignored issues
show
Unused Code introduced by
The parameter $data is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
385
386
	/**
387
	 * Used to check the sites following page for new updates (if supported).
388
	 * This should work much like getTitleData, but instead checks the following page.
389
	 *
390
	 * This must return an array containing arrays of each of the chapters data.
391
	 */
392
	public function doCustomUpdate() {}
393
394
	/**
395
	 * Used by the custom updater to check if a chapter looks newer than the current one.
396
	 *
397
	 * This calls doCustomCheckCompare which handles the majority of the checking.
398
	 * NOTE: Depending on the site, you may need to call getChapterData to get the chapter number to be used with this.
399
	 *
400
	 * @param string $oldChapterString
401
	 * @param string $newChapterString
402
	 * @return bool
403
	 */
404
	public function doCustomCheck(string $oldChapterString, string $newChapterString) : bool {
405
		$oldChapterSegments = explode('/', $this->getChapterData('', $oldChapterString)['number']);
406
		$newChapterSegments = explode('/', $this->getChapterData('', $newChapterString)['number']);
407
408
		$status = $this->doCustomCheckCompare($oldChapterSegments, $newChapterSegments);
409
410
		return $status;
411
	}
412
413
	/**
414
	 * Used by doCustomCheck to check if a chapter looks newer than the current one.
415
	 * Chapter must be in a (v[0-9]+/)?c[0-9]+(\..+)? format.
416
	 *
417
	 * To avoid issues with the occasional off case, this will only ever return true if we are 100% sure that the new chapter is newer than the old one.
418
	 *
419
	 * @param array $oldChapterSegments
420
	 * @param array $newChapterSegments
421
	 * @return bool
422
	 */
423 12
	final public function doCustomCheckCompare(array $oldChapterSegments, array $newChapterSegments) : bool {
424
		//NOTE: We only need to check against the new chapter here, as that is what is used for confirming update.
425 12
		$status = FALSE;
426
427
		//Make sure we have a volume element
428 12
		if(count($oldChapterSegments) === 1) array_unshift($oldChapterSegments, 'v0');
429 12
		if(count($newChapterSegments) === 1) array_unshift($newChapterSegments, 'v0');
430
431 12
		$oldCount = count($oldChapterSegments);
432 12
		$newCount = count($newChapterSegments);
433 12
		if($newCount === $oldCount) {
434
			//Make sure chapter format looks correct.
435
			//NOTE: We only need to check newCount as we know oldCount is the same count.
436 12
			if($newCount === 2) {
437
				//FIXME: Can we loop this?
438 12
				$oldVolume = substr(array_shift($oldChapterSegments), 1);
439 12
				$newVolume = substr(array_shift($newChapterSegments), 1);
440
441
				//Forcing volume to 0 as TBD might not be the latest (although it can be, but that is covered by other checks)
442 12
				if(in_array($oldVolume, ['TBD', 'TBA', 'NA', 'LMT'])) $oldVolume = 0;
443 12
				if(in_array($newVolume, ['TBD', 'TBA', 'NA', 'LMT'])) $newVolume = 0;
444
445 12
				$oldVolume = floatval($oldVolume);
446 12
				$newVolume = floatval($newVolume);
447
			} else {
448
				$oldVolume = 0;
449
				$newVolume = 0;
450
			}
451 12
			$oldChapter = floatval(substr(array_shift($oldChapterSegments), 1));
452 12
			$newChapter = floatval(substr(array_shift($newChapterSegments), 1));
453
454 12
			if($newChapter > $oldChapter && ($oldChapter >= 10 && $newChapter >= 10)) {
455
				//$newChapter is higher than $oldChapter AND $oldChapter and $newChapter are both more than 10
456
				//This is intended to cover the /majority/ of valid updates, as we technically shouldn't have to check volumes.
457
458 4
				$status = TRUE;
459 8
			} elseif($newVolume > $oldVolume && ($oldChapter < 10 && $newChapter < 10)) {
460
				//This is pretty much just to match a one-off case where the site doesn't properly increment chapter numbers across volumes, and instead does something like: v1/c1..v1/c5, v2/c1..v1/c5 (and so on).
461 1
				$status = TRUE;
462 7
			} elseif($newVolume > $oldVolume && $newChapter >= $oldChapter) {
463
				//$newVolume is higher, and chapter is higher so no need to check chapter.
464 2
				$status = TRUE;
465 5
			} elseif($newChapter > $oldChapter) {
466
				//$newVolume isn't higher, but chapter is.
467
				$status = TRUE;
468
			}
469
		}
470
471 12
		return $status;
472
	}
473
}
474
475
abstract class Base_FoolSlide_Site_Model extends Base_Site_Model {
476
	public $titleFormat   = '/^[a-z0-9_-]+$/';
477
	public $chapterFormat = '/^(?:en(?:-us)?|pt|es)\/[0-9]+(?:\/[0-9]+(?:\/[0-9]+(?:\/[0-9]+)?)?)?$/';
478
	public $customType    = 2;
479
480
	public function getFullTitleURL(string $title_url) : string {
481
		return "{$this->baseURL}/series/{$title_url}";
482
	}
483
484
	public function getChapterData(string $title_url, string $chapter) : array {
485
		$chapter_parts = explode('/', $chapter); //returns #LANG#/#VOLUME#/#CHAPTER#/#CHAPTER_EXTRA#(/#PAGE#/)
486
		return [
487
			'url'    => $this->getChapterURL($title_url, $chapter),
488
			'number' => ($chapter_parts[1] !== '0' ? "v{$chapter_parts[1]}/" : '') . "c{$chapter_parts[2]}" . (isset($chapter_parts[3]) ? ".{$chapter_parts[3]}" : '')/*)*/
489
		];
490
	}
491
	public function getChapterURL(string $title_url, string $chapter) : string {
492
		return "{$this->baseURL}/read/{$title_url}/{$chapter}/";
493
	}
494
495
	public function getTitleData(string $title_url, bool $firstGet = FALSE) : ?array {
496
		$titleData = [];
497
498
		$jsonURL = $this->getJSONTitleURL($title_url);
499
		if($content = $this->get_content($jsonURL)) {
500
			$json = json_decode($content['body'], TRUE);
501
			if($json && isset($json['chapters']) && count($json['chapters']) > 0) {
502
				$titleData['title'] = trim($json['comic']['name']);
503
504
				//FoolSlide title API doesn't appear to let you sort (yet every other API method which has chapters does, so we need to sort ourselves..
505
				usort($json['chapters'], function($a, $b) {
506
					return floatval("{$b['chapter']['chapter']}.{$b['chapter']['subchapter']}") <=> floatval("{$a['chapter']['chapter']}.{$a['chapter']['subchapter']}");
507
				});
508
				$latestChapter = reset($json['chapters'])['chapter'];
509
510
				$latestChapterString = "{$latestChapter['language']}/{$latestChapter['volume']}/{$latestChapter['chapter']}";
511
				if($latestChapter['subchapter'] !== '0') {
512
					$latestChapterString .= "/{$latestChapter['subchapter']}";
513
				}
514
				$titleData['latest_chapter'] = $latestChapterString;
515
516
				//No need to use date() here since this is already formatted as such.
517
				$titleData['last_updated'] = ($latestChapter['updated'] !== '0000-00-00 00:00:00' ? $latestChapter['updated'] : $latestChapter['created']);
518
			}
519
		}
520
521
		return (!empty($titleData) ? $titleData : NULL);
522
	}
523
524
	//Since we're just checking the latest updates page and not a following page, we just need to simulate a follow.
525
	//TODO: It would probably be better to have some kind of var which says that the custom update uses a following page..
526
	public function handleCustomFollow(callable $callback, string $data = "", array $extra = []) {
527
		$content = ['status_code' => 200];
528
		$callback($content, $extra['id']);
529
	}
530
	public function doCustomUpdate() {
531
		$titleDataList = [];
532
533
		$jsonURL = $this->getJSONUpdateURL();
534
		if(($content = $this->get_content($jsonURL)) && $content['status_code'] == 200) {
535
			if(($json = json_decode($content['body'], TRUE)) && isset($json['chapters'])) {
536
				//This should fix edge cases where chapters are uploaded in bulk in the wrong order (HelveticaScans does this with Mousou Telepathy).
537
				usort($json['chapters'], function($a, $b) {
538
					$a_date = new DateTime($a['chapter']['updated'] !== '0000-00-00 00:00:00' ? $a['chapter']['updated'] : $a['chapter']['created']);
539
					$b_date = new DateTime($b['chapter']['updated'] !== '0000-00-00 00:00:00' ? $b['chapter']['updated'] : $b['chapter']['created']);
540
					return $b_date <=> $a_date;
541
				});
542
543
				$parsedTitles = [];
544
				foreach($json['chapters'] as $chapterData) {
545
					if(!in_array($chapterData['comic']['stub'], $parsedTitles)) {
546
						$parsedTitles[] = $chapterData['comic']['stub'];
547
548
						$titleData = [];
549
						$titleData['title'] = trim($chapterData['comic']['name']);
550
551
						$latestChapter = $chapterData['chapter'];
552
553
						$latestChapterString = "en/{$latestChapter['volume']}/{$latestChapter['chapter']}";
554
						if($latestChapter['subchapter'] !== '0') {
555
							$latestChapterString .= "/{$latestChapter['subchapter']}";
556
						}
557
						$titleData['latest_chapter'] = $latestChapterString;
558
559
						//No need to use date() here since this is already formatted as such.
560
						$titleData['last_updated'] = ($latestChapter['updated'] !== '0000-00-00 00:00:00' ? $latestChapter['updated'] : $latestChapter['created']);
561
562
						$titleDataList[$chapterData['comic']['stub']] = $titleData;
563
					} else {
564
						//We already have title data for this title.
565
						continue;
566
					}
567
				}
568
			} else {
569
				log_message('error', "{$this->site} - Custom updating failed (no chapters arg?) for {$this->baseURL}.");
570
			}
571
		} else {
572
			log_message('error', "{$this->site} - Custom updating failed for {$this->baseURL}.");
573
		}
574
575
		return $titleDataList;
576
	}
577
578
	public function getJSONTitleURL(string $title_url) : string {
579
		return "{$this->baseURL}/api/reader/comic/stub/{$title_url}/format/json";
580
	}
581
	public function getJSONUpdateURL() : string {
582
		return "{$this->baseURL}/api/reader/chapters/orderby/desc_created/format/json";
583
	}
584
}
585
586
abstract class Base_myMangaReaderCMS_Site_Model extends Base_Site_Model {
587
	public $titleFormat   = '/^[a-zA-Z0-9_-]+$/';
588
	public $chapterFormat = '/^(?:oneshot|(?:chapter-)?[0-9\.]+)$/';
589
	public $customType    = 2;
590
591
	public function getFullTitleURL(string $title_url) : string {
592
		return "{$this->baseURL}/manga/{$title_url}";
593
	}
594
595
	public function getChapterData(string $title_url, string $chapter) : array {
596
		$chapterN = (ctype_digit($chapter) ? "c${chapter}" : $chapter);
597
		return [
598
			'url'    => $this->getChapterURL($title_url, $chapter),
599
			'number' => $chapterN
600
		];
601
	}
602
	public function getChapterURL(string $title_url, string $chapter) : string {
603
		return $this->getFullTitleURL($title_url).'/'.$chapter;
604
	}
605
606
	public function getTitleData(string $title_url, bool $firstGet = FALSE) : ?array {
607
		$titleData = [];
608
609
		$fullURL = $this->getFullTitleURL($title_url);
610
611
		$content = $this->get_content($fullURL);
612
613
		$data = $this->parseTitleDataDOM(
614
			$content,
0 ignored issues
show
Security Bug introduced by
It seems like $content defined by $this->get_content($fullURL) on line 611 can also be of type false; however, Base_Site_Model::parseTitleDataDOM() does only seem to accept array, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
615
			$title_url,
616
			"(//h2[@class='widget-title'])[1]",
617
			"//ul[contains(@class, 'chapters')]/li[not(contains(@class, 'btn'))][1]",
618
			"div[contains(@class, 'action')]/div[@class='date-chapter-title-rtl']",
619
			"h5/a[1] | h3/a[1]",
620
			"Whoops, looks like something went wrong."
621
		);
622
		if($data) {
623
			$titleData['title'] = trim($data['nodes_title']->textContent);
624
625
			$segments = explode('/', (string) $data['nodes_chapter']->getAttribute('href'));
626
			$needle = array_search('manga', array_reverse($segments, TRUE)) + 2;
627
			$titleData['latest_chapter'] = $segments[$needle];
628
629
			$dateString = $data['nodes_latest']->nodeValue;
630
			$titleData['last_updated'] = date("Y-m-d H:i:s", strtotime(preg_replace('/ (-|\[A\]).*$/', '', $dateString)));
631
		}
632
633
		return (!empty($titleData) ? $titleData : NULL);
634
	}
635
636
637
	//Since we're just checking the latest updates page and not a following page, we just need to simulate a follow.
638
	//TODO: It would probably be better to have some kind of var which says that the custom update uses a following page..
639
	public function handleCustomFollow(callable $callback, string $data = "", array $extra = []) {
640
		$content = ['status_code' => 200];
641
		$callback($content, $extra['id']);
642
	}
643
	public function doCustomUpdate() {
644
		$titleDataList = [];
645
646
		$updateURL = "{$this->baseURL}/latest-release";
647
		if(($content = $this->get_content($updateURL)) && $content['status_code'] == 200) {
648
			$data = $content['body'];
649
650
			$data = preg_replace('/^[\s\S]+<dl>/', '<dl>', $data);
651
			$data = preg_replace('/<\/dl>[\s\S]+$/', '</dl>', $data);
652
653
			$dom = new DOMDocument();
654
			libxml_use_internal_errors(TRUE);
655
			$dom->loadHTML($data);
656
			libxml_use_internal_errors(FALSE);
657
658
			$xpath      = new DOMXPath($dom);
659
			$nodes_rows = $xpath->query("//dl/dd | //div[@class='mangalist']/div[@class='manga-item']");
660
			if($nodes_rows->length > 0) {
661
				foreach($nodes_rows as $row) {
662
					$titleData = [];
663
664
					$nodes_title   = $xpath->query("div[@class='events ']/div[@class='events-body']/h3[@class='events-heading']/a | h3/a", $row);
665
					$nodes_chapter = $xpath->query("(div[@class='events '][1]/div[@class='events-body'][1] | div[@class='manga-chapter'][1])/h6[@class='events-subtitle'][1]/a[1]", $row);
666
					$nodes_latest  = $xpath->query("div[@class='time'] | small", $row);
667
668
					if($nodes_title->length === 1 && $nodes_chapter->length === 1 && $nodes_latest->length === 1) {
669
						$title = $nodes_title->item(0);
670
671
						preg_match('/(?<url>[^\/]+(?=\/$|$))/', $title->getAttribute('href'), $title_url_arr);
672
						$title_url = $title_url_arr['url'];
673
674
						if(!array_key_exists($title_url, $titleDataList)) {
675
							$titleData['title'] = trim($title->textContent);
676
677
							$chapter = $nodes_chapter->item(0);
678
							preg_match('/(?<chapter>[^\/]+(?=\/$|$))/', $chapter->getAttribute('href'), $chapter_arr);
679
							$titleData['latest_chapter'] = $chapter_arr['chapter'];
680
681
							$dateString = str_replace('/', '-', trim($nodes_latest->item(0)->nodeValue)); //NOTE: We replace slashes here as it stops strtotime interpreting the date as US date format.
682
							if($dateString == 'T') {
683
								$dateString = date("Y-m-d",now());
684
							}
685
							$titleData['last_updated'] = date("Y-m-d H:i:s", strtotime($dateString . ' 00:00'));
686
687
							$titleDataList[$title_url] = $titleData;
688
						}
689
					} else {
690
						log_message('error', "{$this->site}/Custom | Invalid amount of nodes (TITLE: {$nodes_title->length} | CHAPTER: {$nodes_chapter->length}) | LATEST: {$nodes_latest->length})");
691
					}
692
				}
693
			} else {
694
				log_message('error', "{$this->site} | Following list is empty?");
695
			}
696
		} else {
697
			log_message('error', "{$this->site} - Custom updating failed for {$this->baseURL}.");
698
		}
699
700
		return $titleDataList;
701
	}
702
}
703
704
abstract class Base_GlossyBright_Site_Model extends Base_Site_Model {
705
	public $titleFormat   = '/^[a-zA-Z0-9_-]+$/';
706
	public $chapterFormat = '/^[0-9\.]+$/';
707
708
	public $customType    = 2;
709
710
	public function getFullTitleURL(string $title_url) : string {
711
		return "{$this->baseURL}/{$title_url}";
712
	}
713
714
	public function getChapterData(string $title_url, string $chapter) : array {
715
		return [
716
			'url'    => $this->getFullTitleURL($title_url).$chapter.'/',
717
			'number' => "c{$chapter}"
718
		];
719
	}
720
721
	public function getTitleData(string $title_url, bool $firstGet = FALSE) : ?array {
722
		$titleData = [];
723
724
		$fullURL = "{$this->baseURL}/manga-rss/{$title_url}";
725
		$content = $this->get_content($fullURL);
726
		$data    = $this->parseTitleDataDOM(
727
			$content,
0 ignored issues
show
Security Bug introduced by
It seems like $content defined by $this->get_content($fullURL) on line 725 can also be of type false; however, Base_Site_Model::parseTitleDataDOM() does only seem to accept array, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
728
			$title_url,
729
			"//rss/channel/image/title",
730
			"//rss/channel/item[1]",
731
			"pubdate",
732
			"title"
733
		);
734
		if($data) {
735
			$titleData['title'] = preg_replace('/^Recent chapters of (.*?) manga$/', '$1', trim($data['nodes_title']->textContent));
736
737
			//For whatever reason, DOMDocument breaks the <link> element we need to grab the chapter, so we have to grab it elsewhere.
738
			$titleData['latest_chapter'] = preg_replace('/^.*? - ([0-9\.]+) - .*?$/', '$1', trim($data['nodes_chapter']->textContent));
739
740
			$titleData['last_updated'] =  date("Y-m-d H:i:s", strtotime((string) $data['nodes_latest']->textContent));
741
		}
742
743
		return (!empty($titleData) ? $titleData : NULL);
744
	}
745
746
	public function doCustomUpdate() {
747
		$titleDataList = [];
748
749
		$baseURLRegex = str_replace('.', '\\.', parse_url($this->baseURL, PHP_URL_HOST));
750
		if(($content = $this->get_content($this->baseURL)) && $content['status_code'] == 200) {
751
			$data = $content['body'];
752
753
			$dom = new DOMDocument();
754
			libxml_use_internal_errors(TRUE);
755
			$dom->loadHTML($data);
756
			libxml_use_internal_errors(FALSE);
757
758
			$xpath      = new DOMXPath($dom);
759
			$nodes_rows = $xpath->query("//table[@id='wpm_mng_lst']/tr/td | //*[@id='wpm_mng_lst']/li/div");
760
			if($nodes_rows->length > 0) {
761
				foreach($nodes_rows as $row) {
762
					$titleData = [];
763
764
					$nodes_title   = $xpath->query("a[2]", $row);
765
					$nodes_chapter = $xpath->query("a[2]", $row);
766
					$nodes_latest  = $xpath->query("b | text()[last()]", $row);
767
768
					if($nodes_title->length === 1 && $nodes_chapter->length === 1 && $nodes_latest->length === 1) {
769
						$title   = $nodes_title->item(0);
770
						$chapter = $nodes_chapter->item(0);
771
772
						preg_match('/'.$baseURLRegex.'\/(?<url>.*?)\//', $title->getAttribute('href'), $title_url_arr);
773
						$title_url = $title_url_arr['url'];
774
775
						if(!array_key_exists($title_url, $titleDataList)) {
776
							$titleData['title'] = trim($title->getAttribute('title'));
777
778
							preg_match('/(?<chapter>[^\/]+(?=\/$|$))/', $chapter->getAttribute('href'), $chapter_arr);
779
							$titleData['latest_chapter'] = $chapter_arr['chapter'];
780
781
							$dateString = trim($nodes_latest->item(0)->textContent);
782
							switch($dateString) {
783
								case 'Today':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
784
									$dateString = date("Y-m-d", now());
785
									break;
786
787
								case 'Yesterday':
788
									$dateString = date("Y-m-d", strtotime("-1 days"));
789
									break;
790
791
								default:
792
									//Do nothing
793
									break;
794
							}
795
							$titleData['last_updated'] = date("Y-m-d H:i:s", strtotime($dateString));
796
797
							$titleDataList[$title_url] = $titleData;
798
						}
799
					} else {
800
						log_message('error', "{$this->site}/Custom | Invalid amount of nodes (TITLE: {$nodes_title->length} | CHAPTER: {$nodes_chapter->length}) | LATEST: {$nodes_latest->length})");
801
					}
802
				}
803
			} else {
804
				log_message('error', "{$this->site} | Following list is empty?");
805
			}
806
		} else {
807
			log_message('error', "{$this->site} - Custom updating failed.");
808
		}
809
810
		return $titleDataList;
811
	}
812
}
813
814
abstract class Base_Roku_Site_Model extends Base_Site_Model {
815
	public $titleFormat   = '/^[a-zA-Z0-9-]+$/';
816
	public $chapterFormat = '/^[0-9\.]+$/';
817
818
	public $customType    = 2;
819
820
	public function getFullTitleURL(string $title_url) : string {
821
		return "{$this->baseURL}/series/{$title_url}";
822
	}
823
	public function getChapterData(string $title_url, string $chapter) : array {
824
		return [
825
			'url'    => "{$this->baseURL}/read/{$title_url}/{$chapter}",
826
			'number' => "c{$chapter}"
827
		];
828
	}
829
	public function getTitleData(string $title_url, bool $firstGet = FALSE) : ?array {
830
		$titleData = [];
831
		$fullURL = $this->getFullTitleURL($title_url);
832
		$content = $this->get_content($fullURL);
833
		$data = $this->parseTitleDataDOM(
834
			$content,
0 ignored issues
show
Security Bug introduced by
It seems like $content defined by $this->get_content($fullURL) on line 832 can also be of type false; however, Base_Site_Model::parseTitleDataDOM() does only seem to accept array, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
835
			$title_url,
836
			"//div[@id='activity']/descendant::div[@class='media'][1]/descendant::div[@class='media-body']/h2/text()",
837
			"//ul[contains(@class, 'media-list')]/li[@class='media'][1]/a",
838
			"div[@class='media-body']/span[@class='text-muted']",
839
			""
840
		);
841
		if($data) {
842
			$titleData['title'] = trim(preg_replace('/ Added on .*$/','', $data['nodes_title']->textContent));
843
			$titleData['latest_chapter'] = preg_replace('/^.*\/([0-9\.]+)$/', '$1', (string) $data['nodes_chapter']->getAttribute('href'));
844
845
			$dateString = preg_replace('/^Added (?:on )?/', '',$data['nodes_latest']->textContent);
846
			$titleData['last_updated'] =  date("Y-m-d H:i:s", strtotime($dateString));
847
		}
848
		return (!empty($titleData) ? $titleData : NULL);
849
	}
850
851
852
	public function doCustomUpdate() {
853
		$titleDataList = [];
854
855
		$updateURL = "{$this->baseURL}/latest";
856
		if(($content = $this->get_content($updateURL)) && $content['status_code'] == 200) {
857
			$data = $content['body'];
858
859
			$dom = new DOMDocument();
860
			libxml_use_internal_errors(TRUE);
861
			$dom->loadHTML($data);
862
			libxml_use_internal_errors(FALSE);
863
864
			$xpath      = new DOMXPath($dom);
865
			$nodes_rows = $xpath->query("//div[@class='content-wrapper']/div[@class='row']/div/div");
866
			if($nodes_rows->length > 0) {
867
				foreach($nodes_rows as $row) {
868
					$titleData = [];
869
870
					$nodes_title   = $xpath->query("div[@class='caption']/h6/a", $row);
871
					$nodes_chapter = $xpath->query("div[@class='panel-footer no-padding']/a", $row);
872
					$nodes_latest  = $xpath->query("div[@class='caption']/text()", $row);
873
874
					if($nodes_title->length === 1 && $nodes_chapter->length === 1 && $nodes_latest->length === 1) {
875
						$title = $nodes_title->item(0);
876
877
						preg_match('/(?<url>[^\/]+(?=\/$|$))/', $title->getAttribute('href'), $title_url_arr);
878
						$title_url = $title_url_arr['url'];
879
880
						if(!array_key_exists($title_url, $titleDataList)) {
881
							$titleData['title'] = trim($title->textContent);
882
883
							$chapter = $nodes_chapter->item(0);
884
							preg_match('/(?<chapter>[^\/]+(?=\/$|$))/', $chapter->getAttribute('href'), $chapter_arr);
885
							$titleData['latest_chapter'] = $chapter_arr['chapter'];
886
887
							$dateString = trim(str_replace('Added ', '', $nodes_latest->item(0)->textContent));
888
							$titleData['last_updated'] = date("Y-m-d H:i:s", strtotime($dateString));
889
890
							$titleDataList[$title_url] = $titleData;
891
						}
892
					} else {
893
						log_message('error', "{$this->site}/Custom | Invalid amount of nodes (TITLE: {$nodes_title->length} | CHAPTER: {$nodes_chapter->length}) | LATEST: {$nodes_latest->length})");
894
					}
895
				}
896
			} else {
897
				log_message('error', "{$this->site} | Following list is empty?");
898
			}
899
		} else {
900
			log_message('error', "{$this->site} - Custom updating failed.");
901
		}
902
903
		return $titleDataList;
904
	}
905
}
906