Completed
Push — master ( 4145fd...56b6bc )
by Angus
03:27
created

Tracker_Model::deleteTrackerByIDList()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 18
Code Lines 8

Duplication

Lines 8
Ratio 44.44 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
eloc 8
nc 3
nop 1
dl 8
loc 18
rs 9.2
c 0
b 0
f 0
ccs 0
cts 0
cp 0
crap 20
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 94
	public function __construct() {
8 94
		parent::__construct();
1 ignored issue
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class CI_Model as the method __construct() does only exist in the following sub-classes of CI_Model: Auth_Model, Batoto, DynastyScans, GameOfScanlation, History_Model, KireiCake, KissManga, MangaFox, MangaHere, MangaPanda, MangaStream, Site_Model, Sites_Model, Tracker_Model, User_Model, User_Options_Model, WebToons. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
9
10 94
		$this->load->database();
11
12 94
		$this->enabledCategories = [
13
			'reading'      => 'Reading',
14
			'on-hold'      => 'On-Hold',
15
			'plan-to-read' => 'Plan to Read'
16
		];
17 94
		if($this->User_Options->get('category_custom_1') == 'enabled') {
18
			$this->enabledCategories['custom1'] = $this->User_Options->get('category_custom_1_text');
19
		}
20 94
		if($this->User_Options->get('category_custom_2') == 'enabled') {
21
			$this->enabledCategories['custom2'] = $this->User_Options->get('category_custom_2_text');
22
		}
23 94
		if($this->User_Options->get('category_custom_3') == 'enabled') {
24
			$this->enabledCategories['custom3'] = $this->User_Options->get('category_custom_3_text');
25
		}
26
27 94
		require_once(APPPATH.'models/Site_Model.php');
28 94
		$this->sites = new Sites_Model;
29 94
	}
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,
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 = [];
45
		foreach($this->enabledCategories as $category => $name) {
46
			$arr[$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');
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 27 spaces but found 5 spaces

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
55
				$arr[$row->category]['unread_count'] = (($arr[$row->category]['unread_count'] ?? 0) + !$is_unread);
56
				$arr[$row->category]['manga'][] = [
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 6 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
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
					],
74
					'site_data' => [
75
						'id'         => $row->site_id,
76
						'site'       => $row->site,
77
						'status'     => $row->site_status
78
					]
79
				];
80
			}
81
82
			//NOTE: This does not sort in the same way as tablesorter, but it works better.
83
			foreach (array_keys($arr) as $category) {
84
				usort($arr[$category]['manga'], function ($a, $b) {
85
					return strtolower("{$a['new_chapter_exists']} - {$a['title_data']['title']}") <=> strtolower("{$b['new_chapter_exists']} - {$b['title_data']['title']}");
86
				});
87
			}
88
		}
89
		return $arr;
90
	}
91
92
	public function getSiteDataFromURL(string $site_url) {
93
		$query = $this->db->select('id, site_class')
94
		                  ->from('tracker_sites')
95
		                  ->where('site', $site_url)
96
		                  ->get();
97
98
		if($query->num_rows() > 0) {
99
			$siteData = $query->row();
100
		}
101
102
		return $siteData ?? FALSE;
103
	}
104
105
	public function getTitleID(string $titleURL, int $siteID) {
106
		$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)
107
		                  ->from('tracker_titles')
108
		                  ->join('tracker_sites', 'tracker_sites.id = tracker_titles.site_id', 'left')
109
		                  ->where('tracker_titles.title_url', $titleURL)
110
		                  ->where('tracker_titles.site_id', $siteID)
111
		                  ->get();
112
113
		if($query->num_rows() > 0) {
114
			$id = (int) $query->row('id');
115
116
			//This updates inactive series if they are newly added, as noted in https://github.com/DakuTree/manga-tracker/issues/5#issuecomment-247480804
117
			if(((int) $query->row('active')) === 0 && $query->row('complete') === 'N') {
118
				$titleData = $this->sites->{$query->row('site_class')}->getTitleData($query->row('title_url'));
119
				if(!is_null($titleData['latest_chapter'])) {
120
					if($this->updateTitleById((int) $id, $titleData['latest_chapter'])) {
121
						//Make sure last_checked is always updated on successful run.
122
						//CHECK: Is there a reason we aren't just doing this in updateTitleById?
123
						$this->db->set('last_checked', 'CURRENT_TIMESTAMP', FALSE)
124
						         ->where('id', $id)
125
						         ->update('tracker_titles');
126
					}
127
				} else {
128
					log_message('error', "{$query->row('title')} failed to update successfully");
129
				}
130
			}
131
132
			$titleID = $id;
133
		} else {
134
			//TODO: Check if title is valid URL!
135
			$titleID = $this->addTitle($titleURL, $siteID);
136
		}
137
138
		return $titleID;
139
	}
140
141
	public function updateTracker(int $userID, string $site, string $title, string $chapter) : bool {
142
		$success = FALSE;
143
		if($siteData = $this->Tracker->getSiteDataFromURL($site)) {
144
			//Validate user input
145
			if(!$this->sites->{$siteData->site_class}->isValidTitleURL($title)) {
146
				//Error is already logged via isValidTitleURL
147
				return FALSE;
148
			}
149
			if(!$this->sites->{$siteData->site_class}->isValidChapter($chapter)) {
150
				//Error is already logged via isValidChapter
151
				return FALSE;
152
			}
153
154
			//NOTE: If the title doesn't exist it will be created. This maybe isn't perfect, but it works for now.
155
			$titleID = $this->Tracker->getTitleID($title, (int) $siteData->id);
156
			if($titleID === 0) {
157
				//Something went wrong.
158
				log_message('error', "TitleID = 0 for {$title} @ {$siteData->id}");
159
				return FALSE;
160
			}
161
162
			$idQuery = $this->db->select('id')
163
			                    ->where('user_id', $userID)
164
			                    ->where('title_id', $titleID)
165
			                    ->get('tracker_chapters');
166
			if($idQuery->num_rows() > 0) {
167
				$success = (bool) $this->db->set(['current_chapter' => $chapter, 'active' => 'Y', 'last_updated' => NULL])
168
				                    ->where('user_id', $userID)
169
				                    ->where('title_id', $titleID)
170
				                    ->update('tracker_chapters');
171
172
				if($success) {
173
					$idQueryRow = $idQuery->row();
174
					$this->History->userUpdateTitle((int) $idQueryRow->id, $chapter);
175
				}
176
			} else {
177
				$category = $this->User_Options->get_by_userid('default_series_category', $userID);
178
				$success = (bool) $this->db->insert('tracker_chapters', [
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 2 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
179
					'user_id'         => $userID,
180
					'title_id'        => $titleID,
181
					'current_chapter' => $chapter,
182
					'category'        => $category
183
				]);
184
185
				if($success) {
186
					$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...
187
				}
188
			}
189
		}
190
		return $success;
191
	}
192
193
	public function updateTrackerByID(int $userID, int $chapterID, string $chapter) : bool {
194
		$success = (bool) $this->db->set(['current_chapter' => $chapter, 'active' => 'Y', 'last_updated' => NULL])
195
		                    ->where('user_id', $userID)
196
		                    ->where('id', $chapterID)
197
		                    ->update('tracker_chapters');
198
199
		if($success) {
200
			$this->History->userUpdateTitle($chapterID, $chapter);
201
		}
202
		return  $success;
203
	}
204
205 View Code Duplication
	public function deleteTrackerByID(int $userID, int $chapterID) {
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
206
		//Series are not fully deleted, they are just marked as inactive as to hide them from the user.
207
		//This is to allow user history to function properly.
208
209
		$success = $this->db->set(['active' => 'N', 'last_updated' => NULL])
210
		                    ->where('user_id', $userID)
211
		                    ->where('id', $chapterID)
212
		                    ->update('tracker_chapters');
213
214
		return (bool) $success;
215
	}
216
	private function updateTitleById(int $id, string $latestChapter) {
217
		//FIXME: Really not too happy with how we're doing history stuff here, it just feels messy.
218
		$query = $this->db->select('latest_chapter AS current_chapter')
219
		                  ->from('tracker_titles')
220
		                  ->where('id', $id)
221
		                  ->get();
222
		$row = $query->row();
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
223
224
		$success = $this->db->set(['latest_chapter' => $latestChapter]) //last_updated gets updated via a trigger if something changes
225
		                    ->where('id', $id)
226
		                    ->update('tracker_titles');
227
228
		//Update History
229
		//NOTE: To avoid doing another query to grab the last_updated time, we just use time() which <should> get the same thing.
230
		//FIXME: The <preferable> solution here is we'd just check against the last_updated time, but that can have a few issues.
231
		$this->History->updateTitleHistory($id, $row->current_chapter, $latestChapter, date('Y-m-d H:i:s'));
232
233
		return (bool) $success;
234
	}
235
	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...
236
		$success = $this->db->set($titleData)
237
		                    ->where('id', $id)
238
		                    ->update('tracker_titles');
239
240
		return (bool) $success;
241
	}
242
	private function addTitle(string $titleURL, int $siteID) {
243
		$query = $this->db->select('site, site_class')
244
		                  ->from('tracker_sites')
245
		                  ->where('id', $siteID)
246
		                  ->get();
247
248
		$titleData = $this->sites->{$query->row()->site_class}->getTitleData($titleURL);
249
		$this->db->insert('tracker_titles', array_merge($titleData, ['title_url' => $titleURL, 'site_id' => $siteID]));
250
		$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...
251
252
		$this->History->updateTitleHistory((int) $titleID, NULL, $titleData['latest_chapter'], $titleData['last_updated']);
253
		return $titleID;
254
	}
255
256
	/**
257
	 * Checks for any titles that haven't updated in 16 hours and updates them.
258
	 * This is ran every 6 hours via a cron job.
259
	 */
260
	public function updateLatestChapters() {
261
		$query = $this->db->select('
262
				tracker_titles.id,
263
				tracker_titles.title,
264
				tracker_titles.title_url,
265
				tracker_sites.site,
266
				tracker_sites.site_class,
267
				tracker_sites.status,
268
				tracker_titles.latest_chapter,
269
				tracker_titles.last_updated,
270
				from_unixtime(MAX(auth_users.last_login)) AS timestamp
271
			')
272
			->from('tracker_titles')
273
			->join('tracker_sites', 'tracker_sites.id = tracker_titles.site_id', 'left')
274
			->join('tracker_chapters', 'tracker_titles.id = tracker_chapters.title_id', 'left')
275
			->join('auth_users', 'tracker_chapters.user_id = auth_users.id', 'left')
276
			->where('tracker_sites.status', 'enabled')
277
			->where('tracker_chapters.active', 'Y') //CHECK: Does this apply BEFORE the GROUP BY/HAVING is done?
278
			->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?
279
			->or_where('(`complete` = "Y" AND `last_checked` < DATE_SUB(NOW(), INTERVAL 1 WEEK))', NULL, FALSE)
280
			->group_by('tracker_titles.id')
281
			->having('timestamp IS NOT NULL')
282
			->having('timestamp > DATE_SUB(NOW(), INTERVAL 120 HOUR)')
283
			->order_by('tracker_titles.title', 'ASC')
284
			->get();
285
286
		if($query->num_rows() > 0) {
287
			foreach ($query->result() as $row) {
288
				print "> {$row->title} <{$row->site_class}>"; //Print this prior to doing anything so we can more easily find out if something went wrong
289
				$titleData = $this->sites->{$row->site_class}->getTitleData($row->title_url);
290
				if(!is_null($titleData['latest_chapter'])) {
291
					//FIXME: "At the moment" we don't seem to be doing anything with TitleData['last_updated'].
292
					//       Should we even use this? Y/N
293
					if($this->updateTitleById((int) $row->id, $titleData['latest_chapter'])) {
294
						//Make sure last_checked is always updated on successful run.
295
						//CHECK: Is there a reason we aren't just doing this in updateTitleById?
296
						$this->db->set('last_checked', 'CURRENT_TIMESTAMP', FALSE)
297
						         ->where('id', $row->id)
298
						         ->update('tracker_titles');
299
300
						print " - ({$titleData['latest_chapter']})\n";
301
					}
302
				} else {
303
					log_message('error', "{$row->title} failed to update successfully");
304
					print " - FAILED TO PARSE\n";
305
				}
306
			}
307
		}
308
	}
309
310
	public function exportTrackerFromUserID(int $userID) {
311
		$query = $this->db
312
			->select('tracker_chapters.current_chapter,
313
			          tracker_chapters.category,
314
			          tracker_titles.title_url,
315
			          tracker_sites.site')
316
			->from('tracker_chapters')
317
			->join('tracker_titles', 'tracker_chapters.title_id = tracker_titles.`id', 'left')
318
			->join('tracker_sites', 'tracker_sites.id = tracker_titles.site_id', 'left')
319
			->where('tracker_chapters.user_id', $userID)
320
			->where('tracker_chapters.active', 'Y')
321
			->get();
322
323
		$arr = [];
324
		if($query->num_rows() > 0) {
325
			foreach ($query->result() as $row) {
326
				$arr[$row->category][] = [
327
					'site'            => $row->site,
328
					'title_url'       => $row->title_url,
329
					'current_chapter' => $row->current_chapter
330
				];
331
			}
332
333
			return $arr;
334
		}
335
	}
336
337
	public function importTrackerFromJSON(string $json_string) : array {
338
		//We already know the this is a valid JSON string as it was validated by form_validator.
339
		$json = json_decode($json_string, TRUE);
340
341
		/*
342
		 * 0 = Success
343
		 * 1 = Invalid keys.
344
		 * 2 = Has failed rows
345
		 */
346
		$status = ['code' => 0, 'failed_rows' => []];
347
348
		$categories = array_keys($json);
349
		if(count($categories) === array_intersect(['reading', 'on-hold', 'plan-to-read', 'custom1', 'custom2', 'custom3'], $categories)) {
350
			$json_keys = array_keys(call_user_func_array('array_merge', $json));
351
352
			if(count($json_keys) === 3 && !array_diff(array('site', 'title_url', 'current_chapter'), $json_keys)) {
353
				foreach($categories as $category) {
354
					foreach($json[$category] as $row) {
355
						$success = $this->updateTracker($this->User->id, $row['site'], $row['title_url'], $row['current_chapter']);
356
						if(!$success) {
357
							$status['code']          = 2;
358
							$status['failed_rows'][] = $row;
359
						}
360
					}
361
				}
362
			} else {
363
				$status['code'] = 1;
364
			}
365
		} else {
366
			$status['code'] = 1;
367
		}
368
		return $status;
369
	}
370
371
	public function deleteTrackerByIDList(array $idList) : array {
372
		/*
373
		 * 0 = Success
374
		 * 1 = Invalid IDs
375
		 */
376
		$status = ['code' => 0];
377
378 View Code Duplication
		foreach($idList as $id) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
379
			if(!(ctype_digit($id) && $this->deleteTrackerByID($this->User->id, (int) $id))) {
380
				$status['code'] = 1;
381
			} else {
382
				//Delete was successful, update history too.
383
				$this->History->userRemoveTitle((int) $id);
384
			}
385
		}
386
387
		return $status;
388
	}
389
390
	public function setCategoryByIDList(array $idList, string $category) : array {
391
		/*
392
		 * 0 = Success
393
		 * 1 = Invalid IDs
394
		 * 2 = Invalid category
395
		 */
396
		$status = ['code' => 0];
397
398
		if(in_array($category, array_keys($this->enabledCategories))) {
399 View Code Duplication
			foreach($idList as $id) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
400
				if(!(ctype_digit($id) && $this->setCategoryTrackerByID($this->User->id, (int) $id, $category))) {
401
					$status['code'] = 1;
402
				} else {
403
					//Category update was successful, update history too.
404
					$this->History->userUpdateCategory((int) $id, $category);
405
				}
406
			}
407
		} else {
408
			$status['code'] = 2;
409
		}
410
411
		return $status;
412
	}
413 View Code Duplication
	public function setCategoryTrackerByID(int $userID, int $chapterID, string $category) : bool {
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
414
		$success = $this->db->set(['category' => $category, 'active' => 'Y', 'last_updated' => NULL])
415
		                    ->where('user_id', $userID)
416
		                    ->where('id', $chapterID)
417
		                    ->update('tracker_chapters');
418
419
		return (bool) $success;
420
	}
421
422
423
	public function updateTagsByID(int $userID, int $chapterID, string $tag_string) : bool {
424
		$success = FALSE;
425
		if(preg_match("/^[a-z0-9-_,]{0,255}$/", $tag_string)) {
426
			$success = (bool) $this->db->set(['tags' => $tag_string, 'active' => 'Y', 'last_updated' => NULL])
427
			                           ->where('user_id', $userID)
428
			                           ->where('id', $chapterID)
429
			                           ->update('tracker_chapters');
430
		}
431
432
		if($success) {
433
			//Tag update was successful, update history
434
			$this->History->userUpdateTags($chapterID, $tag_string);
435
		}
436
		return $success;
437
	}
438
439
	public function favouriteChapter(int $userID, string $site, string $title, string $chapter) : array {
440
		$success = array(
441
			'status' => 'Something went wrong',
442
			'bool'   => FALSE
443
		);
444
		if($siteData = $this->Tracker->getSiteDataFromURL($site)) {
445
			//Validate user input
446
			if(!$this->sites->{$siteData->site_class}->isValidTitleURL($title)) {
447
				//Error is already logged via isValidTitleURL
448
				$success['status'] = 'Title URL is not valid';
449
				return $success;
450
			}
451
			if(!$this->sites->{$siteData->site_class}->isValidChapter($chapter)) {
452
				//Error is already logged via isValidChapter
453
				$success['status'] = 'Chapter URL is not valid';
454
				return $success;
455
			}
456
457
			//NOTE: If the title doesn't exist it will be created. This maybe isn't perfect, but it works for now.
458
			$titleID = $this->Tracker->getTitleID($title, (int) $siteData->id);
459
			if($titleID === 0) {
460
				//Something went wrong.
461
				log_message('error', "TitleID = 0 for {$title} @ {$siteData->id}");
462
				return $success;
463
			}
464
465
			//We need the series to be tracked
466
			$idCQuery = $this->db->select('id')
467
			                    ->where('user_id', $userID)
468
			                    ->where('title_id', $titleID)
469
			                    ->get('tracker_chapters');
470
			if($idCQuery->num_rows() > 0) {
471
				$idCQueryRow = $idCQuery->row();
472
473
				//Check if it is already favourited
474
				$idFQuery = $this->db->select('id')
475
				                    ->where('chapter_id', $idCQueryRow->id)
476
				                    ->where('chapter', $chapter)
477
				                    ->get('tracker_favourites');
478
				if($idFQuery->num_rows() > 0) {
479
					//Chapter is already favourited, so remove it from DB
480
					$idFQueryRow = $idFQuery->row();
481
482
					$isSuccess = (bool) $this->db->where('id', $idFQueryRow->id)
483
					                           ->delete('tracker_favourites');
484
485
					if($isSuccess) {
486
						$success = array(
487
							'status' => 'Unfavourited',
488
							'bool'   => TRUE
489
						);
490
						$this->History->userRemoveFavourite((int) $idCQueryRow->id, $chapter);
491
					}
492
				} else {
493
					//Chapter is not favourited, so add to DB.
494
					$isSuccess = (bool) $this->db->insert('tracker_favourites', [
495
						'chapter_id'      => $idCQueryRow->id,
496
						'chapter'         => $chapter,
497
						'updated_at'      => date('Y-m-d H:i:s')
498
					]);
499
500
					if($isSuccess) {
501
						$success = array(
502
							'status' => 'Favourited',
503
							'bool'   => TRUE
504
						);
505
						$this->History->userAddFavourite((int) $idCQueryRow->id, $chapter);
506
					}
507
				}
508
			} else {
509
				$success['status'] = 'Series needs to be tracked before we can favourite chapters';
510
			}
511
		}
512
		return $success;
513
	}
514
	public function getFavourites(int $page) : array {
515
		$rowsPerPage = 50;
516
		$query = $this->db
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 7 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
517
			->select('SQL_CALC_FOUND_ROWS
518
			          tt.title, tt.title_url,
519
			          ts.site, ts.site_class,
520
			          tf.chapter, tf.updated_at', FALSE)
521
			->from('tracker_favourites AS tf')
522
			->join('tracker_chapters AS tc', 'tf.chapter_id = tc.id', 'left')
523
			->join('tracker_titles AS tt', 'tc.title_id = tt.id', 'left')
524
			->join('tracker_sites AS ts', 'tt.site_id = ts.id', 'left')
525
			->where('tc.user_id', $this->User->id) //CHECK: Is this inefficient? Would it be better to have a user_id column in tracker_favourites?
526
			->order_by('tf.id DESC')
527
			->limit($rowsPerPage, ($rowsPerPage * ($page - 1)))
528
			->get();
529
530
		$arr = ['rows' => [], 'totalPages' => 1];
531
		if($query->num_rows() > 0) {
532
			foreach($query->result() as $row) {
533
				$arrRow = [];
534
535
				$arrRow['updated_at'] = $row->updated_at;
536
				$arrRow['title']      = $row->title;
537
				$arrRow['title_url']  = $this->Tracker->sites->{$row->site_class}->getFullTitleURL($row->title_url);
538
539
				$arrRow['site'] = $row->site;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 8 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
540
				$arrRow['site_sprite'] = str_replace('.', '-', $row->site);
541
542
				$chapterData = $this->Tracker->sites->{$row->site_class}->getChapterData($row->title_url, $row->chapter);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 7 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
543
				$arrRow['chapter'] = "<a href=\"{$chapterData['url']}\">{$chapterData['number']}</a>";
544
				$arr['rows'][] = $arrRow;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 5 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
545
			}
546
			$arr['totalPages'] = ceil($this->db->query('SELECT FOUND_ROWS() count;')->row()->count / $rowsPerPage);
547
		}
548
		return $arr;
549
550
	}
551
552
	public function getUsedCategories(int $userID) : array {
0 ignored issues
show
Unused Code introduced by
The parameter $userID 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...
553
		$usedCategories = [];
0 ignored issues
show
Unused Code introduced by
$usedCategories is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
554
555
		$query = $this->db->distinct()
556
		                  ->select('category')
557
		                  ->from('tracker_chapters')
558
		                  ->where('tracker_chapters.active', 'Y')
559
		                  ->get();
560
561
		return array_column($query->result_array(), 'category');
562
	}
563
564
	public function getStats() : array {
565
		$stats = array();
566
567
		//CHECK: Is it possible to merge some of these queries?
568
569
		//$this->db->cache_on();
570
		$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...
571
		                      'COUNT(*) AS total_users',
572
		                      'SUM(CASE WHEN api_key IS NOT NULL THEN 1 ELSE 0 END) AS validated_users',
573
		                      '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'
574
		                  ], FALSE)
575
		                  ->from('auth_users')
576
		                  ->get();
577
		$stats = array_merge($stats, $queryUsers->result_array()[0]);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 6 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
578
579
		$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...
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 18 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
580
		                      'tracker_titles.title',
581
		                      'COUNT(tracker_chapters.title_id) AS count'
582
		                  ], FALSE)
583
		                  ->from('tracker_chapters')
584
		                  ->join('tracker_titles', 'tracker_titles.id = tracker_chapters.title_id', 'left')
585
		                  ->group_by('tracker_chapters.title_id')
586
		                  ->having('count > 1')
587
		                  ->order_by('count DESC')
588
		                  ->get();
589
		$stats['titles_tracked_more'] = count($queryCounts->result_array());
590
		$stats['top_title_name']  = $queryCounts->result_array()[0]['title'] ?? 'N/A';
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 6 spaces but found 2 spaces

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
591
		$stats['top_title_count'] = $queryCounts->result_array()[0]['count'] ?? 'N/A';
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 5 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
592
593
		$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...
594
		                      'COUNT(DISTINCT tracker_titles.id) AS total_titles',
595
		                      'COUNT(DISTINCT tracker_titles.site_id) AS total_sites',
596
		                      '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',
597
		                      'SUM(CASE WHEN (tracker_titles.last_updated > DATE_SUB(NOW(), INTERVAL 24 HOUR)) THEN 1 ELSE 0 END) AS updated_titles'
598
		                  ], FALSE)
599
		                  ->from('tracker_titles')
600
		                  ->join('tracker_sites', 'tracker_sites.id = tracker_titles.site_id', 'left')
601
		                  ->join('tracker_chapters', 'tracker_titles.id = tracker_chapters.title_id', 'left')
602
		                  ->join('auth_users', 'tracker_chapters.user_id = auth_users.id', 'left')
603
		                  ->get();
604
		$stats = array_merge($stats, $queryTitles->result_array()[0]);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 7 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
605
606
		$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...
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 16 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
607
		                      'tracker_sites.site',
608
		                      'COUNT(*) AS count'
609
		                  ], FALSE)
610
		                  ->from('tracker_titles')
611
		                  ->join('tracker_sites', 'tracker_sites.id = tracker_titles.site_id', 'left')
612
		                  ->group_by('tracker_titles.site_id')
613
		                  ->order_by('count DESC')
614
		                  ->limit(3)
615
		                  ->get();
616
		$querySitesResult = $querySites->result_array();
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 10 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
617
		$stats['rank1_site']       = $querySitesResult[0]['site'];
618
		$stats['rank1_site_count'] = $querySitesResult[0]['count'];
619
		$stats['rank2_site']       = $querySitesResult[1]['site'];
620
		$stats['rank2_site_count'] = $querySitesResult[1]['count'];
621
		$stats['rank3_site']       = $querySitesResult[2]['site'];
622
		$stats['rank3_site_count'] = $querySitesResult[2]['count'];
623
624
		$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...
625
		                      'COUNT(*) AS title_updated_count'
626
		                  ], FALSE)
627
		                  ->from('tracker_titles_history')
628
		                  ->get();
629
		$stats = array_merge($stats, $queryTitlesU->result_array()[0]);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 8 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
630
631
		$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...
632
		                      'COUNT(*) AS user_updated_count'
633
		                  ], FALSE)
634
		                  ->from('tracker_user_history')
635
		                  ->get();
636
		$stats = array_merge($stats, $queryUsersU->result_array()[0]);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 7 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
637
638
		$stats['live_time'] = timespan(/*2016-09-10T03:17:19*/ 1473477439, time(), 2);
639
640
		//$this->db->cache_off();
641
		return $stats;
642
	}
643
644
	//FIXME: Should this be moved elsewhere??
645
	public function getNextUpdateTime() : string {
646
		$temp_now = new DateTime();
647
		$temp_now->setTimezone(new DateTimeZone('America/New_York'));
648
		$temp_now_formatted = $temp_now->format('Y-m-d H:i:s');
649
650
		//NOTE: PHP Bug: DateTime:diff doesn't play nice with setTimezone, so we need to create another DT object
651
		$now         = new DateTime($temp_now_formatted);
652
		$future_date = new DateTime($temp_now_formatted);
653
		$now_hour    = (int) $now->format('H');
654
		if($now_hour < 4) {
655
			//Time until 4am
656
			$future_date->setTime(4, 00);
657
		} elseif($now_hour < 8) {
658
			//Time until 8am
659
			$future_date->setTime(8, 00);
660
		} elseif($now_hour < 12) {
661
			//Time until 12pm
662
			$future_date->setTime(12, 00);
663
		} elseif($now_hour < 16) {
664
			//Time until 4pm
665
			$future_date->setTime(16, 00);
666
		} elseif($now_hour < 20) {
667
			//Time until 8pm
668
			$future_date->setTime(20, 00);
669
		} else {
670
			//Time until 12am
671
			$future_date->setTime(00, 00);
672
			$future_date->add(new DateInterval('P1D'));
673
		}
674
675
		$interval = $future_date->diff($now);
676
		return $interval->format("%H:%I:%S");
677
	}
678
679
	public function reportBug(string $text, $userID = NULL, $url = NULL) : bool {
680
		$this->load->library('email');
681
682
		//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).
683
		$body = "".
684
		(!is_null($url) && !empty($url) ? "URL: ".htmlspecialchars(substr($url, 0, 255))."<br>\n" : "").
685
		"Submitted by: ".$this->input->ip_address().(!is_null($userID) ? "| {$userID}" : "")."<br>\n".
686
		"<br>Bug report: ".htmlspecialchars(substr($text, 0, 1000));
687
688
		$success = TRUE;
689
		$this->email->from('[email protected]', $this->config->item('site_title', 'ion_auth'));
690
		$this->email->to($this->config->item('admin_email', 'ion_auth'));
691
		$this->email->subject($this->config->item('site_title', 'ion_auth')." - Bug Report");
692
		$this->email->message($body);
693
		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...
694
			$success = FALSE;
695
		}
696
		return $success;
697
	}
698
699
	/*************************************************/
700
	public function sites() {
701
		return $this;
702
	}
703
}
704