PageRenderTime 23ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/inc/actions/LoginAction.php

http://github.com/jquery/testswarm
PHP | 175 lines | 110 code | 20 blank | 45 comment | 14 complexity | de91190d034922cd3a1c005cb6900513 MD5 | raw file
  1. <?php
  2. /**
  3. * Authenticate with a project and start the session.
  4. *
  5. * @author John Resig
  6. * @author Timo Tijhof
  7. * @since 0.1.0
  8. * @package TestSwarm
  9. */
  10. class LoginAction extends Action {
  11. /**
  12. * @actionMethod POST: Required.
  13. * @actionParam string projectID
  14. * @actionParam string projectPassword
  15. */
  16. public function doAction() {
  17. $context = $this->getContext();
  18. $db = $context->getDB();
  19. $request = $context->getRequest();
  20. $auth = $context->getAuth();
  21. // Already logged-in
  22. if ( $auth ) {
  23. $projectID = $auth->project->id;
  24. // Try logging in
  25. } else {
  26. if ( !$request->wasPosted() ) {
  27. $this->setError( "requires-post" );
  28. return;
  29. }
  30. $projectID = $request->getVal( 'projectID' );
  31. $projectPassword = $request->getVal( 'projectPassword' );
  32. if ( !$projectID || !$projectPassword ) {
  33. $this->setError( "missing-parameters" );
  34. return;
  35. }
  36. $projectRow = $db->getRow(str_queryf(
  37. 'SELECT
  38. id,
  39. display_title,
  40. site_url,
  41. password,
  42. auth_token,
  43. updated,
  44. created
  45. FROM projects
  46. WHERE id = %s;',
  47. $projectID
  48. ));
  49. if ( !$projectRow ) {
  50. $this->setError( "invalid-input" );
  51. return;
  52. }
  53. $passwordHash = $projectRow->password;
  54. unset( $projectRow->password );
  55. if ( self::comparePasswords( $passwordHash, $projectPassword ) ) {
  56. // Start auth session
  57. $request->setSessionData( 'auth', (object) array(
  58. 'project' => $projectRow,
  59. 'sessionToken' => self::generateRandomHash( 40 ),
  60. ) );
  61. } else {
  62. $this->setError( "invalid-input" );
  63. return;
  64. }
  65. }
  66. // We're still here, authentication succeeded!
  67. $this->setData( array(
  68. 'id' => $projectID
  69. ) );
  70. }
  71. /**
  72. * Names may only contain lowercase characters and numbers and must start with a letter.
  73. * (github.com/jquery/testswarm/issues/118)
  74. * @param string $name
  75. * @return bool
  76. */
  77. public static function isValidName( $name ) {
  78. return !!preg_match( '/' . self::getNameValidationRegex() . '/', $name );
  79. }
  80. public static function getNameValidationRegex() {
  81. return '^[a-z][a-z0-9]{0,128}$';
  82. }
  83. protected static function getHashAlgo() {
  84. static $algo = null;
  85. if ( $algo === null ) {
  86. // Use the best acceptable algorithm in this environment
  87. $algos = hash_algos();
  88. foreach ( array( 'whirlpool', 'sha256', 'sha1' ) as $choice ) {
  89. if ( in_array( $choice, $algos ) ) {
  90. $algo = $choice;
  91. return $algo;
  92. }
  93. };
  94. throw new SwarmException( 'No acceptable algorithm available.' );
  95. }
  96. return $algo;
  97. }
  98. public static function generateRandomHash( $length ) {
  99. $hash = '';
  100. while ( strlen( $hash ) < $length ) {
  101. // Various random sources
  102. $rand =
  103. serialize( $_SERVER )
  104. . rand() . uniqid( mt_rand(), true )
  105. . ( function_exists( 'getmypid' ) ? getmypid() : '' )
  106. . ( function_exists( 'memory_get_usage' ) ? memory_get_usage( true ) : '' )
  107. . realpath( __FILE__ )
  108. . serialize( @stat( __FILE__ ) ) // stat() can be a bad boy
  109. ;
  110. $hash .= hash( self::getHashAlgo(), $rand );
  111. }
  112. return substr( $hash, 1, $length );
  113. }
  114. /**
  115. * @param string $password Plaintext password.
  116. * @param string $salt [optional] A salt will be generated, optionally
  117. * pass this to re-use a salt.
  118. * @return string
  119. */
  120. public static function generatePasswordHash( $password, $salt = false ) {
  121. if ( $salt === false ) {
  122. $salt = self::generateRandomHash( 8 );
  123. }
  124. return ':A:' . $salt . ':' . sha1( $salt . '|' . sha1( $password ) );
  125. }
  126. /**
  127. * Generate a ':M:' type blob with user table info from
  128. * an older database.
  129. * @param array $userRow Row from old `users` table
  130. * @return string Raw value for `projects.password` column
  131. */
  132. public static function generatePasswordHashForUserrow( $userRow ) {
  133. return ':M:' . $userRow->seed . ':' . $userRow->password;
  134. }
  135. /**
  136. * @param string $hash Password hash (from database).
  137. * @param string $input Plaintext password for comparison.
  138. * @return bool
  139. */
  140. public static function comparePasswords( $hash, $input ) {
  141. $type = substr( $hash, 0, 3 );
  142. // New algorythm since 1.0.0
  143. if ( $type === ':A:' ) {
  144. list( $salt, $realHash ) = explode( ':', substr( $hash, 3 ), 2 );
  145. return sha1( $salt . '|' . sha1( $input ) ) === $realHash;
  146. // Migrated from old 'users' table (see #generatePasswordHashForUserrow and dbUpdate.php)
  147. } elseif ( $type === ':M:' ) {
  148. list( $salt, $realHash ) = explode( ':', substr( $hash, 3 ), 2 );
  149. // The old users table stores sha1 of seed + plain password
  150. return sha1( $salt . $input ) === $realHash;
  151. } else {
  152. return false;
  153. }
  154. }
  155. }