Browse Source

adding autoLogin component

euromark 13 years ago
parent
commit
8a8bb17338

+ 327 - 0
Controller/Component/AutoLoginComponent.php

@@ -0,0 +1,327 @@
+<?php
+App::uses('Component', 'Controller');
+
+/**
+ * AutoLoginComponent
+ *
+ * A CakePHP Component that will automatically login the Auth session for a duration if the user requested to (saves data to cookies).
+ *
+ * @author		Miles Johnson - http://milesj.me
+ * @copyright	Copyright 2006-2011, Miles Johnson, Inc.
+ * @license		http://opensource.org/licenses/mit-license.php - Licensed under The MIT License
+ * @link		http://milesj.me/code/cakephp/auto-login
+ *
+ * @modified 	Mark Scherer - 2012-01-08 ms
+ * - now works with Controller::beforeFilter() modifications to allow username/email login switch
+ * - can be disabled dynamically and will skip on CakeError view
+ */
+class AutoLoginComponent extends Component {
+
+	/**
+	 * Current version.
+	 *
+	 * @access public
+	 * @var string
+	 */
+	public $version = '3.5';
+
+	/**
+	 * Components.
+	 *
+	 * @access public
+	 * @var array
+	 */
+	public $components = array('Auth', 'Cookie');
+
+	/**
+	 * Settings.
+	 *
+	 * @access public
+	 * @var array
+	 */
+	public $settings = array();
+
+	/**
+	 * Default settings.
+	 *
+	 * @access protected
+	 * @var array
+	 */
+	protected $_defaults = array(
+		'active' => true,
+		'model' => 'User',
+		'username' => 'username',
+		'password' => 'password',
+		'plugin' => '',
+		'controller' => 'users',
+		'loginAction' => 'login',
+		'logoutAction' => 'logout',
+		'cookieName' => 'autoLogin',
+		'expires' => '+2 weeks', # Cookie length (strtotime() format)
+		'redirect' => true,
+		'requirePrompt' => true, # Displayed checkbox determines if cookie is created
+		'debug' => null # Auto-Select based on debug mode or ip range
+	);
+
+	/**
+	 * Determines whether to trigger startup() logic.
+	 *
+	 * @access protected
+	 * @var boolean
+	 */
+	protected $_isValidRequest = false;
+
+	/**
+	 * Initialize settings and debug.
+	 *
+	 * @param ComponentCollection $collection
+	 * @param array $settings
+	 */
+	public function __construct(ComponentCollection $collection, $settings = array()) {
+		$defaultSettings = array_merge($this->_defaults, (array)Configure::read('AutoLogin'));
+		$settings = array_merge($defaultSettings, $settings);
+
+		# make sure an upgrade does reset all cookies stored to avoid conflicts
+		$settings['cookieName'] = $settings['cookieName'].str_replace('.', '', $this->version);
+		$this->settings = $settings;
+		parent::__construct($collection, $settings);
+	}
+
+
+	/**
+	 * Detect debug info.
+	 *
+	 * @access public
+	 * @param Controller $controller
+	 * @return void
+	 */
+	public function initialize(Controller $controller) {
+		if ($controller->name == 'CakeError' || !$this->settings['active']) {
+			return;
+		}
+
+		// Validate the cookie
+		$cookie = $this->_readCookie();
+		$user = $this->Auth->user();
+
+		if (!empty($user) || !$cookie || !$controller->request->is('get')) {
+			return;
+		}
+
+		// Is debug enabled
+		if ($this->settings['debug'] === null) {
+			$this->settings['debug'] = Configure::read('debug') > 0 || !empty($this->settings['ips']) && in_array(env('REMOTE_ADDR'), (array)$this->settings['ips']);
+		}
+
+		if (empty($cookie['hash']) || $cookie['hash'] != $this->Auth->password($cookie['username'] . $cookie['time'])) {
+			$this->debug('hashFail', $cookie, $user);
+			$this->delete();
+			return;
+		}
+
+		// Set the data to identify with
+		$controller->request->data[$this->settings['model']][$this->settings['username']] = $cookie['username'];
+		$controller->request->data[$this->settings['model']][$this->settings['password']] = $cookie['password'];
+
+		// Request is valid, stop startup()
+		$this->_isValidRequest = true;
+	}
+
+	/**
+	 * Automatically login existent Auth session; called after controllers beforeFilter() so that Auth is initialized.
+	 *
+	 * @access public
+	 * @param Controller $controller
+	 * @return void
+	 */
+	public function startup(Controller $controller) {
+		if (!$this->_isValidRequest) {
+			return;
+		}
+
+		if ($this->Auth->login()) {
+			$this->debug('login', $this->Cookie, $this->Auth->user());
+
+			if (in_array('_autoLogin', get_class_methods($controller))) {
+				call_user_func_array(array($controller, '_autoLogin'), array(
+					$this->Auth->user()
+				));
+			}
+			if ($this->settings['redirect']) {
+				$controller->redirect(array(), 301);
+			}
+
+		} else {
+			$this->debug('loginFail', $this->Cookie, $this->Auth->user());
+
+			if (in_array('_autoLoginError', get_class_methods($controller))) {
+				call_user_func_array(array($controller, '_autoLoginError'), array(
+					$this->_readCookie()
+				));
+			}
+		}
+	}
+
+	/**
+	 * Automatically process logic when hitting login/logout actions.
+	 *
+	 * @access public
+	 * @uses Inflector
+	 * @param Controller $controller
+	 * @return void
+	 */
+	public function beforeRedirect(Controller $controller, $url, $status = null, $exit = true) {
+		if (!$this->settings['active']) {
+			return;
+		}
+		$model = $this->settings['model'];
+
+		if (is_array($this->Auth->loginAction)) {
+			if (!empty($this->Auth->loginAction['controller'])) {
+				$this->settings['controller'] = $this->Auth->loginAction['controller'];
+			}
+
+			if (!empty($this->Auth->loginAction['action'])) {
+				$this->settings['loginAction'] = $this->Auth->loginAction['action'];
+			}
+
+			if (!empty($this->Auth->loginAction['plugin'])) {
+				$this->settings['plugin'] = $this->Auth->loginAction['plugin'];
+			}
+		}
+
+		if (empty($this->settings['controller'])) {
+			$this->settings['controller'] = Inflector::pluralize($model);
+		}
+
+		// Is called after user login/logout validates, but before auth redirects
+		if ($controller->plugin == Inflector::camelize($this->settings['plugin']) && $controller->name == Inflector::camelize($this->settings['controller'])) {
+			$data = $controller->request->data;
+			$action = isset($controller->request->params['action']) ? $controller->request->params['action'] : 'login';
+
+			switch ($action) {
+				case $this->settings['loginAction']:
+					if (isset($data[$model])) {
+						$username = $data[$model][$this->settings['username']];
+						$password = $data[$model][$this->settings['password']];
+						$autoLogin = isset($data[$model]['auto_login']) ? $data[$model]['auto_login'] : !$this->settings['requirePrompt'];
+
+						if (!empty($username) && !empty($password) && $autoLogin) {
+							$this->_writeCookie($username, $password);
+
+						} elseif (!$autoLogin) {
+							$this->delete();
+						}
+					}
+					break;
+
+				case $this->settings['logoutAction']:
+					$this->debug('logout', $this->Cookie, $this->Auth->user());
+					$this->delete();
+					break;
+			}
+		}
+	}
+
+	/**
+	 * Delete the cookie.
+	 *
+	 * @access public
+	 * @return void
+	 */
+	public function delete() {
+		$this->Cookie->delete($this->settings['cookieName']);
+	}
+
+	/**
+	 * Debug the current auth and cookies.
+	 *
+	 * @access public
+	 * @param string $key
+	 * @param array $cookie
+	 * @param array $user
+	 * @return void
+	 */
+	public function debug($key, $cookie = array(), $user = array()) {
+		$scopes = array(
+			'login'				=> 'Login Successful',
+			'loginFail'			=> 'Login Failure',
+			'loginCallback'		=> 'Login Callback',
+			'logout'			=> 'Logout',
+			'logoutCallback'	=> 'Logout Callback',
+			'cookieSet'			=> 'Cookie Set',
+			'cookieFail'		=> 'Cookie Mismatch',
+			'hashFail'			=> 'Hash Mismatch',
+			'custom'			=> 'Custom Callback'
+		);
+
+		if ($this->settings['debug'] && isset($scopes[$key])) {
+			$debug = (array)Configure::read('AutoLogin');
+			$content = "";
+
+			if (!empty($cookie) || !empty($user)) {
+				if (!empty($cookie)) {
+					$content .= "Cookie information: \n\n" . print_r($cookie, true) . "\n\n\n";
+				}
+
+				if (!empty($user)) {
+					$content .= "User information: \n\n" . print_r($user, true);
+				}
+			} else {
+				$content = 'No debug information.';
+			}
+
+			if (empty($debug['scope']) || in_array($key, (array)$debug['scope'])) {
+				if (!empty($debug['email'])) {
+					mail($debug['email'], '[AutoLogin] ' . $scopes[$key], $content, 'From: ' . $debug['email']);
+				} else {
+					$this->log($scopes[$key] . ': ' . $content, 'autologin');
+				}
+			}
+		}
+	}
+
+	/**
+	 * Remember the user information and store it in a cookie (encrypted).
+	 *
+	 * @param string $username
+	 * @param string $password
+	 * @return void
+	 */
+	protected function _writeCookie($username, $password) {
+		$time = time();
+
+		$cookie = array();
+		$cookie['username'] = base64_encode($username);
+		$cookie['password'] = base64_encode($password);
+		$cookie['hash'] = $this->Auth->password($username . $time);
+		$cookie['time'] = $time;
+
+		if (env('REMOTE_ADDR') === '127.0.0.1' || env('HTTP_HOST') === 'localhost') {
+			$this->Cookie->domain = false;
+		}
+
+		$this->Cookie->write($this->settings['cookieName'], $cookie, true, $this->settings['expires']);
+		$this->debug('cookieSet', $cookie, $this->Auth->user());
+	}
+
+	/**
+	 * Read cookie and decode it
+	 *
+	 * @return mixed array $cookieData or false on failure
+	 */
+	protected function _readCookie() {
+		$cookie = $this->Cookie->read($this->settings['cookieName']);
+		if (empty($cookie) || !is_array($cookie)) {
+			return false;
+		}
+		if (isset($cookie['username'])) {
+			$cookie['username'] = base64_decode($cookie['username']);
+		}
+		if (isset($cookie['password'])) {
+			$cookie['password'] = base64_decode($cookie['password']);
+		}
+		return $cookie;
+	}
+
+}

+ 1 - 0
Controller/Component/MobileComponent.php

@@ -1,5 +1,6 @@
 <?php
 <?php
 App::uses('Component', 'Controller');
 App::uses('Component', 'Controller');
+App::uses('Router', 'Routing');
 
 
 /**
 /**
  * Uses Session: User.mobile and User.nomobile
  * Uses Session: User.mobile and User.nomobile

+ 161 - 0
Test/Case/Controller/Component/AutoLoginComponentTest.php

@@ -0,0 +1,161 @@
+<?php
+
+App::import('Component', 'Tools.AutoLogin');
+App::uses('Controller', 'Controller');
+
+/**
+ * Short description for class.
+ *
+ * @package       cake.tests
+ * @subpackage    cake.tests.cases.libs.controller.components
+ */
+class AutoLoginComponentTest extends CakeTestCase {
+
+	public $fixtures = array('core.cake_session', 'plugin.tools.user');
+
+	/**
+	 * setUp method
+	 *
+	 * @access public
+	 * @return void
+	 */
+	public function setUp() {
+		Configure::write('AutoLogin.active', 1);
+		Configure::write('AutoLogin.cookieName', 'autoLogin');
+
+		$this->Controller = new AutoLoginTestController(new CakeRequest, new CakeResponse);
+		$this->Controller->AutoLogin = new AutoLoginComponent(new ComponentCollection());
+	}
+
+	/**
+	 * Tear-down method.  Resets environment state.
+	 *
+	 * @access public
+	 * @return void
+	 */
+	public function tearDown() {
+		unset($this->Controller->AutoLogin);
+		unset($this->Controller);
+	}
+
+	/**
+	 * test if suhosin isn't messing up srand() and mt_srand()
+	 * run this on every the environment you want AutoLogin to work!
+	 * It this test fails add `suhosin.srand.ignore = Off`
+	 * in your `/etc/php5/apache2/php.ini`
+	 * And don't forget to restart apache or at least `/etc/init.d/apache2 force-reload`
+	 */
+	public function testIfRandWillWork() {
+		srand('1234567890');
+		$rand1 = rand(0, 255);
+
+		srand('1234567890');
+		$rand2 = rand(0, 255);
+
+		$this->assertSame($rand1, $rand2, 'You have the Suhosin BUG! Add `suhosin.srand.ignore = Off` to your php.ini!');
+	}
+
+	/**
+	 * test merge of configs
+	 */
+	public function testConfigs() {
+		$this->Controller->AutoLogin->initialize($this->Controller);
+		$settings = $this->Controller->AutoLogin->settings;
+		$this->assertTextStartsWith('autoLogin', $settings['cookieName']);
+	}
+
+	/**
+	 * test cookie name
+	 */
+	public function testConfigsWithCustomCookieName() {
+		Configure::write('AutoLogin.cookieName', 'myAutoLogin');
+		$this->Controller->AutoLogin = new AutoLoginComponent(new ComponentCollection());
+		$this->Controller->AutoLogin->initialize($this->Controller);
+		$settings = $this->Controller->AutoLogin->settings;
+		$this->assertTextStartsWith('myAutoLogin', $settings['cookieName']);
+
+		Configure::write('AutoLogin.cookieName', 'myOtherAutoLogin');
+		$this->Controller->AutoLogin = new AutoLoginComponent(new ComponentCollection());
+		$this->Controller->AutoLogin->initialize($this->Controller);
+		$settings = $this->Controller->AutoLogin->settings;
+		$this->assertTextStartsWith('myOtherAutoLogin', $settings['cookieName']);
+	}
+
+	public function testLogin() {
+		$this->Controller->AutoLogin = new AutoLoginComponent(new ComponentCollection());
+		$this->Controller->AutoLogin->initialize($this->Controller);
+		$settings = $this->Controller->AutoLogin->settings;
+		//die(returns($settings));
+		//TODO
+	}
+
+}
+
+
+/**
+ * Short description for class.
+ *
+ * @package       cake.tests
+ * @subpackage    cake.tests.cases.libs.controller.components
+ */
+class AutoLoginTestController extends Controller {
+	/**
+	 * name property
+	 *
+	 * @var string 'SecurityTest'
+	 * @access public
+	 */
+
+	/**
+	 * components property
+	 *
+	 * @var array
+	 * @access public
+	 */
+	public $components = array('Tools.AutoLogin');
+	/**
+	 * failed property
+	 *
+	 * @var bool false
+	 * @access public
+	 */
+	public $failed = false;
+	/**
+	 * Used for keeping track of headers in test
+	 *
+	 * @var array
+	 * @access public
+	 */
+	public $testHeaders = array();
+	/**
+	 * fail method
+	 *
+	 * @access public
+	 * @return void
+	 */
+	public function fail() {
+		$this->failed = true;
+	}
+	/**
+	 * redirect method
+	 *
+	 * @param mixed $option
+	 * @param mixed $code
+	 * @param mixed $exit
+	 * @access public
+	 * @return void
+	 */
+	public function redirect($option, $code, $exit) {
+		return $code;
+	}
+	/**
+	 * Conveinence method for header()
+	 *
+	 * @param string $status
+	 * @return void
+	 * @access public
+	 */
+	public function header($status) {
+		$this->testHeaders[] = $status;
+	}
+}

+ 24 - 0
Test/Case/Controller/Component/CalendarComponentTest.php

@@ -0,0 +1,24 @@
+<?php
+
+App::import('Component', 'Tools.Calendar');
+App::uses('MyCakeTestCase', 'Tools.Lib');
+
+class CalendarComponentTest extends MyCakeTestCase {
+
+	public function setUp() {
+		$this->Calendar = new CalendarComponent(new ComponentCollection());
+	}
+
+	public function tearDown() {
+
+	}
+
+	public function testObject() {
+		$this->assertTrue(is_a($this->Calendar, 'CalendarComponent'));
+	}
+
+	public function testX() {
+		//TODO
+	}
+
+}

+ 241 - 0
Test/Case/Controller/Component/CommonComponentTest.php

@@ -0,0 +1,241 @@
+<?php
+
+App::import('Component', 'Tools.Common');
+App::uses('Component', 'Controller');
+App::uses('AppController', 'Controller');
+
+/**
+ * 2010-11-10 ms
+ */
+class CommonComponentTest extends CakeTestCase {
+/**
+ * setUp method
+ *
+ * @access public
+ * @return void
+ */
+	public function setUp() {
+		$this->Controller = new CommonComponentTestController(new CakeRequest, new CakeResponse);
+		$this->Controller->constructClasses();
+		$this->Controller->startupProcess();
+	}
+/**
+ * Tear-down method.  Resets environment state.
+ *
+ * @access public
+ * @return void
+ */
+	public function tearDown() {
+		unset($this->Controller->Common);
+		unset($this->Controller);
+	}
+
+
+	public function testLoadHelper() {
+		$this->assertTrue(!in_array('Text', $this->Controller->helpers));
+		$this->Controller->Common->loadHelper('Text');
+		$this->assertTrue(in_array('Text', $this->Controller->helpers));
+
+
+	}
+
+	public function testLoadComponent() {
+		$this->assertTrue(!isset($this->Controller->Test));
+		$this->Controller->Common->loadComponent('Test');
+		$this->assertTrue(isset($this->Controller->Test));
+
+		# with plugin
+		$this->Controller->Currency = null;
+		$this->assertTrue(!isset($this->Controller->Currency));
+		$this->Controller->Common->loadComponent('Tools.Currency');
+		$this->assertTrue(isset($this->Controller->Currency));
+
+		# with options
+		$this->Controller->Test = null;
+		$this->assertTrue(!isset($this->Controller->Test));
+		$this->Controller->Common->loadComponent(array('RequestHandler', 'Test'=>array('x'=>'y')));
+		$this->assertTrue(isset($this->Controller->Test));
+		$this->assertTrue($this->Controller->Test->isInit);
+		$this->assertTrue($this->Controller->Test->isStartup);
+	}
+
+	public function testLoadLib() {
+		$this->assertTrue(!isset($this->Controller->RandomLib));
+		$this->Controller->Common->loadLib('Tools.RandomLib');
+		$this->assertTrue(isset($this->Controller->RandomLib));
+
+		$res = $this->Controller->RandomLib->pwd(null, 10);
+		$this->assertTrue(!empty($res));
+
+		# with options
+		$this->assertTrue(!isset($this->Controller->TestLib));
+		$this->Controller->Common->loadLib(array('Tools.RandomLib', 'TestLib'=>array('x'=>'y')));
+		$this->assertTrue(isset($this->Controller->TestLib));
+		$this->assertTrue($this->Controller->TestLib->hasOptions);
+	}
+
+
+
+	public function testGetParams() {
+		$is = $this->Controller->Common->getQueryParam('case');
+		$this->assertTrue(strpos($is, 'CommonComponent') > 0 || $is == 'AllComponentTests' || $is == 'AllPluginTests');
+
+		$is = $this->Controller->Common->getQueryParam('x');
+		$this->assertSame($is, '');
+
+		$is = $this->Controller->Common->getQueryParam('x', 'y');
+		$this->assertSame($is, 'y');
+
+		$is = $this->Controller->Common->getNamedParam('plugin');
+		$this->assertSame($is, '');
+
+		$is = $this->Controller->Common->getNamedParam('x');
+		$this->assertSame($is, '');
+
+		$is = $this->Controller->Common->getNamedParam('x', 'y');
+		$this->assertSame($is, 'y');
+
+	}
+
+	public function testGetDefaultUrlParams() {
+		$is = $this->Controller->Common->defaultUrlParams();
+		debug($is);
+		$this->assertNotEmpty($is);
+	}
+
+
+	public function testTransientFlashMessage() {
+		$is = $this->Controller->Common->transientFlashMessage('xyz', 'success');
+		$this->assertTrue($is);
+
+		$res = Configure::read('messages');
+		debug($res);
+		$this->assertTrue(!empty($res));
+		$this->assertTrue(isset($res['success'][0]) && $res['success'][0] == 'xyz');
+	}
+
+
+	public function testFlashMessage() {
+		$this->Controller->Session->delete('messages');
+		$is = $this->Controller->Common->flashMessage('efg');
+		$this->assertTrue($is);
+
+		$res = $this->Controller->Session->read('messages');
+		debug($res);
+		$this->assertTrue(!empty($res));
+		$this->assertTrue(isset($res['info'][0]) && $res['info'][0] == 'efg');
+	}
+
+
+
+}
+
+
+
+/*** additional helper classes ***/
+
+
+/**
+* Short description for class.
+*
+* @package       cake.tests
+* @subpackage    cake.tests.cases.libs.controller.components
+*/
+class CommonComponentTestController extends AppController {
+/**
+ * name property
+ *
+ * @var string 'SecurityTest'
+ * @access public
+ */
+
+/**
+ * components property
+ *
+ * @var array
+ * @access public
+ */
+	public $components = array('Tools.Common');
+/**
+ * failed property
+ *
+ * @var bool false
+ * @access public
+ */
+	public $failed = false;
+/**
+ * Used for keeping track of headers in test
+ *
+ * @var array
+ * @access public
+ */
+	public $testHeaders = array();
+/**
+ * fail method
+ *
+ * @access public
+ * @return void
+ */
+	public function fail() {
+		$this->failed = true;
+	}
+/**
+ * redirect method
+ *
+ * @param mixed $option
+ * @param mixed $code
+ * @param mixed $exit
+ * @access public
+ * @return void
+ */
+	public function redirect($option, $code, $exit) {
+		return $code;
+	}
+/**
+ * Conveinence method for header()
+ *
+ * @param string $status
+ * @return void
+ * @access public
+ */
+	public function header($status) {
+		$this->testHeaders[] = $status;
+	}
+}
+
+
+class TestComponent extends Component {
+
+	public $Controller;
+	public $isInit = false;
+	public $isStartup = false;
+
+	public function initialize(Controller $Controller) {
+		//$this->Controller = $Controller;
+		$this->isInit = true;
+	}
+
+	public function startup(Controller $Controller) {
+		//$this->Controller = $Controller;
+		$this->isStartup = true;
+	}
+
+}
+
+class TestHelper extends Object {
+
+
+}
+
+class TestLib {
+
+	public $hasOptions = false;
+
+	public function __construct($options = array()) {
+		if (!empty($options)) {
+			$this->hasOptions = true;
+		}
+	}
+}
+
+