Completed
Push — master ( 1cf074...68c2d1 )
by Angus
09:16
created

User_Options_Model::set()   C

Complexity

Conditions 7
Paths 21

Size

Total Lines 35
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 7

Importance

Changes 0
Metric Value
cc 7
eloc 21
nc 21
nop 2
dl 0
loc 35
ccs 1
cts 1
cp 1
crap 7
rs 6.7272
c 0
b 0
f 0
1
<?php declare(strict_types=1); defined('BASEPATH') or exit('No direct script access allowed');
2
3
class User_Options_Model extends CI_Model {
4
	public $options = array(
5
		/** GENERAL OPTIONS **/
6
		'category_custom_1' => array(
7
			'default' => 'disabled',
8
			'type' => 'int',
9
			'valid_options' => array(
10
				0 => 'disabled',
11
				1 => 'enabled'
12
			)
13
		),
14
		'category_custom_2' => array(
15
			'default' => 'disabled',
16
			'type' => 'int',
17
			'valid_options' => array(
18
				0 => 'disabled',
19
				1 => 'enabled'
20
			)
21
		),
22
		'category_custom_3' => array(
23
			'default' => 'disabled',
24
			'type' => 'int',
25
			'valid_options' => array(
26
				0 => 'disabled',
27
				1 => 'enabled'
28
			)
29
		),
30
		'category_custom_1_text' => array(
31
			'default' => 'Custom 1',
32
			'type' => 'string'
33
		),
34
		'category_custom_2_text' => array(
35
			'default' => 'Custom 2',
36
			'type' => 'string'
37
		),
38
		'category_custom_3_text' => array(
39
			'default' => 'Custom 3',
40
			'type' => 'string'
41
		),
42
43
		'enable_live_countdown_timer' => array(
44
			'default' => 'enabled',
45
			'type' => 'int',
46
			'valid_options' => array(
47
				0 => 'disabled',
48
				1 => 'enabled'
49
			)
50
		),
51
52
		'default_series_category' => array(
53
			'default' => 'reading',
54
			'type' => 'int',
55
			'valid_options' => array(
56
				0 => 'reading',
57
				1 => 'on-hold',
58
				2 => 'plan-to-read',
59
60
				//FIXME: (MAJOR) This should only be enabled if the custom categories are enabled
61
				// Problem is we can't easily check for this since the userscript uses it's own UserID, and not $this->User->id
62
				3 => 'custom1',
63
				4 => 'custom2',
64
				5 => 'custom3'
65
			)
66
		),
67
68
		'list_sort_type' => array(
69
			'default' => 'unread',
70
			'type' => 'int',
71
			'valid_options' => array(
72
				0 => 'unread',
73
				1 => 'alphabetical',
74
				2 => 'my_status',
75
				3 => 'latest'
76
			)
77
		),
78
79
		'list_sort_order' => array(
80
			'default' => 'asc',
81
			'type' => 'int',
82
			'valid_options' => array(
83
				0 => 'asc',
84
				1 => 'desc'
85
			)
86
		),
87
88
		'theme' => array(
89
			'default' => 'light',
90
			'type' => 'int',
91
			'valid_options' => array(
92
				0 => 'light',
93
				1 => 'dark'
94
			)
95
		),
96
	);
97
98 96
	public function __construct() {
99 96
		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, MangaCow, MangaFox, MangaHere, MangaPanda, MangaStream, SeaOtterScans, 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...
100 96
	}
101
102
	/**
103
	 * Get user option, or default option if it does not exist.
104
	 * @param string $option
105
	 * @return mixed Returns option value as STRING, or FALSE if option does not exist.
106
	 */
107 96
	public function get(string $option) {
108 96
		return $this->get_by_userid($option, (int) $this->User->id);
109
	}
110 96
	public function get_by_userid(string $option, int $userID) {
111
		//Check if option is valid
112 96
		if(array_key_exists($option, $this->options)) {
113
			//Check if userID is > 0 & user has option set...
114 96
			if($userID) {
115
				//Check if user has option set.
116 3
				if($row = $this->get_db($option, $userID)) {
117
					//User has option set, get proper value.
118
					if($userValue = $this->parse_value($option, $row['value_str'], $row['value_int'])) {
119
						//Value is valid. Everything is good.
120
						$value = $userValue;
121
					}
122
				}
123
			}
124
125
			//Overall fallback method.
126 96
			if(!isset($value)) $value = $this->options[$option]['default'];
127
		} else {
128
			$value = FALSE;
129
		}
130 96
		return $value;
131
	}
132
133
	public function set(string $option, $value) : bool {
134
		//This assumes we have already validated stuff via form_validation.
135
		//CHECK: Maybe this should be renamed input_set???
136
137
		//TODO: Check if user is logged in, get ID
138
		//Check if option is valid
139
		if(array_key_exists($option, $this->options)) {
140
			//Value is valid, pass it to DB.
141
			$type = $this->options[$option]['type'];
142
143
			$data = array(
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...
144
				'user_id' => $this->User->id,
145
				'name'    => $option
146
			);
147
			$dataValues = array();
148
			if($type == 'int') {
149
				$dataValues['value_int'] = array_search($value, $this->options[$option]['valid_options']);
150
			} else {
151
				$dataValues['value_str'] = (string) $value;
152
			}
153
154
			if($this->db->get_where('user_options', $data)->num_rows() === 0) {
0 ignored issues
show
Documentation introduced by
$data is of type array<string,integer|str...eger","name":"string"}>, but the function expects a string|null.

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...
155
				$data['type'] = ($type == 'int' ? 0 : ($type == 'string' ? 1 : 2));
156
				$success = $this->db->insert('user_options', array_merge($data, $dataValues));
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...
157
			} else {
158
				$this->db->where($data);
159
				$success = $this->db->update('user_options', $dataValues);
160
			}
161
162
			if($success) $this->session->unset_tempdata("option_{$option}");
163
		} else {
164
			$success = FALSE;
165
		}
166
		return $success;
167 3
	}
168
169 3
	private function get_db(string $option, int $userID) {
170 3
		//This function assumes we've already done some basic validation.
171 3
		if(!($data = $this->session->tempdata("option_{$option}"))) {
172 3
			$query = $this->db->select('value_str, value_int')
173 3
			                  ->from('user_options')
174 3
			                  ->where('user_id', $userID)
175
			                  ->where('name',    $option)
176
			                  ->limit(1);
177
			$data = $query->get()->row_array();
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...
178
			$this->session->set_tempdata("option_{$option}", $data, 3600);
179
		}
180
		return $data;
181
	}
182
	private function set_db(string $option, $value) : bool {}
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
Unused Code introduced by
The parameter $option 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...
Unused Code introduced by
The parameter $value 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...
183
184
	//FIXME: I really don't like this function.
185
	private function parse_value(string $option, $value_str, $value_int) {
186
		$type = $this->options[$option]['type'];
187
188
		switch($type) {
189
			case 'int':
190
				//TODO: How exactly should we handle INT? Just DB side?
191
				if(in_array($value_int, array_keys($this->options[$option]['valid_options']))) {
192
					$value = $this->options[$option]['valid_options'][$value_int];
193
				}
194
				break;
195
			case 'string':
196
				//TODO: We should have some basically XSS checking here?
197
				$value = (string) $value_str;
198
				break;
199
			default:
200
				//This should never happen.
201
				break;
202
		}
203
		if(!isset($value)) $value = FALSE; //FIXME: This won't play nice with BOOL type false?
204
205
		return $value;
206
	}
207
208
	//Used to quickly generate an array used with form_radio.
209
	public function generate_radio_array(string $option, string $selected_option) {
210
		if(array_key_exists($option, $this->options)) {
211
			$base_attributes = array(
212
				'name' => $option,
213
				'id'   => $option
214
			);
215
			//FIXME: Get a better solution than str_replace for removing special characters
216
			$elements = array();
217
			foreach (array_values($this->options[$option]['valid_options']) as $valid_option) {
218
				$elements[$option.'_'.str_replace(',', '_', $valid_option)] = array_merge($base_attributes, array(
219
					'value' => $valid_option
220
				));
221
			}
222
			if(isset($elements[$option.'_'.str_replace(',', '_', $selected_option)])) {
223
				$elements[$option.'_'.str_replace(',', '_', $selected_option)]['checked'] = TRUE;
224
			} else {
225
				//This should never occur, but fallbacks are always a good idea..
226
				$elements[$option.'_'.$this->options[$option]['default']]['checked'] = TRUE;
227
			}
228
			//CHECK: Should we attach this to body_data here?
229
			return $elements;
230
		} else {
231
			return FALSE;
232
		}
233
	}
234
}
235