Completed
Push — master ( e52041...9b1359 )
by Angus
03:20
created

Tracker_Model::getTitleID()   C

Complexity

Conditions 7
Paths 10

Size

Total Lines 36
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
cc 7
eloc 23
nc 10
nop 2
dl 0
loc 36
ccs 0
cts 0
cp 0
crap 56
rs 6.7272
c 0
b 0
f 0
1
<?php declare(strict_types=1); defined('BASEPATH') OR exit('No direct script access allowed');
2
3
class Tracker_Model extends CI_Model {
4
	public $sites;
5
	public $enabledCategories;
6
7 121
	public function __construct() {
8 121
		parent::__construct();
9
10 121
		$this->load->database();
11
12 121
		$this->enabledCategories = [
13
			'reading'      => 'Reading',
14
			'on-hold'      => 'On-Hold',
15
			'plan-to-read' => 'Plan to Read'
16
		];
17 121
		if($this->User_Options->get('category_custom_1') == 'enabled') {
18
			$this->enabledCategories['custom1'] = $this->User_Options->get('category_custom_1_text');
19
		}
20 121
		if($this->User_Options->get('category_custom_2') == 'enabled') {
21
			$this->enabledCategories['custom2'] = $this->User_Options->get('category_custom_2_text');
22
		}
23 121
		if($this->User_Options->get('category_custom_3') == 'enabled') {
24
			$this->enabledCategories['custom3'] = $this->User_Options->get('category_custom_3_text');
25
		}
26
27 121
		require_once(APPPATH.'models/Site_Model.php');
28 121
		$this->sites = new Sites_Model;
29 121
	}
30
31
	/****** GET TRACKER *******/
32
	public function get_tracker_from_user_id(int $userID) {
33
		$query = $this->db
34
			->select('tracker_chapters.*,
35
			          tracker_titles.site_id, tracker_titles.title, tracker_titles.title_url, tracker_titles.latest_chapter, tracker_titles.last_updated AS title_last_updated, tracker_titles.complete AS title_complete, tracker_titles.last_checked > DATE_SUB(NOW(), INTERVAL 1 WEEK) AS title_active,
36
			          tracker_sites.site, tracker_sites.site_class, tracker_sites.status AS site_status')
37
			->from('tracker_chapters')
38
			->join('tracker_titles', 'tracker_chapters.title_id = tracker_titles.id', 'left')
39
			->join('tracker_sites', 'tracker_sites.id = tracker_titles.site_id', 'left')
40
			->where('tracker_chapters.user_id', $userID)
41
			->where('tracker_chapters.active', 'Y')
42
			->get();
43
44
		$arr = ['series' => [], 'has_inactive' => FALSE];
45
		foreach($this->enabledCategories as $category => $name) {
46
			$arr['series'][$category] = [
47
				'name'         => $name,
48
				'manga'        => [],
49
				'unread_count' => 0
50
			];
51
		}
52
		if($query->num_rows() > 0) {
53
			foreach ($query->result() as $row) {
54
				$is_unread     = intval($row->latest_chapter == $row->current_chapter ? '1' : '0');
55
				$arr['series'][$row->category]['unread_count'] = (($arr['series'][$row->category]['unread_count'] ?? 0) + !$is_unread);
56
				$data = [
57
					'id' => $row->id,
58
					'generated_current_data' => $this->sites->{$row->site_class}->getChapterData($row->title_url, $row->current_chapter),
59
					'generated_latest_data'  => $this->sites->{$row->site_class}->getChapterData($row->title_url, $row->latest_chapter),
60
					'full_title_url'        =>  $this->sites->{$row->site_class}->getFullTitleURL($row->title_url),
61
62
					'new_chapter_exists'    => $is_unread,
63
					'tag_list'              => $row->tags,
64
					'has_tags'              => !empty($row->tags),
65
66
					'title_data' => [
67
						'id'              => $row->title_id,
68
						'title'           => $row->title,
69
						'title_url'       => $row->title_url,
70
						'latest_chapter'  => $row->latest_chapter,
71
						'current_chapter' => $row->current_chapter,
72
						'last_updated'    => $row->title_last_updated,
73
						'active'          => ($row->site_status == 'disabled' || $row->title_complete == 'Y' || $row->title_active == 1)
74
					],
75
					'site_data' => [
76
						'id'         => $row->site_id,
77
						'site'       => $row->site,
78
						'status'     => $row->site_status
79
					]
80
				];
81
				$arr['series'][$row->category]['manga'][] = $data;
82
83
				if(!$arr['has_inactive']) $arr['has_inactive'] = !$data['title_data']['active'];
84
			}
85
86
			//CHECK: Is this good for speed?
87
			//NOTE: This does not sort in the same way as tablesorter, but it works better.
88
			switch($this->User_Options->get('list_sort_type')) {
89
				case 'unread':
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...
90
					foreach (array_keys($arr['series']) as $category) {
91
						usort($arr['series'][$category]['manga'], function ($a, $b) {
92
							$a_text = strtolower("{$a['new_chapter_exists']} - {$a['title_data']['title']}");
93
							$b_text = strtolower("{$b['new_chapter_exists']} - {$b['title_data']['title']}");
94
95
							if($this->User_Options->get('list_sort_order') == 'asc') {
96
								return $a_text <=> $b_text;
97
							} else {
98
								return $b_text <=> $a_text;
99
							}
100
						});
101
					}
102
					break;
103
104 View Code Duplication
				case 'alphabetical':
1 ignored issue
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...
105
					foreach (array_keys($arr['series']) as $category) {
106
						usort($arr['series'][$category]['manga'], function ($a, $b) {
107
							$a_text = strtolower("{$a['title_data']['title']}");
108
							$b_text = strtolower("{$b['title_data']['title']}");
109
110
							if($this->User_Options->get('list_sort_order') == 'asc') {
111
								return $a_text <=> $b_text;
112
							} else {
113
								return $b_text <=> $a_text;
114
							}
115
						});
116
					}
117
					break;
118
119 View Code Duplication
				case 'my_status':
120
					foreach (array_keys($arr['series']) as $category) {
121
						usort($arr['series'][$category]['manga'], function ($a, $b) {
122
							$a_text = strtolower("{$a['generated_current_data']['number']}");
123
							$b_text = strtolower("{$b['generated_current_data']['number']}");
124
125
							if($this->User_Options->get('list_sort_order') == 'asc') {
126
								return $a_text <=> $b_text;
127
							} else {
128
								return $b_text <=> $a_text;
129
							}
130
						});
131
					}
132
					break;
133
134 View Code Duplication
				case 'latest':
135
					foreach (array_keys($arr['series']) as $category) {
136
						usort($arr['series'][$category]['manga'], function ($a, $b) {
137
							$a_text = strtolower("{$a['generated_latest_data']['number']}");
138
							$b_text = strtolower("{$b['generated_latest_data']['number']}");
139
140
							if($this->User_Options->get('list_sort_order') == 'asc') {
141
								return $a_text <=> $b_text;
142
							} else {
143
								return $b_text <=> $a_text;
144
							}
145
						});
146
					}
147
					break;
148
149
				default:
150
					break;
151
			}
152
		}
153
		return $arr;
154
	}
155
156
	public function getSiteDataFromURL(string $site_url) {
157
		$query = $this->db->select('id, site_class')
158
		                  ->from('tracker_sites')
159
		                  ->where('site', $site_url)
160
		                  ->get();
161
162
		if($query->num_rows() > 0) {
163
			$siteData = $query->row();
164
		}
165
166
		return $siteData ?? FALSE;
167
	}
168
169
	public function getTitleID(string $titleURL, int $siteID) {
170
		$query = $this->db->select('tracker_titles.id, tracker_titles.title, tracker_titles.title_url, tracker_titles.complete, tracker_sites.site_class, (tracker_titles.last_checked > DATE_SUB(NOW(), INTERVAL 3 DAY)) AS active', FALSE)
171
		                  ->from('tracker_titles')
172
		                  ->join('tracker_sites', 'tracker_sites.id = tracker_titles.site_id', 'left')
173
		                  ->where('tracker_titles.title_url', $titleURL)
174
		                  ->where('tracker_titles.site_id', $siteID)
175
		                  ->get();
176
177
		if($query->num_rows() > 0) {
178
			$id = (int) $query->row('id');
179
180
			//This updates inactive series if they are newly added, as noted in https://github.com/DakuTree/manga-tracker/issues/5#issuecomment-247480804
181
			if(((int) $query->row('active')) === 0 && $query->row('complete') === 'N') {
182
				$titleData = $this->sites->{$query->row('site_class')}->getTitleData($query->row('title_url'));
183
				if(!is_null($titleData['latest_chapter'])) {
184
					if($this->updateTitleById((int) $id, $titleData['latest_chapter'])) {
185
						//Make sure last_checked is always updated on successful run.
186
						//CHECK: Is there a reason we aren't just doing this in updateTitleById?
187
						$this->db->set('last_checked', 'CURRENT_TIMESTAMP', FALSE)
188
						         ->where('id', $id)
189
						         ->update('tracker_titles');
190
					}
191
				} else {
192
					log_message('error', "{$query->row('title')} failed to update successfully");
193
				}
194
			}
195
196
			$titleID = $id;
197
		} else {
198
			//TODO: Check if title is valid URL!
199
			$titleID = $this->addTitle($titleURL, $siteID);
200
		}
201
		if(!$titleID) $titleID = 0;
202
203
		return $titleID;
204
	}
205
206
	public function updateTracker(int $userID, string $site, string $title, string $chapter) : bool {
207
		$success = FALSE;
208
		if($siteData = $this->Tracker->getSiteDataFromURL($site)) {
209
			//Validate user input
210
			if(!$this->sites->{$siteData->site_class}->isValidTitleURL($title)) {
211
				//Error is already logged via isValidTitleURL
212
				return FALSE;
213
			}
214
			if(!$this->sites->{$siteData->site_class}->isValidChapter($chapter)) {
215
				//Error is already logged via isValidChapter
216
				return FALSE;
217
			}
218
219
			//NOTE: If the title doesn't exist it will be created. This maybe isn't perfect, but it works for now.
220
			$titleID = $this->Tracker->getTitleID($title, (int) $siteData->id);
221
			if($titleID === 0) {
222
				//Something went wrong.
223
				log_message('error', "TitleID = 0 for {$title} @ {$siteData->id}");
224
				return FALSE;
225
			}
226
227
			$idQuery = $this->db->select('id')
228
			                    ->where('user_id', $userID)
229
			                    ->where('title_id', $titleID)
230
			                    ->get('tracker_chapters');
231
			if($idQuery->num_rows() > 0) {
232
				$success = (bool) $this->db->set(['current_chapter' => $chapter, 'active' => 'Y', 'last_updated' => NULL])
233
				                    ->where('user_id', $userID)
234
				                    ->where('title_id', $titleID)
235
				                    ->update('tracker_chapters');
236
237
				if($success) {
238
					$idQueryRow = $idQuery->row();
239
					$this->History->userUpdateTitle((int) $idQueryRow->id, $chapter);
240
				}
241
			} else {
242
				$category = $this->User_Options->get_by_userid('default_series_category', $userID);
243
				$success = (bool) $this->db->insert('tracker_chapters', [
244
					'user_id'         => $userID,
245
					'title_id'        => $titleID,
246
					'current_chapter' => $chapter,
247
					'category'        => $category
248
				]);
249
250
				if($success) {
251
					$this->History->userAddTitle((int) $this->db->insert_id(), $chapter, $category);
0 ignored issues
show
Bug introduced by
The method insert_id() does not exist on CI_DB_query_builder. Did you maybe mean insert()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
252
				}
253
			}
254
		}
255
		return $success;
256
	}
257
258
	public function updateTrackerByID(int $userID, int $chapterID, string $chapter) : bool {
259
		$success = (bool) $this->db->set(['current_chapter' => $chapter, 'active' => 'Y', 'last_updated' => NULL])
260
		                    ->where('user_id', $userID)
261
		                    ->where('id', $chapterID)
262
		                    ->update('tracker_chapters');
263
264
		if($success) {
265
			$this->History->userUpdateTitle($chapterID, $chapter);
266
		}
267
		return  $success;
268
	}
269
270 View Code Duplication
	public function deleteTrackerByID(int $userID, int $chapterID) {
271
		//Series are not fully deleted, they are just marked as inactive as to hide them from the user.
272
		//This is to allow user history to function properly.
273
274
		$success = $this->db->set(['active' => 'N', 'last_updated' => NULL])
275
		                    ->where('user_id', $userID)
276
		                    ->where('id', $chapterID)
277
		                    ->update('tracker_chapters');
278
279
		return (bool) $success;
280
	}
281
	private function updateTitleById(int $id, string $latestChapter) {
282
		//FIXME: Really not too happy with how we're doing history stuff here, it just feels messy.
283
		$query = $this->db->select('latest_chapter AS current_chapter')
284
		                  ->from('tracker_titles')
285
		                  ->where('id', $id)
286
		                  ->get();
287
		$row = $query->row();
288
289
		$success = $this->db->set(['latest_chapter' => $latestChapter]) //last_updated gets updated via a trigger if something changes
290
		                    ->where('id', $id)
291
		                    ->update('tracker_titles');
292
293
		//Update History
294
		//NOTE: To avoid doing another query to grab the last_updated time, we just use time() which <should> get the same thing.
295
		//FIXME: The <preferable> solution here is we'd just check against the last_updated time, but that can have a few issues.
296
		$this->History->updateTitleHistory($id, $row->current_chapter, $latestChapter, date('Y-m-d H:i:s'));
297
298
		return (bool) $success;
299
	}
300
	private function updateTitleDataById(int $id, array $titleData) {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
301
		$success = $this->db->set($titleData)
302
		                    ->where('id', $id)
303
		                    ->update('tracker_titles');
304
305
		return (bool) $success;
306
	}
307
	private function addTitle(string $titleURL, int $siteID) {
308
		$query = $this->db->select('site, site_class')
309
		                  ->from('tracker_sites')
310
		                  ->where('id', $siteID)
311
		                  ->get();
312
313
		$titleData = $this->sites->{$query->row()->site_class}->getTitleData($titleURL);
314
315
		//FIXME: getTitleData can fail, which will in turn cause the below to fail aswell, we should try and account for that
316
		if($titleData) {
317
			$this->db->insert('tracker_titles', array_merge($titleData, ['title_url' => $titleURL, 'site_id' => $siteID]));
318
			$titleID = $this->db->insert_id();
0 ignored issues
show
Bug introduced by
The method insert_id() does not exist on CI_DB_query_builder. Did you maybe mean insert()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
319
320
			$this->History->updateTitleHistory((int) $titleID, NULL, $titleData['latest_chapter'], $titleData['last_updated']);
321
		} else {
322
			log_message('error', "getTitleData failed for: {$query->row()->site_class} | {$titleURL}");
323
		}
324
		return $titleID ?? 0;
325
	}
326
327
	/**
328
	 * Checks for any titles that haven't updated in 16 hours and updates them.
329
	 * This is ran every 6 hours via a cron job.
330
	 */
331
	public function updateLatestChapters() {
332
		$query = $this->db->select('
333
				tracker_titles.id,
334
				tracker_titles.title,
335
				tracker_titles.title_url,
336
				tracker_sites.site,
337
				tracker_sites.site_class,
338
				tracker_sites.status,
339
				tracker_titles.latest_chapter,
340
				tracker_titles.last_updated,
341
				from_unixtime(MAX(auth_users.last_login)) AS timestamp
342
			')
343
			->from('tracker_titles')
344
			->join('tracker_sites', 'tracker_sites.id = tracker_titles.site_id', 'left')
345
			->join('tracker_chapters', 'tracker_titles.id = tracker_chapters.title_id', 'left')
346
			->join('auth_users', 'tracker_chapters.user_id = auth_users.id', 'left')
347
			->where('tracker_sites.status', 'enabled')
348
			->where('tracker_chapters.active', 'Y') //CHECK: Does this apply BEFORE the GROUP BY/HAVING is done?
349
			->where('(`complete` = "N" AND (`latest_chapter` = NULL OR `last_checked` < DATE_SUB(NOW(), INTERVAL 12 HOUR)))', NULL, FALSE) //TODO: Each title should have specific interval time?
350
			->or_where('(`complete` = "Y" AND `last_checked` < DATE_SUB(NOW(), INTERVAL 1 WEEK))', NULL, FALSE)
351
			->group_by('tracker_titles.id')
352
			->having('timestamp IS NOT NULL')
353
			->having('timestamp > DATE_SUB(NOW(), INTERVAL 120 HOUR)')
354
			->order_by('tracker_titles.title', 'ASC')
355
			->get();
356
357
		if($query->num_rows() > 0) {
358
			foreach ($query->result() as $row) {
359
				print "> {$row->title} <{$row->site_class}>"; //Print this prior to doing anything so we can more easily find out if something went wrong
360
				$titleData = $this->sites->{$row->site_class}->getTitleData($row->title_url);
361
				if(!is_null($titleData['latest_chapter'])) {
362
					//FIXME: "At the moment" we don't seem to be doing anything with TitleData['last_updated'].
363
					//       Should we even use this? Y/N
364
					if($this->updateTitleById((int) $row->id, $titleData['latest_chapter'])) {
365
						//Make sure last_checked is always updated on successful run.
366
						//CHECK: Is there a reason we aren't just doing this in updateTitleById?
367
						$this->db->set('last_checked', 'CURRENT_TIMESTAMP', FALSE)
368
						         ->where('id', $row->id)
369
						         ->update('tracker_titles');
370
371
						print " - ({$titleData['latest_chapter']})\n";
372
					}
373
				} else {
374
					log_message('error', "{$row->title} failed to update successfully");
375
					print " - FAILED TO PARSE\n";
376
				}
377
			}
378
		}
379
	}
380
381
	public function exportTrackerFromUserID(int $userID) {
382
		$query = $this->db
383
			->select('tracker_chapters.current_chapter,
384
			          tracker_chapters.category,
385
			          tracker_titles.title_url,
386
			          tracker_sites.site')
387
			->from('tracker_chapters')
388
			->join('tracker_titles', 'tracker_chapters.title_id = tracker_titles.`id', 'left')
389
			->join('tracker_sites', 'tracker_sites.id = tracker_titles.site_id', 'left')
390
			->where('tracker_chapters.user_id', $userID)
391
			->where('tracker_chapters.active', 'Y')
392
			->get();
393
394
		$arr = [];
395
		if($query->num_rows() > 0) {
396
			foreach ($query->result() as $row) {
397
				$arr[$row->category][] = [
398
					'site'            => $row->site,
399
					'title_url'       => $row->title_url,
400
					'current_chapter' => $row->current_chapter
401
				];
402
			}
403
404
			return $arr;
405
		}
406
	}
407
408
	public function importTrackerFromJSON(string $json_string) : array {
409
		//We already know the this is a valid JSON string as it was validated by form_validator.
410
		$json = json_decode($json_string, TRUE);
411
412
		/*
413
		 * 0 = Success
414
		 * 1 = Invalid keys.
415
		 * 2 = Has failed rows
416
		 */
417
		$status = ['code' => 0, 'failed_rows' => []];
418
419
		$categories = array_keys($json);
420
		if(count($categories) === count(array_intersect(['reading', 'on-hold', 'plan-to-read', 'custom1', 'custom2', 'custom3'], $categories))) {
421
			$json_keys = array_keys(call_user_func_array('array_merge', call_user_func_array('array_merge', $json)));
422
423
			if(count($json_keys) === 3 && !array_diff(array('site', 'title_url', 'current_chapter'), $json_keys)) {
424
				foreach($categories as $category) {
425
					foreach($json[$category] as $row) {
426
						$success = $this->updateTracker($this->User->id, $row['site'], $row['title_url'], $row['current_chapter']);
427
						if(!$success) {
428
							$status['code']          = 2;
429
							$status['failed_rows'][] = $row;
430
						}
431
					}
432
				}
433
			} else {
434
				$status['code'] = 1;
435
			}
436
		} else {
437
			$status['code'] = 1;
438
		}
439
		return $status;
440
	}
441
442
	public function deleteTrackerByIDList(array $idList) : array {
443
		/*
444
		 * 0 = Success
445
		 * 1 = Invalid IDs
446
		 */
447
		$status = ['code' => 0];
448
449 View Code Duplication
		foreach($idList as $id) {
450
			if(!(ctype_digit($id) && $this->deleteTrackerByID($this->User->id, (int) $id))) {
451
				$status['code'] = 1;
452
			} else {
453
				//Delete was successful, update history too.
454
				$this->History->userRemoveTitle((int) $id);
455
			}
456
		}
457
458
		return $status;
459
	}
460
461
	public function setCategoryByIDList(array $idList, string $category) : array {
462
		/*
463
		 * 0 = Success
464
		 * 1 = Invalid IDs
465
		 * 2 = Invalid category
466
		 */
467
		$status = ['code' => 0];
468
469
		if(in_array($category, array_keys($this->enabledCategories))) {
470 View Code Duplication
			foreach($idList as $id) {
471
				if(!(ctype_digit($id) && $this->setCategoryTrackerByID($this->User->id, (int) $id, $category))) {
472
					$status['code'] = 1;
473
				} else {
474
					//Category update was successful, update history too.
475
					$this->History->userUpdateCategory((int) $id, $category);
476
				}
477
			}
478
		} else {
479
			$status['code'] = 2;
480
		}
481
482
		return $status;
483
	}
484 View Code Duplication
	public function setCategoryTrackerByID(int $userID, int $chapterID, string $category) : bool {
485
		$success = $this->db->set(['category' => $category, 'active' => 'Y', 'last_updated' => NULL])
486
		                    ->where('user_id', $userID)
487
		                    ->where('id', $chapterID)
488
		                    ->update('tracker_chapters');
489
490
		return (bool) $success;
491
	}
492
493
494
	public function updateTagsByID(int $userID, int $chapterID, string $tag_string) : bool {
495
		$success = FALSE;
496
		if(preg_match("/^[a-z0-9\\-_,:]{0,255}$/", $tag_string)) {
497
			$success = (bool) $this->db->set(['tags' => $tag_string, 'active' => 'Y', 'last_updated' => NULL])
498
			                           ->where('user_id', $userID)
499
			                           ->where('id', $chapterID)
500
			                           ->update('tracker_chapters');
501
		}
502
503
		if($success) {
504
			//Tag update was successful, update history
505
			$this->History->userUpdateTags($chapterID, $tag_string);
506
		}
507
		return $success;
508
	}
509
510
	public function favouriteChapter(int $userID, string $site, string $title, string $chapter) : array {
511
		$success = array(
512
			'status' => 'Something went wrong',
513
			'bool'   => FALSE
514
		);
515
		if($siteData = $this->Tracker->getSiteDataFromURL($site)) {
516
			//Validate user input
517
			if(!$this->sites->{$siteData->site_class}->isValidTitleURL($title)) {
518
				//Error is already logged via isValidTitleURL
519
				$success['status'] = 'Title URL is not valid';
520
				return $success;
521
			}
522
			if(!$this->sites->{$siteData->site_class}->isValidChapter($chapter)) {
523
				//Error is already logged via isValidChapter
524
				$success['status'] = 'Chapter URL is not valid';
525
				return $success;
526
			}
527
528
			//NOTE: If the title doesn't exist it will be created. This maybe isn't perfect, but it works for now.
529
			$titleID = $this->Tracker->getTitleID($title, (int) $siteData->id);
530
			if($titleID === 0) {
531
				//Something went wrong.
532
				log_message('error', "TitleID = 0 for {$title} @ {$siteData->id}");
533
				return $success;
534
			}
535
536
			//We need the series to be tracked
537
			$idCQuery = $this->db->select('id')
538
			                    ->where('user_id', $userID)
539
			                    ->where('title_id', $titleID)
540
			                    ->get('tracker_chapters');
541
			if($idCQuery->num_rows() > 0) {
542
				$idCQueryRow = $idCQuery->row();
543
544
				//Check if it is already favourited
545
				$idFQuery = $this->db->select('id')
546
				                    ->where('chapter_id', $idCQueryRow->id)
547
				                    ->where('chapter', $chapter)
548
				                    ->get('tracker_favourites');
549
				if($idFQuery->num_rows() > 0) {
550
					//Chapter is already favourited, so remove it from DB
551
					$idFQueryRow = $idFQuery->row();
552
553
					$isSuccess = (bool) $this->db->where('id', $idFQueryRow->id)
554
					                           ->delete('tracker_favourites');
555
556
					if($isSuccess) {
557
						$success = array(
558
							'status' => 'Unfavourited',
559
							'bool'   => TRUE
560
						);
561
						$this->History->userRemoveFavourite((int) $idCQueryRow->id, $chapter);
562
					}
563
				} else {
564
					//Chapter is not favourited, so add to DB.
565
					$isSuccess = (bool) $this->db->insert('tracker_favourites', [
566
						'chapter_id'      => $idCQueryRow->id,
567
						'chapter'         => $chapter,
568
						'updated_at'      => date('Y-m-d H:i:s')
569
					]);
570
571
					if($isSuccess) {
572
						$success = array(
573
							'status' => 'Favourited',
574
							'bool'   => TRUE
575
						);
576
						$this->History->userAddFavourite((int) $idCQueryRow->id, $chapter);
577
					}
578
				}
579
			} else {
580
				$success['status'] = 'Series needs to be tracked before we can favourite chapters';
581
			}
582
		}
583
		return $success;
584
	}
585
	public function getFavourites(int $page) : array {
586
		$rowsPerPage = 50;
587
		$query = $this->db
588
			->select('SQL_CALC_FOUND_ROWS
589
			          tt.title, tt.title_url,
590
			          ts.site, ts.site_class,
591
			          tf.chapter, tf.updated_at', FALSE)
592
			->from('tracker_favourites AS tf')
593
			->join('tracker_chapters AS tc', 'tf.chapter_id = tc.id', 'left')
594
			->join('tracker_titles AS tt', 'tc.title_id = tt.id', 'left')
595
			->join('tracker_sites AS ts', 'tt.site_id = ts.id', 'left')
596
			->where('tc.user_id', $this->User->id) //CHECK: Is this inefficient? Would it be better to have a user_id column in tracker_favourites?
597
			->order_by('tf.id DESC')
598
			->limit($rowsPerPage, ($rowsPerPage * ($page - 1)))
599
			->get();
600
601
		$arr = ['rows' => [], 'totalPages' => 1];
602
		if($query->num_rows() > 0) {
603
			foreach($query->result() as $row) {
604
				$arrRow = [];
605
606
				$arrRow['updated_at'] = $row->updated_at;
607
				$arrRow['title']      = $row->title;
608
				$arrRow['title_url']  = $this->Tracker->sites->{$row->site_class}->getFullTitleURL($row->title_url);
609
610
				$arrRow['site'] = $row->site;
611
				$arrRow['site_sprite'] = str_replace('.', '-', $row->site);
612
613
				$chapterData = $this->Tracker->sites->{$row->site_class}->getChapterData($row->title_url, $row->chapter);
614
				$arrRow['chapter'] = "<a href=\"{$chapterData['url']}\">{$chapterData['number']}</a>";
615
				$arr['rows'][] = $arrRow;
616
			}
617
			$arr['totalPages'] = ceil($this->db->query('SELECT FOUND_ROWS() count;')->row()->count / $rowsPerPage);
618
		}
619
		return $arr;
620
621
	}
622
623
	public function getUsedCategories(int $userID) : array {
624
		$query = $this->db->distinct()
625
		                  ->select('category')
626
		                  ->from('tracker_chapters')
627
		                  ->where('tracker_chapters.active', 'Y')
628
		                  ->where('user_id', $userID)
629
		                  ->get();
630
631
		return array_column($query->result_array(), 'category');
632
	}
633
634
	public function getStats() : array {
635
		if(!($stats = $this->cache->get('site_stats'))) {
636
			$stats = array();
637
638
			//CHECK: Is it possible to merge some of these queries?
639
			$queryUsers = $this->db->select([
0 ignored issues
show
Documentation introduced by
array('COUNT(*) AS total... END) AS active_users') is of type array<integer,string,{"0..."string","2":"string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
640
			                            'COUNT(*) AS total_users',
641
			                            'SUM(CASE WHEN api_key IS NOT NULL THEN 1 ELSE 0 END) AS validated_users',
642
			                            'SUM(CASE WHEN (api_key IS NOT NULL AND from_unixtime(last_login) > DATE_SUB(NOW(), INTERVAL 7 DAY)) THEN 1 ELSE 0 END) AS active_users'
643
			                       ], FALSE)
644
			                       ->from('auth_users')
645
			                       ->get();
646
			$stats = array_merge($stats, $queryUsers->result_array()[0]);
647
648
			$queryCounts = $this->db->select([
0 ignored issues
show
Documentation introduced by
array('tracker_titles.ti...rs.title_id) AS count') is of type array<integer,string,{"0":"string","1":"string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
649
			                             'tracker_titles.title',
650
			                             'COUNT(tracker_chapters.title_id) AS count'
651
			                        ], FALSE)
652
			                        ->from('tracker_chapters')
653
			                        ->join('tracker_titles', 'tracker_titles.id = tracker_chapters.title_id', 'left')
654
			                        ->group_by('tracker_chapters.title_id')
655
			                        ->having('count > 1')
656
			                        ->order_by('count DESC')
657
			                        ->get();
658
			$stats['titles_tracked_more'] = count($queryCounts->result_array());
659
			$stats['top_title_name']  = $queryCounts->result_array()[0]['title'] ?? 'N/A';
660
			$stats['top_title_count'] = $queryCounts->result_array()[0]['count'] ?? 'N/A';
661
662
			$queryTitles = $this->db->select([
0 ignored issues
show
Documentation introduced by
array('COUNT(DISTINCT tr...ND) AS updated_titles') is of type array<integer,string,{"0..."string","3":"string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
663
			                             'COUNT(DISTINCT tracker_titles.id) AS total_titles',
664
			                             'COUNT(DISTINCT tracker_titles.site_id) AS total_sites',
665
			                             'SUM(CASE WHEN from_unixtime(auth_users.last_login) > DATE_SUB(NOW(), INTERVAL 120 HOUR) IS NOT NULL THEN 0 ELSE 1 END) AS inactive_titles',
666
			                             'SUM(CASE WHEN (tracker_titles.last_updated > DATE_SUB(NOW(), INTERVAL 24 HOUR)) THEN 1 ELSE 0 END) AS updated_titles'
667
			                        ], FALSE)
668
			                        ->from('tracker_titles')
669
			                        ->join('tracker_sites', 'tracker_sites.id = tracker_titles.site_id', 'left')
670
			                        ->join('tracker_chapters', 'tracker_titles.id = tracker_chapters.title_id', 'left')
671
			                        ->join('auth_users', 'tracker_chapters.user_id = auth_users.id', 'left')
672
			                        ->get();
673
			$stats = array_merge($stats, $queryTitles->result_array()[0]);
674
675
			$querySites = $this->db->select([
0 ignored issues
show
Documentation introduced by
array('tracker_sites.site', 'COUNT(*) AS count') is of type array<integer,string,{"0":"string","1":"string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
676
			                           'tracker_sites.site',
677
			                           'COUNT(*) AS count'
678
			                       ], FALSE)
679
			                       ->from('tracker_titles')
680
			                       ->join('tracker_sites', 'tracker_sites.id = tracker_titles.site_id', 'left')
681
			                       ->group_by('tracker_titles.site_id')
682
			                       ->order_by('count DESC')
683
			                       ->limit(3)
684
			                       ->get();
685
			$querySitesResult = $querySites->result_array();
686
			$stats['rank1_site']       = $querySitesResult[0]['site'];
687
			$stats['rank1_site_count'] = $querySitesResult[0]['count'];
688
			$stats['rank2_site']       = $querySitesResult[1]['site'];
689
			$stats['rank2_site_count'] = $querySitesResult[1]['count'];
690
			$stats['rank3_site']       = $querySitesResult[2]['site'];
691
			$stats['rank3_site_count'] = $querySitesResult[2]['count'];
692
693
			$queryTitlesU = $this->db->select([
0 ignored issues
show
Documentation introduced by
array('COUNT(*) AS title_updated_count') is of type array<integer,string,{"0":"string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
694
			                             'COUNT(*) AS title_updated_count'
695
			                         ], FALSE)
696
			                         ->from('tracker_titles_history')
697
			                         ->get();
698
			$stats = array_merge($stats, $queryTitlesU->result_array()[0]);
699
700
			$queryUsersU = $this->db->select([
0 ignored issues
show
Documentation introduced by
array('COUNT(*) AS user_updated_count') is of type array<integer,string,{"0":"string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
701
			                            'COUNT(*) AS user_updated_count'
702
			                        ], FALSE)
703
			                        ->from('tracker_user_history')
704
			                        ->get();
705
			$stats = array_merge($stats, $queryUsersU->result_array()[0]);
706
707
			$stats['live_time'] = timespan(/*2016-09-10T03:17:19*/ 1473477439, time(), 2);
708
709
			$this->cache->save('site_stats', $stats, 3600); //Cache for an hour
710
		}
711
712
		return $stats;
713
	}
714
715
	//FIXME: Should this be moved elsewhere??
716
	public function getNextUpdateTime() : string {
717
		$temp_now = new DateTime();
718
		$temp_now->setTimezone(new DateTimeZone('America/New_York'));
719
		$temp_now_formatted = $temp_now->format('Y-m-d H:i:s');
720
721
		//NOTE: PHP Bug: DateTime:diff doesn't play nice with setTimezone, so we need to create another DT object
722
		$now         = new DateTime($temp_now_formatted);
723
		$future_date = new DateTime($temp_now_formatted);
724
		$now_hour    = (int) $now->format('H');
725
		if($now_hour < 4) {
726
			//Time until 4am
727
			$future_date->setTime(4, 00);
728
		} elseif($now_hour < 8) {
729
			//Time until 8am
730
			$future_date->setTime(8, 00);
731
		} elseif($now_hour < 12) {
732
			//Time until 12pm
733
			$future_date->setTime(12, 00);
734
		} elseif($now_hour < 16) {
735
			//Time until 4pm
736
			$future_date->setTime(16, 00);
737
		} elseif($now_hour < 20) {
738
			//Time until 8pm
739
			$future_date->setTime(20, 00);
740
		} else {
741
			//Time until 12am
742
			$future_date->setTime(00, 00);
743
			$future_date->add(new DateInterval('P1D'));
744
		}
745
746
		$interval = $future_date->diff($now);
747
		return $interval->format("%H:%I:%S");
748
	}
749
750
	public function reportBug(string $text, $userID = NULL, $url = NULL) : bool {
751
		$this->load->library('email');
752
753
		//This is pretty barebones bug reporting, and honestly not a great way to do it, but it works for now (until the Github is public).
754
		$body = "".
755
		(!is_null($url) && !empty($url) ? "URL: ".htmlspecialchars(substr($url, 0, 255))."<br>\n" : "").
756
		"Submitted by: ".$this->input->ip_address().(!is_null($userID) ? "| {$userID}" : "")."<br>\n".
757
		"<br>Bug report: ".htmlspecialchars(substr($text, 0, 1000));
758
759
		$success = TRUE;
760
		$this->email->from('[email protected]', $this->config->item('site_title', 'ion_auth'));
761
		$this->email->to($this->config->item('admin_email', 'ion_auth'));
762
		$this->email->subject($this->config->item('site_title', 'ion_auth')." - Bug Report");
763
		$this->email->message($body);
764
		if(!$this->email->send()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->email->send() of type null|boolean is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
765
			$success = FALSE;
766
		}
767
		return $success;
768
	}
769
770
	/*************************************************/
771
	public function sites() {
772
		return $this;
773
	}
774
}
775