Scrutinizer GitHub App not installed

We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.

Install GitHub App

Passed
Pull Request — master (#833)
by Dan
03:33
created

SmrSession::getRequestVarIntArray()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 4
rs 10
1
<?php declare(strict_types=1);
2
3
if (!defined('USING_AJAX')) {
4
	define('USING_AJAX', false);
5
}
6
7
class SmrSession {
8
	const ALWAYS_AVAILABLE = 999999;
9
	const TIME_BEFORE_EXPIRY = 3600;
10
11
	// Defines the number of pages that can be loaded after
12
	// this page before the links on this page become invalid
13
	// (i.e. before you get a back button error).
14
	private const URL_DEFAULT_REMAINING_PAGE_LOADS = array(
15
			'alliance_broadcast.php' => self::ALWAYS_AVAILABLE,
16
			'alliance_forces.php' => self::ALWAYS_AVAILABLE,
17
			'alliance_list.php' => self::ALWAYS_AVAILABLE,
18
			'alliance_message_view.php' => self::ALWAYS_AVAILABLE,
19
			'alliance_message.php' => self::ALWAYS_AVAILABLE,
20
			'alliance_mod.php' => self::ALWAYS_AVAILABLE,
21
			'alliance_option.php' => self::ALWAYS_AVAILABLE,
22
			'alliance_pick.php' => self::ALWAYS_AVAILABLE,
23
			'alliance_remove_member.php' => self::ALWAYS_AVAILABLE,
24
			'alliance_roster.php' => self::ALWAYS_AVAILABLE,
25
			'beta_functions.php' => self::ALWAYS_AVAILABLE,
26
			'bug_report.php' => self::ALWAYS_AVAILABLE,
27
			'cargo_dump.php' => self::ALWAYS_AVAILABLE,
28
			'council_list.php' => self::ALWAYS_AVAILABLE,
29
			'course_plot.php' => self::ALWAYS_AVAILABLE,
30
			'changelog_view.php' => self::ALWAYS_AVAILABLE,
31
			'chat_rules.php' => self::ALWAYS_AVAILABLE,
32
			'chess_play.php' => self::ALWAYS_AVAILABLE,
33
			'combat_log_list.php' => self::ALWAYS_AVAILABLE,
34
			'combat_log_viewer.php' => self::ALWAYS_AVAILABLE,
35
			'current_sector.php' => self::ALWAYS_AVAILABLE,
36
			'configure_hardware.php' => self::ALWAYS_AVAILABLE,
37
			'contact.php' => self::ALWAYS_AVAILABLE,
38
			'council_embassy.php' => self::ALWAYS_AVAILABLE,
39
			'council_list.php' => self::ALWAYS_AVAILABLE,
40
			'council_politics.php' => self::ALWAYS_AVAILABLE,
41
			'council_send_message.php' => self::ALWAYS_AVAILABLE,
42
			'council_vote.php' => self::ALWAYS_AVAILABLE,
43
			'current_players.php' => self::ALWAYS_AVAILABLE,
44
			'donation.php' => self::ALWAYS_AVAILABLE,
45
			'feature_request_comments.php' => self::ALWAYS_AVAILABLE,
46
			'feature_request.php' => self::ALWAYS_AVAILABLE,
47
			'forces_list.php' => self::ALWAYS_AVAILABLE,
48
			'forces_mass_refresh.php' => self::ALWAYS_AVAILABLE,
49
			'government.php' => 1,
50
			'hall_of_fame_player_new.php' => self::ALWAYS_AVAILABLE,
51
			'hall_of_fame_player_detail.php' => self::ALWAYS_AVAILABLE,
52
			'leave_newbie.php' => self::ALWAYS_AVAILABLE,
53
			'logoff.php' => self::ALWAYS_AVAILABLE,
54
			'map_local.php' => self::ALWAYS_AVAILABLE,
55
			'message_view.php' => self::ALWAYS_AVAILABLE,
56
			'message_send.php' => self::ALWAYS_AVAILABLE,
57
			'news_read_advanced.php' => self::ALWAYS_AVAILABLE,
58
			'news_read_current.php' => 1,
59
			'news_read.php' => self::ALWAYS_AVAILABLE,
60
			'planet_construction.php' => self::ALWAYS_AVAILABLE,
61
			'planet_defense.php' => self::ALWAYS_AVAILABLE,
62
			'planet_financial.php' => self::ALWAYS_AVAILABLE,
63
			'planet_main.php' => self::ALWAYS_AVAILABLE,
64
			'planet_ownership.php' => self::ALWAYS_AVAILABLE,
65
			'planet_stockpile.php' => self::ALWAYS_AVAILABLE,
66
			'planet_list.php' => self::ALWAYS_AVAILABLE,
67
			'planet_list_financial.php' => self::ALWAYS_AVAILABLE,
68
			'preferences.php' => self::ALWAYS_AVAILABLE,
69
			'rankings_alliance_death.php' => self::ALWAYS_AVAILABLE,
70
			'rankings_alliance_experience.php' => self::ALWAYS_AVAILABLE,
71
			'rankings_alliance_kills.php' => self::ALWAYS_AVAILABLE,
72
			'rankings_alliance_vs_alliance.php' => self::ALWAYS_AVAILABLE,
73
			'rankings_player_death.php' => self::ALWAYS_AVAILABLE,
74
			'rankings_player_experience.php' => self::ALWAYS_AVAILABLE,
75
			'rankings_player_kills.php' => self::ALWAYS_AVAILABLE,
76
			'rankings_player_profit.php' => self::ALWAYS_AVAILABLE,
77
			'rankings_race_death.php' => self::ALWAYS_AVAILABLE,
78
			'rankings_race_kills.php' => self::ALWAYS_AVAILABLE,
79
			'rankings_race.php' => self::ALWAYS_AVAILABLE,
80
			'rankings_sector_kill.php' => self::ALWAYS_AVAILABLE,
81
			'rankings_player_kills.php' => self::ALWAYS_AVAILABLE,
82
			'rankings_player_kills.php' => self::ALWAYS_AVAILABLE,
83
			'rankings_player_kills.php' => self::ALWAYS_AVAILABLE,
84
			'rankings_view.php' => self::ALWAYS_AVAILABLE,
85
			'sector_scan.php' => self::ALWAYS_AVAILABLE,
86
			'trader_bounties.php' => self::ALWAYS_AVAILABLE,
87
			'trader_relations.php' => self::ALWAYS_AVAILABLE,
88
			'trader_savings.php' => self::ALWAYS_AVAILABLE,
89
			'trader_search_result.php' => self::ALWAYS_AVAILABLE,
90
			'trader_search.php' => self::ALWAYS_AVAILABLE,
91
			'trader_status.php' => self::ALWAYS_AVAILABLE,
92
			'weapon_reorder.php' => self::ALWAYS_AVAILABLE,
93
			//Processing pages
94
			'alliance_message_add_processing.php' => self::ALWAYS_AVAILABLE,
95
			'alliance_message_delete_processing.php' => self::ALWAYS_AVAILABLE,
96
			'alliance_pick_processing.php' => self::ALWAYS_AVAILABLE,
97
			'chess_move_processing.php' => self::ALWAYS_AVAILABLE,
98
			'toggle_processing.php' => self::ALWAYS_AVAILABLE,
99
			//Admin pages
100
			'account_edit.php' => self::ALWAYS_AVAILABLE,
101
			'album_moderate.php' => self::ALWAYS_AVAILABLE,
102
			'box_view.php' => self::ALWAYS_AVAILABLE,
103
			'changelog.php' => self::ALWAYS_AVAILABLE,
104
			'comp_share.php' => self::ALWAYS_AVAILABLE,
105
			'form_open.php' => self::ALWAYS_AVAILABLE,
106
			'ip_view_results.php' => self::ALWAYS_AVAILABLE,
107
			'ip_view.php' => self::ALWAYS_AVAILABLE,
108
			'permission_manage.php' => self::ALWAYS_AVAILABLE,
109
			'word_filter.php' => self::ALWAYS_AVAILABLE,
110
			//Uni gen
111
			'1.6/check_map.php' => self::ALWAYS_AVAILABLE,
112
			'1.6/universe_create_locations.php' => self::ALWAYS_AVAILABLE,
113
			'1.6/universe_create_planets.php' => self::ALWAYS_AVAILABLE,
114
			'1.6/universe_create_ports.php' => self::ALWAYS_AVAILABLE,
115
			'1.6/universe_create_sector_details.php' => self::ALWAYS_AVAILABLE,
116
			'1.6/universe_create_sectors.php' => self::ALWAYS_AVAILABLE,
117
			'1.6/universe_create_warps.php' => self::ALWAYS_AVAILABLE,
118
		);
119
120
	private const URL_LOAD_DELAY = array(
121
		'configure_hardware.php' => .4,
122
		'forces_drop.php' => .4,
123
		'forces_drop_processing.php' => .5,
124
		'forces_refresh_processing.php' => .4,
125
		'sector_jump_processing.php' => .4,
126
		'sector_move_processing.php' => .4,
127
		'sector_scan.php' => .4,
128
		'shop_goods_processing.php' => .4,
129
		'trader_attack_processing.php' => .75,
130
		'trader_examine.php' => .75
131
	);
132
133
	protected static $db;
134
135
	private static $session_id;
136
	private static $game_id;
137
	private static $var;
138
	private static $commonIDs;
139
	private static $generate;
140
	private static $SN = '';
141
	private static $lastSN;
142
	private static $account_id;
143
	public static $last_accessed;
144
145
	protected static $previousAjaxReturns;
146
	protected static $ajaxReturns = array();
147
148
	public static function init() {
149
		// Return immediately if the SmrSession is already initialized
150
		if (isset(self::$session_id)) {
151
			return;
152
		}
153
154
		// Initialize the db connector here, since `init` is always called
155
		self::$db = new SmrMySqlDatabase();
156
157
		// now try the cookie
158
		if (isset($_COOKIE['session_id']) && strlen($_COOKIE['session_id']) === 32) {
159
			self::$session_id = $_COOKIE['session_id'];
160
		} else {
161
			// create a new session id
162
			do {
163
				self::$session_id = md5(uniqid(strval(rand())));
164
				self::$db->query('SELECT 1 FROM active_session WHERE session_id = ' . self::$db->escapeString(self::$session_id) . ' LIMIT 1');
165
			} while (self::$db->nextRecord()); //Make sure we haven't somehow clashed with someone else's session.
166
			if (!defined('NPC_SCRIPT')) {
167
				setcookie('session_id', self::$session_id);
168
			}
169
		}
170
171
		// try to get current session
172
		self::$db->query('DELETE FROM active_session WHERE last_accessed < ' . self::$db->escapeNumber(time() - self::TIME_BEFORE_EXPIRY));
173
		self::fetchVarInfo();
174
175
		if (!USING_AJAX && isset($_REQUEST['sn']) && isset(self::$var[$_REQUEST['sn']]) && !empty(self::$var[$_REQUEST['sn']])) {
176
			$var = self::$var[$_REQUEST['sn']];
177
			$currentPage = $var['url'] == 'skeleton.php' ? $var['body'] : $var['url'];
178
			$loadDelay = self::URL_LOAD_DELAY[$currentPage] ?? 0;
179
			$initialTimeBetweenLoads = microtime(true) - $var['PreviousRequestTime'];
180
			while (($timeBetweenLoads = microtime(true) - $var['PreviousRequestTime']) < $loadDelay) {
181
				$sleepTime = IRound(($loadDelay - $timeBetweenLoads) * 1000000);
182
			//	echo 'Sleeping for: ' . $sleepTime . 'us';
183
				usleep($sleepTime);
184
			}
185
			if (ENABLE_DEBUG) {
186
				self::$db->query('INSERT INTO debug VALUES (' . self::$db->escapeString('Delay: ' . $currentPage) . ',' . self::$db->escapeNumber(self::$account_id) . ',' . self::$db->escapeNumber($initialTimeBetweenLoads) . ',' . self::$db->escapeNumber($timeBetweenLoads) . ')');
187
			}
188
		}
189
	}
190
191
	public static function fetchVarInfo() {
192
		self::$db->query('SELECT * FROM active_session WHERE session_id = ' . self::$db->escapeString(self::$session_id) . ' LIMIT 1');
193
		if (self::$db->nextRecord()) {
194
			self::$generate = false;
195
			self::$session_id = self::$db->getField('session_id');
196
			self::$account_id = self::$db->getInt('account_id');
197
			self::$game_id = self::$db->getInt('game_id');
198
			self::$last_accessed = self::$db->getInt('last_accessed');
199
			self::$commonIDs = array();
200
			self::$lastSN = self::$db->getField('last_sn');
201
			// We may not have ajax_returns if ajax was disabled
202
			$ajaxReturns = self::$db->getField('ajax_returns');
203
			if (!empty($ajaxReturns)) {
204
				self::$previousAjaxReturns = unserialize(gzuncompress($ajaxReturns));
205
			}
206
			self::$var = self::$db->getField('session_var');
207
			if (self::$var != '') {
208
				self::$var = unserialize(gzuncompress(self::$var));
209
			}
210
			if (!is_array(self::$var)) {
211
				self::$account_id = 0;
212
				self::$game_id = 0;
213
				self::$var = array();
214
			} else {
215
				foreach (self::$var as $key => &$value) {
216
					if ($value['Expires'] > 0 && $value['Expires'] <= TIME) { // Use 0 for infinity
217
						//This link is no longer valid
218
						unset(self::$var[$key]);
219
					} elseif ($value['RemainingPageLoads'] < 0) {
220
						//This link is no longer valid
221
						unset(self::$var[$key]);
222
					} else {
223
						--$value['RemainingPageLoads'];
224
						if (isset($value['CommonID'])) {
225
							self::$commonIDs[$value['CommonID']] = $key;
226
						}
227
					}
228
				} unset($value);
229
			}
230
		} else {
231
			self::$generate = true;
232
			self::$account_id = 0;
233
			self::$game_id = 0;
234
			self::$var = array();
235
			self::$commonIDs = array();
236
		}
237
	}
238
239
	public static function update() {
240
		foreach (self::$var as $key => &$value) {
241
			if ($value['RemainingPageLoads'] <= 0) {
242
				//This link was valid this load but will not be in the future, removing it now saves database space and data transfer.
243
				unset(self::$var[$key]);
244
			}
245
		} unset($value);
246
		$compressed = gzcompress(serialize(self::$var));
247
		if (!self::$generate) {
248
			self::$db->query('UPDATE active_session SET account_id=' . self::$db->escapeNumber(self::$account_id) . ',game_id=' . self::$db->escapeNumber(self::$game_id) . (!USING_AJAX ? ',last_accessed=' . self::$db->escapeNumber(TIME) : '') . ',session_var=' . self::$db->escapeBinary($compressed) .
249
					',last_sn=' . self::$db->escapeString(self::$SN) .
250
					' WHERE session_id=' . self::$db->escapeString(self::$session_id) . (USING_AJAX ? ' AND last_sn=' . self::$db->escapeString(self::$lastSN) : '') . ' LIMIT 1');
251
		} else {
252
			self::$db->query('DELETE FROM active_session WHERE account_id = ' . self::$db->escapeNumber(self::$account_id) . ' AND game_id = ' . self::$db->escapeNumber(self::$game_id));
253
			self::$db->query('INSERT INTO active_session (session_id, account_id, game_id, last_accessed, session_var) VALUES(' . self::$db->escapeString(self::$session_id) . ',' . self::$db->escapeNumber(self::$account_id) . ',' . self::$db->escapeNumber(self::$game_id) . ',' . self::$db->escapeNumber(TIME) . ',' . self::$db->escapeBinary($compressed) . ')');
254
			self::$generate = false;
255
		}
256
	}
257
258
	/**
259
	 * Returns the Game ID associated with the session.
260
	 */
261
	public static function getGameID() {
262
		return self::$game_id;
263
	}
264
265
	/**
266
	 * Returns true if the session is inside a game, false otherwise.
267
	 */
268
	public static function hasGame() {
269
		return self::$game_id != 0;
270
	}
271
272
	public static function hasAccount() {
273
		return self::$account_id > 0;
274
	}
275
276
	public static function getAccountID() {
277
		return self::$account_id;
278
	}
279
280
	public static function getAccount() {
281
		return SmrAccount::getAccount(self::$account_id);
282
	}
283
284
	/**
285
	 * Sets the `account_id` attribute of this session.
286
	 */
287
	public static function setAccount(AbstractSmrAccount $account) {
288
		self::$account_id = $account->getAccountID();
289
	}
290
291
	/**
292
	 * Updates the `game_id` attribute of the session and deletes any other
293
	 * active sessions in this game for this account.
294
	 */
295
	public static function updateGame($gameID) {
296
		if (self::$game_id == $gameID) {
297
			return;
298
		}
299
		self::$game_id = $gameID;
300
		self::$db->query('DELETE FROM active_session WHERE account_id = ' . self::$db->escapeNumber(self::$account_id) . ' AND game_id = ' . self::$game_id);
301
		self::$db->query('UPDATE active_session SET game_id=' . self::$db->escapeNumber(self::$game_id) . ' WHERE session_id=' . self::$db->escapeString(self::$session_id));
302
	}
303
304
	/**
305
	 * Returns true if the current SN is different than the previous SN.
306
	 */
307
	public static function hasChangedSN() {
308
		return self::$SN != self::$lastSN;
309
	}
310
311
	private static function updateSN() {
312
		if (!USING_AJAX) {
313
			self::$db->query('UPDATE active_session SET last_sn=' . self::$db->escapeString(self::$SN) .
314
				' WHERE session_id=' . self::$db->escapeString(self::$session_id) . ' LIMIT 1');
315
		}
316
	}
317
318
	public static function destroy() {
319
		self::$db->query('DELETE FROM active_session WHERE session_id = ' . self::$db->escapeString(self::$session_id));
320
		self::$session_id = null;
321
		self::$account_id = 0;
322
		self::$game_id = 0;
323
	}
324
325
	/**
326
	 * Retrieve the session var for the page given by $sn.
327
	 * If $sn is not specified, use the current page (i.e. self::$SN).
328
	 */
329
	public static function retrieveVar($sn = null) {
330
		if (is_null($sn)) {
331
			$sn = self::$SN;
332
		}
333
		if (empty(self::$var[$sn])) {
334
			return false;
335
		}
336
		self::$SN = $sn;
337
		SmrSession::updateSN();
338
		if (isset(self::$var[$sn]['body']) && isset(self::$var[$sn]['CommonID'])) {
339
//			if(preg_match('/processing/',self::$var[$sn]['body']))
340
			unset(self::$commonIDs[self::$var[$sn]['CommonID']]); //Do not store common id for current page
341
			unset(self::$var[$sn]['CommonID']);
342
		}
343
344
		self::$var[$sn]['RemainingPageLoads'] += 1; // Allow refreshing
345
		self::$var[$sn]['Expires'] = 0; // Allow refreshing forever
346
		return self::$var[$sn];
347
	}
348
349
	/**
350
	 * Gets a var from $var, $_REQUEST, or $default. Then stores it in the
351
	 * session so that it can still be retrieved when the page auto-refreshes.
352
	 * This is the recommended way to get $_REQUEST data for display pages.
353
	 * For processing pages, see the Request class.
354
	 */
355
	public static function getRequestVar(string $varName, string $default = null) : string {
356
		$result = Request::getVar($varName, $default);
357
		self::updateVar($varName, $result);
358
		return $result;
359
	}
360
361
	public static function getRequestVarInt(string $varName, int $default = null) : int {
362
		$result = Request::getVarInt($varName, $default);
363
		self::updateVar($varName, $result);
364
		return $result;
365
	}
366
367
	public static function getRequestVarIntArray(string $varName, array $default = null) : array {
368
		$result = Request::getVarIntArray($varName, $default);
369
		self::updateVar($varName, $result);
370
		return $result;
371
	}
372
373
	public static function resetLink($container, $sn) { //Do not allow sharing SN, useful for forwarding.
374
		global $lock;
375
		if (isset(self::$var[$sn]['CommonID'])) {
376
			unset(self::$commonIDs[self::$var[$sn]['CommonID']]); //Do not store common id for reset page, to allow refreshing to always give the same page in response
377
		}
378
		self::$SN = $sn;
379
		if (!isset($container['Expires'])) {
380
			$container['Expires'] = 0; // Lasts forever
381
		}
382
		if (!isset($container['RemainingPageLoads'])) {
383
			$container['RemainingPageLoads'] = 1; // Allow refreshing
384
		}
385
		if (!isset($container['PreviousRequestTime'])) {
386
			if (isset(self::$var[$sn]['PreviousRequestTime'])) {
387
				$container['PreviousRequestTime'] = self::$var[$sn]['PreviousRequestTime']; // Copy across the previous request time if not explicitly set.
388
			}
389
		}
390
391
		self::$var[$sn] = $container;
392
		if (!$lock && !USING_AJAX) {
393
			self::update();
394
		}
395
		return $sn;
396
	}
397
398
	public static function updateVar($key, $value) {
399
		global $var;
400
		if ($value === null) {
401
			unset($var[$key]);
402
			unset(self::$var[self::$SN][$key]);
403
		} else {
404
			$var[$key] = $value;
405
			self::$var[self::$SN][$key] = $value;
406
		}
407
	}
408
409
	public static function clearLinks() {
410
		self::$var = array(self::$SN => self::$var[self::$SN]);
411
		self::$commonIDs = array();
412
	}
413
414
	protected static function addLink($container, $sn = false) { // Container['ID'] MUST be unique to a specific action, if there will be two different outcomes from containers given the same ID then problems will likely arise.
415
		if (!isset($container['Expires'])) {
416
			$container['Expires'] = 0; // Lasts forever
417
		}
418
		if (!isset($container['RemainingPageLoads'])) {
419
			$pageURL = $container['url'] == 'skeleton.php' ? $container['body'] : $container['url'];
420
			$container['RemainingPageLoads'] = self::URL_DEFAULT_REMAINING_PAGE_LOADS[$pageURL] ?? 1; // Allow refreshing
421
		}
422
423
		if ($sn === false) {
424
			$sn = self::generateSN($container);
425
		} else {
426
			// If we've been provided an SN to use then copy over the existing 'PreviousRequestTime'
427
			$container['PreviousRequestTime'] = self::$var[$sn]['PreviousRequestTime'];
428
		}
429
		self::$var[$sn] = $container;
430
		return $sn;
431
	}
432
433
	protected static function generateSN(&$container) {
434
		$container['CommonID'] = self::getCommonID($container);
435
		if (isset(self::$commonIDs[$container['CommonID']])) {
436
			$sn = self::$commonIDs[$container['CommonID']];
437
			$container['PreviousRequestTime'] = isset(self::$var[$sn]) ? self::$var[$sn]['PreviousRequestTime'] : MICRO_TIME;
438
		} else {
439
			do {
440
				$sn = substr(md5(strval(rand())), 0, 8);
441
			} while (isset(self::$var[$sn]));
442
			$container['PreviousRequestTime'] = MICRO_TIME;
443
		}
444
		self::$commonIDs[$container['CommonID']] = $sn;
445
		return $sn;
446
	}
447
448
	protected static function getCommonID($commonContainer) {
449
		unset($commonContainer['Expires']);
450
		unset($commonContainer['RemainingPageLoads']);
451
		unset($commonContainer['PreviousRequestTime']);
452
		unset($commonContainer['CommonID']);
453
		// NOTE: This ID will change if the order of elements in the container
454
		// changes. If this causes unnecessary SN changes, sort the container!
455
		return md5(serialize($commonContainer));
456
	}
457
458
	public static function getNewHREF($container, $forceFullURL = false) {
459
		$sn = self::addLink($container);
460
		if ($forceFullURL === true || stripos($_SERVER['REQUEST_URI'], 'loader.php') === false) {
461
			return '/loader.php?sn=' . $sn;
462
		}
463
		return '?sn=' . $sn;
464
	}
465
466
	public static function addAjaxReturns($element, $contents) {
467
		self::$ajaxReturns[$element] = $contents;
468
		return isset(self::$previousAjaxReturns[$element]) && self::$previousAjaxReturns[$element] == $contents;
469
	}
470
471
	public static function saveAjaxReturns() {
472
		if (empty(self::$ajaxReturns)) {
473
			return;
474
		}
475
		$compressed = gzcompress(serialize(self::$ajaxReturns));
476
		self::$db->query('UPDATE active_session SET ajax_returns=' . self::$db->escapeBinary($compressed) .
477
				' WHERE session_id=' . self::$db->escapeString(self::$session_id) . ' LIMIT 1');
478
	}
479
}
480
481
SmrSession::init();
482