Completed
Push — master ( 0f6b5a...5522bb )
by Angus
02:56
created

Base_Site_Model::stripChapter()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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