Completed
Push — master ( cc7fc1...a5df07 )
by Angus
02:51
created

Base_Site_Model::_getSiteRateLimit()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 2
nop 0
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 6
rs 10
c 0
b 0
f 0
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
	public $canHaveNoChapters = FALSE;
52
53
	public $siteRateLimit = 600;
54
55 16
	public function __construct() {
56 16
		parent::__construct();
57
58 16
		$this->load->database();
59
60 16
		$this->site = get_class($this);
61 16
	}
62
63
	/**
64
	 * Generates URL to the title page of the requested series.
65
	 *
66
	 * 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)
67
	 *       When storing additional data, we use ':--:' as a delimiter to separate the data. Make sure to handle this as needed.
68
	 *
69
	 * Example:
70
	 *    return "http://mangafox.me/manga/{$title_url}/";
71
	 *
72
	 * Example (with extra data):
73
	 *    $title_parts = explode(':--:', title_url);
74
	 *    return "https://bato.to/comic/_/comics/-r".$title_parts[0];
75
	 *
76
	 * @param string $title_url
77
	 * @return string
78
	 */
79
	abstract public function getFullTitleURL(string $title_url) : string;
80
81
	/**
82
	 * Generates chapter data from given $title_url and $chapter.
83
	 *
84
	 * Chapter must be in a (v[0-9]+/)?c[0-9]+(\..+)? format.
85
	 *
86
	 * NOTE: In some cases, we are required to store the chapter number, and the segment required to generate the chapter URL separately.
87
	 *       Much like when generating the title URL, we use ':--:' as a delimiter to separate the data. Make sure to handle this as needed.
88
	 *
89
	 * Example:
90
	 *     return [
91
	 *        'url'    => $this->getFullTitleURL($title_url).'/'.$chapter,
92
	 *        'number' => "c{$chapter}"
93
	 *    ];
94
	 *
95
	 * @param string $title_url
96
	 * @param string $chapter
97
	 * @return array [url, number]
98
	 */
99
	abstract public function getChapterData(string $title_url, string $chapter) : array;
100
101
	/**
102
	 * Used to get the latest chapter of given $title_url.
103
	 *
104
	 * 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.
105
	 *
106
	 * $titleData params must be set accordingly:
107
	 * * `title` should always be used with html_entity_decode.
108
	 * * `latest_chapter` must match $this->chapterFormat.
109
	 * * `last_updated` should always be in date("Y-m-d H:i:s") format.
110
	 * * `followed` should never be set within via getTitleData, with the exception of via a array_merge with doCustomFollow.
111
	 *
112
	 * $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).
113
	 *
114
	 * @param string $title_url
115
	 * @param bool   $firstGet
116
	 * @return array|null [title,latest_chapter,last_updated,followed?]
117
	 */
118
	abstract public function getTitleData(string $title_url, bool $firstGet = FALSE) : ?array;
119
120
	public function handleBatchUpdate(string $title_url) : array {
121
		$return = [
122
			'limited'   => FALSE,
123
			'titleData' => NULL
124
		];
125
		if(($rateLimit = $this->_getSiteRateLimit()) >= $this->siteRateLimit) {
126
			$this->_setSiteRateLimit($rateLimit);
127
128
			$return['titleData'] = $this->getTitleData($title_url);
129
		} else {
130
			$return['limited'] = TRUE;
131
		}
132
		return $return;
133
	}
134
135
	/**
136
	 * Validates given $title_url against titleFormat.
137
	 *
138
	 * Failure to match against titleFormat will stop the series from being added to the DB.
139
	 *
140
	 * @param string $title_url
141
	 * @return bool
142
	 */
143 2
	final public function isValidTitleURL(string $title_url) : bool {
144 2
		$success = (bool) preg_match($this->titleFormat, $title_url);
145 2
		if(!$success) log_message('error', "Invalid Title URL ({$this->site}): {$title_url}");
146 2
		return $success;
147
	}
148
149
	/**
150
	 * Validates given $chapter against chapterFormat.
151
	 *
152
	 * Failure to match against chapterFormat will stop the chapter being updated.
153
	 *
154
	 * @param string $chapter
155
	 * @return bool
156
	 */
157 2
	final public function isValidChapter(string $chapter) : bool {
158 2
		$success = (bool) preg_match($this->chapterFormat, $chapter);
159 2
		if(!$success) log_message('error', "Invalid Chapter ({$this->site}): {$chapter}");
160 2
		return $success;
161
	}
162
163
	/**
164
	 * Used by getTitleData (& similar functions) to get the requested page data.
165
	 *
166
	 * @param string $url
167
	 * @param string $cookie_string
168
	 * @param string $cookiejar_path
169
	 * @param bool   $follow_redirect
170
	 * @param bool   $isPost
171
	 * @param array  $postFields
172
	 *
173
	 * @return array|bool
174
	 */
175
	final protected function get_content(string $url, string $cookie_string = "", string $cookiejar_path = "", bool $follow_redirect = FALSE, bool $isPost = FALSE, array $postFields = []) {
176
		$refresh = TRUE; //For sites that have CloudFlare, we want to loop get_content again.
177
		$loops   = 0;
178
		while($refresh && $loops < 2) {
179
			$refresh = FALSE;
180
			$loops++;
181
182
			$ch = curl_init();
183
			curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
184
			curl_setopt($ch, CURLOPT_ENCODING , "gzip");
185
			//curl_setopt($ch, CURLOPT_VERBOSE, 1);
186
			curl_setopt($ch, CURLOPT_HEADER, 1);
187
188
			if($follow_redirect)        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
189
190
			if($cookies = $this->cache->get("cloudflare_{$this->site}")) {
191
				$cookie_string .= "; {$cookies}";
192
			}
193
194
			if(!empty($cookie_string))  curl_setopt($ch, CURLOPT_COOKIE, $cookie_string);
195
			if(!empty($cookiejar_path)) curl_setopt($ch, CURLOPT_COOKIEFILE, $cookiejar_path);
196
197
			//Some sites check the useragent for stuff, use a pre-defined user-agent to avoid stuff.
198
			curl_setopt($ch, CURLOPT_USERAGENT, $this->userAgent);
199
200
			//NOTE: This is required for SSL URLs for now. Without it we tend to get error code 60.
201
			curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); //FIXME: This isn't safe, but it allows us to grab SSL URLs
202
203
			curl_setopt($ch, CURLOPT_URL, $url);
204
205
			if($isPost) {
206
				curl_setopt($ch,CURLOPT_POST, count($postFields));
207
				curl_setopt($ch,CURLOPT_POSTFIELDS, http_build_query($postFields));
208
			}
209
210
			$response = curl_exec($ch);
211
212
			$this->Tracker->admin->incrementRequests();
213
214
			if($response === FALSE) {
215
				log_message('error', "curl failed with error: ".curl_errno($ch)." | ".curl_error($ch));
216
				//FIXME: We don't always account for FALSE return
217
				return FALSE;
218
			}
219
220
			$status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
221
			$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
222
			$headers     = http_parse_headers(substr($response, 0, $header_size));
223
			$body        = substr($response, $header_size);
224
			curl_close($ch);
225
226
			if($status_code === 503) $refresh = $this->handleCloudFlare($url, $body);
227
		}
228
229
		return [
230
			'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...
231
			'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...
232
			'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...
233
		];
234
	}
235
236
	final private function handleCloudFlare(string $url, string $body) : bool {
237
		$refresh = FALSE;
238
239
		if((strpos($body, 'DDoS protection by Cloudflare') !== FALSE) || (strpos($body, '<input type="hidden" id="jschl-answer" name="jschl_answer"/>') !== FALSE)) {
240
			//print "Cloudflare detected? Grabbing Cookies.\n";
241
			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...
242
				//TODO: Site appears to have enabled CloudFlare, disable it and contact admin.
243
				//      We'll continue to bypass CloudFlare as this may occur in a loop.
244
			}
245
246
			$urlData = [
247
				'url'        => $url,
248
				'user_agent' => $this->userAgent
249
			];
250
			//TODO: shell_exec seems bad since the URLs "could" be user inputted? Better way of doing this?
251
			$result = shell_exec('python '.APPPATH.'../_scripts/get_cloudflare_cookie.py '.escapeshellarg(json_encode($urlData)));
252
			$cookieData = json_decode($result, TRUE);
253
254
			$this->cache->save("cloudflare_{$this->site}", $cookieData['cookies'],  31536000 /* 1 year, or until we renew it */);
255
			log_message('debug', "Saving CloudFlare Cookies for {$this->site}");
256
257
			$refresh = TRUE;
258
		} 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...
259
			//Either site doesn't have CloudFlare or we have bypassed it. Either is good!
260
		}
261
		return $refresh;
262
	}
263
264
	/**
265
	 * Used by getTitleData to get the title, latest_chapter & last_updated data from the data returned by get_content.
266
	 *
267
	 * parseTitleDataDOM checks if the data returned by get_content is valid via a few simple checks.
268
	 * * 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.
269
	 *
270
	 * Data is cleaned by cleanTitleDataDOM prior to being passed to DOMDocument.
271
	 *
272
	 * All $node_* params must be XPath to the requested node, and must only return 1 result. Anything else will throw a failure.
273
	 *
274
	 * @param array        $content
275
	 * @param string       $title_url
276
	 * @param string       $node_title_string
277
	 * @param string       $node_row_string
278
	 * @param string       $node_latest_string
279
	 * @param string       $node_chapter_string
280
	 * @param string       $failure_string
281
	 * @param closure|null $noChaptersCall
282
	 * @param closure|null $extraCall
283
	 *
284
	 * @return DOMElement[]|false [nodes_title,nodes_chapter,nodes_latest]
285
	 */
286
	final protected function parseTitleDataDOM(
287
		$content, string $title_url,
288
		string $node_title_string, string $node_row_string,
289
		string $node_latest_string, string $node_chapter_string,
290
		string $failure_string = "", closure $noChaptersCall = NULL, closure $extraCall = NULL) {
291
292
		if(!is_array($content)) {
293
			log_message('error', "{$this->site} : {$title_url} | Failed to grab URL (See above curl error)");
294
		} else {
295
			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...
296
297
			if(!($status_code >= 200 && $status_code < 300)) {
298
				if($status_code === 502) {
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...
299
					// Site is overloaded, no need to log this.
300
				} else {
301
					log_message('error', "{$this->site} : {$title_url} | Bad Status Code ({$status_code})");
302
				}
303
			} else if(empty($data)) {
304
				log_message('error', "{$this->site} : {$title_url} | Data is empty? (Status code: {$status_code})");
305
			} else if($failure_string !== "" && strpos($data, $failure_string) !== FALSE) {
306
				log_message('error', "{$this->site} : {$title_url} | Failure string matched");
307
			} else {
308
				$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.
309
310
				$dom = new DOMDocument();
311
				libxml_use_internal_errors(TRUE);
312
				$dom->loadHTML('<?xml encoding="utf-8" ?>' . $data);
313
				libxml_use_internal_errors(FALSE);
314
315
				$xpath = new DOMXPath($dom);
316
				$nodes_title = $xpath->query($node_title_string);
317
				$nodes_row   = $xpath->query($node_row_string);
318
				if($nodes_title->length === 1) {
319
					if($nodes_row->length === 1) {
320
						$firstRow      = $nodes_row->item(0);
321
						$nodes_latest  = $xpath->query($node_latest_string,  $firstRow);
322
323
						if($node_chapter_string !== '') {
324
							$nodes_chapter = $xpath->query($node_chapter_string, $firstRow);
325
						} else {
326
							$nodes_chapter = $nodes_row;
327
						}
328
329
						if($nodes_latest->length === 1 && $nodes_chapter->length === 1) {
330
							$returnData = [
331
								'nodes_title'   => $nodes_title->item(0),
332
								'nodes_latest'  => $nodes_latest->item(0),
333
								'nodes_chapter' => $nodes_chapter->item(0)
334
							];
335
336
							if(is_callable($extraCall)) $extraCall($xpath, $returnData);
337
338
							return $returnData;
339
						} else {
340
							log_message('error', "{$this->site} : {$title_url} | Invalid amount of nodes (LATEST: {$nodes_latest->length} | CHAPTER: {$nodes_chapter->length})");
341
						}
342
					} elseif($this->canHaveNoChapters && !is_null($noChaptersCall) && is_callable($noChaptersCall)) {
343
						$returnData = [
344
							'nodes_title'   => $nodes_title->item(0)
345
						];
346
347
						$noChaptersCall($data, $xpath, $returnData);
348
349
						if(is_array($returnData)) {
350
							if(is_callable($extraCall) && is_array($returnData)) $extraCall($xpath, $returnData);
351
						} else {
352
							log_message('error', "{$this->site} : {$title_url} | canHaveNoChapters set, but doesn't match possible checks! XPath is probably broken.");
353
						}
354
355
						return $returnData;
356
					} else {
357
						log_message('error', "{$this->site} : {$title_url} | Invalid amount of nodes (ROW: {$nodes_row->length})");
358
					}
359
				} else {
360
					log_message('error', "{$this->site} : {$title_url} | Invalid amount of nodes (TITLE: {$nodes_title->length})");
361
				}
362
			}
363
		}
364
365
		return FALSE;
366
	}
367
368
	/**
369
	 * Used by parseTitleDataDOM to clean the data prior to passing it to DOMDocument & DOMXPath.
370
	 * 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.
371
	 *
372
	 * @param string $data
373
	 * @return string
374
	 */
375
	public function cleanTitleDataDOM(string $data) : string {
376
		return $data;
377
	}
378
379
	/**
380
	 * Used to follow a series on given site if supported.
381
	 *
382
	 * This is called by getTitleData if $firstGet is true (which occurs when the series is first being added to the DB).
383
	 *
384
	 * Most of the actual following is done by handleCustomFollow.
385
	 *
386
	 * @param string $data
387
	 * @param array  $extra
388
	 * @return array
389
	 */
390
	final public function doCustomFollow(string $data = "", array $extra = []) : array {
391
		$titleData = [];
392
		$this->handleCustomFollow(function($content, $id, closure $successCallback = NULL) use(&$titleData) {
393
			if(is_array($content)) {
394
				if(array_key_exists('status_code', $content)) {
395
					$statusCode = $content['status_code'];
396
					if($statusCode === 200) {
397
						$isCallable = is_callable($successCallback);
398
						if(($isCallable && $successCallback($content['body'])) || !$isCallable) {
399
							$titleData['followed'] = 'Y';
400
401
							log_message('info', "doCustomFollow succeeded for {$id}");
402
						} else {
403
							log_message('error', "doCustomFollow failed (Invalid response?) for {$id}");
404
						}
405
					} else {
406
						log_message('error', "doCustomFollow failed (Invalid status code ({$statusCode})) for {$id}");
407
					}
408
				} else {
409
					log_message('error', "doCustomFollow failed (Missing status code?) for {$id}");
410
				}
411
			} else {
412
				log_message('error', "doCustomFollow failed (Failed request) for {$id}");
413
			}
414
		}, $data, $extra);
415
		return $titleData;
416
	}
417
418
	/**
419
	 * Used by doCustomFollow to handle following series on sites.
420
	 *
421
	 * Uses get_content to get data.
422
	 *
423
	 * $callback must return ($content, $id, closure $successCallback = NULL).
424
	 * * $content is simply just the get_content data.
425
	 * * $id is the dbID. This should be passed by the $extra arr.
426
	 * * $successCallback is an optional success check to make sure the series was properly followed.
427
	 *
428
	 * @param callable $callback
429
	 * @param string   $data
430
	 * @param array    $extra
431
	 */
432
	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...
433
		if($this->customType === 2) {
434
			$content = ['status_code' => 200];
435
			$callback($content, $extra['id']);
436
		}
437
	}
438
439
	/**
440
	 * Used to check the sites following page for new updates (if supported).
441
	 * This should work much like getTitleData, but instead checks the following page.
442
	 *
443
	 * This must return an array containing arrays of each of the chapters data.
444
	 */
445
	public function doCustomUpdate() {}
446
447
	/**
448
	 * Used by the custom updater to check if a chapter looks newer than the current one.
449
	 *
450
	 * This calls doCustomCheckCompare which handles the majority of the checking.
451
	 * NOTE: Depending on the site, you may need to call getChapterData to get the chapter number to be used with this.
452
	 *
453
	 * @param string $oldChapterString
454
	 * @param string $newChapterString
455
	 * @return bool
456
	 */
457
	public function doCustomCheck(?string $oldChapterString, string $newChapterString) : bool {
458
		if(!is_null($oldChapterString)) {
459
			$oldChapterSegments = explode('/', $this->getChapterData('', $oldChapterString)['number']);
460
			$newChapterSegments = explode('/', $this->getChapterData('', $newChapterString)['number']);
461
462
			$status = $this->doCustomCheckCompare($oldChapterSegments, $newChapterSegments);
463
		} else {
464
			$status = TRUE;
465
		}
466
467
		return $status;
468
	}
469
470
	/**
471
	 * Used by doCustomCheck to check if a chapter looks newer than the current one.
472
	 * Chapter must be in a (v[0-9]+/)?c[0-9]+(\..+)? format.
473
	 *
474
	 * 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.
475
	 *
476
	 * @param array $oldChapterSegments
477
	 * @param array $newChapterSegments
478
	 * @return bool
479
	 */
480 12
	final public function doCustomCheckCompare(array $oldChapterSegments, array $newChapterSegments) : bool {
481
		//NOTE: We only need to check against the new chapter here, as that is what is used for confirming update.
482 12
		$status = FALSE;
483
484
		//Make sure we have a volume element
485 12
		if(count($oldChapterSegments) === 1) array_unshift($oldChapterSegments, 'v0');
486 12
		if(count($newChapterSegments) === 1) array_unshift($newChapterSegments, 'v0');
487
488 12
		$oldCount = count($oldChapterSegments);
489 12
		$newCount = count($newChapterSegments);
490 12
		if($newCount === $oldCount) {
491
			//Make sure chapter format looks correct.
492
			//NOTE: We only need to check newCount as we know oldCount is the same count.
493 12
			if($newCount === 2) {
494
				//FIXME: Can we loop this?
495 12
				$oldVolume = substr(array_shift($oldChapterSegments), 1);
496 12
				$newVolume = substr(array_shift($newChapterSegments), 1);
497
498
				//Forcing volume to 0 as TBD might not be the latest (although it can be, but that is covered by other checks)
499 12
				if(in_array($oldVolume, ['TBD', 'TBA', 'NA', 'LMT'])) $oldVolume = 0;
500 12
				if(in_array($newVolume, ['TBD', 'TBA', 'NA', 'LMT'])) $newVolume = 0;
501
502 12
				$oldVolume = floatval($oldVolume);
503 12
				$newVolume = floatval($newVolume);
504
			} else {
505
				$oldVolume = 0;
506
				$newVolume = 0;
507
			}
508 12
			$oldChapter = floatval(substr(array_shift($oldChapterSegments), 1));
509 12
			$newChapter = floatval(substr(array_shift($newChapterSegments), 1));
510
511 12
			if($newChapter > $oldChapter && ($oldChapter >= 10 && $newChapter >= 10)) {
512
				//$newChapter is higher than $oldChapter AND $oldChapter and $newChapter are both more than 10
513
				//This is intended to cover the /majority/ of valid updates, as we technically shouldn't have to check volumes.
514
515 4
				$status = TRUE;
516 8
			} elseif($newVolume > $oldVolume && ($oldChapter < 10 && $newChapter < 10)) {
517
				//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).
518 1
				$status = TRUE;
519 7
			} elseif($newVolume > $oldVolume && $newChapter >= $oldChapter) {
520
				//$newVolume is higher, and chapter is higher so no need to check chapter.
521 2
				$status = TRUE;
522 5
			} elseif($newChapter > $oldChapter) {
523
				//$newVolume isn't higher, but chapter is.
524
				$status = TRUE;
525
			}
526
		}
527
528 12
		return $status;
529
	}
530
531
	final private function _getSiteRateLimit() : int {
532
		return (int) ($this->cache->get("{$this->site}_ratelimit") ?: 0);
533
	}
534
	final private function _setSiteRateLimit(?int $rateLimit = NULL) : bool {
535
		//We would just use increment(), but we can't set ttl with it...
536
		$currentRateLimit = $rateLimit ?: $this->_getSiteRateLimit();
537
		return $this->cache->save("{$this->site}_ratelimit", $currentRateLimit + 1,3600);
538
	}
539
}
540
541
abstract class Base_FoolSlide_Site_Model extends Base_Site_Model {
542
	public $titleFormat   = '/^[a-z0-9_-]+$/';
543
	public $chapterFormat = '/^(?:en(?:-us)?|pt|es)\/[0-9]+(?:\/[0-9]+(?:\/[0-9]+(?:\/[0-9]+)?)?)?$/';
544
	public $customType    = 2;
545
546
	public function getFullTitleURL(string $title_url) : string {
547
		return "{$this->baseURL}/series/{$title_url}";
548
	}
549
550
	public function getChapterData(string $title_url, string $chapter) : array {
551
		$chapter_parts = explode('/', $chapter); //returns #LANG#/#VOLUME#/#CHAPTER#/#CHAPTER_EXTRA#(/#PAGE#/)
552
		return [
553
			'url'    => $this->getChapterURL($title_url, $chapter),
554
			'number' => ($chapter_parts[1] !== '0' ? "v{$chapter_parts[1]}/" : '') . "c{$chapter_parts[2]}" . (isset($chapter_parts[3]) ? ".{$chapter_parts[3]}" : '')/*)*/
555
		];
556
	}
557
	public function getChapterURL(string $title_url, string $chapter) : string {
558
		return "{$this->baseURL}/read/{$title_url}/{$chapter}/";
559
	}
560
561
	public function getTitleData(string $title_url, bool $firstGet = FALSE) : ?array {
562
		$titleData = [];
563
564
		$jsonURL = $this->getJSONTitleURL($title_url);
565
		if($content = $this->get_content($jsonURL)) {
566
			$json = json_decode($content['body'], TRUE);
567
			if($json && isset($json['chapters']) && count($json['chapters']) > 0) {
568
				$titleData['title'] = trim($json['comic']['name']);
569
570
				//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..
571
				usort($json['chapters'], function($a, $b) {
572
					return floatval("{$b['chapter']['chapter']}.{$b['chapter']['subchapter']}") <=> floatval("{$a['chapter']['chapter']}.{$a['chapter']['subchapter']}");
573
				});
574
				$latestChapter = reset($json['chapters'])['chapter'];
575
576
				$latestChapterString = "{$latestChapter['language']}/{$latestChapter['volume']}/{$latestChapter['chapter']}";
577
				if($latestChapter['subchapter'] !== '0') {
578
					$latestChapterString .= "/{$latestChapter['subchapter']}";
579
				}
580
				$titleData['latest_chapter'] = $latestChapterString;
581
582
				//No need to use date() here since this is already formatted as such.
583
				$titleData['last_updated'] = ($latestChapter['updated'] !== '0000-00-00 00:00:00' ? $latestChapter['updated'] : $latestChapter['created']);
584
			}
585
		}
586
587
		return (!empty($titleData) ? $titleData : NULL);
588
	}
589
590
	public function doCustomUpdate() {
591
		$titleDataList = [];
592
593
		$jsonURL = $this->getJSONUpdateURL();
594
		if(($content = $this->get_content($jsonURL)) && $content['status_code'] == 200) {
595
			if(($json = json_decode($content['body'], TRUE)) && isset($json['chapters'])) {
596
				//This should fix edge cases where chapters are uploaded in bulk in the wrong order (HelveticaScans does this with Mousou Telepathy).
597
				usort($json['chapters'], function($a, $b) {
598
					$a_date = new DateTime($a['chapter']['updated'] !== '0000-00-00 00:00:00' ? $a['chapter']['updated'] : $a['chapter']['created']);
599
					$b_date = new DateTime($b['chapter']['updated'] !== '0000-00-00 00:00:00' ? $b['chapter']['updated'] : $b['chapter']['created']);
600
					return $b_date <=> $a_date;
601
				});
602
603
				$parsedTitles = [];
604
				foreach($json['chapters'] as $chapterData) {
605
					if(!in_array($chapterData['comic']['stub'], $parsedTitles)) {
606
						$parsedTitles[] = $chapterData['comic']['stub'];
607
608
						$titleData = [];
609
						$titleData['title'] = trim($chapterData['comic']['name']);
610
611
						$latestChapter = $chapterData['chapter'];
612
613
						$latestChapterString = "en/{$latestChapter['volume']}/{$latestChapter['chapter']}";
614
						if($latestChapter['subchapter'] !== '0') {
615
							$latestChapterString .= "/{$latestChapter['subchapter']}";
616
						}
617
						$titleData['latest_chapter'] = $latestChapterString;
618
619
						//No need to use date() here since this is already formatted as such.
620
						$titleData['last_updated'] = ($latestChapter['updated'] !== '0000-00-00 00:00:00' ? $latestChapter['updated'] : $latestChapter['created']);
621
622
						$titleDataList[$chapterData['comic']['stub']] = $titleData;
623
					} else {
624
						//We already have title data for this title.
625
						continue;
626
					}
627
				}
628
			} else {
629
				log_message('error', "{$this->site} - Custom updating failed (no chapters arg?) for {$this->baseURL}.");
630
			}
631
		} else {
632
			log_message('error', "{$this->site} - Custom updating failed for {$this->baseURL}.");
633
		}
634
635
		return $titleDataList;
636
	}
637
638
	public function getJSONTitleURL(string $title_url) : string {
639
		return "{$this->baseURL}/api/reader/comic/stub/{$title_url}/format/json";
640
	}
641
	public function getJSONUpdateURL() : string {
642
		return "{$this->baseURL}/api/reader/chapters/orderby/desc_created/format/json";
643
	}
644
}
645
646
abstract class Base_myMangaReaderCMS_Site_Model extends Base_Site_Model {
647
	public $titleFormat   = '/^[a-zA-Z0-9_-]+$/';
648
	public $chapterFormat = '/^(?:oneshot|(?:chapter-)?[0-9\.]+)$/';
649
	public $customType    = 2;
650
651
	public function getFullTitleURL(string $title_url) : string {
652
		return "{$this->baseURL}/manga/{$title_url}";
653
	}
654
655
	public function getChapterData(string $title_url, string $chapter) : array {
656
		$chapterN = (ctype_digit($chapter) ? "c${chapter}" : $chapter);
657
		return [
658
			'url'    => $this->getChapterURL($title_url, $chapter),
659
			'number' => $chapterN
660
		];
661
	}
662
	public function getChapterURL(string $title_url, string $chapter) : string {
663
		return $this->getFullTitleURL($title_url).'/'.$chapter;
664
	}
665
666
	public function getTitleData(string $title_url, bool $firstGet = FALSE) : ?array {
667
		$titleData = [];
668
669
		$fullURL = $this->getFullTitleURL($title_url);
670
671
		$content = $this->get_content($fullURL);
672
673
		$data = $this->parseTitleDataDOM(
674
			$content,
0 ignored issues
show
Security Bug introduced by
It seems like $content defined by $this->get_content($fullURL) on line 671 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...
675
			$title_url,
676
			"(//h2[@class='widget-title'])[1]",
677
			"//ul[contains(@class, 'chapters')]/li[not(contains(@class, 'btn'))][1]",
678
			"div[contains(@class, 'action')]/div[@class='date-chapter-title-rtl']",
679
			"h5/a[1] | h3/a[1]",
680
			"Whoops, looks like something went wrong."
681
		);
682
		if($data) {
683
			$titleData['title'] = trim($data['nodes_title']->textContent);
684
685
			$segments = explode('/', (string) $data['nodes_chapter']->getAttribute('href'));
686
			$needle = array_search('manga', array_reverse($segments, TRUE)) + 2;
687
			$titleData['latest_chapter'] = $segments[$needle];
688
689
			$dateString = $data['nodes_latest']->nodeValue;
690
			$titleData['last_updated'] = date("Y-m-d H:i:s", strtotime(preg_replace('/ (-|\[A\]).*$/', '', $dateString)));
691
		}
692
693
		return (!empty($titleData) ? $titleData : NULL);
694
	}
695
696
	public function doCustomUpdate() {
697
		$titleDataList = [];
698
699
		$updateURL = "{$this->baseURL}/latest-release";
700
		if(($content = $this->get_content($updateURL)) && $content['status_code'] == 200) {
701
			$data = $content['body'];
702
703
			$data = preg_replace('/^[\s\S]+<dl>/', '<dl>', $data);
704
			$data = preg_replace('/<\/dl>[\s\S]+$/', '</dl>', $data);
705
706
			$dom = new DOMDocument();
707
			libxml_use_internal_errors(TRUE);
708
			$dom->loadHTML($data);
709
			libxml_use_internal_errors(FALSE);
710
711
			$xpath      = new DOMXPath($dom);
712
			$nodes_rows = $xpath->query("//dl/dd | //div[@class='mangalist']/div[@class='manga-item']");
713
			if($nodes_rows->length > 0) {
714
				foreach($nodes_rows as $row) {
715
					$titleData = [];
716
717
					$nodes_title   = $xpath->query("div[@class='events ']/div[@class='events-body']/h3[@class='events-heading']/a | h3/a", $row);
718
					$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);
719
					$nodes_latest  = $xpath->query("div[@class='time'] | small", $row);
720
721
					if($nodes_title->length === 1 && $nodes_chapter->length === 1 && $nodes_latest->length === 1) {
722
						$title = $nodes_title->item(0);
723
724
						preg_match('/(?<url>[^\/]+(?=\/$|$))/', $title->getAttribute('href'), $title_url_arr);
725
						$title_url = $title_url_arr['url'];
726
727
						if(!array_key_exists($title_url, $titleDataList)) {
728
							$titleData['title'] = trim($title->textContent);
729
730
							$chapter = $nodes_chapter->item(0);
731
							preg_match('/(?<chapter>[^\/]+(?=\/$|$))/', $chapter->getAttribute('href'), $chapter_arr);
732
							$titleData['latest_chapter'] = $chapter_arr['chapter'];
733
734
							$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.
735
							if($dateString == 'T') {
736
								$dateString = date("Y-m-d",now());
737
							}
738
							$titleData['last_updated'] = date("Y-m-d H:i:s", strtotime($dateString . ' 00:00'));
739
740
							$titleDataList[$title_url] = $titleData;
741
						}
742
					} else {
743
						log_message('error', "{$this->site}/Custom | Invalid amount of nodes (TITLE: {$nodes_title->length} | CHAPTER: {$nodes_chapter->length}) | LATEST: {$nodes_latest->length})");
744
					}
745
				}
746
			} else {
747
				log_message('error', "{$this->site} | Following list is empty?");
748
			}
749
		} else {
750
			log_message('error', "{$this->site} - Custom updating failed for {$this->baseURL}.");
751
		}
752
753
		return $titleDataList;
754
	}
755
}
756
757
abstract class Base_GlossyBright_Site_Model extends Base_Site_Model {
758
	public $titleFormat   = '/^[a-zA-Z0-9_-]+$/';
759
	public $chapterFormat = '/^[0-9\.]+$/';
760
761
	public $customType    = 2;
762
763
	public function getFullTitleURL(string $title_url) : string {
764
		return "{$this->baseURL}/{$title_url}";
765
	}
766
767
	public function getChapterData(string $title_url, string $chapter) : array {
768
		return [
769
			'url'    => $this->getFullTitleURL($title_url).'/'.$chapter.'/',
770
			'number' => "c{$chapter}"
771
		];
772
	}
773
774
	public function getTitleData(string $title_url, bool $firstGet = FALSE) : ?array {
775
		$titleData = [];
776
777
		$fullURL = "{$this->baseURL}/manga-rss/{$title_url}";
778
		$content = $this->get_content($fullURL);
779
		$data    = $this->parseTitleDataDOM(
780
			$content,
0 ignored issues
show
Security Bug introduced by
It seems like $content defined by $this->get_content($fullURL) on line 778 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...
781
			$title_url,
782
			"//rss/channel/image/title",
783
			"//rss/channel/item[1]",
784
			"pubdate",
785
			"title"
786
		);
787
		if($data) {
788
			$titleData['title'] = preg_replace('/^Recent chapters of (.*?) manga$/', '$1', trim($data['nodes_title']->textContent));
789
790
			//For whatever reason, DOMDocument breaks the <link> element we need to grab the chapter, so we have to grab it elsewhere.
791
			$titleData['latest_chapter'] = preg_replace('/^.*? - ([0-9\.]+) - .*?$/', '$1', trim($data['nodes_chapter']->textContent));
792
793
			$titleData['last_updated'] =  date("Y-m-d H:i:s", strtotime((string) $data['nodes_latest']->textContent));
794
		}
795
796
		return (!empty($titleData) ? $titleData : NULL);
797
	}
798
799
	public function doCustomUpdate() {
800
		$titleDataList = [];
801
802
		$baseURLRegex = str_replace('.', '\\.', parse_url($this->baseURL, PHP_URL_HOST));
803
		if(($content = $this->get_content($this->baseURL)) && $content['status_code'] == 200) {
804
			$data = $content['body'];
805
806
			$dom = new DOMDocument();
807
			libxml_use_internal_errors(TRUE);
808
			$dom->loadHTML($data);
809
			libxml_use_internal_errors(FALSE);
810
811
			$xpath      = new DOMXPath($dom);
812
			$nodes_rows = $xpath->query("//div[@id='wpm_mng_lst']/div | //*[@id='wpm_mng_lst']/li/div");
813
			if($nodes_rows->length > 0) {
814
				foreach($nodes_rows as $row) {
815
					$titleData = [];
816
817
					$nodes_title   = $xpath->query("a[2]", $row);
818
					$nodes_chapter = $xpath->query("a[2]", $row);
819
					$nodes_latest  = $xpath->query("b", $row);
820
821
					if($nodes_latest->length === 0) {
822
						$nodes_latest = $xpath->query('text()[last()]', $row);
823
					}
824
825
					if($nodes_title->length === 1 && $nodes_chapter->length === 1 && $nodes_latest->length === 1) {
826
						$title   = $nodes_title->item(0);
827
						$chapter = $nodes_chapter->item(0);
828
829
						preg_match('/'.$baseURLRegex.'\/(?<url>.*?)\//', $title->getAttribute('href'), $title_url_arr);
830
						$title_url = $title_url_arr['url'];
831
832
						if(!array_key_exists($title_url, $titleDataList)) {
833
							$titleData['title'] = trim($title->getAttribute('title'));
834
835
							preg_match('/(?<chapter>[^\/]+(?=\/$|$))/', $chapter->getAttribute('href'), $chapter_arr);
836
							$titleData['latest_chapter'] = $chapter_arr['chapter'];
837
838
							$dateString = trim($nodes_latest->item(0)->textContent);
839
							switch($dateString) {
840
								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...
841
									$dateString = date("Y-m-d", now());
842
									break;
843
844
								case 'Yesterday':
845
									$dateString = date("Y-m-d", strtotime("-1 days"));
846
									break;
847
848
								default:
849
									//Do nothing
850
									break;
851
							}
852
							$titleData['last_updated'] = date("Y-m-d H:i:s", strtotime($dateString));
853
854
							$titleDataList[$title_url] = $titleData;
855
						}
856
					} else {
857
						log_message('error', "{$this->site}/Custom | Invalid amount of nodes (TITLE: {$nodes_title->length} | CHAPTER: {$nodes_chapter->length}) | LATEST: {$nodes_latest->length})");
858
					}
859
				}
860
			} else {
861
				log_message('error', "{$this->site} | Following list is empty?");
862
			}
863
		} else {
864
			log_message('error', "{$this->site} - Custom updating failed.");
865
		}
866
867
		return $titleDataList;
868
	}
869
}
870
871
abstract class Base_Roku_Site_Model extends Base_Site_Model {
872
	public $titleFormat   = '/^[a-zA-Z0-9-]+$/';
873
	public $chapterFormat = '/^[0-9\.]+$/';
874
875
	public $customType    = 2;
876
877
	public function getFullTitleURL(string $title_url) : string {
878
		return "{$this->baseURL}/series/{$title_url}";
879
	}
880
	public function getChapterData(string $title_url, string $chapter) : array {
881
		return [
882
			'url'    => "{$this->baseURL}/read/{$title_url}/{$chapter}",
883
			'number' => "c{$chapter}"
884
		];
885
	}
886
	public function getTitleData(string $title_url, bool $firstGet = FALSE) : ?array {
887
		$titleData = [];
888
		$fullURL = $this->getFullTitleURL($title_url);
889
		$content = $this->get_content($fullURL);
890
		$data = $this->parseTitleDataDOM(
891
			$content,
0 ignored issues
show
Security Bug introduced by
It seems like $content defined by $this->get_content($fullURL) on line 889 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...
892
			$title_url,
893
			"//div[@id='activity']/descendant::div[@class='media'][1]/descendant::div[@class='media-body']/h2/text()",
894
			"//ul[contains(@class, 'media-list')]/li[@class='media'][1]/a",
895
			"div[@class='media-body']/span[@class='text-muted']",
896
			""
897
		);
898
		if($data) {
899
			$titleData['title'] = trim(preg_replace('/ Added on .*$/','', $data['nodes_title']->textContent));
900
			$titleData['latest_chapter'] = preg_replace('/^.*\/([0-9\.]+)$/', '$1', (string) $data['nodes_chapter']->getAttribute('href'));
901
902
			$dateString = preg_replace('/^Added (?:on )?/', '',$data['nodes_latest']->textContent);
903
			$titleData['last_updated'] =  date("Y-m-d H:i:s", strtotime($dateString));
904
		}
905
		return (!empty($titleData) ? $titleData : NULL);
906
	}
907
908
909
	public function doCustomUpdate() {
910
		$titleDataList = [];
911
912
		$updateURL = "{$this->baseURL}/latest";
913
		if(($content = $this->get_content($updateURL)) && $content['status_code'] == 200) {
914
			$data = $content['body'];
915
916
			$dom = new DOMDocument();
917
			libxml_use_internal_errors(TRUE);
918
			$dom->loadHTML($data);
919
			libxml_use_internal_errors(FALSE);
920
921
			$xpath      = new DOMXPath($dom);
922
			$nodes_rows = $xpath->query("//div[@class='content-wrapper']/div[@class='row']/div/div");
923
			if($nodes_rows->length > 0) {
924
				foreach($nodes_rows as $row) {
925
					$titleData = [];
926
927
					$nodes_title   = $xpath->query("div[@class='caption']/h6/a", $row);
928
					$nodes_chapter = $xpath->query("div[@class='panel-footer no-padding']/a", $row);
929
					$nodes_latest  = $xpath->query("div[@class='caption']/text()", $row);
930
931
					if($nodes_title->length === 1 && $nodes_chapter->length === 1 && $nodes_latest->length === 1) {
932
						$title = $nodes_title->item(0);
933
934
						preg_match('/(?<url>[^\/]+(?=\/$|$))/', $title->getAttribute('href'), $title_url_arr);
935
						$title_url = $title_url_arr['url'];
936
937
						if(!array_key_exists($title_url, $titleDataList)) {
938
							$titleData['title'] = trim($title->textContent);
939
940
							$chapter = $nodes_chapter->item(0);
941
							preg_match('/(?<chapter>[^\/]+(?=\/$|$))/', $chapter->getAttribute('href'), $chapter_arr);
942
							$titleData['latest_chapter'] = $chapter_arr['chapter'];
943
944
							$dateString = trim(str_replace('Added ', '', $nodes_latest->item(0)->textContent));
945
							$titleData['last_updated'] = date("Y-m-d H:i:s", strtotime($dateString));
946
947
							$titleDataList[$title_url] = $titleData;
948
						}
949
					} else {
950
						log_message('error', "{$this->site}/Custom | Invalid amount of nodes (TITLE: {$nodes_title->length} | CHAPTER: {$nodes_chapter->length}) | LATEST: {$nodes_latest->length})");
951
					}
952
				}
953
			} else {
954
				log_message('error', "{$this->site} | Following list is empty?");
955
			}
956
		} else {
957
			log_message('error', "{$this->site} - Custom updating failed.");
958
		}
959
960
		return $titleDataList;
961
	}
962
}
963