Completed
Push — master ( 56b6bc...1f1723 )
by Angus
03:24
created

Tracker_Model::get_tracker_from_user_id()   C

Complexity

Conditions 9
Paths 4

Size

Total Lines 64
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

Changes 0
Metric Value
cc 9
eloc 45
nc 4
nop 1
dl 0
loc 64
ccs 0
cts 0
cp 0
crap 90
rs 6.5449
c 0
b 0
f 0

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

1
<?php declare(strict_types=1); defined('BASEPATH') OR exit('No direct script access allowed');
2
3
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, 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');
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 37 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['series'][$row->category]['unread_count'] = (($arr['series'][$row->category]['unread_count'] ?? 0) + !$is_unread);
56
				$data = [
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 42 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
						'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(!$data['title_data']['active']) { print $row->title.' + '.$row->site_status.'<br>'; }
84
				if(!$arr['has_inactive']) $arr['has_inactive'] = !$data['title_data']['active'];
85
			}
86
87
			//NOTE: This does not sort in the same way as tablesorter, but it works better.
88
			foreach (array_keys($arr['series']) as $category) {
89
				usort($arr['series'][$category]['manga'], function ($a, $b) {
90
					return strtolower("{$a['new_chapter_exists']} - {$a['title_data']['title']}") <=> strtolower("{$b['new_chapter_exists']} - {$b['title_data']['title']}");
91
				});
92
			}
93
		}
94
		return $arr;
95
	}
96
97
	public function getSiteDataFromURL(string $site_url) {
98
		$query = $this->db->select('id, site_class')
99
		                  ->from('tracker_sites')
100
		                  ->where('site', $site_url)
101
		                  ->get();
102
103
		if($query->num_rows() > 0) {
104
			$siteData = $query->row();
105
		}
106
107
		return $siteData ?? FALSE;
108
	}
109
110
	public function getTitleID(string $titleURL, int $siteID) {
111
		$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)
112
		                  ->from('tracker_titles')
113
		                  ->join('tracker_sites', 'tracker_sites.id = tracker_titles.site_id', 'left')
114
		                  ->where('tracker_titles.title_url', $titleURL)
115
		                  ->where('tracker_titles.site_id', $siteID)
116
		                  ->get();
117
118
		if($query->num_rows() > 0) {
119
			$id = (int) $query->row('id');
120
121
			//This updates inactive series if they are newly added, as noted in https://github.com/DakuTree/manga-tracker/issues/5#issuecomment-247480804
122
			if(((int) $query->row('active')) === 0 && $query->row('complete') === 'N') {
123
				$titleData = $this->sites->{$query->row('site_class')}->getTitleData($query->row('title_url'));
124
				if(!is_null($titleData['latest_chapter'])) {
125
					if($this->updateTitleById((int) $id, $titleData['latest_chapter'])) {
126
						//Make sure last_checked is always updated on successful run.
127
						//CHECK: Is there a reason we aren't just doing this in updateTitleById?
128
						$this->db->set('last_checked', 'CURRENT_TIMESTAMP', FALSE)
129
						         ->where('id', $id)
130
						         ->update('tracker_titles');
131
					}
132
				} else {
133
					log_message('error', "{$query->row('title')} failed to update successfully");
134
				}
135
			}
136
137
			$titleID = $id;
138
		} else {
139
			//TODO: Check if title is valid URL!
140
			$titleID = $this->addTitle($titleURL, $siteID);
141
		}
142
143
		return $titleID;
144
	}
145
146
	public function updateTracker(int $userID, string $site, string $title, string $chapter) : bool {
147
		$success = FALSE;
148
		if($siteData = $this->Tracker->getSiteDataFromURL($site)) {
149
			//Validate user input
150
			if(!$this->sites->{$siteData->site_class}->isValidTitleURL($title)) {
151
				//Error is already logged via isValidTitleURL
152
				return FALSE;
153
			}
154
			if(!$this->sites->{$siteData->site_class}->isValidChapter($chapter)) {
155
				//Error is already logged via isValidChapter
156
				return FALSE;
157
			}
158
159
			//NOTE: If the title doesn't exist it will be created. This maybe isn't perfect, but it works for now.
160
			$titleID = $this->Tracker->getTitleID($title, (int) $siteData->id);
161
			if($titleID === 0) {
162
				//Something went wrong.
163
				log_message('error', "TitleID = 0 for {$title} @ {$siteData->id}");
164
				return FALSE;
165
			}
166
167
			$idQuery = $this->db->select('id')
168
			                    ->where('user_id', $userID)
169
			                    ->where('title_id', $titleID)
170
			                    ->get('tracker_chapters');
171
			if($idQuery->num_rows() > 0) {
172
				$success = (bool) $this->db->set(['current_chapter' => $chapter, 'active' => 'Y', 'last_updated' => NULL])
173
				                    ->where('user_id', $userID)
174
				                    ->where('title_id', $titleID)
175
				                    ->update('tracker_chapters');
176
177
				if($success) {
178
					$idQueryRow = $idQuery->row();
179
					$this->History->userUpdateTitle((int) $idQueryRow->id, $chapter);
180
				}
181
			} else {
182
				$category = $this->User_Options->get_by_userid('default_series_category', $userID);
183
				$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...
184
					'user_id'         => $userID,
185
					'title_id'        => $titleID,
186
					'current_chapter' => $chapter,
187
					'category'        => $category
188
				]);
189
190
				if($success) {
191
					$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...
192
				}
193
			}
194
		}
195
		return $success;
196
	}
197
198
	public function updateTrackerByID(int $userID, int $chapterID, string $chapter) : bool {
199
		$success = (bool) $this->db->set(['current_chapter' => $chapter, 'active' => 'Y', 'last_updated' => NULL])
200
		                    ->where('user_id', $userID)
201
		                    ->where('id', $chapterID)
202
		                    ->update('tracker_chapters');
203
204
		if($success) {
205
			$this->History->userUpdateTitle($chapterID, $chapter);
206
		}
207
		return  $success;
208
	}
209
210 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...
211
		//Series are not fully deleted, they are just marked as inactive as to hide them from the user.
212
		//This is to allow user history to function properly.
213
214
		$success = $this->db->set(['active' => 'N', 'last_updated' => NULL])
215
		                    ->where('user_id', $userID)
216
		                    ->where('id', $chapterID)
217
		                    ->update('tracker_chapters');
218
219
		return (bool) $success;
220
	}
221
	private function updateTitleById(int $id, string $latestChapter) {
222
		//FIXME: Really not too happy with how we're doing history stuff here, it just feels messy.
223
		$query = $this->db->select('latest_chapter AS current_chapter')
224
		                  ->from('tracker_titles')
225
		                  ->where('id', $id)
226
		                  ->get();
227
		$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...
228
229
		$success = $this->db->set(['latest_chapter' => $latestChapter]) //last_updated gets updated via a trigger if something changes
230
		                    ->where('id', $id)
231
		                    ->update('tracker_titles');
232
233
		//Update History
234
		//NOTE: To avoid doing another query to grab the last_updated time, we just use time() which <should> get the same thing.
235
		//FIXME: The <preferable> solution here is we'd just check against the last_updated time, but that can have a few issues.
236
		$this->History->updateTitleHistory($id, $row->current_chapter, $latestChapter, date('Y-m-d H:i:s'));
237
238
		return (bool) $success;
239
	}
240
	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...
241
		$success = $this->db->set($titleData)
242
		                    ->where('id', $id)
243
		                    ->update('tracker_titles');
244
245
		return (bool) $success;
246
	}
247
	private function addTitle(string $titleURL, int $siteID) {
248
		$query = $this->db->select('site, site_class')
249
		                  ->from('tracker_sites')
250
		                  ->where('id', $siteID)
251
		                  ->get();
252
253
		$titleData = $this->sites->{$query->row()->site_class}->getTitleData($titleURL);
254
		$this->db->insert('tracker_titles', array_merge($titleData, ['title_url' => $titleURL, 'site_id' => $siteID]));
255
		$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...
256
257
		$this->History->updateTitleHistory((int) $titleID, NULL, $titleData['latest_chapter'], $titleData['last_updated']);
258
		return $titleID;
259
	}
260
261
	/**
262
	 * Checks for any titles that haven't updated in 16 hours and updates them.
263
	 * This is ran every 6 hours via a cron job.
264
	 */
265
	public function updateLatestChapters() {
266
		$query = $this->db->select('
267
				tracker_titles.id,
268
				tracker_titles.title,
269
				tracker_titles.title_url,
270
				tracker_sites.site,
271
				tracker_sites.site_class,
272
				tracker_sites.status,
273
				tracker_titles.latest_chapter,
274
				tracker_titles.last_updated,
275
				from_unixtime(MAX(auth_users.last_login)) AS timestamp
276
			')
277
			->from('tracker_titles')
278
			->join('tracker_sites', 'tracker_sites.id = tracker_titles.site_id', 'left')
279
			->join('tracker_chapters', 'tracker_titles.id = tracker_chapters.title_id', 'left')
280
			->join('auth_users', 'tracker_chapters.user_id = auth_users.id', 'left')
281
			->where('tracker_sites.status', 'enabled')
282
			->where('tracker_chapters.active', 'Y') //CHECK: Does this apply BEFORE the GROUP BY/HAVING is done?
283
			->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?
284
			->or_where('(`complete` = "Y" AND `last_checked` < DATE_SUB(NOW(), INTERVAL 1 WEEK))', NULL, FALSE)
285
			->group_by('tracker_titles.id')
286
			->having('timestamp IS NOT NULL')
287
			->having('timestamp > DATE_SUB(NOW(), INTERVAL 120 HOUR)')
288
			->order_by('tracker_titles.title', 'ASC')
289
			->get();
290
291
		if($query->num_rows() > 0) {
292
			foreach ($query->result() as $row) {
293
				print "> {$row->title} <{$row->site_class}>"; //Print this prior to doing anything so we can more easily find out if something went wrong
294
				$titleData = $this->sites->{$row->site_class}->getTitleData($row->title_url);
295
				if(!is_null($titleData['latest_chapter'])) {
296
					//FIXME: "At the moment" we don't seem to be doing anything with TitleData['last_updated'].
297
					//       Should we even use this? Y/N
298
					if($this->updateTitleById((int) $row->id, $titleData['latest_chapter'])) {
299
						//Make sure last_checked is always updated on successful run.
300
						//CHECK: Is there a reason we aren't just doing this in updateTitleById?
301
						$this->db->set('last_checked', 'CURRENT_TIMESTAMP', FALSE)
302
						         ->where('id', $row->id)
303
						         ->update('tracker_titles');
304
305
						print " - ({$titleData['latest_chapter']})\n";
306
					}
307
				} else {
308
					log_message('error', "{$row->title} failed to update successfully");
309
					print " - FAILED TO PARSE\n";
310
				}
311
			}
312
		}
313
	}
314
315
	public function exportTrackerFromUserID(int $userID) {
316
		$query = $this->db
317
			->select('tracker_chapters.current_chapter,
318
			          tracker_chapters.category,
319
			          tracker_titles.title_url,
320
			          tracker_sites.site')
321
			->from('tracker_chapters')
322
			->join('tracker_titles', 'tracker_chapters.title_id = tracker_titles.`id', 'left')
323
			->join('tracker_sites', 'tracker_sites.id = tracker_titles.site_id', 'left')
324
			->where('tracker_chapters.user_id', $userID)
325
			->where('tracker_chapters.active', 'Y')
326
			->get();
327
328
		$arr = [];
329
		if($query->num_rows() > 0) {
330
			foreach ($query->result() as $row) {
331
				$arr[$row->category][] = [
332
					'site'            => $row->site,
333
					'title_url'       => $row->title_url,
334
					'current_chapter' => $row->current_chapter
335
				];
336
			}
337
338
			return $arr;
339
		}
340
	}
341
342
	public function importTrackerFromJSON(string $json_string) : array {
343
		//We already know the this is a valid JSON string as it was validated by form_validator.
344
		$json = json_decode($json_string, TRUE);
345
346
		/*
347
		 * 0 = Success
348
		 * 1 = Invalid keys.
349
		 * 2 = Has failed rows
350
		 */
351
		$status = ['code' => 0, 'failed_rows' => []];
352
353
		$categories = array_keys($json);
354
		if(count($categories) === array_intersect(['reading', 'on-hold', 'plan-to-read', 'custom1', 'custom2', 'custom3'], $categories)) {
355
			$json_keys = array_keys(call_user_func_array('array_merge', $json));
356
357
			if(count($json_keys) === 3 && !array_diff(array('site', 'title_url', 'current_chapter'), $json_keys)) {
358
				foreach($categories as $category) {
359
					foreach($json[$category] as $row) {
360
						$success = $this->updateTracker($this->User->id, $row['site'], $row['title_url'], $row['current_chapter']);
361
						if(!$success) {
362
							$status['code']          = 2;
363
							$status['failed_rows'][] = $row;
364
						}
365
					}
366
				}
367
			} else {
368
				$status['code'] = 1;
369
			}
370
		} else {
371
			$status['code'] = 1;
372
		}
373
		return $status;
374
	}
375
376
	public function deleteTrackerByIDList(array $idList) : array {
377
		/*
378
		 * 0 = Success
379
		 * 1 = Invalid IDs
380
		 */
381
		$status = ['code' => 0];
382
383 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...
384
			if(!(ctype_digit($id) && $this->deleteTrackerByID($this->User->id, (int) $id))) {
385
				$status['code'] = 1;
386
			} else {
387
				//Delete was successful, update history too.
388
				$this->History->userRemoveTitle((int) $id);
389
			}
390
		}
391
392
		return $status;
393
	}
394
395
	public function setCategoryByIDList(array $idList, string $category) : array {
396
		/*
397
		 * 0 = Success
398
		 * 1 = Invalid IDs
399
		 * 2 = Invalid category
400
		 */
401
		$status = ['code' => 0];
402
403
		if(in_array($category, array_keys($this->enabledCategories))) {
404 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...
405
				if(!(ctype_digit($id) && $this->setCategoryTrackerByID($this->User->id, (int) $id, $category))) {
406
					$status['code'] = 1;
407
				} else {
408
					//Category update was successful, update history too.
409
					$this->History->userUpdateCategory((int) $id, $category);
410
				}
411
			}
412
		} else {
413
			$status['code'] = 2;
414
		}
415
416
		return $status;
417
	}
418 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...
419
		$success = $this->db->set(['category' => $category, 'active' => 'Y', 'last_updated' => NULL])
420
		                    ->where('user_id', $userID)
421
		                    ->where('id', $chapterID)
422
		                    ->update('tracker_chapters');
423
424
		return (bool) $success;
425
	}
426
427
428
	public function updateTagsByID(int $userID, int $chapterID, string $tag_string) : bool {
429
		$success = FALSE;
430
		if(preg_match("/^[a-z0-9-_,]{0,255}$/", $tag_string)) {
431
			$success = (bool) $this->db->set(['tags' => $tag_string, 'active' => 'Y', 'last_updated' => NULL])
432
			                           ->where('user_id', $userID)
433
			                           ->where('id', $chapterID)
434
			                           ->update('tracker_chapters');
435
		}
436
437
		if($success) {
438
			//Tag update was successful, update history
439
			$this->History->userUpdateTags($chapterID, $tag_string);
440
		}
441
		return $success;
442
	}
443
444
	public function favouriteChapter(int $userID, string $site, string $title, string $chapter) : array {
445
		$success = array(
446
			'status' => 'Something went wrong',
447
			'bool'   => FALSE
448
		);
449
		if($siteData = $this->Tracker->getSiteDataFromURL($site)) {
450
			//Validate user input
451
			if(!$this->sites->{$siteData->site_class}->isValidTitleURL($title)) {
452
				//Error is already logged via isValidTitleURL
453
				$success['status'] = 'Title URL is not valid';
454
				return $success;
455
			}
456
			if(!$this->sites->{$siteData->site_class}->isValidChapter($chapter)) {
457
				//Error is already logged via isValidChapter
458
				$success['status'] = 'Chapter URL is not valid';
459
				return $success;
460
			}
461
462
			//NOTE: If the title doesn't exist it will be created. This maybe isn't perfect, but it works for now.
463
			$titleID = $this->Tracker->getTitleID($title, (int) $siteData->id);
464
			if($titleID === 0) {
465
				//Something went wrong.
466
				log_message('error', "TitleID = 0 for {$title} @ {$siteData->id}");
467
				return $success;
468
			}
469
470
			//We need the series to be tracked
471
			$idCQuery = $this->db->select('id')
472
			                    ->where('user_id', $userID)
473
			                    ->where('title_id', $titleID)
474
			                    ->get('tracker_chapters');
475
			if($idCQuery->num_rows() > 0) {
476
				$idCQueryRow = $idCQuery->row();
477
478
				//Check if it is already favourited
479
				$idFQuery = $this->db->select('id')
480
				                    ->where('chapter_id', $idCQueryRow->id)
481
				                    ->where('chapter', $chapter)
482
				                    ->get('tracker_favourites');
483
				if($idFQuery->num_rows() > 0) {
484
					//Chapter is already favourited, so remove it from DB
485
					$idFQueryRow = $idFQuery->row();
486
487
					$isSuccess = (bool) $this->db->where('id', $idFQueryRow->id)
488
					                           ->delete('tracker_favourites');
489
490
					if($isSuccess) {
491
						$success = array(
492
							'status' => 'Unfavourited',
493
							'bool'   => TRUE
494
						);
495
						$this->History->userRemoveFavourite((int) $idCQueryRow->id, $chapter);
496
					}
497
				} else {
498
					//Chapter is not favourited, so add to DB.
499
					$isSuccess = (bool) $this->db->insert('tracker_favourites', [
500
						'chapter_id'      => $idCQueryRow->id,
501
						'chapter'         => $chapter,
502
						'updated_at'      => date('Y-m-d H:i:s')
503
					]);
504
505
					if($isSuccess) {
506
						$success = array(
507
							'status' => 'Favourited',
508
							'bool'   => TRUE
509
						);
510
						$this->History->userAddFavourite((int) $idCQueryRow->id, $chapter);
511
					}
512
				}
513
			} else {
514
				$success['status'] = 'Series needs to be tracked before we can favourite chapters';
515
			}
516
		}
517
		return $success;
518
	}
519
	public function getFavourites(int $page) : array {
520
		$rowsPerPage = 50;
521
		$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...
522
			->select('SQL_CALC_FOUND_ROWS
523
			          tt.title, tt.title_url,
524
			          ts.site, ts.site_class,
525
			          tf.chapter, tf.updated_at', FALSE)
526
			->from('tracker_favourites AS tf')
527
			->join('tracker_chapters AS tc', 'tf.chapter_id = tc.id', 'left')
528
			->join('tracker_titles AS tt', 'tc.title_id = tt.id', 'left')
529
			->join('tracker_sites AS ts', 'tt.site_id = ts.id', 'left')
530
			->where('tc.user_id', $this->User->id) //CHECK: Is this inefficient? Would it be better to have a user_id column in tracker_favourites?
531
			->order_by('tf.id DESC')
532
			->limit($rowsPerPage, ($rowsPerPage * ($page - 1)))
533
			->get();
534
535
		$arr = ['rows' => [], 'totalPages' => 1];
536
		if($query->num_rows() > 0) {
537
			foreach($query->result() as $row) {
538
				$arrRow = [];
539
540
				$arrRow['updated_at'] = $row->updated_at;
541
				$arrRow['title']      = $row->title;
542
				$arrRow['title_url']  = $this->Tracker->sites->{$row->site_class}->getFullTitleURL($row->title_url);
543
544
				$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...
545
				$arrRow['site_sprite'] = str_replace('.', '-', $row->site);
546
547
				$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...
548
				$arrRow['chapter'] = "<a href=\"{$chapterData['url']}\">{$chapterData['number']}</a>";
549
				$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...
550
			}
551
			$arr['totalPages'] = ceil($this->db->query('SELECT FOUND_ROWS() count;')->row()->count / $rowsPerPage);
552
		}
553
		return $arr;
554
555
	}
556
557
	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...
558
		$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...
559
560
		$query = $this->db->distinct()
561
		                  ->select('category')
562
		                  ->from('tracker_chapters')
563
		                  ->where('tracker_chapters.active', 'Y')
564
		                  ->get();
565
566
		return array_column($query->result_array(), 'category');
567
	}
568
569
	public function getStats() : array {
570
		$stats = array();
571
572
		//CHECK: Is it possible to merge some of these queries?
573
574
		//$this->db->cache_on();
575
		$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...
576
		                      'COUNT(*) AS total_users',
577
		                      'SUM(CASE WHEN api_key IS NOT NULL THEN 1 ELSE 0 END) AS validated_users',
578
		                      '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'
579
		                  ], FALSE)
580
		                  ->from('auth_users')
581
		                  ->get();
582
		$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...
583
584
		$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...
585
		                      'tracker_titles.title',
586
		                      'COUNT(tracker_chapters.title_id) AS count'
587
		                  ], FALSE)
588
		                  ->from('tracker_chapters')
589
		                  ->join('tracker_titles', 'tracker_titles.id = tracker_chapters.title_id', 'left')
590
		                  ->group_by('tracker_chapters.title_id')
591
		                  ->having('count > 1')
592
		                  ->order_by('count DESC')
593
		                  ->get();
594
		$stats['titles_tracked_more'] = count($queryCounts->result_array());
595
		$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...
596
		$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...
597
598
		$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...
599
		                      'COUNT(DISTINCT tracker_titles.id) AS total_titles',
600
		                      'COUNT(DISTINCT tracker_titles.site_id) AS total_sites',
601
		                      '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',
602
		                      'SUM(CASE WHEN (tracker_titles.last_updated > DATE_SUB(NOW(), INTERVAL 24 HOUR)) THEN 1 ELSE 0 END) AS updated_titles'
603
		                  ], FALSE)
604
		                  ->from('tracker_titles')
605
		                  ->join('tracker_sites', 'tracker_sites.id = tracker_titles.site_id', 'left')
606
		                  ->join('tracker_chapters', 'tracker_titles.id = tracker_chapters.title_id', 'left')
607
		                  ->join('auth_users', 'tracker_chapters.user_id = auth_users.id', 'left')
608
		                  ->get();
609
		$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...
610
611
		$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...
612
		                      'tracker_sites.site',
613
		                      'COUNT(*) AS count'
614
		                  ], FALSE)
615
		                  ->from('tracker_titles')
616
		                  ->join('tracker_sites', 'tracker_sites.id = tracker_titles.site_id', 'left')
617
		                  ->group_by('tracker_titles.site_id')
618
		                  ->order_by('count DESC')
619
		                  ->limit(3)
620
		                  ->get();
621
		$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...
622
		$stats['rank1_site']       = $querySitesResult[0]['site'];
623
		$stats['rank1_site_count'] = $querySitesResult[0]['count'];
624
		$stats['rank2_site']       = $querySitesResult[1]['site'];
625
		$stats['rank2_site_count'] = $querySitesResult[1]['count'];
626
		$stats['rank3_site']       = $querySitesResult[2]['site'];
627
		$stats['rank3_site_count'] = $querySitesResult[2]['count'];
628
629
		$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...
630
		                      'COUNT(*) AS title_updated_count'
631
		                  ], FALSE)
632
		                  ->from('tracker_titles_history')
633
		                  ->get();
634
		$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...
635
636
		$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...
637
		                      'COUNT(*) AS user_updated_count'
638
		                  ], FALSE)
639
		                  ->from('tracker_user_history')
640
		                  ->get();
641
		$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...
642
643
		$stats['live_time'] = timespan(/*2016-09-10T03:17:19*/ 1473477439, time(), 2);
644
645
		//$this->db->cache_off();
646
		return $stats;
647
	}
648
649
	//FIXME: Should this be moved elsewhere??
650
	public function getNextUpdateTime() : string {
651
		$temp_now = new DateTime();
652
		$temp_now->setTimezone(new DateTimeZone('America/New_York'));
653
		$temp_now_formatted = $temp_now->format('Y-m-d H:i:s');
654
655
		//NOTE: PHP Bug: DateTime:diff doesn't play nice with setTimezone, so we need to create another DT object
656
		$now         = new DateTime($temp_now_formatted);
657
		$future_date = new DateTime($temp_now_formatted);
658
		$now_hour    = (int) $now->format('H');
659
		if($now_hour < 4) {
660
			//Time until 4am
661
			$future_date->setTime(4, 00);
662
		} elseif($now_hour < 8) {
663
			//Time until 8am
664
			$future_date->setTime(8, 00);
665
		} elseif($now_hour < 12) {
666
			//Time until 12pm
667
			$future_date->setTime(12, 00);
668
		} elseif($now_hour < 16) {
669
			//Time until 4pm
670
			$future_date->setTime(16, 00);
671
		} elseif($now_hour < 20) {
672
			//Time until 8pm
673
			$future_date->setTime(20, 00);
674
		} else {
675
			//Time until 12am
676
			$future_date->setTime(00, 00);
677
			$future_date->add(new DateInterval('P1D'));
678
		}
679
680
		$interval = $future_date->diff($now);
681
		return $interval->format("%H:%I:%S");
682
	}
683
684
	public function reportBug(string $text, $userID = NULL, $url = NULL) : bool {
685
		$this->load->library('email');
686
687
		//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).
688
		$body = "".
689
		(!is_null($url) && !empty($url) ? "URL: ".htmlspecialchars(substr($url, 0, 255))."<br>\n" : "").
690
		"Submitted by: ".$this->input->ip_address().(!is_null($userID) ? "| {$userID}" : "")."<br>\n".
691
		"<br>Bug report: ".htmlspecialchars(substr($text, 0, 1000));
692
693
		$success = TRUE;
694
		$this->email->from('[email protected]', $this->config->item('site_title', 'ion_auth'));
695
		$this->email->to($this->config->item('admin_email', 'ion_auth'));
696
		$this->email->subject($this->config->item('site_title', 'ion_auth')." - Bug Report");
697
		$this->email->message($body);
698
		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...
699
			$success = FALSE;
700
		}
701
		return $success;
702
	}
703
704
	/*************************************************/
705
	public function sites() {
706
		return $this;
707
	}
708
}
709