Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
48.99% |
390 / 796 |
|
23.68% |
9 / 38 |
CRAP | |
0.00% |
0 / 1 |
| UsersController | |
48.35% |
380 / 786 |
|
23.68% |
9 / 38 |
3509.97 | |
0.00% |
0 / 1 |
| showPublishSchedule | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
2 | |||
| resetpassword | |
88.24% |
15 / 17 |
|
0.00% |
0 / 1 |
3.01 | |||
| _getEmailer | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| newpassword | |
89.47% |
17 / 19 |
|
0.00% |
0 / 1 |
4.02 | |||
| routine0 | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
12 | |||
| routine6 | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
| userstats | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
42 | |||
| userstats3 | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
42 | |||
| uploads | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
6 | |||
| adminstats | |
75.51% |
37 / 49 |
|
0.00% |
0 / 1 |
11.47 | |||
| getUserFromNameOrEmail | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
4.03 | |||
| login | |
91.67% |
22 / 24 |
|
0.00% |
0 / 1 |
7.03 | |||
| signRedirectUrl | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getVerifiedRedirectUrl | |
77.78% |
7 / 9 |
|
0.00% |
0 / 1 |
5.27 | |||
| isRelativeUrl | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
4 | |||
| add | |
74.07% |
20 / 27 |
|
0.00% |
0 / 1 |
6.63 | |||
| highscore | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
| rating | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
| added_tags | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
| achievements | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
| highscore3 | |
0.00% |
0 / 110 |
|
0.00% |
0 / 1 |
342 | |||
| leaderboard | |
95.65% |
22 / 23 |
|
0.00% |
0 / 1 |
8 | |||
| view | |
85.94% |
55 / 64 |
|
0.00% |
0 / 1 |
9.23 | |||
| authors | |
100.00% |
62 / 62 |
|
100.00% |
1 / 1 |
2 | |||
| success | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
12 | |||
| penalty | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
| sets | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| logout | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| validateLogin | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
| googlesignin | |
0.00% |
0 / 40 |
|
0.00% |
0 / 1 |
56 | |||
| delete_account | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
| demote_admin | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
30 | |||
| solveHistory | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
| acceptSGFProposal | |
68.75% |
11 / 16 |
|
0.00% |
0 / 1 |
4.49 | |||
| rejectSGFProposal | |
73.68% |
14 / 19 |
|
0.00% |
0 / 1 |
4.29 | |||
| acceptTagConnectionProposal | |
70.59% |
12 / 17 |
|
0.00% |
0 / 1 |
4.41 | |||
| rejectTagConnectionProposal | |
78.26% |
18 / 23 |
|
0.00% |
0 / 1 |
4.16 | |||
| deleteOldTsumegoStatuses | |
76.92% |
10 / 13 |
|
0.00% |
0 / 1 |
4.20 | |||
| 1 | <?php |
| 2 | |
| 3 | App::uses('CakeEmail', 'Network/Email'); |
| 4 | App::uses('Constants', 'Utility'); |
| 5 | App::uses('SgfParser', 'Utility'); |
| 6 | App::uses('AdminActivityLogger', 'Utility'); |
| 7 | App::uses('TagConnectionProposalsRenderer', 'Utility'); |
| 8 | App::uses('AdminActivityRenderer', 'Utility'); |
| 9 | App::uses('SGFProposalsRenderer', 'Utility'); |
| 10 | App::uses('TagProposalsRenderer', 'Utility'); |
| 11 | App::uses('AdminActivityType', 'Model'); |
| 12 | App::uses('CookieFlash', 'Utility'); |
| 13 | |
| 14 | class UsersController extends AppController |
| 15 | { |
| 16 | public $name = 'Users'; |
| 17 | |
| 18 | public $pageTitle = 'Users'; |
| 19 | |
| 20 | public $helpers = ['Html', 'Form']; |
| 21 | |
| 22 | // shows the publish schedule |
| 23 | public function showPublishSchedule(): void |
| 24 | { |
| 25 | $this->loadModel('Tsumego'); |
| 26 | $this->loadModel('Set'); |
| 27 | $this->loadModel('Schedule'); |
| 28 | $this->loadModel('SetConnection'); |
| 29 | |
| 30 | $p = $this->Schedule->find('all', ['order' => 'date ASC', 'conditions' => ['published' => 0]]); |
| 31 | |
| 32 | $pCount = count($p); |
| 33 | for ($i = 0; $i < $pCount; $i++) |
| 34 | { |
| 35 | $t = $this->Tsumego->findById($p[$i]['Schedule']['tsumego_id']); |
| 36 | $scT = $this->SetConnection->find('first', ['conditions' => ['tsumego_id' => $t['Tsumego']['id']]]); |
| 37 | $t['Tsumego']['set_id'] = $scT['SetConnection']['set_id']; |
| 38 | $s = $this->Set->findById($t['Tsumego']['set_id']); |
| 39 | $p[$i]['Schedule']['num'] = $scT['SetConnection']['num']; |
| 40 | $p[$i]['Schedule']['set'] = $s['Set']['title'] . ' ' . $s['Set']['title2'] . ' '; |
| 41 | } |
| 42 | $this->set('p', $p); |
| 43 | } |
| 44 | |
| 45 | /** |
| 46 | * @return void |
| 47 | */ |
| 48 | public function resetpassword() |
| 49 | { |
| 50 | $this->set('_page', 'user'); |
| 51 | $this->set('_title', 'Tsumego Hero - Sign In'); |
| 52 | $this->set('sent', !empty($this->data)); |
| 53 | if (empty($this->data)) |
| 54 | return; |
| 55 | |
| 56 | $user = $this->User->findByEmail($this->data['User']['email']); |
| 57 | if (!$user) |
| 58 | return; |
| 59 | $randomString = Util::generateRandomString(20); |
| 60 | $user['User']['passwordreset'] = $randomString; |
| 61 | $this->User->save($user); |
| 62 | |
| 63 | $email = $this->_getEmailer(); |
| 64 | $email->from(['me@tsumego.com' => 'https://tsumego.com']); |
| 65 | $email->to($this->data['User']['email']); |
| 66 | $email->subject('Password reset for your Tsumego Hero account'); |
| 67 | $email->send('Click the following button to reset your password. If you have not requested the password reset, |
| 68 | then ignore this email. https://' . $_SERVER['HTTP_HOST'] . '/users/newpassword/' . $randomString); |
| 69 | } |
| 70 | |
| 71 | public function _getEmailer() |
| 72 | { |
| 73 | return new CakeEmail(); |
| 74 | } |
| 75 | |
| 76 | // @param string|null $checksum Password reset checksum |
| 77 | public function newpassword($checksum = null): mixed |
| 78 | { |
| 79 | $this->set('_page', 'user'); |
| 80 | $this->set('_title', 'Tsumego Hero - Sign In'); |
| 81 | $done = false; |
| 82 | if ($checksum == null) |
| 83 | $checksum = 1; |
| 84 | $user = $this->User->find('first', ['conditions' => ['passwordreset' => $checksum]]); |
| 85 | $valid = ($user != null); |
| 86 | if (!$user) |
| 87 | return null; |
| 88 | |
| 89 | if ($this->data['User']['password']) |
| 90 | { |
| 91 | $user['User']['passwordreset'] = null; |
| 92 | $user['User']['password_hash'] = password_hash($this->data['User']['password'], PASSWORD_DEFAULT); |
| 93 | $this->User->save($user); |
| 94 | CookieFlash::set("Password changed", 'success'); |
| 95 | return $this->redirect("/users/login"); |
| 96 | } |
| 97 | |
| 98 | $this->set('valid', $valid); |
| 99 | $this->set('done', $done); |
| 100 | $this->set('checksum', $checksum); |
| 101 | return null; |
| 102 | } |
| 103 | |
| 104 | /** |
| 105 | * @return void |
| 106 | */ |
| 107 | public function routine0() //23:55 signed in users today |
| 108 | { |
| 109 | $this->loadModel('Answer'); |
| 110 | |
| 111 | $activity = $this->User->find('all', ['order' => ['User.reuse3 DESC']]); |
| 112 | $todaysUsers = []; |
| 113 | $today = date('Y-m-d', strtotime('today')); |
| 114 | $activityCount = count($activity); |
| 115 | for ($i = 0; $i < $activityCount; $i++) |
| 116 | { |
| 117 | $a = new DateTime($activity[$i]['User']['created']); |
| 118 | if ($a->format('Y-m-d') == $today) |
| 119 | array_push($todaysUsers, $activity[$i]['User']); |
| 120 | } |
| 121 | |
| 122 | $token = []; |
| 123 | $this->Answer->create(); |
| 124 | $token['Answer']['dismissed'] = count($todaysUsers); |
| 125 | $token['Answer']['created'] = date('Y-m-d H:i:s'); |
| 126 | $this->Answer->save($token); |
| 127 | |
| 128 | $this->set('u', count($todaysUsers)); |
| 129 | } |
| 130 | |
| 131 | /** |
| 132 | * @return void |
| 133 | */ |
| 134 | public function routine6() //0:30 update user solved field |
| 135 | { |
| 136 | $this->loadModel('Answer'); |
| 137 | $this->loadModel('TsumegoStatus'); |
| 138 | $a = $this->Answer->findById(1); |
| 139 | $uLast = $this->User->find('first', ['order' => 'id DESC']); |
| 140 | if ($uLast['User']['id'] < $a['Answer']['message']) |
| 141 | { |
| 142 | $a['Answer']['message'] = 0; |
| 143 | $a['Answer']['dismissed'] = 300; |
| 144 | } |
| 145 | else |
| 146 | { |
| 147 | $a['Answer']['message'] += 300; |
| 148 | $a['Answer']['dismissed'] += 300; |
| 149 | } |
| 150 | $this->Answer->save($a); |
| 151 | } |
| 152 | |
| 153 | /** |
| 154 | * @param string|int|null $uid User ID |
| 155 | * @return void |
| 156 | */ |
| 157 | public function userstats($uid = null) |
| 158 | { |
| 159 | $this->set('_page', 'user'); |
| 160 | $this->set('_title', 'USER STATS'); |
| 161 | $this->loadModel('TsumegoAttempt'); |
| 162 | $this->loadModel('Set'); |
| 163 | $this->loadModel('Tsumego'); |
| 164 | $this->loadModel('SetConnection'); |
| 165 | if ($uid == null) |
| 166 | $ur = $this->TsumegoAttempt->find('all', ['limit' => 500, 'order' => 'created DESC']); |
| 167 | elseif ($uid == 99) |
| 168 | $ur = $this->TsumegoAttempt->find('all', [ |
| 169 | 'order' => 'created DESC', |
| 170 | 'conditions' => [ |
| 171 | 'tsumego_id >=' => 19752, |
| 172 | 'tsumego_id <=' => 19761, |
| 173 | ], |
| 174 | ]); |
| 175 | else |
| 176 | $ur = $this->TsumegoAttempt->find('all', ['limit' => 500, 'order' => 'created DESC', 'conditions' => ['user_id' => $uid]]); |
| 177 | |
| 178 | $urCount = count($ur); |
| 179 | for ($i = 0; $i < $urCount; $i++) |
| 180 | { |
| 181 | $u = $this->User->findById($ur[$i]['TsumegoAttempt']['user_id']); |
| 182 | $ur[$i]['TsumegoAttempt']['user_name'] = $u['User']['name']; |
| 183 | $ur[$i]['TsumegoAttempt']['level'] = $u['User']['level']; |
| 184 | $t = $this->Tsumego->findById($ur[$i]['TsumegoAttempt']['tsumego_id']); |
| 185 | $scT = $this->SetConnection->find('first', ['conditions' => ['tsumego_id' => $t['Tsumego']['id']]]); |
| 186 | $t['Tsumego']['set_id'] = $scT['SetConnection']['set_id']; |
| 187 | $ur[$i]['TsumegoAttempt']['tsumego_num'] = $scT['SetConnection']['num']; |
| 188 | $ur[$i]['TsumegoAttempt']['tsumego_xp'] = $t['Tsumego']['difficulty']; |
| 189 | $s = $this->Set->findById($t['Tsumego']['set_id']); |
| 190 | $ur[$i]['TsumegoAttempt']['set_name'] = $s['Set']['title']; |
| 191 | } |
| 192 | |
| 193 | $noIndex = false; |
| 194 | if ($uid != null) |
| 195 | $noIndex = true; |
| 196 | if (isset($this->params['url']['c'])) |
| 197 | $this->set('count', 1); |
| 198 | else |
| 199 | $this->set('count', 0); |
| 200 | $this->set('noIndex', $noIndex); |
| 201 | $this->set('ur', $ur); |
| 202 | $this->set('uid', $uid); |
| 203 | } |
| 204 | |
| 205 | /** |
| 206 | * @param string|int|null $sid Set ID |
| 207 | * @return void |
| 208 | */ |
| 209 | public function userstats3($sid = null) |
| 210 | { |
| 211 | $this->set('_page', 'user'); |
| 212 | $this->set('_title', 'USER STATS'); |
| 213 | $this->loadModel('TsumegoAttempt'); |
| 214 | $this->loadModel('Set'); |
| 215 | $this->loadModel('Tsumego'); |
| 216 | $this->loadModel('SetConnection'); |
| 217 | |
| 218 | $ts = TsumegoUtil::collectTsumegosFromSet($sid); |
| 219 | $ids = []; |
| 220 | $tsCount = count($ts); |
| 221 | for ($i = 0; $i < $tsCount; $i++) |
| 222 | array_push($ids, $ts[$i]['Tsumego']['id']); |
| 223 | |
| 224 | if ($sid == null) |
| 225 | $ur = $this->TsumegoAttempt->find('all', ['limit' => 500, 'order' => 'created DESC']); |
| 226 | else |
| 227 | $ur = $this->TsumegoAttempt->find('all', ['order' => 'updated DESC', 'conditions' => ['tsumego_id' => $ids]]); |
| 228 | |
| 229 | $urCount = count($ur); |
| 230 | for ($i = 0; $i < $urCount; $i++) |
| 231 | { |
| 232 | $u = $this->User->findById($ur[$i]['TsumegoAttempt']['user_id']); |
| 233 | $ur[$i]['TsumegoAttempt']['user_name'] = $u['User']['name']; |
| 234 | $t = $this->Tsumego->findById($ur[$i]['TsumegoAttempt']['tsumego_id']); |
| 235 | $ur[$i]['TsumegoAttempt']['tsumego_num'] = $t['Tsumego']['num']; |
| 236 | $ur[$i]['TsumegoAttempt']['tsumego_xp'] = $t['Tsumego']['difficulty']; |
| 237 | $scT = $this->SetConnection->find('first', ['conditions' => ['tsumego_id' => $t['Tsumego']['id']]]); |
| 238 | $t['Tsumego']['set_id'] = $scT['SetConnection']['set_id']; |
| 239 | $s = $this->Set->findById($t['Tsumego']['set_id']); |
| 240 | $ur[$i]['TsumegoAttempt']['set_name'] = $s['Set']['title']; |
| 241 | } |
| 242 | |
| 243 | $noIndex = false; |
| 244 | if ($sid != null) |
| 245 | $noIndex = true; |
| 246 | if (isset($this->params['url']['c'])) |
| 247 | $this->set('count', 1); |
| 248 | else |
| 249 | $this->set('count', 0); |
| 250 | $this->set('noIndex', $noIndex); |
| 251 | $this->set('ur', $ur); |
| 252 | //$this->set('uid', $uid); |
| 253 | } |
| 254 | |
| 255 | /** |
| 256 | * @return void |
| 257 | */ |
| 258 | public function uploads() |
| 259 | { |
| 260 | $this->set('_page', 'set'); |
| 261 | $this->set('_title', 'Uploads'); |
| 262 | $this->loadModel('Sgf'); |
| 263 | $this->loadModel('Tsumego'); |
| 264 | $this->loadModel('Set'); |
| 265 | $this->loadModel('SetConnection'); |
| 266 | |
| 267 | $s = $this->Sgf->find('all', [ |
| 268 | 'limit' => 250, |
| 269 | 'order' => 'created DESC', |
| 270 | ]); |
| 271 | |
| 272 | $sCount = count($s); |
| 273 | for ($i = 0; $i < $sCount; $i++) |
| 274 | { |
| 275 | $s[$i]['Sgf']['sgf'] = str_replace("\r", '', $s[$i]['Sgf']['sgf']); |
| 276 | $s[$i]['Sgf']['sgf'] = str_replace("\n", '"+"\n"+"', $s[$i]['Sgf']['sgf']); |
| 277 | |
| 278 | $u = $this->User->findById($s[$i]['Sgf']['user_id']); |
| 279 | $s[$i]['Sgf']['user'] = $u['User']['name']; |
| 280 | $t = $this->Tsumego->findById($s[$i]['Sgf']['tsumego_id']); |
| 281 | $scT = $this->SetConnection->find('first', ['conditions' => ['tsumego_id' => $t['Tsumego']['id']]]); |
| 282 | $t['Tsumego']['set_id'] = $scT['SetConnection']['set_id']; |
| 283 | $set = $this->Set->findById($t['Tsumego']['set_id']); |
| 284 | $s[$i]['Sgf']['title'] = $set['Set']['title'] . ' ' . $set['Set']['title2'] . ' #' . $t['Tsumego']['num']; |
| 285 | $s[$i]['Sgf']['num'] = $t['Tsumego']['num']; |
| 286 | |
| 287 | $s[$i]['Sgf']['delete'] = false; |
| 288 | $sDiff = $this->Sgf->find('all', ['order' => 'id DESC', 'limit' => 2, 'conditions' => ['tsumego_id' => $s[$i]['Sgf']['tsumego_id']]]); |
| 289 | $s[$i]['Sgf']['diff'] = $sDiff[1]['Sgf']['id']; |
| 290 | } |
| 291 | $this->set('s', $s); |
| 292 | } |
| 293 | |
| 294 | public function adminstats(): void |
| 295 | { |
| 296 | $this->set('_page', 'user'); |
| 297 | $this->set('_title', 'Admin Panel'); |
| 298 | $this->loadModel('User'); |
| 299 | $this->loadModel('DayRecord'); |
| 300 | $this->loadModel('Tsumego'); |
| 301 | $this->loadModel('Set'); |
| 302 | $this->loadModel('Tag'); |
| 303 | |
| 304 | if (Auth::isAdmin()) |
| 305 | if (isset($this->params['url']['delete']) && isset($this->params['url']['hash'])) |
| 306 | { |
| 307 | $toDelete = $this->User->findById($this->params['url']['delete'] / 1111); |
| 308 | $del1 = $this->TsumegoStatus->find('all', ['conditions' => ['user_id' => $toDelete['User']['id']]]); |
| 309 | $del2 = $this->TsumegoAttempt->find('all', ['conditions' => ['user_id' => $toDelete['User']['id']]]); |
| 310 | if (md5($toDelete['User']['name']) == $this->params['url']['hash']) |
| 311 | { |
| 312 | foreach ($del1 as $item) |
| 313 | $this->TsumegoStatus->delete($item['TsumegoStatus']['id']); |
| 314 | foreach ($del2 as $item) |
| 315 | $this->TsumegoAttempt->delete($item['TsumegoAttempt']['id']); |
| 316 | $this->User->delete($toDelete['User']['id']); |
| 317 | echo '<pre>'; |
| 318 | print_r('Deleted user ' . $toDelete['User']['name']); |
| 319 | echo '</pre>'; |
| 320 | } |
| 321 | } |
| 322 | |
| 323 | // Pagination setup |
| 324 | $perPage = 100; |
| 325 | $tagNamesPage = isset($this->params['url']['tagnames_page']) ? max(1, (int) $this->params['url']['tagnames_page']) : 1; |
| 326 | |
| 327 | $tagNamesOffset = ($tagNamesPage - 1) * $perPage; |
| 328 | |
| 329 | // Get total counts |
| 330 | $this->set('tagNamesTotal', $this->Tag->find('count', ['conditions' => ['approved' => 0]])); |
| 331 | |
| 332 | $tags = $this->Tag->find('all', [ |
| 333 | 'conditions' => ['approved' => 0], |
| 334 | 'limit' => $perPage, |
| 335 | 'offset' => $tagNamesOffset, |
| 336 | 'order' => 'created DESC' |
| 337 | ]); |
| 338 | |
| 339 | $tagsByKey = $this->Tag->find('all'); |
| 340 | $tKeys = []; |
| 341 | $tagsByKeyCount = count($tagsByKey); |
| 342 | for ($i = 0; $i < $tagsByKeyCount; $i++) |
| 343 | $tKeys[$tagsByKey[$i]['Tag']['id']] = $tagsByKey[$i]['Tag']['name']; |
| 344 | |
| 345 | $tagTsumegos = []; |
| 346 | |
| 347 | $tagNamesCount = count($tags); |
| 348 | for ($i = 0; $i < $tagNamesCount; $i++) |
| 349 | { |
| 350 | $au = $this->User->findById($tags[$i]['Tag']['user_id']); |
| 351 | $tags[$i]['Tag']['user'] = $this->checkPicture($au); |
| 352 | } |
| 353 | |
| 354 | $requestDeletion = $this->User->find('all', ['conditions' => ['dbstorage' => 1111]]); |
| 355 | |
| 356 | $this->set('requestDeletion', $requestDeletion); |
| 357 | $this->set('tagNames', $tags); |
| 358 | $this->set('tagTsumegos', $tagTsumegos); |
| 359 | |
| 360 | // Pagination data |
| 361 | $this->set('sgfProposalsRenderer', new SGFProposalsRenderer($this->params['url'])); |
| 362 | $this->set('tagProposalsRenderer', new TagProposalsRenderer($this->params['url'])); |
| 363 | $this->set('adminActivityRenderer', new AdminActivityRenderer($this->params['url'])); |
| 364 | $this->set('tagConnectionProposalsRenderer', new TagConnectionProposalsRenderer($this->params['url'])); |
| 365 | } |
| 366 | |
| 367 | private function getUserFromNameOrEmail() |
| 368 | { |
| 369 | $input = $this->data['username']; |
| 370 | if (empty($input)) |
| 371 | return null; |
| 372 | if ($user = $this->User->findByName($input)) |
| 373 | return $user; |
| 374 | if ($user = $this->User->findByEmail($input)) |
| 375 | return $user; |
| 376 | return null; |
| 377 | } |
| 378 | |
| 379 | public function login() |
| 380 | { |
| 381 | $this->set('_page', 'login'); |
| 382 | $this->set('_title', 'Tsumego Hero - Sign In'); |
| 383 | |
| 384 | // On GET request, prepare redirect URL with HMAC signature (fully stateless) |
| 385 | if (!$this->request->is('post')) |
| 386 | { |
| 387 | $referer = $this->referer(null, true); |
| 388 | // Don't redirect back to login page itself |
| 389 | $redirectUrl = ($referer && strpos($referer, '/users/login') === false) ? $referer : '/sets/'; |
| 390 | |
| 391 | // Sign the redirect URL with HMAC to prevent tampering (no session needed) |
| 392 | $signature = $this->signRedirectUrl($redirectUrl); |
| 393 | |
| 394 | // Pass to view for hidden field and Google data-state |
| 395 | $this->set('redirectUrl', $redirectUrl); |
| 396 | $this->set('redirectSignature', $signature); |
| 397 | return null; |
| 398 | } |
| 399 | |
| 400 | if (!$this->data['username']) |
| 401 | return null; |
| 402 | $user = $this->getUserFromNameOrEmail(); |
| 403 | if (!$user) |
| 404 | { |
| 405 | CookieFlash::set('Unknown user', 'error'); |
| 406 | return null; |
| 407 | } |
| 408 | |
| 409 | if (!$this->validateLogin($this->data, $user)) |
| 410 | { |
| 411 | CookieFlash::set('Incorrect password', 'error'); |
| 412 | return null; |
| 413 | } |
| 414 | |
| 415 | $this->signIn($user); |
| 416 | |
| 417 | // Verify and use redirect URL from POST data |
| 418 | $redirect = $this->getVerifiedRedirectUrl( |
| 419 | $this->request->data('redirect'), |
| 420 | $this->request->data('redirect_signature') |
| 421 | ); |
| 422 | return $this->redirect($redirect); |
| 423 | } |
| 424 | |
| 425 | /** |
| 426 | * Sign a redirect URL with HMAC to prevent tampering. |
| 427 | * This allows stateless redirect URL handling without sessions. |
| 428 | */ |
| 429 | private function signRedirectUrl(string $redirectUrl): string |
| 430 | { |
| 431 | return hash_hmac('sha256', $redirectUrl, Configure::read('Security.salt')); |
| 432 | } |
| 433 | |
| 434 | /** |
| 435 | * Verify redirect URL signature and return safe redirect URL. |
| 436 | * Returns default redirect if signature is invalid or URL is not relative. |
| 437 | */ |
| 438 | private function getVerifiedRedirectUrl(?string $redirectUrl, ?string $signature): string |
| 439 | { |
| 440 | $defaultRedirect = '/sets/'; |
| 441 | |
| 442 | if (!$redirectUrl || !$signature) |
| 443 | return $defaultRedirect; |
| 444 | |
| 445 | // Verify HMAC signature |
| 446 | $expectedSignature = $this->signRedirectUrl($redirectUrl); |
| 447 | if (!hash_equals($expectedSignature, $signature)) |
| 448 | return $defaultRedirect; |
| 449 | |
| 450 | // Prevent open redirect attacks - only allow relative URLs |
| 451 | if (!$this->isRelativeUrl($redirectUrl)) |
| 452 | return $defaultRedirect; |
| 453 | |
| 454 | return $redirectUrl; |
| 455 | } |
| 456 | |
| 457 | /** |
| 458 | * Check if URL is relative (starts with / but not //) |
| 459 | */ |
| 460 | private function isRelativeUrl(string $url): bool |
| 461 | { |
| 462 | return strlen($url) > 0 && $url[0] === '/' && (strlen($url) < 2 || $url[1] !== '/'); |
| 463 | } |
| 464 | |
| 465 | public function add() |
| 466 | { |
| 467 | $this->set('_page', 'user'); |
| 468 | $this->set('_title', 'Tsumego Hero - Sign Up'); |
| 469 | |
| 470 | // Prepare signed redirect URL for Google Sign-In state (fully stateless) |
| 471 | $redirectUrl = '/sets/'; |
| 472 | $signature = $this->signRedirectUrl($redirectUrl); |
| 473 | $this->set('redirectUrl', $redirectUrl); |
| 474 | $this->set('redirectSignature', $signature); |
| 475 | |
| 476 | if (empty($this->data)) |
| 477 | return; |
| 478 | |
| 479 | if ($this->data['User']['password1'] != $this->data['User']['password2']) |
| 480 | { |
| 481 | CookieFlash::set('passwords don\'t match', 'error'); |
| 482 | return; |
| 483 | } |
| 484 | |
| 485 | $userData = $this->data; |
| 486 | $userData['User']['password_hash'] = password_hash($this->data['User']['password1'], PASSWORD_DEFAULT); |
| 487 | $userData['User']['name'] = $this->data['User']['name']; |
| 488 | $userData['User']['email'] = $this->data['User']['email']; |
| 489 | |
| 490 | $this->User->create(); |
| 491 | try |
| 492 | { |
| 493 | if (!$this->User->save($userData, true)) |
| 494 | { |
| 495 | CookieFlash::set('Unable to create user with this name', 'error'); |
| 496 | return; |
| 497 | } |
| 498 | } |
| 499 | catch (Exception $e) |
| 500 | { |
| 501 | CookieFlash::set('Unable to create user with this name', 'error'); |
| 502 | return; |
| 503 | } |
| 504 | |
| 505 | $user = ClassRegistry::init('User')->find('first', ['conditions' => ['name' => $this->data['User']['name']]]); |
| 506 | if (!$user) |
| 507 | die("New user created, but it is not possible to load it."); |
| 508 | CookieFlash::set(__('Registration successful.'), 'success'); |
| 509 | return $this->redirect(['controller' => 'sets', 'action' => 'index']); |
| 510 | } |
| 511 | |
| 512 | /** |
| 513 | * @return void |
| 514 | */ |
| 515 | public function highscore() |
| 516 | { |
| 517 | $this->set('_page', 'levelHighscore'); |
| 518 | $this->set('_title', 'Tsumego Hero - Highscore'); |
| 519 | |
| 520 | $this->loadModel('Tsumego'); |
| 521 | $this->loadModel('Activate'); |
| 522 | |
| 523 | $activate = false; |
| 524 | if (Auth::isLoggedIn()) |
| 525 | $activate = $this->Activate->find('first', ['conditions' => ['user_id' => Auth::getUserID()]]); |
| 526 | |
| 527 | $users = $this->User->find('all', ['limit' => 1000, 'order' => 'level DESC, xp DESC']); |
| 528 | $this->set('users', $users); |
| 529 | $this->set('activate', $activate); |
| 530 | } |
| 531 | |
| 532 | public function rating(): void |
| 533 | { |
| 534 | $this->set('_page', 'ratingHighscore'); |
| 535 | $this->set('_title', 'Tsumego Hero - Rating'); |
| 536 | |
| 537 | $this->loadModel('TsumegoStatus'); |
| 538 | $this->loadModel('Tsumego'); |
| 539 | if (Auth::isLoggedIn()) |
| 540 | { |
| 541 | $ux = $this->User->findById(Auth::getUserID()); |
| 542 | $ux['User']['lastHighscore'] = 2; |
| 543 | $this->User->save($ux); |
| 544 | } |
| 545 | |
| 546 | $users = $this->User->find('all', ['limit' => 1000, 'order' => 'rating DESC']); |
| 547 | $this->set('users', $users); |
| 548 | } |
| 549 | |
| 550 | /** |
| 551 | * @return void |
| 552 | */ |
| 553 | public function added_tags() |
| 554 | { |
| 555 | $this->set('_page', 'timeHighscore'); |
| 556 | $this->set('_title', 'Tsumego Hero - Added Tags'); |
| 557 | |
| 558 | $this->set('tagContributors', Util::query(" |
| 559 | SELECT |
| 560 | user.id as user_id, |
| 561 | user.name as user_name, |
| 562 | user.external_id as user_external_id, |
| 563 | user.picture as user_picture, |
| 564 | user.rating as user_rating, |
| 565 | count(*) AS tag_count |
| 566 | FROM |
| 567 | tag_connection |
| 568 | JOIN user on tag_connection.user_id = user.id |
| 569 | WHERE tag_connection.approved = true |
| 570 | GROUP BY user_id |
| 571 | ORDER BY tag_count DESC |
| 572 | LIMIT 100")); |
| 573 | } |
| 574 | |
| 575 | public function achievements(): void |
| 576 | { |
| 577 | $this->set('_page', 'achievementHighscore'); |
| 578 | $this->set('_title', 'Tsumego Hero - Achievements Highscore'); |
| 579 | |
| 580 | if (Auth::isLoggedIn()) |
| 581 | { |
| 582 | $ux = $this->User->findById(Auth::getUserID()); |
| 583 | $ux['User']['lastHighscore'] = 2; |
| 584 | $this->User->save($ux); |
| 585 | } |
| 586 | |
| 587 | $this->set('users', Util::query(" |
| 588 | SELECT |
| 589 | user.id AS id, |
| 590 | user.name AS name, |
| 591 | user.rating AS rating, |
| 592 | user.picture AS picture, |
| 593 | user.external_id AS external_id, |
| 594 | SUM(achievement_status.value) AS achievement_score |
| 595 | FROM user |
| 596 | JOIN achievement_status |
| 597 | ON achievement_status.user_id = user.id |
| 598 | GROUP BY user.id |
| 599 | ORDER BY achievement_score DESC |
| 600 | LIMIT 100;")); |
| 601 | } |
| 602 | |
| 603 | /** |
| 604 | * @return void |
| 605 | */ |
| 606 | public function highscore3() |
| 607 | { |
| 608 | $this->set('_page', 'timeHighscore'); |
| 609 | $this->set('_title', 'Tsumego Hero - Time Highscore'); |
| 610 | |
| 611 | $this->loadModel('TsumegoStatus'); |
| 612 | $this->loadModel('Tsumego'); |
| 613 | $this->loadModel('TimeModeSession'); |
| 614 | $currentRank = ''; |
| 615 | $params1 = ''; |
| 616 | $params2 = ''; |
| 617 | |
| 618 | if (Auth::isLoggedIn()) |
| 619 | { |
| 620 | $ux = $this->User->findById(Auth::getUserID()); |
| 621 | $ux['User']['lastHighscore'] = 2; |
| 622 | $this->User->save($ux); |
| 623 | } |
| 624 | |
| 625 | if (isset($this->params['url']['category'])) |
| 626 | { |
| 627 | $ro = $this->TimeModeSession->find('all', [ |
| 628 | 'order' => 'points DESC', |
| 629 | 'conditions' => [ |
| 630 | 'mode' => $this->params['url']['category'], |
| 631 | 'TimeModeAttempt' => $this->params['url']['TimeModeAttempt'], |
| 632 | ], |
| 633 | ]); |
| 634 | $currentRank = $this->params['url']['TimeModeAttempt']; |
| 635 | $params1 = $this->params['url']['category']; |
| 636 | $params2 = $this->params['url']['TimeModeAttempt']; |
| 637 | } |
| 638 | else |
| 639 | { |
| 640 | if (Auth::isLoggedIn()) |
| 641 | $lastModex = Auth::getUser()['lastMode'] - 1; |
| 642 | else |
| 643 | $lastModex = 2; |
| 644 | |
| 645 | $params1 = $lastModex; |
| 646 | $params2 = '15k'; |
| 647 | $currentRank = $params2; |
| 648 | $ro = $this->TimeModeSession->find('all', [ |
| 649 | 'order' => 'points DESC', |
| 650 | 'conditions' => [ |
| 651 | 'mode' => $params1, |
| 652 | 'TimeModeAttempt' => $params2, |
| 653 | ], |
| 654 | ]); |
| 655 | } |
| 656 | $roAll = []; |
| 657 | $roAll['user'] = []; |
| 658 | $roAll['picture'] = []; |
| 659 | $roAll['points'] = []; |
| 660 | $roAll['result'] = []; |
| 661 | |
| 662 | $roCount = count($ro); |
| 663 | for ($i = 0; $i < $roCount; $i++) |
| 664 | { |
| 665 | $us = $this->User->findById($ro[$i]['TimeModeSession']['user_id']); |
| 666 | $alreadyIn = false; |
| 667 | $roAllCount = count($roAll['user']); |
| 668 | for ($j = 0; $j < $roAllCount; $j++) |
| 669 | if ($roAll['user'][$j] == $us['User']['name']) |
| 670 | $alreadyIn = true; |
| 671 | if (!$alreadyIn) |
| 672 | { |
| 673 | array_push($roAll['user'], $us['User']['name']); |
| 674 | array_push($roAll['picture'], $us['User']['picture']); |
| 675 | array_push($roAll['points'], $ro[$i]['TimeModeSession']['points']); |
| 676 | array_push($roAll['result'], $ro[$i]['TimeModeSession']['status']); |
| 677 | } |
| 678 | } |
| 679 | |
| 680 | $modes = []; |
| 681 | $modes[0] = []; |
| 682 | $modes[1] = []; |
| 683 | $modes[2] = []; |
| 684 | for ($i = 0; $i < 3; $i++) |
| 685 | { |
| 686 | $rank = 15; |
| 687 | $j = 0; |
| 688 | while ($rank > -5) |
| 689 | { |
| 690 | $kd = 'k'; |
| 691 | $rank2 = $rank; |
| 692 | if ($rank >= 1) |
| 693 | $kd = 'k'; |
| 694 | else |
| 695 | { |
| 696 | $rank2 = ($rank - 1) * (-1); |
| 697 | $kd = 'd'; |
| 698 | } |
| 699 | $modes[$i][$j] = $rank2 . $kd; |
| 700 | $rank--; |
| 701 | $j++; |
| 702 | } |
| 703 | } |
| 704 | $modes2 = []; |
| 705 | $modes2[0] = []; |
| 706 | $modes2[1] = []; |
| 707 | $modes2[2] = []; |
| 708 | for ($i = 0; $i < 3; $i++) |
| 709 | { |
| 710 | $rank = 15; |
| 711 | $j = 0; |
| 712 | while ($rank > -5) |
| 713 | { |
| 714 | $kd = 'k'; |
| 715 | $rank2 = $rank; |
| 716 | if ($rank >= 1) |
| 717 | $kd = 'k'; |
| 718 | else |
| 719 | { |
| 720 | $rank2 = ($rank - 1) * (-1); |
| 721 | $kd = 'd'; |
| 722 | } |
| 723 | $modes2[$i][$j] = $rank2 . $kd; |
| 724 | $rank--; |
| 725 | $j++; |
| 726 | } |
| 727 | } |
| 728 | |
| 729 | $modesCount = count($modes); |
| 730 | for ($i = 0; $i < $modesCount; $i++) |
| 731 | { |
| 732 | $modesCount = count($modes[$i]); |
| 733 | for ($j = 0; $j < $modesCount; $j++) |
| 734 | { |
| 735 | $mx = $this->TimeModeSession->find('first', [ |
| 736 | 'conditions' => [ |
| 737 | 'TimeModeAttempt' => $modes[$i][$j], |
| 738 | 'mode' => $i, |
| 739 | ], |
| 740 | ]); |
| 741 | if ($mx) |
| 742 | $modes[$i][$j] = 1; |
| 743 | } |
| 744 | } |
| 745 | |
| 746 | if (Auth::isLoggedIn()) |
| 747 | { |
| 748 | $ux = $this->User->findById(Auth::getUserID()); |
| 749 | $ux['User']['lastHighscore'] = 4; |
| 750 | $this->User->save($ux); |
| 751 | } |
| 752 | |
| 753 | $this->set('roAll', $roAll); |
| 754 | $this->set('TimeModeAttempt', $currentRank); |
| 755 | $this->set('params1', $params1); |
| 756 | $this->set('params2', $params2); |
| 757 | $this->set('modes', $modes); |
| 758 | $this->set('modes2', $modes2); |
| 759 | } |
| 760 | |
| 761 | /** |
| 762 | * @return void |
| 763 | */ |
| 764 | public function leaderboard() |
| 765 | { |
| 766 | $this->set('_page', 'dailyHighscore'); |
| 767 | $this->set('_title', 'Tsumego Hero - Daily Highscore'); |
| 768 | $this->loadModel('TsumegoStatus'); |
| 769 | $this->loadModel('Tsumego'); |
| 770 | $this->loadModel('DayRecord'); |
| 771 | |
| 772 | $adminsList = $this->User->find('all', ['order' => 'id ASC', 'conditions' => ['isAdmin >' => 0]]) ?: []; |
| 773 | $admins = []; |
| 774 | foreach ($adminsList as $admin) |
| 775 | $admins [] = $admin['User']['name']; |
| 776 | $dayRecord = $this->DayRecord->find('all', ['limit' => 2, 'order' => 'id DESC']); |
| 777 | $userYesterdayName = 'Unknown'; |
| 778 | if (count($dayRecord) > 0 && isset($dayRecord[0]['DayRecord']['user_id'])) |
| 779 | { |
| 780 | $userYesterday = $this->User->findById($dayRecord[0]['DayRecord']['user_id']); |
| 781 | if ($userYesterday && isset($userYesterday['User']['name'])) |
| 782 | $userYesterdayName = $userYesterday['User']['name']; |
| 783 | } |
| 784 | |
| 785 | $users = ClassRegistry::init('User')->query('SELECT user.id, user.name, user.rating, user.external_id, user.picture, user.daily_xp, user.daily_solved FROM user WHERE daily_xp > 0 ORDER BY daily_xp DESC'); |
| 786 | $exportedUsers = []; |
| 787 | foreach ($users as $user) |
| 788 | $exportedUsers [] = $user['user']; |
| 789 | |
| 790 | $this->set('leaderboard', $exportedUsers); |
| 791 | $this->set('uNum', "TODO"); |
| 792 | $this->set('admins', $admins); |
| 793 | $this->set('dayRecord', $userYesterdayName); |
| 794 | } |
| 795 | |
| 796 | public function view($id = null): mixed |
| 797 | { |
| 798 | $this->set('_page', 'user'); |
| 799 | $this->loadModel('TsumegoStatus'); |
| 800 | $this->loadModel('TsumegoAttempt'); |
| 801 | $this->loadModel('Tsumego'); |
| 802 | $this->loadModel('Set'); |
| 803 | $this->loadModel('Achievement'); |
| 804 | $this->loadModel('AchievementStatus'); |
| 805 | $this->loadModel('SetConnection'); |
| 806 | $this->loadModel('TimeModeSession'); |
| 807 | |
| 808 | $as = $this->AchievementStatus->find('all', ['limit' => 12, 'order' => 'created DESC', 'conditions' => ['user_id' => $id]]); |
| 809 | $ach = $this->Achievement->find('all'); |
| 810 | |
| 811 | $user = $this->User->findById($id); |
| 812 | if (!$user) |
| 813 | return $this->redirect('/sets'); |
| 814 | $this->set('_title', 'Profile of ' . $user['User']['name']); |
| 815 | |
| 816 | // user edit |
| 817 | // TODO: should be its own action |
| 818 | if ($id == Auth::getUserID()) |
| 819 | { |
| 820 | if (!empty($this->data)) |
| 821 | if (isset($this->data['User']['email'])) |
| 822 | { |
| 823 | Auth::getUser()['email'] = $this->data['User']['email']; |
| 824 | Auth::saveUser(); |
| 825 | $this->set('data', $this->data['User']['email']); |
| 826 | } |
| 827 | if (isset($this->params['url']['undo'])) |
| 828 | if ($this->params['url']['undo'] / 1111 == $id) |
| 829 | { |
| 830 | Auth::getUser()['dbstorage'] = 1; |
| 831 | Auth::saveUser(); |
| 832 | } |
| 833 | } |
| 834 | |
| 835 | $tsumegoStatusToRestCount = Util::query(" |
| 836 | SELECT |
| 837 | COUNT(*) AS total |
| 838 | FROM tsumego_status |
| 839 | WHERE |
| 840 | user_id = ? AND |
| 841 | tsumego_status.status IN ('S', 'C', 'W') AND |
| 842 | tsumego_status.updated <= NOW() - INTERVAL 1 YEAR;", [$id])[0]['total']; |
| 843 | $this->set('tsumegoStatusToRestCount', $tsumegoStatusToRestCount); |
| 844 | |
| 845 | $oldest = new DateTime(date('Y-m-d', strtotime('-183 days'))); |
| 846 | $oldest = $oldest->format('Y-m-d'); |
| 847 | |
| 848 | $dailyResults = Util::query(" |
| 849 | SELECT |
| 850 | DATE(created) AS day, |
| 851 | SUM(CASE WHEN solved = 1 THEN 1 ELSE 0 END) AS Solves, |
| 852 | SUM(CASE WHEN solved = 0 THEN 1 ELSE 0 END) AS Fails, |
| 853 | MAX(user_rating) AS Rating |
| 854 | FROM tsumego_attempt |
| 855 | WHERE user_id = :user_id |
| 856 | AND created > :oldest |
| 857 | GROUP BY DATE(created) |
| 858 | ORDER BY day ASC |
| 859 | ", ['user_id' => $id, 'oldest' => $oldest]); |
| 860 | |
| 861 | $this->set('timeModeRanks', Util::query(" |
| 862 | SELECT |
| 863 | c.name AS category_name, |
| 864 | |
| 865 | -- 1) Best solved session rank name (highest rank.id) |
| 866 | ( |
| 867 | SELECT time_mode_rank.name |
| 868 | FROM time_mode_session s2 |
| 869 | JOIN time_mode_rank ON time_mode_rank.id = s2.time_mode_rank_id |
| 870 | WHERE |
| 871 | s2.time_mode_category_id = c.id AND |
| 872 | s2.time_mode_session_status_id = " . TimeModeUtil::$SESSION_STATUS_SOLVED . " AND |
| 873 | s2.user_id = ? |
| 874 | ORDER BY time_mode_rank.id DESC -- highest id = best rank |
| 875 | LIMIT 1 |
| 876 | ) AS best_solved_rank_name, |
| 877 | |
| 878 | -- 2) Number of sessions in this category |
| 879 | COUNT(s.id) AS session_count |
| 880 | |
| 881 | FROM |
| 882 | time_mode_category c |
| 883 | LEFT JOIN time_mode_session s ON s.user_id = ? AND s.time_mode_category_id = c.id |
| 884 | GROUP BY c.id, c.name;", [$user['User']['id'], $user['User']['id']])); |
| 885 | |
| 886 | $this->set('timeGraph', Util::query(' |
| 887 | SELECT |
| 888 | DATE(time_mode_session.created) AS category, |
| 889 | SUM(CASE WHEN time_mode_session.time_mode_session_status_id = ' . TimeModeUtil::$SESSION_STATUS_SOLVED . ' THEN 1 ELSE 0 END) AS Passes, |
| 890 | SUM(CASE WHEN time_mode_session.time_mode_session_status_id = ' . TimeModeUtil::$SESSION_STATUS_FAILED . ' THEN 1 ELSE 0 END) AS Fails |
| 891 | FROM time_mode_session |
| 892 | WHERE time_mode_session.user_id = ? |
| 893 | GROUP BY DATE(time_mode_session.created) |
| 894 | ORDER BY category DESC', [$user['User']['id']])); |
| 895 | |
| 896 | $tsumegoCount = TsumegoFilters::empty()->calculateCount(); |
| 897 | $canResetOldTsumegoStatuses = Util::getPercent($user['User']['solved'], $tsumegoCount) >= Constants::$MINIMUM_PERCENT_OF_TSUMEGOS_TO_BE_SOLVED_BEFORE_RESET_IS_ALLOWED; |
| 898 | |
| 899 | $asCount = count($as); |
| 900 | for ($i = 0; $i < $asCount; $i++) |
| 901 | { |
| 902 | $as[$i]['AchievementStatus']['a_title'] = $ach[$as[$i]['AchievementStatus']['achievement_id'] - 1]['Achievement']['name']; |
| 903 | $as[$i]['AchievementStatus']['a_description'] = $ach[$as[$i]['AchievementStatus']['achievement_id'] - 1]['Achievement']['description']; |
| 904 | $as[$i]['AchievementStatus']['a_image'] = $ach[$as[$i]['AchievementStatus']['achievement_id'] - 1]['Achievement']['image']; |
| 905 | $as[$i]['AchievementStatus']['a_color'] = $ach[$as[$i]['AchievementStatus']['achievement_id'] - 1]['Achievement']['color']; |
| 906 | $as[$i]['AchievementStatus']['a_id'] = $ach[$as[$i]['AchievementStatus']['achievement_id'] - 1]['Achievement']['id']; |
| 907 | $as[$i]['AchievementStatus']['a_xp'] = $ach[$as[$i]['AchievementStatus']['achievement_id'] - 1]['Achievement']['xp']; |
| 908 | } |
| 909 | |
| 910 | $aNum = $this->AchievementStatus->find('all', ['conditions' => ['user_id' => $id]]); |
| 911 | $asx = $this->AchievementStatus->find('first', ['conditions' => ['user_id' => $id, 'achievement_id' => 46]]); |
| 912 | $aNumx = count($aNum); |
| 913 | if ($asx != null) |
| 914 | $aNumx = $aNumx + $asx['AchievementStatus']['value'] - 1; |
| 915 | |
| 916 | $user['User']['name'] = $this->checkPicture($user['User']); |
| 917 | |
| 918 | $aCount = $this->Achievement->find('all'); |
| 919 | |
| 920 | $this->set('dailyResults', $dailyResults); |
| 921 | $this->set('user', $user); |
| 922 | $this->set('tsumegoCount', $tsumegoCount); |
| 923 | $this->set('as', $as); |
| 924 | $this->set('aNum', $aNumx); |
| 925 | $this->set('aCount', $aCount); |
| 926 | $this->set('canResetOldTsumegoStatuses', $canResetOldTsumegoStatuses); |
| 927 | return null; |
| 928 | } |
| 929 | |
| 930 | /** |
| 931 | * @return void |
| 932 | */ |
| 933 | public function authors() |
| 934 | { |
| 935 | $this->loadModel('Tsumego'); |
| 936 | $this->loadModel('Set'); |
| 937 | |
| 938 | $this->set('_page', 'about'); |
| 939 | $this->set('_title', 'Tsumego Hero - About'); |
| 940 | |
| 941 | $authors = $this->Tsumego->find('all', [ |
| 942 | 'order' => 'created DESC', |
| 943 | 'conditions' => [ |
| 944 | 'NOT' => [ |
| 945 | 'author' => ['Joschka Zimdars'], |
| 946 | ], |
| 947 | ], |
| 948 | ]); |
| 949 | $set = $this->Set->find('all'); |
| 950 | $setMap = []; |
| 951 | $setMap2 = []; |
| 952 | $setCount = count($set); |
| 953 | for ($i = 0; $i < $setCount; $i++) |
| 954 | { |
| 955 | $divider = ' '; |
| 956 | $setMap[$set[$i]['Set']['id']] = $set[$i]['Set']['title'] . $divider . $set[$i]['Set']['title2']; |
| 957 | $setMap2[$set[$i]['Set']['id']] = $set[$i]['Set']['public']; |
| 958 | } |
| 959 | |
| 960 | $count = []; |
| 961 | $count[0]['author'] = 'Innokentiy Zabirov'; |
| 962 | $count[0]['collections'] = 's: <a href="/sets/view/41">Life & Death - Intermediate</a> and <a href="/sets/view/122">Gokyo Shumyo 1-4</a>'; |
| 963 | $count[0]['count'] = 0; |
| 964 | $count[1]['author'] = 'Alexandre Dinerchtein'; |
| 965 | $count[1]['collections'] = ': <a href="/sets/view/109">Problems from Professional Games</a>'; |
| 966 | $count[1]['count'] = 0; |
| 967 | $count[2]['author'] = 'David Ulbricht'; |
| 968 | $count[2]['collections'] = ': <a href="/sets/view/41">Life & Death - Intermediate</a>'; |
| 969 | $count[2]['count'] = 0; |
| 970 | $count[3]['author'] = 'Bradford Malbon'; |
| 971 | $count[3]['collections'] = 's: <a href="/sets/view/104">Easy Life</a> and <a href="/sets/view/105">Easy Kill</a>'; |
| 972 | $count[3]['count'] = 0; |
| 973 | $count[4]['author'] = 'Ryan Smith'; |
| 974 | $count[4]['collections'] = 's: <a href="/sets/view/67">Korean Problem Academy 1-4</a>'; |
| 975 | $count[4]['count'] = 0; |
| 976 | $count[5]['author'] = 'Fupfv'; |
| 977 | $count[5]['collections'] = ': <a href="/sets/view/139">Gokyo Shumyo 4</a>'; |
| 978 | $count[5]['count'] = 0; |
| 979 | $count[6]['author'] = 'саша черных'; |
| 980 | $count[6]['collections'] = ': <a href="/sets/view/137">Tsumego Master</a>'; |
| 981 | $count[6]['count'] = 0; |
| 982 | $count[7]['author'] = 'Timo Kreuzer'; |
| 983 | $count[7]['collections'] = ': <a href="/sets/view/137">Tsumego Master</a>'; |
| 984 | $count[7]['count'] = 0; |
| 985 | $count[8]['author'] = 'David Mitchell'; |
| 986 | $count[8]['collections'] = ': <a href="/sets/view/143">Diabolical</a>'; |
| 987 | $count[8]['count'] = 10; |
| 988 | $count[9]['author'] = 'Omicron'; |
| 989 | $count[9]['collections'] = ': <a href="/sets/view/145">Tesujis in Real Board Positions</a>'; |
| 990 | $count[9]['count'] = 0; |
| 991 | $count[10]['author'] = 'Sadaharu'; |
| 992 | $count[10]['collections'] = ': <a href="/sets/view/146">Tsumego of Fortitude</a>, <a href="/sets/view/166">Secret Tsumego from Hong Dojo</a>, <a href="/sets/view/158">Beautiful Tsumego</a> and more.'; |
| 993 | $count[10]['count'] = 0; |
| 994 | $count[11]['author'] = 'Jérôme Hubert'; |
| 995 | $count[11]['collections'] = ': <a href="/sets/view/150">Kanzufu</a> and more.'; |
| 996 | $count[11]['count'] = 0; |
| 997 | $count[12]['author'] = 'Kaan Malçok'; |
| 998 | $count[12]['collections'] = ': <a href="/sets/view/163">Xuanxuan Qijing</a>'; |
| 999 | $count[12]['count'] = 0; |
| 1000 | |
| 1001 | $this->set('count', $count); |
| 1002 | $this->set('t', $authors); |
| 1003 | } |
| 1004 | |
| 1005 | /** |
| 1006 | * @param string|int|null $id Success ID |
| 1007 | * @return void |
| 1008 | */ |
| 1009 | public function success($id = null) |
| 1010 | { |
| 1011 | $this->set('_page', 'home'); |
| 1012 | $this->set('_title', 'Tsumego Hero - Success'); |
| 1013 | |
| 1014 | $s = $this->User->findById(Auth::getUserID()); |
| 1015 | Auth::getUser()['reward'] = date('Y-m-d H:i:s'); |
| 1016 | Auth::getUser()['premium'] = 1; |
| 1017 | Auth::saveUser(); |
| 1018 | |
| 1019 | $Email = new CakeEmail(); |
| 1020 | $Email->from(['me@joschkazimdars.com' => 'https://tsumego.com']); |
| 1021 | $Email->to('joschka.zimdars@googlemail.com'); |
| 1022 | $Email->subject('Upgrade'); |
| 1023 | if (Auth::isLoggedIn()) |
| 1024 | $ans = Auth::getUser()['name'] . ' ' . Auth::getUser()['email']; |
| 1025 | else |
| 1026 | $ans = 'no login'; |
| 1027 | $Email->send($ans); |
| 1028 | if (Auth::isLoggedIn()) |
| 1029 | { |
| 1030 | $Email = new CakeEmail(); |
| 1031 | $Email->from(['me@joschkazimdars.com' => 'https://tsumego.com']); |
| 1032 | $Email->to(Auth::getUser()['email']); |
| 1033 | $Email->subject('Tsumego Hero'); |
| 1034 | $ans = ' |
| 1035 | Hello ' . Auth::getUser()['name'] . ', |
| 1036 | |
| 1037 | Thank you!. Your account should be upgraded automatically. |
| 1038 | |
| 1039 | -- |
| 1040 | Best Regards |
| 1041 | Joschka Zimdars'; |
| 1042 | $Email->send($ans); |
| 1043 | } |
| 1044 | $this->set('id', $id); |
| 1045 | } |
| 1046 | |
| 1047 | /** |
| 1048 | * @param string|int|null $id Penalty ID |
| 1049 | * @return void |
| 1050 | */ |
| 1051 | public function penalty($id = null) |
| 1052 | { |
| 1053 | $this->set('_page', 'home'); |
| 1054 | $this->set('_title', 'Tsumego Hero - Penalty'); |
| 1055 | Auth::getUser()['penalty'] = Auth::getUser()['penalty'] + 1; |
| 1056 | Auth::saveUser(); |
| 1057 | $this->set('id', $id); |
| 1058 | } |
| 1059 | |
| 1060 | /** |
| 1061 | * @param string|int|null $id Set ID |
| 1062 | * @return void |
| 1063 | */ |
| 1064 | public function sets($id = null) |
| 1065 | { |
| 1066 | $this->set('id', $id); |
| 1067 | } |
| 1068 | |
| 1069 | /** |
| 1070 | * @return void |
| 1071 | */ |
| 1072 | public function logout() |
| 1073 | { |
| 1074 | Auth::logout(); |
| 1075 | } |
| 1076 | |
| 1077 | private function validateLogin($data, $user): bool |
| 1078 | { |
| 1079 | if (!$user) |
| 1080 | return false; |
| 1081 | return password_verify($data['password'], $user['User']['password_hash']); |
| 1082 | } |
| 1083 | |
| 1084 | /** |
| 1085 | * @return CakeResponse|null |
| 1086 | */ |
| 1087 | public function googlesignin() |
| 1088 | { |
| 1089 | $name = ''; |
| 1090 | $email = ''; |
| 1091 | $picture = ''; |
| 1092 | $id_token = $_POST['credential']; |
| 1093 | $client_id = '986748597524-05gdpjqrfop96k6haga9gvj1f61sji6v.apps.googleusercontent.com'; |
| 1094 | $token_info = file_get_contents('https://oauth2.googleapis.com/tokeninfo?id_token=' . $id_token); |
| 1095 | $token_data = json_decode($token_info, true); |
| 1096 | if (isset($token_data['aud']) && $token_data['aud'] == $client_id) |
| 1097 | { |
| 1098 | $name = $token_data['name']; |
| 1099 | $email = $token_data['email']; |
| 1100 | $picture = $token_data['picture']; |
| 1101 | } |
| 1102 | else |
| 1103 | echo 'Invalid token'; |
| 1104 | $externalId = 'g__' . $token_data['sub']; |
| 1105 | $u = $this->User->find('first', ['conditions' => ['external_id' => $externalId]]); |
| 1106 | if ($u == null) |
| 1107 | { |
| 1108 | $imageUrl = $picture; |
| 1109 | $imageContent = file_get_contents($imageUrl); |
| 1110 | |
| 1111 | $userData = []; |
| 1112 | $userData['User']['name'] = 'g__' . $name; |
| 1113 | $userData['User']['email'] = 'g__' . $email; |
| 1114 | $userData['User']['password_hash'] = 'not used'; |
| 1115 | $userData['User']['external_id'] = $externalId; |
| 1116 | |
| 1117 | if ($imageContent === false) |
| 1118 | $userData['User']['picture'] = 'default.png'; |
| 1119 | else |
| 1120 | { |
| 1121 | $userData['User']['picture'] = $externalId . '.png'; |
| 1122 | file_put_contents('img/google/' . $externalId . '.png', $imageContent); |
| 1123 | } |
| 1124 | $this->User->create(); |
| 1125 | $this->User->save($userData, true); |
| 1126 | $u = $this->User->find('first', ['conditions' => ['external_id' => $externalId]]); |
| 1127 | } |
| 1128 | $this->signIn($u); |
| 1129 | |
| 1130 | // Get redirect URL from state parameter (set via data-state in the button) |
| 1131 | // Google Sign-In provides built-in CSRF protection via g_csrf_token cookie |
| 1132 | // We use HMAC signature to prevent redirect URL tampering (stateless) |
| 1133 | $redirect = '/sets/'; |
| 1134 | $stateJson = $_POST['state'] ?? null; |
| 1135 | if ($stateJson) |
| 1136 | { |
| 1137 | $stateData = json_decode(base64_decode($stateJson), true); |
| 1138 | if ($stateData) |
| 1139 | $redirect = $this->getVerifiedRedirectUrl( |
| 1140 | $stateData['redirect'] ?? null, |
| 1141 | $stateData['signature'] ?? null |
| 1142 | ); |
| 1143 | } |
| 1144 | return $this->redirect($redirect); |
| 1145 | } |
| 1146 | |
| 1147 | /** |
| 1148 | * @return void |
| 1149 | */ |
| 1150 | public function delete_account() |
| 1151 | { |
| 1152 | $redirect = false; |
| 1153 | $status = ''; |
| 1154 | |
| 1155 | if (!empty($this->data)) |
| 1156 | if (isset($this->data['User']['delete'])) |
| 1157 | if (password_verify($this->data['User']['delete'], Auth::getUser()['password_hash'])) |
| 1158 | { |
| 1159 | Auth::getUser()['dbstorage'] = 1111; |
| 1160 | Auth::saveUser(); |
| 1161 | $redirect = true; |
| 1162 | } |
| 1163 | else |
| 1164 | $status = '<p style="color:#d63a49">Password incorrect.</p>'; |
| 1165 | Auth::getUser()['name'] = $this->checkPicture(Auth::getUser()); |
| 1166 | |
| 1167 | $this->set('redirect', $redirect); |
| 1168 | $this->set('status', $status); |
| 1169 | $this->set('u', Auth::getUser()); |
| 1170 | } |
| 1171 | |
| 1172 | /** |
| 1173 | * @return void |
| 1174 | */ |
| 1175 | public function demote_admin() |
| 1176 | { |
| 1177 | $redirect = false; |
| 1178 | $status = ''; |
| 1179 | if (!Auth::isLoggedIn()) |
| 1180 | return; |
| 1181 | |
| 1182 | if (!empty($this->data)) |
| 1183 | if (isset($this->data['User']['demote'])) |
| 1184 | if (password_verify($this->data['User']['demote'], Auth::getUser()['password_hash'])) |
| 1185 | { |
| 1186 | Auth::getUser()['isAdmin'] = 0; |
| 1187 | Auth::saveUser(); |
| 1188 | $redirect = true; |
| 1189 | } |
| 1190 | else |
| 1191 | $status = '<p style="color:#d63a49">Password incorrect.</p>'; |
| 1192 | Auth::getUser()['name'] = $this->checkPicture(Auth::getUser()); |
| 1193 | |
| 1194 | $this->set('redirect', $redirect); |
| 1195 | $this->set('status', $status); |
| 1196 | $this->set('u', Auth::getUser()); |
| 1197 | } |
| 1198 | |
| 1199 | public function solveHistory($userID) |
| 1200 | { |
| 1201 | $PAGE_SIZE = 500; |
| 1202 | $pageIndex = isset($this->params->query['page']) ? max(1, (int) $this->params->query['page']) : 1; |
| 1203 | $count = Util::query("SELECT COUNT(*) FROM tsumego_attempt where user_id = ?", [$userID])[0]['COUNT(*)']; |
| 1204 | $offset = ($pageIndex - 1) * $PAGE_SIZE; |
| 1205 | |
| 1206 | $attempts = Util::query(" |
| 1207 | SELECT |
| 1208 | set.title AS set_title, |
| 1209 | tsumego_attempt.tsumego_id AS tsumego_id, |
| 1210 | set_connection.id AS set_connection_id, |
| 1211 | set_connection.num AS num, |
| 1212 | tsumego_status.status AS status, |
| 1213 | tsumego_attempt.created AS created, |
| 1214 | tsumego_attempt.gain AS xp_gain, |
| 1215 | tsumego_attempt.solved AS solved, |
| 1216 | tsumego_attempt.misplays AS misplays, |
| 1217 | tsumego_attempt.user_rating AS user_rating |
| 1218 | FROM |
| 1219 | tsumego_attempt |
| 1220 | JOIN set_connection ON set_connection.tsumego_id = tsumego_attempt.tsumego_id |
| 1221 | LEFT JOIN tsumego_status ON tsumego_status.user_id = ? AND tsumego_status.tsumego_id = tsumego_attempt.tsumego_id |
| 1222 | JOIN `set` ON set_connection.set_id = `set`.id |
| 1223 | WHERE |
| 1224 | tsumego_attempt.user_id=? |
| 1225 | ORDER BY created DESC |
| 1226 | LIMIT " . $PAGE_SIZE . " |
| 1227 | OFFSET " . $offset, [$userID, $userID]); |
| 1228 | |
| 1229 | $this->set('_page', 'solveHistory'); |
| 1230 | $this->set('_title', 'Solve history'); |
| 1231 | $this->set('count', $count); |
| 1232 | $this->set('pageIndex', $pageIndex); |
| 1233 | $this->set('PAGE_SIZE', $PAGE_SIZE); |
| 1234 | $this->set('attempts', $attempts); |
| 1235 | } |
| 1236 | |
| 1237 | public function acceptSGFProposal($sgfID) |
| 1238 | { |
| 1239 | if (!Auth::isAdmin()) |
| 1240 | return $this->redirect('/sets'); |
| 1241 | |
| 1242 | $proposalToApprove = ClassRegistry::init('Sgf')->findById($sgfID); |
| 1243 | if (!$proposalToApprove) |
| 1244 | { |
| 1245 | CookieFlash::set('Sgf proposal doesn\'t exist.', 'fail'); |
| 1246 | return $this->redirect('/users/adminstats'); |
| 1247 | } |
| 1248 | |
| 1249 | $proposalToApprove = $proposalToApprove['Sgf']; |
| 1250 | if ($proposalToApprove['accepted'] != 0) |
| 1251 | { |
| 1252 | CookieFlash::set('Sgf proposal was already accepted.', 'fail'); |
| 1253 | return $this->redirect('/users/adminstats'); |
| 1254 | } |
| 1255 | $proposalToApprove['accepted'] = 1; |
| 1256 | ClassRegistry::init('Sgf')->save($proposalToApprove); |
| 1257 | |
| 1258 | AppController::handleContribution(Auth::getUserID(), 'reviewed'); |
| 1259 | AppController::handleContribution($proposalToApprove['user_id'], 'made_proposal'); |
| 1260 | CookieFlash::set('Sgf proposal accepted', 'success'); |
| 1261 | return $this->redirect('/users/adminstats'); |
| 1262 | } |
| 1263 | |
| 1264 | public function rejectSGFProposal($sgfID) |
| 1265 | { |
| 1266 | if (!Auth::isAdmin()) |
| 1267 | return $this->redirect('/sets'); |
| 1268 | |
| 1269 | $proposalToReject = ClassRegistry::init('Sgf')->findById($sgfID); |
| 1270 | if (!$proposalToReject) |
| 1271 | { |
| 1272 | CookieFlash::set('Sgf proposal doesn\'t exist.', 'fail'); |
| 1273 | return $this->redirect('/users/adminstats'); |
| 1274 | } |
| 1275 | |
| 1276 | $proposalToReject = $proposalToReject['Sgf']; |
| 1277 | |
| 1278 | if ($proposalToReject['accepted'] != 0) |
| 1279 | { |
| 1280 | CookieFlash::set('Sgf proposal was already accepted.', 'fail'); |
| 1281 | return $this->redirect('/users/adminstats'); |
| 1282 | ; |
| 1283 | } |
| 1284 | |
| 1285 | $reject = []; |
| 1286 | $reject['user_id'] = $proposalToReject['user_id']; |
| 1287 | $reject['tsumego_id'] = $proposalToReject['tsumego_id']; |
| 1288 | $reject['type'] = 'proposal'; |
| 1289 | ClassRegistry::init('Reject')->create(); |
| 1290 | ClassRegistry::init('Reject')->save($reject); |
| 1291 | ClassRegistry::init('Sgf')->delete($proposalToReject['id']); |
| 1292 | |
| 1293 | CookieFlash::set('Sgf proposal rejected', 'success'); |
| 1294 | return $this->redirect('/users/adminstats'); |
| 1295 | } |
| 1296 | |
| 1297 | public function acceptTagConnectionProposal($tagConnectionID) |
| 1298 | { |
| 1299 | if (!Auth::isAdmin()) |
| 1300 | return $this->redirect('/sets'); |
| 1301 | |
| 1302 | $proposalToApprove = ClassRegistry::init('TagConnection')->findById($tagConnectionID); |
| 1303 | if (!$proposalToApprove) |
| 1304 | { |
| 1305 | CookieFlash::set('Tag proposal doesn\'t exist.', 'fail'); |
| 1306 | return $this->redirect('/users/adminstats'); |
| 1307 | } |
| 1308 | |
| 1309 | $proposalToApprove = $proposalToApprove['TagConnection']; |
| 1310 | if ($proposalToApprove['approved'] != 0) |
| 1311 | { |
| 1312 | CookieFlash::set('Tag proposal was already accepted.', 'fail'); |
| 1313 | return $this->redirect('/users/adminstats'); |
| 1314 | } |
| 1315 | $proposalToApprove['approved'] = 1; |
| 1316 | |
| 1317 | $tag = ClassRegistry::init('Tag')->findById($proposalToApprove['tag_id'])['Tag']; |
| 1318 | AdminActivityLogger::log(AdminActivityType::ACCEPT_TAG, $proposalToApprove['tsumego_id'], null, null, $tag['name']); |
| 1319 | ClassRegistry::init('TagConnection')->save($proposalToApprove); |
| 1320 | AppController::handleContribution(Auth::getUserID(), 'reviewed'); |
| 1321 | AppController::handleContribution($proposalToApprove['user_id'], 'added_tag'); |
| 1322 | return $this->redirect('/users/adminstats'); |
| 1323 | } |
| 1324 | |
| 1325 | public function rejectTagConnectionProposal($tagConnectionID) |
| 1326 | { |
| 1327 | if (!Auth::isAdmin()) |
| 1328 | return $this->redirect('/sets'); |
| 1329 | |
| 1330 | $proposalToReject = ClassRegistry::init('TagConnection')->findById($tagConnectionID); |
| 1331 | if (!$proposalToReject) |
| 1332 | { |
| 1333 | CookieFlash::set('Tag proposal doesn\'t exist.', 'fail'); |
| 1334 | return $this->redirect('/users/adminstats'); |
| 1335 | } |
| 1336 | |
| 1337 | $proposalToReject = $proposalToReject['TagConnection']; |
| 1338 | |
| 1339 | if ($proposalToReject['approved'] != 0) |
| 1340 | { |
| 1341 | CookieFlash::set('Tag proposal was already accepted.', 'fail'); |
| 1342 | return $this->redirect('/users/adminstats'); |
| 1343 | } |
| 1344 | |
| 1345 | $reject = []; |
| 1346 | $reject['user_id'] = $proposalToReject['user_id']; |
| 1347 | $reject['tsumego_id'] = $proposalToReject['tsumego_id']; |
| 1348 | $reject['type'] = 'tag'; |
| 1349 | $tagName = ClassRegistry::init('Tag')->findById($proposalToReject['tag_id'])['Tag']; |
| 1350 | $reject['type'] = $tagName['name']; |
| 1351 | |
| 1352 | $tag = ClassRegistry::init('Tag')->findById($proposalToReject['tag_id'])['Tag']; |
| 1353 | AdminActivityLogger::log(AdminActivityType::REJECT_TAG, $proposalToReject['tsumego_id'], null, $tag['name'], null); |
| 1354 | |
| 1355 | ClassRegistry::init('Reject')->create(); |
| 1356 | ClassRegistry::init('Reject')->save($reject); |
| 1357 | ClassRegistry::init('TagConnection')->delete($proposalToReject['id']); |
| 1358 | |
| 1359 | CookieFlash::set('Tag proposal rejected', 'success'); |
| 1360 | return $this->redirect('/users/adminstats'); |
| 1361 | } |
| 1362 | |
| 1363 | public function deleteOldTsumegoStatuses($id): mixed |
| 1364 | { |
| 1365 | if (!Auth::isLoggedIn()) |
| 1366 | return $this->redirect('/sets'); |
| 1367 | |
| 1368 | // extra check of id, to make sure random link from someone doesn't just delete the progress |
| 1369 | if ($id != Auth::getUserID()) |
| 1370 | return $this->redirect('/sets'); |
| 1371 | $tsumegoCount = TsumegoFilters::empty()->calculateCount(); |
| 1372 | if (Util::getPercent(Auth::getUser()['solved'], $tsumegoCount) < Constants::$MINIMUM_PERCENT_OF_TSUMEGOS_TO_BE_SOLVED_BEFORE_RESET_IS_ALLOWED) |
| 1373 | return $this->redirect('/users/view/' . Auth::getUserID()); |
| 1374 | |
| 1375 | $deleted = Util::query("SELECT COUNT(*) AS total FROM tsumego_status WHERE user_id = ? AND tsumego_status.updated <= NOW() - INTERVAL 1 YEAR", [Auth::getUserID()])[0]['total']; |
| 1376 | Util::query("DELETE FROM tsumego_status WHERE user_id = ? AND tsumego_status.updated <= NOW() - INTERVAL 1 YEAR", [Auth::getUserID()]); |
| 1377 | Util::query("UPDATE user |
| 1378 | LEFT JOIN ( |
| 1379 | SELECT user_id, COUNT(*) AS cnt |
| 1380 | FROM tsumego_status |
| 1381 | WHERE status IN ('S', 'W', 'C', 'X') |
| 1382 | GROUP BY user_id |
| 1383 | ) ts ON ts.user_id = user.id |
| 1384 | SET user.solved = COALESCE(ts.cnt, 0)"); |
| 1385 | CookieFlash::set('Deleted progress on ' . $deleted . ' problems', 'success'); |
| 1386 | return $this->redirect('/users/view/' . Auth::getUserID()); |
| 1387 | } |
| 1388 | } |