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

Issues (412)

src/lib/Smr/VoteLink.php (1 issue)

Labels
Severity
1
<?php declare(strict_types=1);
2
3
namespace Smr;
4
5
use Exception;
6
use Smr\Pages\Player\VoteLinkProcessor;
7
8
/**
9
 * Site-independent handling of links to external game voting sites.
10
 * This is used to award free turns to players for voting.
11
 */
12
class VoteLink {
13
14
	public const TIME_BETWEEN_VOTING = 84600; // 23.5 hours
15
16
	/** @var ?array<int, int> */
17
	private static ?array $CACHE_TIMEOUTS = null;
18
19
	/** @var array<string, mixed> */
20
	public readonly array $data;
21
22
	public function __construct(
23
		public readonly VoteSite $site,
24
		public readonly int $accountID,
25
		public readonly int $gameID,
26
	) {
27
		$this->data = $site->getData();
0 ignored issues
show
The property data is declared read-only in Smr\VoteLink.
Loading history...
28
	}
29
30
	public static function clearCache(): void {
31
		self::$CACHE_TIMEOUTS = null;
32
	}
33
34
	/**
35
	 * Returns the earliest time (in seconds) until free turns
36
	 * are available across all voting sites.
37
	 */
38
	public static function getMinTimeUntilFreeTurns(int $accountID, int $gameID): int {
39
		$waitTimes = [];
40
		foreach (VoteSite::cases() as $site) {
41
			$link = new self($site, $accountID, $gameID);
42
			if ($link->givesFreeTurns()) {
43
				$waitTimes[] = $link->getTimeUntilFreeTurns();
44
			}
45
		}
46
		if (count($waitTimes) === 0) {
47
			throw new Exception('No enabled vote sites give free turns!');
48
		}
49
		return min($waitTimes);
50
	}
51
52
	/**
53
	 * Does this VoteSite have a voting callback that can be used
54
	 * to award free turns?
55
	 */
56
	public function givesFreeTurns(): bool {
57
		return isset($this->data['img_star']);
58
	}
59
60
	/**
61
	 * Time until the account can get free turns from voting at this site.
62
	 * If the time is 0, this site is eligible for free turns.
63
	 */
64
	public function getTimeUntilFreeTurns(bool $forceUpdate = false): int {
65
		if (!$this->givesFreeTurns()) {
66
			throw new Exception('This vote site cannot award free turns!');
67
		}
68
69
		// Populate timeout cache from the database
70
		if ($forceUpdate || !isset(self::$CACHE_TIMEOUTS)) {
71
			self::$CACHE_TIMEOUTS = []; // ensure this is set
72
			$db = Database::getInstance();
73
			$dbResult = $db->read('SELECT link_id, timeout FROM vote_links WHERE account_id=' . $db->escapeNumber($this->accountID));
74
			foreach ($dbResult->records() as $dbRecord) {
75
				// 'timeout' is the last time the player claimed free turns (or 0, if unclaimed)
76
				self::$CACHE_TIMEOUTS[$dbRecord->getInt('link_id')] = $dbRecord->getInt('timeout');
77
			}
78
		}
79
80
		// If not in the vote_link database, this site is eligible now.
81
		$lastClaimTime = self::$CACHE_TIMEOUTS[$this->site->value] ?? 0;
82
		return max(0, $lastClaimTime + self::TIME_BETWEEN_VOTING - Epoch::time());
83
	}
84
85
	/**
86
	 * Register that the player has clicked on a vote site that is eligible
87
	 * for free turns, so that we will accept incoming votes. This ensures
88
	 * that voting is done through an authenticated SMR session.
89
	 */
90
	public function setClicked(): void {
91
		$db = Database::getInstance();
92
		$db->lockTable('vote_links');
93
		try {
94
			if (!$this->freeTurnsReady(true)) {
95
				// Player has clicked a free turns link after already getting
96
				// free turns. This should not occur naturally.
97
				throw new Exception('Account ID ' . $this->accountID . ' attempted vote link abuse');
98
			}
99
100
			// Don't start the timeout until the vote actually goes through.
101
			$db->replace('vote_links', [
102
				'account_id' => $db->escapeNumber($this->accountID),
103
				'link_id' => $db->escapeNumber($this->site->value),
104
				'timeout' => $db->escapeNumber(0),
105
				'turns_claimed' => $db->escapeBoolean(false),
106
			]);
107
		} finally {
108
			$db->unlock();
109
		}
110
	}
111
112
	/**
113
	 * Register that the player has been awarded their free turns.
114
	 *
115
	 * @return bool True if account was eligible for free turns (i.e. in the setClicked state).
116
	 */
117
	public function setFreeTurnsAwarded(): bool {
118
		$db = Database::getInstance();
119
		$db->write('UPDATE vote_links SET timeout = ' . Epoch::time() . ', turns_claimed = ' . $db->escapeBoolean(true) . ' WHERE account_id = ' . $db->escapeNumber($this->accountID) . ' AND link_id = ' . $db->escapeNumber($this->site->value) . ' AND timeout = 0 AND turns_claimed = ' . $db->escapeBoolean(false));
120
		return $db->getChangedRows() === 1;
121
	}
122
123
	/**
124
	 * Returns true if account can currently receive free turns at this site.
125
	 */
126
	public function freeTurnsReady(bool $forceUpdate = false): bool {
127
		return $this->givesFreeTurns() && $this->gameID != 0 && $this->getTimeUntilFreeTurns($forceUpdate) <= 0;
128
	}
129
130
	/**
131
	 * Returns the image to display for this voting site.
132
	 */
133
	public function getImg(): string {
134
		if (!$this->freeTurnsReady()) {
135
			return $this->data['img_default'];
136
		}
137
		return $this->data['img_star'];
138
	}
139
140
	/**
141
	 * Returns the URL that should be used for this voting site.
142
	 */
143
	public function getUrl(): string {
144
		if (!$this->freeTurnsReady()) {
145
			return $this->data['url_base'];
146
		}
147
		return $this->data['url_func']($this->data['url_base'], $this->accountID, $this->gameID);
148
	}
149
150
	/**
151
	 * Returns the SN to redirect the current page to if free turns are
152
	 * available; otherwise, returns false.
153
	 */
154
	public function getSN(): string|false {
155
		if (!$this->freeTurnsReady()) {
156
			return false;
157
		}
158
		// This page will prepare the account for the voting callback.
159
		return (new VoteLinkProcessor($this->site))->href();
160
	}
161
162
}
163