enwikipedia-acc /
waca
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
| 1 | <?php |
||
| 2 | /****************************************************************************** |
||
| 3 | * Wikipedia Account Creation Assistance tool * |
||
| 4 | * * |
||
| 5 | * All code in this file is released into the public domain by the ACC * |
||
| 6 | * Development Team. Please see team.json for a list of contributors. * |
||
| 7 | ******************************************************************************/ |
||
| 8 | |||
| 9 | namespace Waca\Pages\UserAuth\Login; |
||
| 10 | |||
| 11 | use PDO; |
||
| 12 | use Waca\DataObjects\User; |
||
| 13 | use Waca\Exceptions\ApplicationLogicException; |
||
| 14 | use Waca\Exceptions\OAuthException; |
||
| 15 | use Waca\Helpers\OAuthUserHelper; |
||
| 16 | use Waca\PdoDatabase; |
||
| 17 | use Waca\Security\AuthenticationManager; |
||
| 18 | use Waca\SessionAlert; |
||
| 19 | use Waca\Tasks\InternalPageBase; |
||
| 20 | use Waca\WebRequest; |
||
| 21 | |||
| 22 | abstract class LoginCredentialPageBase extends InternalPageBase |
||
| 23 | { |
||
| 24 | /** @var User */ |
||
| 25 | protected $partialUser = null; |
||
| 26 | protected $nextPageMap = array( |
||
| 27 | 'yubikeyotp' => 'otp', |
||
| 28 | 'totp' => 'otp', |
||
| 29 | 'scratch' => 'otp', |
||
| 30 | 'u2f' => 'u2f', |
||
| 31 | ); |
||
| 32 | protected $names = array( |
||
| 33 | 'yubikeyotp' => 'Yubikey OTP', |
||
| 34 | 'totp' => 'TOTP (phone code generator)', |
||
| 35 | 'scratch' => 'scratch token', |
||
| 36 | 'u2f' => 'U2F security token', |
||
| 37 | ); |
||
| 38 | |||
| 39 | /** |
||
| 40 | * Main function for this page, when no specific actions are called. |
||
| 41 | * @return void |
||
| 42 | */ |
||
| 43 | protected function main() |
||
| 44 | { |
||
| 45 | if (!$this->enforceHttps()) { |
||
| 46 | return; |
||
| 47 | } |
||
| 48 | |||
| 49 | if (WebRequest::wasPosted()) { |
||
| 50 | $this->validateCSRFToken(); |
||
| 51 | |||
| 52 | $database = $this->getDatabase(); |
||
| 53 | try { |
||
| 54 | list($partialId, $partialStage) = WebRequest::getAuthPartialLogin(); |
||
| 55 | |||
| 56 | if ($partialStage === null) { |
||
| 57 | $partialStage = 1; |
||
| 58 | } |
||
| 59 | |||
| 60 | if ($partialId === null) { |
||
| 61 | $username = WebRequest::postString('username'); |
||
| 62 | |||
| 63 | if ($username === null || trim($username) === '') { |
||
| 64 | throw new ApplicationLogicException('No username specified.'); |
||
| 65 | } |
||
| 66 | |||
| 67 | $user = User::getByUsername($username, $database); |
||
| 68 | } |
||
| 69 | else { |
||
| 70 | $user = User::getById($partialId, $database); |
||
| 71 | } |
||
| 72 | |||
| 73 | if ($user === false) { |
||
| 74 | throw new ApplicationLogicException("Authentication failed"); |
||
| 75 | } |
||
| 76 | |||
| 77 | $authMan = new AuthenticationManager($database, $this->getSiteConfiguration(), |
||
| 78 | $this->getHttpHelper()); |
||
| 79 | |||
| 80 | $credential = $this->getProviderCredentials(); |
||
| 81 | |||
| 82 | $authResult = $authMan->authenticate($user, $credential, $partialStage); |
||
| 83 | |||
| 84 | if ($authResult === AuthenticationManager::AUTH_FAIL) { |
||
| 85 | throw new ApplicationLogicException("Authentication failed"); |
||
| 86 | } |
||
| 87 | |||
| 88 | if ($authResult === AuthenticationManager::AUTH_REQUIRE_NEXT_STAGE) { |
||
| 89 | $this->processJumpNextStage($user, $partialStage, $database); |
||
| 90 | |||
| 91 | return; |
||
| 92 | } |
||
| 93 | |||
| 94 | if ($authResult === AuthenticationManager::AUTH_OK) { |
||
| 95 | $this->processLoginSuccess($user); |
||
| 96 | |||
| 97 | return; |
||
| 98 | } |
||
| 99 | } |
||
| 100 | catch (ApplicationLogicException $ex) { |
||
| 101 | WebRequest::clearAuthPartialLogin(); |
||
| 102 | |||
| 103 | SessionAlert::error($ex->getMessage()); |
||
| 104 | $this->redirect('login'); |
||
| 105 | |||
| 106 | return; |
||
| 107 | } |
||
| 108 | } |
||
| 109 | else { |
||
| 110 | $this->assign('showSignIn', true); |
||
| 111 | |||
| 112 | $this->setupPartial(); |
||
| 113 | $this->assignCSRFToken(); |
||
| 114 | $this->providerSpecificSetup(); |
||
| 115 | } |
||
| 116 | } |
||
| 117 | |||
| 118 | protected function isProtectedPage() |
||
| 119 | { |
||
| 120 | return false; |
||
| 121 | } |
||
| 122 | |||
| 123 | /** |
||
| 124 | * Enforces HTTPS on the login form |
||
| 125 | * |
||
| 126 | * @return bool |
||
| 127 | */ |
||
| 128 | private function enforceHttps() |
||
| 129 | { |
||
| 130 | if ($this->getSiteConfiguration()->getUseStrictTransportSecurity() !== false) { |
||
| 131 | if (WebRequest::isHttps()) { |
||
| 132 | // Client can clearly use HTTPS, so let's enforce it for all connections. |
||
| 133 | $this->headerQueue[] = "Strict-Transport-Security: max-age=15768000"; |
||
| 134 | } |
||
| 135 | else { |
||
| 136 | // This is the login form, not the request form. We need protection here. |
||
| 137 | $this->redirectUrl('https://' . WebRequest::serverName() . WebRequest::requestUri()); |
||
| 138 | |||
| 139 | return false; |
||
| 140 | } |
||
| 141 | } |
||
| 142 | |||
| 143 | return true; |
||
| 144 | } |
||
| 145 | |||
| 146 | protected abstract function providerSpecificSetup(); |
||
|
0 ignored issues
–
show
|
|||
| 147 | |||
| 148 | protected function setupPartial() |
||
| 149 | { |
||
| 150 | $database = $this->getDatabase(); |
||
| 151 | |||
| 152 | // default stuff |
||
| 153 | $this->assign('alternatives', array()); // 'u2f' => array('U2F token'), 'otp' => array('TOTP', 'scratch', 'yubiotp'))); |
||
| 154 | |||
| 155 | // is this stage one? |
||
| 156 | list($partialId, $partialStage) = WebRequest::getAuthPartialLogin(); |
||
| 157 | if ($partialStage === null || $partialId === null) { |
||
| 158 | WebRequest::clearAuthPartialLogin(); |
||
| 159 | } |
||
| 160 | |||
| 161 | // Check to see if we have a partial login in progress |
||
| 162 | $username = null; |
||
| 163 | if ($partialId !== null) { |
||
| 164 | // Yes, enforce this username |
||
| 165 | $this->partialUser = User::getById($partialId, $database); |
||
|
0 ignored issues
–
show
It seems like
\Waca\DataObjects\User::...($partialId, $database) can also be of type false. However, the property $partialUser is declared as type object<Waca\DataObjects\User>. Maybe add an additional type check?
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly. For example, imagine you have a variable Either this assignment is in error or a type check should be added for that assignment. class Id
{
public $id;
public function __construct($id)
{
$this->id = $id;
}
}
class Account
{
/** @var Id $id */
public $id;
}
$account_id = false;
if (starsAreRight()) {
$account_id = new Id(42);
}
$account = new Account();
if ($account instanceof Id)
{
$account->id = $account_id;
}
Loading history...
|
|||
| 166 | $username = $this->partialUser->getUsername(); |
||
| 167 | |||
| 168 | $this->setupAlternates($this->partialUser, $partialStage, $database); |
||
|
0 ignored issues
–
show
|
|||
| 169 | } |
||
| 170 | else { |
||
| 171 | // No, see if we've preloaded a username |
||
| 172 | $preloadUsername = WebRequest::getString('tplUsername'); |
||
| 173 | if ($preloadUsername !== null) { |
||
| 174 | $username = $preloadUsername; |
||
| 175 | } |
||
| 176 | } |
||
| 177 | |||
| 178 | if ($partialStage === null) { |
||
| 179 | $partialStage = 1; |
||
| 180 | } |
||
| 181 | |||
| 182 | $this->assign('partialStage', $partialStage); |
||
| 183 | $this->assign('username', $username); |
||
| 184 | } |
||
| 185 | |||
| 186 | /** |
||
| 187 | * Redirect the user back to wherever they came from after a successful login |
||
| 188 | * |
||
| 189 | * @param User $user |
||
| 190 | */ |
||
| 191 | protected function goBackWhenceYouCame(User $user) |
||
| 192 | { |
||
| 193 | // Redirect to wherever the user came from |
||
| 194 | $redirectDestination = WebRequest::clearPostLoginRedirect(); |
||
| 195 | if ($redirectDestination !== null) { |
||
| 196 | $this->redirectUrl($redirectDestination); |
||
| 197 | } |
||
| 198 | else { |
||
| 199 | if ($user->isNewUser()) { |
||
| 200 | // home page isn't allowed, go to preferences instead |
||
| 201 | $this->redirect('preferences'); |
||
| 202 | } |
||
| 203 | else { |
||
| 204 | // go to the home page |
||
| 205 | $this->redirect(''); |
||
| 206 | } |
||
| 207 | } |
||
| 208 | } |
||
| 209 | |||
| 210 | private function processLoginSuccess(User $user) |
||
| 211 | { |
||
| 212 | // Touch force logout |
||
| 213 | $user->setForceLogout(false); |
||
| 214 | $user->save(); |
||
| 215 | |||
| 216 | $oauth = new OAuthUserHelper($user, $this->getDatabase(), $this->getOAuthProtocolHelper(), |
||
| 217 | $this->getSiteConfiguration()); |
||
| 218 | |||
| 219 | if ($oauth->isFullyLinked()) { |
||
| 220 | try { |
||
| 221 | // Reload the user's identity ticket. |
||
| 222 | $oauth->refreshIdentity(); |
||
| 223 | |||
| 224 | // Check for blocks |
||
| 225 | if ($oauth->getIdentity()->getBlocked()) { |
||
| 226 | // blocked! |
||
| 227 | SessionAlert::error("You are currently blocked on-wiki. You will not be able to log in until you are unblocked."); |
||
| 228 | $this->redirect('login'); |
||
| 229 | |||
| 230 | return; |
||
| 231 | } |
||
| 232 | } |
||
| 233 | catch (OAuthException $ex) { |
||
| 234 | // Oops. Refreshing ticket failed. Force a re-auth. |
||
| 235 | $authoriseUrl = $oauth->getRequestToken(); |
||
| 236 | WebRequest::setOAuthPartialLogin($user); |
||
| 237 | $this->redirectUrl($authoriseUrl); |
||
| 238 | |||
| 239 | return; |
||
| 240 | } |
||
| 241 | } |
||
| 242 | |||
| 243 | if (($this->getSiteConfiguration()->getEnforceOAuth() && !$oauth->isFullyLinked()) |
||
| 244 | || $oauth->isPartiallyLinked() |
||
| 245 | ) { |
||
| 246 | $authoriseUrl = $oauth->getRequestToken(); |
||
| 247 | WebRequest::setOAuthPartialLogin($user); |
||
| 248 | $this->redirectUrl($authoriseUrl); |
||
| 249 | |||
| 250 | return; |
||
| 251 | } |
||
| 252 | |||
| 253 | WebRequest::setLoggedInUser($user); |
||
| 254 | |||
| 255 | $this->goBackWhenceYouCame($user); |
||
| 256 | } |
||
| 257 | |||
| 258 | protected abstract function getProviderCredentials(); |
||
|
0 ignored issues
–
show
For interfaces and abstract methods it is generally a good practice to add a
@return annotation even if it is just @return void or @return null, so that implementors know what to do in the overridden method.
For interface and abstract methods, it is impossible to infer the return type
from the immediate code. In these cases, it is generally advisible to explicitly
annotate these methods with a Loading history...
|
|||
| 259 | |||
| 260 | /** |
||
| 261 | * @param User $user |
||
| 262 | * @param int $partialStage |
||
| 263 | * @param PdoDatabase $database |
||
| 264 | * |
||
| 265 | * @throws ApplicationLogicException |
||
| 266 | */ |
||
| 267 | private function processJumpNextStage(User $user, $partialStage, PdoDatabase $database) |
||
| 268 | { |
||
| 269 | WebRequest::setAuthPartialLogin($user->getId(), $partialStage + 1); |
||
| 270 | |||
| 271 | $sql = 'SELECT type FROM credential WHERE user = :user AND factor = :stage AND disabled = 0 ORDER BY priority'; |
||
| 272 | $statement = $database->prepare($sql); |
||
| 273 | $statement->execute(array(':user' => $user->getId(), ':stage' => $partialStage + 1)); |
||
| 274 | $nextStage = $statement->fetchColumn(); |
||
| 275 | $statement->closeCursor(); |
||
| 276 | |||
| 277 | if (!isset($this->nextPageMap[$nextStage])) { |
||
| 278 | throw new ApplicationLogicException('Unknown page handler for next authentication stage.'); |
||
| 279 | } |
||
| 280 | |||
| 281 | $this->redirect("login/" . $this->nextPageMap[$nextStage]); |
||
| 282 | } |
||
| 283 | |||
| 284 | private function setupAlternates(User $user, $partialStage, PdoDatabase $database) |
||
| 285 | { |
||
| 286 | // get the providers available |
||
| 287 | $sql = 'SELECT type FROM credential WHERE user = :user AND factor = :stage AND disabled = 0'; |
||
| 288 | $statement = $database->prepare($sql); |
||
| 289 | $statement->execute(array(':user' => $user->getId(), ':stage' => $partialStage)); |
||
| 290 | $alternates = $statement->fetchAll(PDO::FETCH_COLUMN); |
||
| 291 | |||
| 292 | $types = array(); |
||
| 293 | foreach ($alternates as $item) { |
||
| 294 | $type = $this->nextPageMap[$item]; |
||
| 295 | if (!isset($types[$type])) { |
||
| 296 | $types[$type] = array(); |
||
| 297 | } |
||
| 298 | |||
| 299 | $types[$type][] = $item; |
||
| 300 | } |
||
| 301 | |||
| 302 | $userOptions = array(); |
||
| 303 | if (get_called_class() === PageOtpLogin::class) { |
||
| 304 | $userOptions = $this->setupUserOptionsForType($types, 'u2f', $userOptions); |
||
| 305 | } |
||
| 306 | |||
| 307 | if (get_called_class() === PageU2FLogin::class) { |
||
| 308 | $userOptions = $this->setupUserOptionsForType($types, 'otp', $userOptions); |
||
| 309 | } |
||
| 310 | |||
| 311 | $this->assign('alternatives', $userOptions); |
||
| 312 | } |
||
| 313 | |||
| 314 | /** |
||
| 315 | * @param $types |
||
| 316 | * @param $type |
||
| 317 | * @param $userOptions |
||
| 318 | * |
||
| 319 | * @return mixed |
||
| 320 | */ |
||
| 321 | private function setupUserOptionsForType($types, $type, $userOptions) |
||
| 322 | { |
||
| 323 | if (isset($types[$type])) { |
||
| 324 | $options = $types[$type]; |
||
| 325 | |||
| 326 | array_walk($options, function(&$val) { |
||
| 327 | $val = $this->names[$val]; |
||
| 328 | }); |
||
| 329 | |||
| 330 | $userOptions[$type] = $options; |
||
| 331 | } |
||
| 332 | |||
| 333 | return $userOptions; |
||
| 334 | } |
||
| 335 | } |
||
| 336 |
For interface and abstract methods, it is impossible to infer the return type from the immediate code. In these cases, it is generally advisible to explicitly annotate these methods with a
@returndoc comment to communicate to implementors of these methods what they are expected to return.