diff --git a/.gitignore b/.gitignore new file mode 100755 index 000000000..4e2c08050 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/tmp +/vendor +/.idea +composer.lock diff --git a/.semver b/.semver old mode 100644 new mode 100755 index c8cec4bb8..93bdf64f8 --- a/.semver +++ b/.semver @@ -1,5 +1,5 @@ --- -:major: 2 +:major: 3 :minor: 1 -:patch: 1 +:patch: 6 :special: '' diff --git a/.travis.yml b/.travis.yml old mode 100644 new mode 100755 index b9b76039b..cc347134b --- a/.travis.yml +++ b/.travis.yml @@ -1,40 +1,48 @@ language: php php: - - 5.3 - 5.4 - 5.5 + - 5.6 -env: - global: - - PLUGIN_NAME=Users - - DB=mysql - - REQUIRE="phpunit/phpunit:3.7.31" +sudo: false +env: matrix: - - DB=mysql CAKE_VERSION=master + - DB=mysql db_dsn='mysql://travis@0.0.0.0/cakephp_test' + - DB=pgsql db_dsn='postgres://postgres@127.0.0.1/cakephp_test' + - DB=sqlite db_dsn='sqlite:///:memory:' + + global: + - DEFAULT=1 matrix: + fast_finish: true + include: - - php: 5.3 - env: - - CAKE_VERSION=master - - php: 5.4 - env: - - CAKE_VERSION=master - php: 5.5 - env: - - CAKE_VERSION=master + env: PHPCS=1 DEFAULT=0 + + - php: 5.5 + env: COVERALLS=1 DEFAULT=0 DB=mysql db_dsn='mysql://travis@0.0.0.0/cakephp_test' before_script: - - git clone https://github.com/burzum/travis.git --depth 1 ../travis - - ../travis/before_script.sh + - composer self-update + - composer install --prefer-dist --no-interaction -script: - - ../travis/script.sh + - sh -c "if [ '$DB' = 'mysql' ]; then mysql -e 'CREATE DATABASE cakephp_test;'; fi" + - sh -c "if [ '$DB' = 'pgsql' ]; then psql -c 'CREATE DATABASE cakephp_test;' -U postgres; fi" -after_success: - - ../travis/after_success.sh + - sh -c "if [ '$PHPCS' = '1' ]; then composer require cakephp/cakephp-codesniffer:dev-master; fi" + + - sh -c "if [ '$COVERALLS' = '1' ]; then composer require --dev satooshi/php-coveralls:dev-master; fi" + - sh -c "if [ '$COVERALLS' = '1' ]; then mkdir -p build/logs; fi" + +script: + - sh -c "if [ '$DEFAULT' = '1' ]; then phpunit --stderr; fi" + - sh -c "if [ '$PHPCS' = '1' ]; then ./vendor/bin/phpcs -p -n --extensions=php --standard=vendor/cakephp/cakephp-codesniffer/CakePHP ./src ./tests; fi" + - sh -c "if [ '$COVERALLS' = '1' ]; then phpunit --stderr --coverage-clover build/logs/clover.xml; fi" + - sh -c "if [ '$COVERALLS' = '1' ]; then php vendor/bin/coveralls -c .coveralls.yml -v; fi" notifications: email: false diff --git a/CHANGELOG.md b/CHANGELOG.md old mode 100644 new mode 100755 index 7346d789c..502f4841e --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,46 +1,42 @@ Changelog ========= -Release 2.1.1 +Releases for CakePHP 3 ------------- -https://github.com/CakeDC/users/tree/2.1.1 +* 3.1.5 + * SocialAuthenticate improvements + * Authorize Rules. Owner rule + * Docs improvements - * [73aa350](https://github.com/cakedc/users/commit/73aa350) Fixing an issue with pagination settings for the admin_index() - * [3dd162d](https://github.com/cakedc/users/commit/3dd162d) Forgot password translation - * [09499fb](https://github.com/cakedc/users/commit/09499fb) Fixing some strings in the AllUsersTest.php to make sure the naming is correct - * [83f502b](https://github.com/cakedc/users/commit/83f502b) Changing the handling of the return_to parameter in the UsersController - * [3e9a378](https://github.com/cakedc/users/commit/3e9a378) Refs https://github.com/CakeDC/users/issues/189 Fixing the Email default "from" setting, some CS fixes and some code refactoring as well - * [d0f330e](https://github.com/cakedc/users/commit/d0f330e) Update Installation.md +* 3.1.4 + * SocialAuthenticate refactored to drop Opauth in favor of Muffin/OAuth2 and league/oauth2 +* 3.1.3 + * UserHelper improvements -Release 2.1.0 +* 3.1.2 + * Fixes in RBAC permission matchers + +* 3.1.0 Migration to CakePHP 3.0 + * Unit test coverage improvements + * Refactor UsersTable to Behavior + * Add google authentication + * Add reCaptcha + * Link social accounts in profile + +Releases for CakePHP 2 ------------- -https://github.com/CakeDC/users/tree/2.1.0 - - * [aa5b58d](https://github.com/CakeDC/users/commit/aa5b58d) Fixing the PrgComponent mock so that the plugin tests work until the fix for the 2nd arg of the PrgConstuctor appears in the master branch. - * [f4f0726](https://github.com/CakeDC/users/commit/f4f0726) Fixing tests and working on the documentation - * [4b0b210](https://github.com/CakeDC/users/commit/4b0b210) Replacing Set with Hash - * [f9c07ec](https://github.com/CakeDC/users/commit/f9c07ec) Adding information about the legacy user details to the documentation - * [50f0597](https://github.com/CakeDC/users/commit/50f0597) Adding semver and CONTRIBUTING.md - * [a4e6a95](https://github.com/CakeDC/users/commit/a4e6a95) Fixing https://github.com/CakeDC/users/pull/174 - * [3a66dd2](https://github.com/CakeDC/users/commit/3a66dd2) Fixing some upper cased controller names in sidebar.ctp which causes the urls to not work - * [29537ad](https://github.com/CakeDC/users/commit/29537ad) Changing .travis and updating composer.json - * [effa068](https://github.com/CakeDC/users/commit/effa068) Fixing typo in error message - * [835a2a5](https://github.com/CakeDC/users/commit/835a2a5) Fixing missing ' in the readme.md - * [33b7029](https://github.com/CakeDC/users/commit/33b7029) Removed testTest - * [69b901e](https://github.com/CakeDC/users/commit/69b901e) Controller Test Fixed - * [aea36c3](https://github.com/CakeDC/users/commit/aea36c3) Fix testEditPassword - * [c07e7c6](https://github.com/CakeDC/users/commit/c07e7c6) Updated deprecated assertEqual - * [c62aa19](https://github.com/CakeDC/users/commit/c62aa19) Formatting fixes - * [f53b19c](https://github.com/CakeDC/users/commit/f53b19c) User password edit / unit tests - * [b63001b](https://github.com/CakeDC/users/commit/b63001b) Redundant Cookie::destroy() - * [eea58c1](https://github.com/CakeDC/users/commit/eea58c1) Added missing dependency (Utils Plugin) - * [4c08b47](https://github.com/CakeDC/users/commit/4c08b47) Changed portuguese translation path and fixed some strings - * [3caf6db](https://github.com/CakeDC/users/commit/3caf6db) Fixed some linting issues - * [226e3a4](https://github.com/CakeDC/users/commit/226e3a4) Code typos in readme - * [f93ce41](https://github.com/CakeDC/users/commit/f93ce41) Minor improvement to the flash message that shows the username in the UsersController.php - * [b6ee0ad](https://github.com/CakeDC/users/commit/b6ee0ad) Refs https://github.com/CakeDC/users/issues/145, fixing the links in the sidebar when not used from the plugin - * [720ebb5](https://github.com/CakeDC/users/commit/720ebb5) Refs https://github.com/CakeDC/users/pull/146 - * [46d6321](https://github.com/CakeDC/users/commit/46d6321) Renamed locale fre => fra, since 2.3 CakePHP uses ISO standard. \ No newline at end of file +* 2.1.3 + * Fixed unit tests for compatibility with CakePHP 2.7 + +* 2.1.2 + * Minor improvements + * New translations (german and portuguese) + +* 2.1.1 + * Forgot password + +* 2.1.0 + * Bugfixes and improvements \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md old mode 100644 new mode 100755 index 408e81198..26f53081b --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ Contributing ============ -This repository follows the [CakeDC Plugin Standard](http://cakedc.com/plugins). If you'd like to contribute new features, enhancements or bug fixes to the plugin, please read our [Contribution Guidelines](http://cakedc.com/plugins) for detailed instructions. \ No newline at end of file +This repository follows the [CakeDC Plugin Standard](http://cakedc.com/plugin-standard). If you'd like to contribute new features, enhancements or bug fixes to the plugin, please read our [Contribution Guidelines](http://cakedc.com/contribution-guidelines) for detailed instructions. diff --git a/Docs/Documentation/ApiKeyAuthenticate.md b/Docs/Documentation/ApiKeyAuthenticate.md new file mode 100755 index 000000000..9befe32b2 --- /dev/null +++ b/Docs/Documentation/ApiKeyAuthenticate.md @@ -0,0 +1,29 @@ +ApiKeyAuthenticate +============= + +Setup +--------------- + +ApiKeyAuthenticate default configuration is +```php + protected $_defaultConfig = [ + //type, can be either querystring or header + 'type' => self::TYPE_QUERYSTRING, + //name to retrieve the api key value from + 'name' => 'api_key', + //db field where the key is stored + 'field' => 'api_token', + //require SSL to pass the token. You should always require SSL to use tokens for Auth + 'require_ssl' => true, + ]; +``` + +We are using query strings for passing the api_key token. And we require SSL by default. +Note you can override these options using + +```php +$config['Auth']['authenticate']['CakeDC/Users.ApiKey'] = [ + 'type' => 'header', + ]; +``` + diff --git a/Docs/Documentation/Configuration.md b/Docs/Documentation/Configuration.md old mode 100644 new mode 100755 index 195238470..68bd89a77 --- a/Docs/Documentation/Configuration.md +++ b/Docs/Documentation/Configuration.md @@ -1,104 +1,170 @@ Configuration ============= -Email Templates ---------------- +Overriding the default configuration +------------------------- -To modify the templates as needed copy them to +For easier configuration, you can specify an array of config files to override the default plugin keys this way: +config/bootstrap.php ``` -/app/View/Plugin/Users/Emails/ +Configure::write('Users.config', ['users']); +Plugin::load('CakeDC/Users', ['routes' => true, 'bootstrap' => true]); +Configure::write('Users.Social.login', true); //to enable social login ``` -Note that you will have to overwrite any view that is linking to the plugin like the email verification email. - -Disable Slugs -------------- - -If the Utils plugin is present the users model will auto attach and use the sluggable behavior. +Configuration for social login +--------------------- -If you don't want to create slugs for new users put this in your configuration: +Create the facebook, twitter, etc applications you want to use and setup the configuration like this: +In this example, we are using 2 providers: facebook and twitter. Note you'll need to add the providers to +your composer.json file. -```php -Configure::write('Users.disableSlugs', true); +``` +$ composer require league/oauth2-facebook:@stable +$ composer require league/oauth1-client:@stable ``` -Email configuration -------------------- - -The plugin uses the $default email configuration (should be present in your Config/email.php file), but you can override it using +NOTE: twitter uses league/oauth1-client package -```php -Configure::write('Users.emailConfig', 'default'); +config/bootstrap.php ``` +Configure::write('OAuth.providers.facebook.options.clientId', 'YOUR APP ID'); +Configure::write('OAuth.providers.facebook.options.clientSecret', 'YOUR APP SECRET'); -To change the plugins default "from" setting for outgoing emails put this into your bootstrap.php - -```php -Configure::write('App.defaultEmail', 'your@email.com'); +Configure::write('OAuth.providers.twitter.options.clientId', 'YOUR APP ID'); +Configure::write('OAuth.providers.twitter.options.clientSecret', 'YOUR APP SECRET'); ``` -If not configured it will use 'noreply@' . env('HTTP_HOST'); as default from email address. +Or use the config override option when loading the plugin (see above) -Roles Management ----------------- +Configuration options +--------------------- -You can add `Users.roles` in `app/Config/bootstrap.php` file and these roles will be used on Admin Add / Edit pages. i.e: +The plugin is configured via the Configure class. Check the `vendor/cakedc/users/config/users.php` +for a complete list of all the configuration keys. + +Loading the UsersAuthComponent and using the right configuration values will setup the Users plugin, +the AuthComponent and the OAuth component for your application. + +If you prefer to setup AuthComponent by yourself, you'll need to load AuthComponent before UsersAuthComponent +and set +``` +Configure::write('Users.auth', false); +``` + +Interesting UsersAuthComponent options and defaults + +NOTE: SOME keys were hidden in this doc page, please refer to `vendor/cakedc/users/config/users.php` for the complete list + +``` + 'Users' => [ + //Table used to manage users + 'table' => 'CakeDC/Users.Users', + //configure Auth component + 'auth' => true, + 'Email' => [ + //determines if the user should include email + 'required' => true, + //determines if registration workflow includes email validation + 'validate' => true, + ], + 'Registration' => [ + //determines if the register is enabled + 'active' => true, + //determines if the reCaptcha is enabled for registration + 'reCaptcha' => true, + ], + 'Tos' => [ + //determines if the user should include tos accepted + 'required' => true, + ], + 'Social' => [ + //enable social login + 'login' => false, + ], + //Avatar placeholder + 'Avatar' => ['placeholder' => 'CakeDC/Users.avatar_placeholder.png'], + 'RememberMe' => [ + //configure Remember Me component + 'active' => true, + ], + ], +//default configuration used to auto-load the Auth Component, override to change the way Auth works + 'Auth' => [ + 'authenticate' => [ + 'all' => [ + 'scope' => ['active' => 1] + ], + 'CakeDC/Users.RememberMe', + 'Form', + ], + 'authorize' => [ + 'CakeDC/Users.Superuser', + 'CakeDC/Users.SimpleRbac', + ], + ], +]; + +``` + +Default Authenticate and Authorize Objects used +------------------------ + +Using the UsersAuthComponent default initialization, the component will load the following objects into AuthComponent: +* Authenticate + * 'Form' + * 'Social' check [SocialAuthenticate](SocialAuthenticate.md) for configuration options + * 'RememberMe' check [SocialAuthenticate](RememberMeAuthenticate.md) for configuration options +* Authorize + * 'Users.Superuser' check [SuperuserAuthorize](SuperuserAuthorize.md) for configuration options + * 'Users.SimpleRbac' check [SimpleRbacAuthorize](SimpleRbacAuthorize.md) for configuration options + +## Using the user's email to login + +You need to configure 2 things: +* Change the Auth.authenticate.Form.fields configuration to let AuthComponent use the email instead of the username for user identify. Add this line to your bootstrap.ctp file, after CakeDC/Users Plugin is loaded ```php -Configure::write('Users.roles', array( - 'admin' => 'Admin', - 'registered' => 'Registered' -)); +Configure::write('Auth.authenticate.Form.fields.username', 'email'); ``` -If you don't specify roles it will use 'admin' role (if `is_admin` is checked) or 'registered' role otherwise. You can override 'registered' role setting Users.defaultRole in `app/Config/bootstrap.php`. i.e: +* Override the login.ctp template to change the Form->input to "email". Add (or copy from the https://github.com/CakeDC/users/blob/master/src/Template/Users/login.ctp) the file login.ctp to path /src/Template/Plugin/CakeDC/Users/Users/login.ctp and ensure it has the following content ```php -Configure::write('Users.defaultRole', 'user_registered'); -``` - -Enabling / Disabling Registration ---------------------------------- - -Some application won't need to have registration enable so you can define `Users.allowRegistration` in `app/Config/bootstrap.php` to enable or disable registration. By default registration will be enabled. - -``` -// Disables the registration -Configure::write('Users.allowRegistration', false); + // ... inside the Form + Form->input('email', ['required' => true]) ?> + Form->input('password', ['required' => true]) ?> + // ... rest of your login.ctp code ``` -Configuration options ---------------------- -The configuration settings can be written by using the Configure class. + -``` -Users.disableDefaultAuth -``` +Email Templates +--------------- -Disables/enables the default auth setup that is implemented in the plugins `UsersController::_setupAuth()`. +To modify the templates as needed copy them to your application ``` -Users.allowRegistration +cp -r vendor/cakedc/users/src/Template/Email/* src/Template/Plugin/CakeDC/Users/Email/ ``` -Disables/enables the user registration. +Then customize the email templates as you need under the src/Template/Plugin/CakeDC/Users/Email/ directory -``` -Users.roles -``` +Plugin Templates +--------------- -Optional array of user roles if you need it. This is not actively used by the plugin by default. +Similar to Email Templates customization, follow the CakePHP conventions to put your new templates under +src/Template/Plugin/CakeDC/Users/[Controller]/[view].ctp -``` -Users.sendPassword -``` +Check http://book.cakephp.org/3.0/en/plugins.html#overriding-plugin-templates-from-inside-your-application -Disables/enables the password reset functionality. +Flash Messages +--------------- -``` -Users.emailConfig -``` +To modify the flash messages, use the standard PO file provided by the plugin and customize the messages +Check http://book.cakephp.org/3.0/en/core-libraries/internationalization-and-localization.html#setting-up-translations +for more details about how the PO files should be managed in your application. -Email configuration settings array used by this plugin. +We've included an updated POT file with all the `Users` domain keys for your customization. diff --git a/Docs/Documentation/Events.md b/Docs/Documentation/Events.md old mode 100644 new mode 100755 index 5107404ba..13e810165 --- a/Docs/Documentation/Events.md +++ b/Docs/Documentation/Events.md @@ -1,19 +1,55 @@ Events ====== -If you're not familiar with events please look them up in [the official documentation](http://book.cakephp.org/2.0/en/core-libraries/events.html). - The events in this plugin follow these conventions ...: -* Users.Controller.Users.someCallBack -* Users.Model.User.someCallBack -* ... +* 'Users.Component.UsersAuth.isAuthorized' +* 'Users.Component.UsersAuth.beforeLogin' +* 'Users.Component.UsersAuth.afterLogin' +* 'Users.Component.UsersAuth.afterCookieLogin' +* 'Users.Component.UsersAuth.beforeRegister' +* 'Users.Component.UsersAuth.afterRegister' +* 'Users.Component.UsersAuth.beforeLogout' +* 'Users.Component.UsersAuth.afterLogout' + +The events allow you to inject data into the plugin on the before* plugins and use the data for your +own business login in the after* events, for example -Events that are triggered in this plugin are: +``` + /** + * Forced login using a beforeLogin event + */ + public function eventLogin() + { + $this->eventManager()->on(UsersAuthComponent::EVENT_BEFORE_LOGIN, function () { + //the callback function should return the user data array to force login + return [ + 'id' => 1337, + 'username' => 'forceLogin', + 'email' => 'event@example.com', + 'active' => true, + ]; + }); + $this->login(); + $this->render('login'); + } - * Users.Controller.Users.beforeRegister - * Users.Controller.Users.afterRegister - * Users.Controller.Users.beforeLogin - * Users.Controller.Users.afterLogin - * Users.Model.User.beforeRegister - * Users.Model.User.afterRegister \ No newline at end of file + /** + * beforeRegister event + */ + public function eventRegister() + { + $this->eventManager()->on(UsersAuthComponent::EVENT_BEFORE_REGISTER, function ($event) { + //the callback function should return the user data array to force register + return $event->data['usersTable']->newEntity([ + 'username' => 'forceEventRegister', + 'email' => 'eventregister@example.com', + 'password' => 'password', + 'active' => true, + 'tos' => true, + ]); + }); + $this->register(); + $this->render('register'); + } +``` \ No newline at end of file diff --git a/Docs/Documentation/Extending-the-Plugin.md b/Docs/Documentation/Extending-the-Plugin.md old mode 100644 new mode 100755 index ab62128f1..27056682e --- a/Docs/Documentation/Extending-the-Plugin.md +++ b/Docs/Documentation/Extending-the-Plugin.md @@ -1,65 +1,148 @@ Extending the Plugin ==================== -Extending the controller ------------------------- +Extending the Model (Table/Entity) +------------------- + +Create a new Table and Entity in your app, matching the table you want to use for storing the +users data. Check the initial users migration to know the default columns expected in the table. +If your column names doesn't match the columns in your current table, you could use the Entity to +match the colums using accessors & mutators as described here http://book.cakephp.org/3.0/en/orm/entities.html#accessors-mutators -Declare the controller class. It is important to set the ```$name``` property here to make sure the controller initializes everything correct. If not setting the name the name setting is inherited and won't match the new controllers name. +Example: we are going to use a custom table in our application ```my_users``` +* Create a new Table under src/Model/Table/MyUsersTable.php ```php -App::uses('UsersController', 'Users.Controller'); -class AppUsersController extends UsersController { - public $name = 'AppUsers'; +namespace App\Model\Table; + +use CakeDC\Users\Model\Table\UsersTable; + +/** + * Users Model + */ +class MyUsersTable extends UsersTable +{ } ``` -In the case you want to extend also the user model it's required to set the right user class in the beforeFilter() because the controller will use the inherited model which would be Users.User. +* Create a new Entity under src/Model/Entity/MyUser.php ```php -public function beforeFilter() { - parent::beforeFilter(); - $this->User = ClassRegistry::init('AppUser'); - $this->set('model', 'AppUser'); +namespace App\Model\Entity; + +use CakeDC\Users\Model\Entity\User; + +class MyUser extends User +{ } ``` -You can overwrite the ```Controller::render()``` method to fall back to the plugin views in the case you want to use some of them +* Pass the new table configuration to Users Plugin Configuration + +config/bootstrap.php +``` +Configure::write('Users.config', ['users']); +Plugin::load('Users', ['routes' => true, 'bootstrap' => true]); +``` + +Then in your config/users.php +``` +return [ + 'Users.table' => 'MyUsers', +]; +``` + +Now the Users Plugin will use MyUsers Table and Entity to register and login user in. Use the +Entity to match your own columns in case they don't match the default column names: + +```sql +CREATE TABLE IF NOT EXISTS `users` ( + `id` char(36) NOT NULL, + `username` varchar(255) NOT NULL, + `email` varchar(255) DEFAULT NULL, + `password` varchar(255) NOT NULL, + `first_name` varchar(50) DEFAULT NULL, + `last_name` varchar(50) DEFAULT NULL, + `token` varchar(255) DEFAULT NULL, + `token_expires` datetime DEFAULT NULL, + `api_token` varchar(255) DEFAULT NULL, + `activation_date` datetime DEFAULT NULL, + `tos_date` datetime DEFAULT NULL, + `active` int(1) NOT NULL DEFAULT '0', + `is_superuser` int(1) NOT NULL DEFAULT '0', + `role` varchar(255) DEFAULT 'user', + `created` datetime NOT NULL, + `modified` datetime NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +``` + +Extending the Controller +------------------- + +You want to use one of your controllers to handle all the users features in your app, and keep the +login/register/etc actions from Users Plugin, ```php -public function render($view = null, $layout = null) { - if (is_null($view)) { - $view = $this->action; - } - $viewPath = substr(get_class($this), 0, strlen(get_class($this)) - 10); - if (!file_exists(APP . 'View' . DS . $viewPath . DS . $view . '.ctp')) { - $this->plugin = 'Users'; - } else { - $this->viewPath = $viewPath; - } - return parent::render($view, $layout); +getUsersTable()->find() + ->where(['id' => $userId]) + ->hydrate(false) + ->first(); + $this->Auth->setUser($user); + return $this->redirect('/'); + } } ``` +Updating the Templates +------------------- -It's important to override the ```AppUser::$useTable``` property with the ```users``` table. It won't use the correct table otherwise. +Use the standard CakePHP conventions to override Plugin views using your application views +http://book.cakephp.org/3.0/en/plugins.html#overriding-plugin-templates-from-inside-your-application -You can override / extend all methods or properties like validation rules to suit your needs. -Routing to preserve the /users URL when extending the plugin ------------------------------------------------------------- -See the [Routing](Routing.md) documentation. \ No newline at end of file diff --git a/Docs/Documentation/Installation.md b/Docs/Documentation/Installation.md old mode 100644 new mode 100755 index 1c21786ba..a586c144c --- a/Docs/Documentation/Installation.md +++ b/Docs/Documentation/Installation.md @@ -1,52 +1,87 @@ Installation ============ -To install the plugin, place the files in a directory labelled "Users/" in your "app/Plugin/" directory. +Composer +------ -Then, include the following line in your `app/Config/bootstrap.php` to load the plugin in your application. +``` +composer require cakedc/users +``` + +if you want to use social login features... ``` -CakePlugin::load('Users'); +composer require league/oauth2-facebook:@stable +composer require league/oauth2-instagram::@stable +composer require league/oauth2-google::@stable +composer require league/oauth2-linkedin::@stable +composer require league/oauth1-client:@stable ``` -Git Submodule -------------- +NOTE: you'll need to enable social login in your bootstrap.php file if you want to use it, social +login is disabled by default. Check the [Configuration](Configuration.md) page for more details. -If you're using git for version control, you may want to add the **Users** plugin as a submodule on your repository. To do so, run the following command from the base of your repository: +``` +Configure::write('Users.Social.login', true); //to enable social login +``` + +Creating Required Tables +------------------------ +If you want to use the Users tables to store your users and social accounts: ``` -git submodule add git@github.com:CakeDC/users.git app/Plugin/Users +bin/cake migrations migrate -p CakeDC/Users ``` -After doing so, you will see the submodule in your changes pending, plus the file ".gitmodules". Simply commit and push to your repository. +Note you don't need to use the provided tables, you could customize the table names, fields etc in your +application and then use the plugin configuration to use your own tables instead. Please refer to the [Extending the Plugin](Extending-the-Plugin.md) +section to check all the customization options -To initialize the submodule(s) run the following command: +Load the Plugin +----------- + +Ensure the Users Plugin is loaded in your config/bootstrap.php file ``` -git submodule update --init --recursive +Plugin::load('CakeDC/Users', ['routes' => true, 'bootstrap' => true]); ``` -To retreive the latest updates to the plugin, assuming you're using the "master" branch, go to "app/Plugin/Users" and run the following command: +Customization +---------- +config/bootstrap.php ``` -git pull origin master +Configure::write('Users.config', ['users']); +Plugin::load('CakeDC/Users', ['routes' => true, 'bootstrap' => true]); +Configure::write('Users.Social.login', true); //to enable social login ``` -If you're using another branch, just change "master" for the branch you are currently using. +If you want to use social login, then in your config/users.php +``` +return [ + 'OAuth.providers.facebook.options.clientId' => 'YOUR APP ID', + 'OAuth.providers.facebook.options.clientSecret' => 'YOUR APP SECRET', + 'OAuth.providers.twitter.options.clientId' => 'YOUR APP ID', + 'OAuth.providers.titter.options.clientSecret' => 'YOUR APP SECRET', + //etc +]; -If any updates are added, go back to the base of your own repository, commit and push your changes. This will update your repository to point to the latest updates to the plugin. +``` -Composer --------- +Note: using social authentication is not required. -The plugin also provides a "composer.json" file, to easily use the plugin through the Composer dependency manager. +For more details, check the Configuration doc page -Creating Required Tables ------------------------- -You can create database tables using either the schema shell or the [CakeDC Migrations plugin](http://github.com/CakeDC/migrations): - - ./Console/cake schema create users --plugin Users +Load the UsersAuth Component +--------------------- -or +Load the Component in your src/Controller/AppController.php, and use the passed Component configuration to customize the Users Plugin: - ./Console/cake Migrations.migration run all --plugin Users +``` + public function initialize() + { + parent::initialize(); + $this->loadComponent('Flash'); + $this->loadComponent('CakeDC/Users.UsersAuth'); + } +``` diff --git a/Docs/Documentation/Overview.md b/Docs/Documentation/Overview.md old mode 100644 new mode 100755 index 4b7164bbc..0e98fc30e --- a/Docs/Documentation/Overview.md +++ b/Docs/Documentation/Overview.md @@ -5,12 +5,14 @@ You can use the plugin as it comes if you're happy with it or, more common, exte The plugin itself is already capable of: -* User registration (Enable by default) +* User registration * Account verification by a token sent via email * User login (email / password) +* Social login (Twitter, Facebook) * Password reset based on requesting a token by email and entering a new password -* User search (requires the [CakeDC Search](http://github.com/CakeDC/search) plugin) -* User management using the "admin" section (add / edit / delete) +* User management (add / edit / delete) * Simple roles management +* Simple Rbac and SuperUser Authorize +* RememberMe using cookie feature +* reCaptcha for user registration -The default password reset process requires the user to enter his email address, an email is sent to the user with a link and a token. When the user accesses the URL with the token he can enter a new password. diff --git a/Docs/Documentation/OwnerRule.md b/Docs/Documentation/OwnerRule.md new file mode 100755 index 000000000..db98eb455 --- /dev/null +++ b/Docs/Documentation/OwnerRule.md @@ -0,0 +1,86 @@ +Owner Rule +============= + +Setup +--------------- + +SimpleRbacAuthorize will pick and use specific Rule classes (implementing the \CakeDC\Users\Auth\Rules\Rule interface). +You only need to create your own permission rules, implement the required interface and pass the instance in the +'allowed' param. + +We provide an AbstractRule you could extend too, in case you want to use rules accessing the database and using the +default table associated to the current controller. + +Owner rule configuration +----------------- + +The Owner rule can be configured to use the following options +```php + //field in the owned table matching the user_id + 'ownerForeignKey' => 'user_id', + /* + * request key type to retrieve the table id, could be "params", "query", "data" to locate the table id + * example: + * yoursite.com/controller/action/XXX would be + * tableKeyType => 'params', 'tableIdParamsKey' => 'pass.0' + * yoursite.com/controlerr/action?post_id=XXX would be + * tableKeyType => 'query', 'tableIdParamsKey' => 'post_id' + * yoursite.com/controller/action [posted form with a field named post_id] would be + * tableKeyType => 'data', 'tableIdParamsKey' => 'post_id' + */ + 'tableKeyType' => 'params', + // request->params key path to retrieve the owned table id + 'tableIdParamsKey' => 'pass.0', + /* define table to use or pick it from controller name defaults if null + * if null, table used will be based on current controller's default table + * if string, TableRegistry::get will be used + * if Table, the table object will be used + */ + 'table' => null, + 'conditions' => [], +``` + +Example: + +(in your permissions.php file) +```php +[ + 'role' => ['user'], + 'controller' => ['Posts'], + 'action' => ['edit'], + 'allowed' => new Owner([ + 'ownerForeignKey' => 'owner_id', + ]), +], +``` + +In this example, action `/posts/edit/55` will be allowed if: + * The user is logged in + * The user role is 'user' + * There is a post in posts table with id 55 + * The 'owner_id' field in the post matches the user id + +Checking ownership in belongsToMany associations +----------------- + +Let's say you have users, posts, posts_users tables, and Posts belongsToMany Users, +you could check the ownership configuring the Owner rule: +```php +[ + 'role' => ['user'], + 'controller' => ['Posts'], + 'action' => ['edit'], + 'allowed' => new Owner([ + 'table' => 'PostsUsers', + 'id' => 'post_id', + 'ownerForeignKey' => 'owner_id' + ]), +], +``` + +In this example, action `/posts/edit/55` will be allowed if: + * The user is logged in + * The user role is 'user' + * There is a row in posts_users table matching + * 'owner_id' = user id + * 'post_id' = 55 diff --git a/Docs/Documentation/SimpleRbacAuthorize.md b/Docs/Documentation/SimpleRbacAuthorize.md new file mode 100755 index 000000000..ef21ca282 --- /dev/null +++ b/Docs/Documentation/SimpleRbacAuthorize.md @@ -0,0 +1,97 @@ +SimpleRbacAuthorize +============= + +Setup +--------------- + +SimpleRbacAuthorize is configured by default, but you can customize the way it works easily + +Example, in your AppController, you can configure the way the SimpleRbac works by setting the options: + +```php +$config['Auth']['authorize']['Users.SimpleRbac'] = [ + //autoload permissions.php + 'autoload_config' => 'permissions', + //role field in the Users table + 'role_field' => 'role', + //default role, used in new users registered and also as role matcher when no role is available + 'default_role' => 'user', + /* + * This is a quick roles-permissions implementation + * Rules are evaluated top-down, first matching rule will apply + * Each line define + * [ + * 'role' => 'admin', + * 'plugin', (optional, default = null) + * 'controller', + * 'action', + * 'allowed' (optional, default = true) + * ] + * You could use '*' to match anything + * Suggestion: put your rules into a specific config file + */ + 'permissions' => [], // you could set an array of permissions or load them using a file 'autoload_config' + ]; +``` + +This is the default configuration, based on it the Authorize object will first check your ```config/permissions.php``` +file and load the permissions using the configuration key ```Users.SimpleRbac.permissions```, there is an +example file you can copy into your ```config/permissions.php``` under the Plugin's config directory. + +If you don't want to use a file for configuring the permissions, you just need to tweak the configuration and set +```'autoload_config' => false,``` then define all your rules in AppController (not a good practice as the rules +tend to grow over time). + +The Users Plugin will use the field ```role_field``` in the Users Table to match the role of the user and +check if there is a rule allowing him to access the url. + +The ```default_role``` will be used to set the role of the registered users by default. + +Permission rules syntax +----------------- + +* Rules are evaluated top-down, first matching rule will apply +* Each rule is defined: +```php +[ + 'role' => 'REQUIRED_NAME_OF_THE_ROLE_OR_[]_OR_*', + 'prefix' => 'OPTIONAL_PREFIX_USED_OR_[]_OR_*_DEFAULT_NULL', + 'plugin' => 'OPTIONAL_NAME_OF_THE_PLUGIN_OR_[]_OR_*_DEFAULT_NULL', + 'controller' => 'REQUIRED_NAME_OF_THE_CONTROLLER_OR_[]_OR_*' + 'action' => 'REQUIRED_NAME_OF_ACTION_OR_[]_OR_*', + 'allowed' => 'OPTIONAL_BOOLEAN_OR_CALLABLE_OR_INSTANCE_OF_RULE_DEFAULT_TRUE' +] +``` +* If no rule allowed = true is matched for a given user role and url, default return value will be false +* Note for Superadmin access (permission to access ALL THE THINGS in your app) there is a specific Authorize Object provided + +Permission Callbacks +----------------- +You could use a callback in your 'allowed' to process complex authentication, like + - ownership + - permissions stored in your database + - permission based on an external service API call + +Example *ownership* callback, to allow users to edit their own Posts: + +```php + 'allowed' => function (array $user, $role, Request $request) { + $postId = Hash::get($request->params, 'pass.0'); + $post = TableRegistry::get('Posts')->get($postId); + $userId = Hash::get($user, 'id'); + if (!empty($post->user_id) && !empty($userId)) { + return $post->user_id === $userId; + } + return false; + } +``` + +Permission Rules +---------------- +You could use an instance of the \CakeDC\Users\Auth\Rules\Rule interface to reuse your custom rules +Examples: +```php +'allowed' => new Owner() //will pick by default the post id from the first pass param +``` +Check the [Owner Rule](OwnerRule.md) documentation for more details + diff --git a/Docs/Documentation/SocialAuthenticate.md b/Docs/Documentation/SocialAuthenticate.md new file mode 100755 index 000000000..602611076 --- /dev/null +++ b/Docs/Documentation/SocialAuthenticate.md @@ -0,0 +1,68 @@ +SocialAuthenticate +============= + +Setup +--------------------- + +Create the Facebook/Twitter applications you want to use and setup the configuration like this: + +Config/bootstrap.php +``` +Configure::write('OAuth.providers.facebook.options.clientId', 'YOUR APP ID'); +Configure::write('OAuth.providers.facebook.options.clientSecret', 'YOUR APP SECRET'); + +Configure::write('OAuth.providers.twitter.options.clientId', 'YOUR APP ID'); +Configure::write('OAuth.providers.twitter.options.clientSecret', 'YOUR APP SECRET'); +``` + +You can also change the default settings for social authenticate: + +``` +Configure::write('Users', [ + 'Email' => [ + //determines if the user should include email + 'required' => true, + //determines if registration workflow includes email validation + 'validate' => true, + ], + 'Social' => [ + //enable social login + 'login' => false, + ], + 'Key' => [ + 'Session' => [ + //session key to store the social auth data + 'social' => 'Users.social', + ], + //form key to store the social auth data + 'Form' => [ + 'social' => 'social' + ], + 'Data' => [ + //data key to store email coming from social networks + 'socialEmail' => 'info.email', + ], + ], +]); +``` + +If email is required and the social network does not return the user email then the user will be required to input the email. Additionally, validation could be enabled, in that case the user will be asked to validate the email before be able to login. There are some cases where the email address already exists onto database, if so, the user will receive an email and will be asked to validate the social account in the app. It is important to take into account that the user account itself will remain active and accessible by other ways (other social network account or username/password). + +In most situations you would not need to change any Oauth setting besides applications details. + +User Helper +--------------------- + +You can use the helper included with the plugin to create Facebook/Twitter buttons: + +In templates +``` +$this->User->facebookLogin(); + +$this->User->twitterLogin(); +``` + +We recommend the use of [Bootstrap Social](http://lipis.github.io/bootstrap-social/) in order to automatically apply styles to buttons. Anyway you can always add your own style to the buttons. + +Social Authentication was inspired by [UseMuffin/OAuth2](https://github.com/UseMuffin/OAuth2) library. + diff --git a/Docs/Documentation/SuperuserAuthorize.md b/Docs/Documentation/SuperuserAuthorize.md new file mode 100755 index 000000000..7db2cb042 --- /dev/null +++ b/Docs/Documentation/SuperuserAuthorize.md @@ -0,0 +1,18 @@ +SuperuserAuthorize +============= + +Setup +--------------- + +SuperuserAuthorize is here to provide full permissions to specific "SUPER" users in your app. + +```php +$config['Auth']['authorize']['Users.Superuser'] = [ + //superuser field in the Users table + 'superuser_field' => 'is_superuser', + ]; +``` + +If the current user 'superuser_field' is true, he'll get full permissions in your app. + +Note if you don't have superusers, you can disable the SuperuserAuthorize in AuthComponent initialization \ No newline at end of file diff --git a/Docs/Documentation/UserHelper.md b/Docs/Documentation/UserHelper.md new file mode 100755 index 000000000..062b8111e --- /dev/null +++ b/Docs/Documentation/UserHelper.md @@ -0,0 +1,75 @@ +UserHelper +============= + +The User Helper has some methods that may be needed if you want to improve your templates and add features to your app in an easy way. + +Setup +--------------- + +Enable the Helper in `src/view/AppView.php`: +```php +class AppView extends View +{ + public function initialize() + { + parent::initialize(); + $this->loadHelper('CakeDC/Users.User'); + } +} +``` + +Social Login +----------------- + +You can use the helper included with the plugin to create Facebook/Twitter buttons: + +In templates +```php +$this->User->facebookLogin(); + +$this->User->twitterLogin(); +``` + +We recommend the use of [Bootstrap Social](http://lipis.github.io/bootstrap-social/) in order to automatically apply styles to buttons. Anyway you can always add your own style to the buttons. + +Logout link +----------------- + +It allows to add a logout link anywhere in the app. + +```php +$this->User->logout(); +``` + +RBAC link +----------------- + +This function validates if you have access to a link and it displays it based on that. + +```php +$this->User->link(); +``` + +Welcome and profile link +----------------- + +It displays a welcome message for the user including the name and a link to the profile page + +```php +$this->User->welcome(); +``` + +reCAPTCHA +----------------- + +If you have configured reCAPTCHA for registration and have the proper key/secret configured then you will see the reCAPTCHA in registration page automatically. + +You could also use it in another templates with the following methods: + +```php +$this->User->addReCaptchaScript(); + +$this->User->addReCaptcha(); +``` + +Note that the script is added automatically if the feature is enabled in config. diff --git a/Docs/Home.md b/Docs/Home.md old mode 100644 new mode 100755 index ca146c92a..4250dea39 --- a/Docs/Home.md +++ b/Docs/Home.md @@ -1,19 +1,20 @@ Home ==== -The **Users** plugin is for allowing users to register and login manage their profile. It also allows admins to manage the users. +IMPORANT: Current status is **BETA**. We are still working on improving the unit test coverage and docs +Plugin is usable now if you want to download and take a look. PRs and Issues welcome! + +The **Users** Plugin allow users to register and login, manage their profile, etc. It also allows admins to manage the users. The plugin is thought as a base to extend your app specific users controller and model from. -That it works out of the box doesn't mean it is thought to be used exactly like it is but to provide you a kick start. You will have to extend the plugin on app level to customize it. Read the instructions carefully. +That it works out of the box doesn't mean it is thought to be used exactly like it is but to provide you a kick start. Requirements ------------ -* CakePHP 2.5+ -* PHP 5.2.8+ -* [CakeDC Utils plugin](http://github.com/CakeDC/utils) (Optional but recommended) -* [CakeDC Search plugin](http://github.com/CakeDC/search) (Optional but recommended) +* CakePHP 3.1.* +* PHP 5.4.16+ Documentation ------------- @@ -21,9 +22,9 @@ Documentation * [Overview](Documentation/Overview.md) * [Installation](Documentation/Installation.md) * [Configuration](Documentation/Configuration.md) -* [Routing](Documentation/Routing.md) -* [Auth in the Users Plugin](Documentation/Auth-in-the-Users-Plugin.md) -* [User Details (Legacy)](Documentation/User-Details.md) +* [SimpleRbacAuthorize](Documentation/SimpleRbacAuthorize.md) +* [SuperuserAuthorize](Documentation/SuperuserAuthorize.md) +* [SocialAuthenticate](Documentation/SocialAuthenticate.md) +* [UserHelper](Documentation/UserHelper.md) * [Events](Documentation/Events.md) -* [Remember Me Component](Documentation/Remember-Me-Component.md) * [Extending the Plugin](Documentation/Extending-the-Plugin.md) diff --git a/LICENSE.txt b/LICENSE.txt old mode 100644 new mode 100755 index e6bdcdf69..6ee7e8fc0 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -4,6 +4,7 @@ Copyright 2009-2014 Cake Development Corporation 1785 E. Sahara Avenue, Suite 490-423 Las Vegas, Nevada 89104 +Phone: +1 702 425 5085 http://cakedc.com Permission is hereby granted, free of charge, to any person obtaining a diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 7ca37d52c..f20f65a9f --- a/README.md +++ b/README.md @@ -4,23 +4,50 @@ CakeDC Users Plugin [![Bake Status](https://secure.travis-ci.org/CakeDC/users.png?branch=master)](http://travis-ci.org/CakeDC/users) [![Downloads](https://poser.pugx.org/CakeDC/users/d/total.png)](https://packagist.org/packages/CakeDC/users) [![Latest Version](https://poser.pugx.org/CakeDC/users/v/stable.png)](https://packagist.org/packages/CakeDC/users) +[![License](https://poser.pugx.org/CakeDC/users/license.svg)](https://packagist.org/packages/CakeDC/users) -**IMPORTANT: 3.x version is ALPHA status now and you can check it out from [CakeDC Users Plugin for CakePHP 3.x](https://github.com/cakedc/users/tree/3.x)!, we are constantly improving and testing it now.** +IMPORTANT: 3.1.x version is BETA status now, we are still improving and testing it. -The **Users** plugin is for allowing users to register and login manage their profile. It also allows admins to manage the users. +Versions and branches +--------------------- -The plugin is thought as a base to extend your app specific users controller and model from. +| CakePHP | CakeDC Users Plugin | Tag | Notes | +| :-------------: | :------------------------: | :--: | :---- | +| 2.x | [2.x](https://github.com/cakedc/users/tree/2.x) | 2.1.2 | Note CakePHP 2.7 is currently not supported, we are working on it now | +| 3.x | [master](https://github.com/cakedc/users/tree/master) | 3.x | stability is beta, but pretty stable now | +| 3.x | [develop](https://github.com/cakedc/users/tree/develop) | 3.x | stability is beta, unstable | +| 3.0 | [3.0.x](https://github.com/cakedc/users/tree/3.0.x) | 3.0.0 | stability is beta, but pretty stable now | +| 3.1 | [3.1.x](https://github.com/cakedc/users/tree/3.1.x) | 3.1.6 | stability is beta, but pretty stable now | -That it works out of the box doesn't mean it is thought to be used exactly like it is but to provide you a kick start. You will have to extend the plugin on app level to customize it. Read the instructions carefully. +The **Users** plugin is back! +It covers the following features: +* User registration +* Login/logout +* Social login (Facebook, Twitter, Instagram, Google, Linkedin, etc) +* Simple RBAC +* Remember me (Cookie) +* Manage user's profile +* Admin management + +The plugin is here to provide users related features following 2 approaches: +* Quick drop-in working solution for users login/registration. Get users working in 5 minutes. +* Extensible solution for a bigger/custom application. You'll be able to extend: + * UsersAuth Component + * Use your own UsersTable + * Use your own Controller + +On the previous versions of the plugin, extensibility was an issue, and one of the main +objectives of the 3.0 rewrite is to guarantee all the pieces could be extended/reused as +easily. + +Another decision made was limiting the plugin dependencies on other packages as much as possible. Requirements ------------ -* CakePHP 2.5+ -* PHP 5.2.8+ -* [CakeDC Utils plugin](http://github.com/CakeDC/utils) (Optional but recommended) -* [CakeDC Search plugin](http://github.com/CakeDC/search) (Optional but recommended) +* CakePHP 3.1+ +* PHP 5.4.16+ Note CakePHP 3.2 requires PHP 5.5 so 5.4 compatibility would be dropped sooner than later... Documentation ------------- @@ -42,6 +69,6 @@ This repository follows the [CakeDC Plugin Standard](http://cakedc.com/plugin-st License ------- -Copyright 2007-2014 Cake Development Corporation (CakeDC). All rights reserved. +Copyright 2015 Cake Development Corporation (CakeDC). All rights reserved. Licensed under the [MIT](http://www.opensource.org/licenses/mit-license.php) License. Redistributions of the source code included in this repository must retain the copyright notice found in each file. diff --git a/composer.json b/composer.json old mode 100644 new mode 100755 index 994be689f..ec3d24a3f --- a/composer.json +++ b/composer.json @@ -1,34 +1,60 @@ { - "name": "cakedc/users", - "description": "Users Plugin for CakePHP", - "type": "cakephp-plugin", - "keywords": ["cakephp","users","auth"], - "homepage": "http://cakedc.com", - "license": "MIT", - "authors": [ - { - "name": "Cake Development Corporation", - "email": "team@cakedc.com", - "homepage": "http://cakedc.com" - } - ], - "support": { - "email": "team@cakedc.com", - "issues": "https://github.com/CakeDC/users/issues", - "forum": "http://stackoverflow.com/tags/cakedc", - "wiki": "https://github.com/CakeDC/users/blob/master/Docs/Home.md", - "irc": "irc://irc.freenode.org/cakephp", - "source": "https://github.com/CakeDC/users" - }, - "require": { - "php": ">=5.2.8", - "composer/installers": "*" - }, - "suggest": { - "cakedc/search": "Search functionality for the admin.", - "cakedc/utils": "If you want to generate slugs based on the username." - }, - "extra": { - "installer-name": "Users" - } + "name": "cakedc/users", + "description": "Users plugin for CakePHP 3.1", + "type": "cakephp-plugin", + "keywords": [ + "cakephp", + "oauth2", + "auth", + "authentication", + "cakedc" + ], + "homepage": "https://github.com/CakeDC/users", + "license": "MIT", + "authors": [ + { + "name": "CakeDC", + "homepage": "http://www.cakedc.com", + "role": "Author" + }, + { + "name": "Others", + "homepage": "https://github.com/CakeDC/users/graphs/contributors" + } + ], + "support": { + "issues": "https://github.com/CakeDC/users/issues", + "source": "https://github.com/CakeDC/users" + }, + "require": { + "php": ">=5.4.16", + "cakephp/cakephp": "~3.1" + }, + "require-dev": { + "phpunit/phpunit": "*", + "cakephp/cakephp-codesniffer": "^2.0", + "league/oauth2-facebook": "@stable", + "league/oauth2-instagram": "@stable", + "league/oauth2-google": "@stable", + "league/oauth2-linkedin": "@stable" + }, + "suggest": { + "league/oauth1-client": "Provides Social Authentication with Twitter", + "league/oauth2-facebook": "Provides Social Authentication with Facebook", + "league/oauth2-instagram": "Provides Social Authentication with Instagram", + "league/oauth2-google": "Provides Social Authentication with Google+", + "league/oauth2-linkedin": "Provides Social Authentication with LinkedIn", + "google/recaptcha": "Provides reCAPTCHA validation for registration form" + }, + "autoload": { + "psr-4": { + "CakeDC\\Users\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "CakeDC\\Users\\Test\\": "tests", + "CakeDC\\Users\\Test\\Fixture\\": "tests" + } + } } diff --git a/config/Migrations/20150513201111_initial.php b/config/Migrations/20150513201111_initial.php new file mode 100755 index 000000000..d1e11e0ec --- /dev/null +++ b/config/Migrations/20150513201111_initial.php @@ -0,0 +1,183 @@ +table('social_accounts', ['id' => false, 'primary_key' => ['id']]); + $table + ->addColumn('id', 'char', [ + 'default' => null, + 'limit' => 36, + 'null' => false, + ]) + ->addColumn('user_id', 'string', [ + 'default' => null, + 'limit' => 36, + 'null' => false, + ]) + ->addColumn('provider', 'string', [ + 'default' => null, + 'limit' => 255, + 'null' => false, + ]) + ->addColumn('username', 'string', [ + 'default' => null, + 'limit' => 255, + 'null' => true, + ]) + ->addColumn('reference', 'string', [ + 'default' => null, + 'limit' => 255, + 'null' => false, + ]) + ->addColumn('avatar', 'string', [ + 'default' => null, + 'limit' => 255, + 'null' => true, + ]) + ->addColumn('description', 'text', [ + 'default' => null, + 'limit' => null, + 'null' => true, + ]) + ->addColumn('link', 'string', [ + 'default' => null, + 'limit' => 255, + 'null' => false, + ]) + ->addColumn('token', 'string', [ + 'default' => null, + 'limit' => 500, + 'null' => false, + ]) + ->addColumn('token_secret', 'string', [ + 'default' => null, + 'limit' => 500, + 'null' => true, + ]) + ->addColumn('token_expires', 'datetime', [ + 'default' => null, + 'limit' => null, + 'null' => true, + ]) + ->addColumn('active', 'boolean', [ + 'default' => true, + 'null' => false, + ]) + ->addColumn('data', 'text', [ + 'default' => null, + 'limit' => null, + 'null' => false, + ]) + ->addColumn('created', 'datetime', [ + 'default' => null, + 'limit' => null, + 'null' => false, + ]) + ->addColumn('modified', 'datetime', [ + 'default' => null, + 'limit' => null, + 'null' => false, + ]) + ->create(); + $table = $this->table('users', ['id' => false, 'primary_key' => ['id']]); + $table + ->addColumn('id', 'char', [ + 'default' => null, + 'limit' => 36, + 'null' => false, + ]) + ->addColumn('username', 'string', [ + 'default' => null, + 'limit' => 255, + 'null' => false, + ]) + ->addColumn('email', 'string', [ + 'default' => null, + 'limit' => 255, + 'null' => true, + ]) + ->addColumn('password', 'string', [ + 'default' => null, + 'limit' => 255, + 'null' => false, + ]) + ->addColumn('first_name', 'string', [ + 'default' => null, + 'limit' => 50, + 'null' => true, + ]) + ->addColumn('last_name', 'string', [ + 'default' => null, + 'limit' => 50, + 'null' => true, + ]) + ->addColumn('token', 'string', [ + 'default' => null, + 'limit' => 255, + 'null' => true, + ]) + ->addColumn('token_expires', 'datetime', [ + 'default' => null, + 'limit' => null, + 'null' => true, + ]) + ->addColumn('api_token', 'string', [ + 'default' => null, + 'limit' => 255, + 'null' => true, + ]) + ->addColumn('activation_date', 'datetime', [ + 'default' => null, + 'limit' => null, + 'null' => true, + ]) + ->addColumn('tos_date', 'datetime', [ + 'default' => null, + 'limit' => null, + 'null' => true, + ]) + ->addColumn('active', 'boolean', [ + 'default' => false, + 'null' => false, + ]) + ->addColumn('is_superuser', 'boolean', [ + 'default' => false, + 'null' => false, + ]) + ->addColumn('role', 'string', [ + 'default' => 'user', + 'limit' => 255, + 'null' => true, + ]) + ->addColumn('created', 'datetime', [ + 'default' => null, + 'limit' => null, + 'null' => false, + ]) + ->addColumn('modified', 'datetime', [ + 'default' => null, + 'limit' => null, + 'null' => false, + ]) + ->create(); + } + + public function down() + { + $this->dropTable('social_accounts'); + $this->dropTable('users'); + } +} diff --git a/config/bootstrap.php b/config/bootstrap.php new file mode 100755 index 000000000..f931659c4 --- /dev/null +++ b/config/bootstrap.php @@ -0,0 +1,46 @@ +each(function ($file) { + Configure::load($file); +}); + +if (Configure::check('Users.auth')) { + Configure::write('Auth.authenticate.all.userModel', Configure::read('Users.table')); +} + +if (Configure::read('Users.Social.login') && php_sapi_name() != 'cli') { + try { + EventManager::instance()->on(\CakeDC\Users\Controller\Component\UsersAuthComponent::EVENT_FAILED_SOCIAL_LOGIN, [new \CakeDC\Users\Controller\UsersController(), 'failedSocialLoginListener']); + } catch (MissingPluginException $e) { + Log::error($e->getMessage()); + } +} + +$oauthPath = Configure::read('OAuth.path'); +if (is_array($oauthPath)) { + Router::scope('/auth', function ($routes) use ($oauthPath) { + $routes->connect( + '/:provider', + $oauthPath, + ['provider' => implode('|', array_keys(Configure::read('OAuth.providers')))] + ); + }); +} \ No newline at end of file diff --git a/config/permissions.php b/config/permissions.php new file mode 100755 index 000000000..0f8451259 --- /dev/null +++ b/config/permissions.php @@ -0,0 +1,79 @@ + 'role' | ['roles'] | '*' + * 'prefix' => 'Prefix' | , (default = null) + * 'plugin' => 'Plugin' | , (default = null) + * 'controller' => 'Controller' | ['Controllers'] | '*', + * 'action' => 'action' | ['actions'] | '*', + * 'allowed' => true | false | callback (default = true) + * ] + * You could use '*' to match anything + * 'allowed' will be considered true if not defined. It allows a callable to manage complex + * permissions, like this + * 'allowed' => function (array $user, $role, Request $request) {} + * + * Example, using allowed callable to define permissions only for the owner of the Posts to edit/delete + * + * (remember to add the 'uses' at the top of the permissions.php file for Hash, TableRegistry and Request + [ + 'role' => ['user'], + 'controller' => ['Posts'], + 'action' => ['edit', 'delete'], + 'allowed' => function(array $user, $role, Request $request) { + $postId = Hash::get($request->params, 'pass.0'); + $post = TableRegistry::get('Posts')->get($postId); + $userId = Hash::get($user, 'id'); + if (!empty($post->user_id) && !empty($userId)) { + return $post->user_id === $userId; + } + return false; + } + ], + */ + +return [ + 'Users.SimpleRbac.permissions' => [ + [ + 'role' => '*', + 'plugin' => 'CakeDC/Users', + 'controller' => '*', + 'action' => '*', + ], + [ + 'role' => 'user', + 'plugin' => 'CakeDC/Users', + 'controller' => 'Users', + 'action' => ['register', 'edit', 'view'], + ], + [ + 'role' => 'user', + 'plugin' => 'CakeDC/Users', + 'controller' => 'Users', + 'action' => '*', + 'allowed' => false, + ], + [ + 'role' => ['user'], + 'controller' => ['Pages'], + 'action' => ['other', 'display'], + 'allowed' => true, + ], + ]]; diff --git a/config/routes.php b/config/routes.php new file mode 100755 index 000000000..6c4331798 --- /dev/null +++ b/config/routes.php @@ -0,0 +1,31 @@ + '/users'], function ($routes) { + $routes->fallbacks('DashedRoute'); + }); + +Router::connect('/auth/twitter', [ + 'plugin' => 'CakeDC/Users', + 'controller' => 'Users', + 'action' => 'twitterLogin', + 'provider' => 'twitter' + ]); +Router::connect('/accounts/validate/*', [ + 'plugin' => 'CakeDC/Users', + 'controller' => 'SocialAccounts', + 'action' => 'validate' + ]); +Router::connect('/profile/*', ['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'profile']); +Router::connect('/login', ['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'login']); +Router::connect('/logout', ['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'logout']); \ No newline at end of file diff --git a/config/users.php b/config/users.php new file mode 100755 index 000000000..582da58f2 --- /dev/null +++ b/config/users.php @@ -0,0 +1,143 @@ + [ + //Table used to manage users + 'table' => 'CakeDC/Users.Users', + //configure Auth component + 'auth' => true, + //Password Hasher + 'passwordHasher' => '\Cake\Auth\DefaultPasswordHasher', + //token expiration, 1 hour + 'Token' => ['expiration' => 3600], + 'Email' => [ + //determines if the user should include email + 'required' => true, + //determines if registration workflow includes email validation + 'validate' => true, + ], + 'Registration' => [ + //determines if the register is enabled + 'active' => true, + //determines if the reCaptcha is enabled for registration + 'reCaptcha' => true, + ], + 'Tos' => [ + //determines if the user should include tos accepted + 'required' => true, + ], + 'Social' => [ + //enable social login + 'login' => false, + ], + 'Profile' => [ + //Allow view other users profiles + 'viewOthers' => true, + 'route' => ['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'profile'], + ], + 'Key' => [ + 'Session' => [ + //session key to store the social auth data + 'social' => 'Users.social', + //userId key used in reset password workflow + 'resetPasswordUserId' => 'Users.resetPasswordUserId', + ], + //form key to store the social auth data + 'Form' => [ + 'social' => 'social' + ], + 'Data' => [ + //data key to store the users email + 'email' => 'email', + //data key to store email coming from social networks + 'socialEmail' => 'info.email', + //data key to check if the remember me option is enabled + 'rememberMe' => 'remember_me', + ], + ], + //Avatar placeholder + 'Avatar' => ['placeholder' => 'CakeDC/Users.avatar_placeholder.png'], + 'RememberMe' => [ + //configure Remember Me component + 'active' => true, + 'Cookie' => [ + 'name' => 'remember_me', + 'Config' => [ + 'expires' => '1 month', + 'httpOnly' => true, + ] + ] + ], + ], +//default configuration used to auto-load the Auth Component, override to change the way Auth works + 'Auth' => [ + 'loginAction' => [ + 'plugin' => 'CakeDC/Users', + 'controller' => 'Users', + 'action' => 'login', + 'prefix' => false + ], + 'authenticate' => [ + 'all' => [ + 'scope' => ['active' => 1] + ], + 'CakeDC/Users.ApiKey', + 'CakeDC/Users.RememberMe', + 'Form', + ], + 'authorize' => [ + 'CakeDC/Users.Superuser', + 'CakeDC/Users.SimpleRbac', + ], + ], + 'OAuth' => [ + 'path' => ['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'socialLogin', 'prefix' => false], + 'providers' => [ + 'facebook' => [ + 'className' => 'League\OAuth2\Client\Provider\Facebook', + 'options' => [ + 'graphApiVersion' => 'v2.5', + 'redirectUri' => Router::url('/auth/facebook', true) + ] + ], + 'twitter' => [ + 'options' => [ + 'redirectUri' => Router::url('/auth/twitter', true) + ] + ], + 'linkedIn' => [ + 'className' => 'League\OAuth2\Client\Provider\LinkedIn', + 'options' => [ + 'redirectUri' => Router::url('/auth/linkedIn', true) + ] + ], + 'instagram' => [ + 'className' => 'League\OAuth2\Client\Provider\Instagram', + 'options' => [ + 'redirectUri' => Router::url('/auth/instagram', true) + ] + ], + 'google' => [ + 'className' => 'League\OAuth2\Client\Provider\Google', + 'options' => [ + 'userFields' => ['url', 'aboutMe'], + 'redirectUri' => Router::url('/auth/google', true) + ] + ], + ], + ] +]; + +return $config; diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100755 index 000000000..500c19975 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,36 @@ + + + + + + + + + + + ./tests/TestCase + + + + + ./src + + + + + + + + + + + + diff --git a/src/Auth/ApiKeyAuthenticate.php b/src/Auth/ApiKeyAuthenticate.php new file mode 100755 index 000000000..e667d29bb --- /dev/null +++ b/src/Auth/ApiKeyAuthenticate.php @@ -0,0 +1,107 @@ + self::TYPE_QUERYSTRING, + //name to retrieve the api key value from + 'name' => 'api_key', + //db field where the key is stored + 'field' => 'api_token', + //require SSL to pass the token. You should always require SSL to use tokens for Auth + 'require_ssl' => true, + ]; + + /** + * Authenticate callback + * Reads the API Key based on configuration and login the user + * + * @param Request $request Cake request object. + * @param Response $response Cake response object. + * @return mixed + */ + public function authenticate(Request $request, Response $response) + { + $type = $this->config('type'); + if (!in_array($type, $this->types)) { + throw new OutOfBoundsException(__d('Users', 'Type {0} is not valid', $type)); + } + + if (!is_callable([$this, $type])) { + throw new OutOfBoundsException(__d('Users', 'Type {0} has no associated callable', $type)); + } + + $apiKey = $this->$type($request); + if (empty($apiKey)) { + return false; + } + + if ($this->config('require_ssl') && !$request->is('ssl')) { + throw new ForbiddenException(__d('Users', 'SSL is required for ApiKey Authentication', $type)); + } + + $this->_config['fields']['username'] = $this->config('field'); + $this->_config['userModel'] = Configure::read('Users.table'); + $this->_config['finder'] = 'all'; + $result = $this->_query($apiKey)->first(); + + if (empty($result)) { + return false; + } + + return $result->toArray(); + //idea: add array with checks to be passed to $request->is(...) + } + + /** + * Get the api key from the querystring + * + * @param Request $request request + * @return string api key + */ + public function querystring(Request $request) + { + $name = $this->config('name'); + return $request->query($name); + } + + /** + * Get the api key from the header + * + * @param Request $request request + * @return string api key + */ + public function header(Request $request) + { + $name = $this->config('name'); + return $request->header($name); + } +} diff --git a/src/Auth/Exception/InvalidProviderException.php b/src/Auth/Exception/InvalidProviderException.php new file mode 100755 index 000000000..1825d475c --- /dev/null +++ b/src/Auth/Exception/InvalidProviderException.php @@ -0,0 +1,10 @@ +_registry->Cookie->read($cookieName); + if (empty($cookie)) { + return false; + } + $this->config('fields.username', 'id'); + $user = $this->_findUser($cookie['id']); + if ($user && + !empty($cookie['user_agent']) && + $request->header('User-Agent') === $cookie['user_agent']) { + return $user; + } + + return false; + } +} diff --git a/src/Auth/Rules/AbstractRule.php b/src/Auth/Rules/AbstractRule.php new file mode 100755 index 000000000..f8955fbbc --- /dev/null +++ b/src/Auth/Rules/AbstractRule.php @@ -0,0 +1,92 @@ +config($config); + } + + /** + * Get a table from the alias, table object or inspecting the request for a default table + * + * @param Request $request request + * @param mixed $table table + * @return \Cake\ORM\Table + * @throw OutOfBoundsException if table alias is empty + */ + protected function _getTable(Request $request, $table = null) + { + if (empty($table)) { + return $this->_getTableFromRequest($request); + } + if ($table instanceof Table) { + return $table; + } + return TableRegistry::get($table); + } + + /** + * Inspect the request and try to retrieve a table based on the current controller + * + * @param Request $request request + * @return Table + * @throws OutOfBoundsException if table alias can't be extracted from request + */ + protected function _getTableFromRequest(Request $request) + { + $plugin = Hash::get($request->params, 'plugin'); + $controller = Hash::get($request->params, 'controller'); + $modelClass = ($plugin ? $plugin . '.' : '') . $controller; + + $this->modelFactory('Table', [$this->tableLocator(), 'get']); + if (empty($modelClass)) { + throw new OutOfBoundsException(__d('Users', 'Table alias is empty, please define a table alias, we could not extract a default table from the request')); + } + return $this->loadModel($modelClass); + } + + /** + * Check the current entity is owned by the logged in user + * + * @param array $user Auth array with the logged in data + * @param string $role role of the user + * @param Request $request current request, used to get a default table if not provided + * @return bool + * @throws OutOfBoundsException if table is not found or it doesn't have the expected fields + */ + abstract public function allowed(array $user, $role, Request $request); +} diff --git a/src/Auth/Rules/Owner.php b/src/Auth/Rules/Owner.php new file mode 100755 index 000000000..5be38835c --- /dev/null +++ b/src/Auth/Rules/Owner.php @@ -0,0 +1,83 @@ + 'user_id', + /* + * request key type to retrieve the table id, could be "params", "query", "data" to locate the table id + * example: + * yoursite.com/controller/action/XXX would be + * tableKeyType => 'params', 'tableIdParamsKey' => 'pass.0' + * yoursite.com/controlerr/action?post_id=XXX would be + * tableKeyType => 'query', 'tableIdParamsKey' => 'post_id' + * yoursite.com/controller/action [posted form with a field named post_id] would be + * tableKeyType => 'data', 'tableIdParamsKey' => 'post_id' + */ + 'tableKeyType' => 'params', + // request->params key path to retrieve the owned table id + 'tableIdParamsKey' => 'pass.0', + /* + * define table to use or pick it from controller name defaults if null + * if null, table used will be based on current controller's default table + * if string, TableRegistry::get will be used + * if Table, the table object will be used + */ + 'table' => null, + /* + * define the table id to be used to match the row id, this is useful when checking belongsToMany associations + * Example: If checking ownership in a PostsUsers table, we should use 'id' => 'post_id' + * If value is null, we'll use the $table->primaryKey() + */ + 'id' => null, + 'conditions' => [], + ]; + + /** + * {@inheritdoc} + */ + public function allowed(array $user, $role, Request $request) + { + $table = $this->_getTable($request, $this->config('table')); + //retrieve table id from request + $id = Hash::get($request->{$this->config('tableKeyType')}, $this->config('tableIdParamsKey')); + $userId = Hash::get($user, 'id'); + + try { + if (!$table->hasField($this->config('ownerForeignKey'))) { + throw new OutOfBoundsException(__d('Users', 'Missing column {0} in table {1} while checking ownership permissions for user {2}', $this->config('ownerForeignKey'), $table->alias(), $userId)); + } + } catch (Exception $ex) { + throw new OutOfBoundsException(__d('Users', 'Missing column {0} in table {1} while checking ownership permissions for user {2}', $this->config('ownerForeignKey'), $table->alias(), $userId)); + } + $idColumn = $this->config('id'); + if (empty($idColumn)) { + $idColumn = $table->primaryKey(); + } + $conditions = array_merge([ + $idColumn => $id, + $this->config('ownerForeignKey') => $userId + ], $this->config('conditions')); + + return $table->exists($conditions); + } +} diff --git a/src/Auth/Rules/Rule.php b/src/Auth/Rules/Rule.php new file mode 100755 index 000000000..7ba6c438d --- /dev/null +++ b/src/Auth/Rules/Rule.php @@ -0,0 +1,27 @@ + 'permissions', + //role field in the Users table + 'role_field' => 'role', + //default role, used in new users registered and also as role matcher when no role is available + 'default_role' => 'user', + /* + * This is a quick roles-permissions implementation + * Rules are evaluated top-down, first matching rule will apply + * Each line define + * [ + * 'role' => 'admin', + * 'plugin', (optional, default = null) + * 'prefix', (optional, default = null) + * 'controller', + * 'action', + * 'allowed' (optional, default = true) + * ] + * You could use '*' to match anything + * You could use [] to match an array of options, example 'role' => ['adm1', 'adm2'] + * You could use a callback in your 'allowed' to process complex authentication, like + * - ownership + * - permissions stored in your database + * - permission based on an external service API call + * You could use an instance of the \CakeDC\Users\Auth\Rules\Rule interface to reuse your custom rules + * + * Examples: + * 1. Callback to allow users editing their own Posts: + * + * 'allowed' => function (array $user, $role, Request $request) { + * $postId = Hash::get($request->params, 'pass.0'); + * $post = TableRegistry::get('Posts')->get($postId); + * $userId = Hash::get($user, 'id'); + * if (!empty($post->user_id) && !empty($userId)) { + * return $post->user_id === $userId; + * } + * return false; + * } + * 2. Using the Owner Rule + * 'allowed' => new Owner() //will pick by default the post id from the first pass param + * + * Check the Owner Rule docs for more details + * + * + */ + 'permissions' => [], + ]; + + /** + * Default permissions to be loaded if no provided permissions + * + * @var array + */ + protected $_defaultPermissions = [ + //admin role allowed to use CakeDC\Users plugin actions + [ + 'role' => 'admin', + 'plugin' => '*', + 'controller' => '*', + 'action' => '*', + ], + //specific actions allowed for the user role in Users plugin + [ + 'role' => 'user', + 'plugin' => 'CakeDC/Users', + 'controller' => 'Users', + 'action' => ['profile', 'logout'], + ], + //all roles allowed to Pages/display + [ + 'role' => '*', + 'plugin' => null, + 'controller' => ['Pages'], + 'action' => ['display'], + ], + ]; + + /** + * Autoload permission configuration + * @param ComponentRegistry $registry component registry + * @param array $config config + */ + public function __construct(ComponentRegistry $registry, array $config = []) + { + parent::__construct($registry, $config); + $autoload = $this->config('autoload_config'); + if ($autoload) { + $loadedPermissions = $this->_loadPermissions($autoload, 'default'); + $this->config('permissions', $loadedPermissions); + } + } + + /** + * Load config and retrieve permissions + * If the configuration file does not exist, or the permissions key not present, return defaultPermissions + * To be mocked + * + * @param string $key name of the configuration file to read permissions from + * @return array permissions + */ + protected function _loadPermissions($key) + { + try { + Configure::load($key, 'default'); + $permissions = Configure::read('Users.SimpleRbac.permissions'); + } catch (Exception $ex) { + $msg = __d('Users', 'Missing configuration file: "config/{0}.php". Using default permissions', $key); + $this->log($msg, LogLevel::WARNING); + } + + if (empty($permissions)) { + return $this->_defaultPermissions; + } + + return $permissions; + } + + /** + * Match the current plugin/controller/action against loaded permissions + * Set a default role if no role is provided + * + * @param array $user user data + * @param Request $request request + * @return bool + */ + public function authorize($user, Request $request) + { + $roleField = $this->config('role_field'); + $role = $this->config('default_role'); + if (Hash::check($user, $roleField)) { + $role = Hash::get($user, $roleField); + } + + $allowed = $this->_checkRules($user, $role, $request); + return $allowed; + } + + /** + * Match against permissions, return if matched + * Permissions are processed based on the 'permissions' config values + * + * @param array $user current user array + * @param string $role effective role for the current user + * @param Request $request request + * @return bool true if there is a match in permissions + */ + protected function _checkRules(array $user, $role, Request $request) + { + $permissions = $this->config('permissions'); + foreach ($permissions as $permission) { + $allowed = $this->_matchRule($permission, $user, $role, $request); + if ($allowed !== null) { + return $allowed; + } + } + + return false; + } + + /** + * Match the rule for current permission + * + * @param array $permission configuration + * @param array $user current user + * @param string $role effective user role + * @param Request $request request + * @return bool if rule matched, null if rule not matched + */ + protected function _matchRule($permission, $user, $role, $request) + { + $plugin = $request->plugin; + $controller = $request->controller; + $action = $request->action; + $prefix = null; + if (!empty($request->params['prefix'])) { + $prefix = $request->params['prefix']; + } + if ($this->_matchOrAsterisk($permission, 'role', $role) && + $this->_matchOrAsterisk($permission, 'prefix', $prefix, true) && + $this->_matchOrAsterisk($permission, 'plugin', $plugin, true) && + $this->_matchOrAsterisk($permission, 'controller', $controller) && + $this->_matchOrAsterisk($permission, 'action', $action)) { + $allowed = Hash::get($permission, 'allowed'); + + if ($allowed === null) { + //allowed will be true by default + return true; + } elseif (is_callable($allowed)) { + return (bool)call_user_func($allowed, $user, $role, $request); + } elseif ($allowed instanceof Rule) { + return (bool)$allowed->allowed($user, $role, $request); + } else { + return (bool)$allowed; + } + } + + return null; + } + + /** + * Check if rule matched or '*' present in rule matching anything + * + * @param string $permission permission configuration + * @param string $key key to retrieve and check in permissions configuration + * @param string $value value to check with (coming from the request) We'll check the DASHERIZED value too + * @param bool $allowEmpty true if we allow + * @return bool + */ + protected function _matchOrAsterisk($permission, $key, $value, $allowEmpty = false) + { + $possibleValues = (array)Hash::get($permission, $key); + if ($allowEmpty && empty($possibleValues) && $value === null) { + return true; + } + if (Hash::get($permission, $key) === '*' || + in_array($value, $possibleValues) || + in_array(Inflector::camelize($value, '-'), $possibleValues)) { + return true; + } + + return false; + } +} diff --git a/src/Auth/Social/Mapper/AbstractMapper.php b/src/Auth/Social/Mapper/AbstractMapper.php new file mode 100755 index 000000000..ac045815d --- /dev/null +++ b/src/Auth/Social/Mapper/AbstractMapper.php @@ -0,0 +1,114 @@ + 'id', + 'username' => 'username', + 'full_name' => 'name', + 'first_name' => 'first_name', + 'last_name' => 'last_name', + 'email' => 'email', + 'avatar' => 'avatar', + 'gender' => 'gender', + 'link' => 'link', + 'bio' => 'bio', + 'locale' => 'locale', + 'validated' => 'validated' + ]; + + /** + * Constructor + * + * @param mixed $rawData raw data + * @param mixed $mapFields map fields + */ + public function __construct($rawData, $mapFields = null) + { + $this->_rawData = $rawData; + if (!is_null($mapFields)) { + $this->_mapFields = $mapFields; + } + $this->_mapFields = array_merge($this->_defaultMapFields, $this->_mapFields); + } + /** + * Invoke method + * + * @return mixed + */ + public function __invoke() + { + return $this->_map(); + } + + /** + * If email is present the user is validated + * + * @return bool + */ + protected function _validated() + { + $email = Hash::get($this->_rawData, $this->_mapFields['email']); + return !empty($email); + } + + /** + * Maps raw data using mapFields + * + * @return mixed + */ + protected function _map() + { + $result = []; + collection($this->_mapFields)->each(function ($mappedField, $field) use (&$result) { + $value = Hash::get($this->_rawData, $mappedField); + $function = '_' . $field; + if (method_exists($this, $function)) { + $value = $this->{$function}(); + } + $result[$field] = $value; + }); + $token = Hash::get($this->_rawData, 'token'); + $result['credentials'] = [ + 'token' => is_array($token) ? Hash::get($token, 'accessToken') : $token->getToken(), + 'secret' => is_array($token) ? Hash::get($token, 'tokenSecret') : null, + 'expires' => is_array($token) ? Hash::get($token, 'expires') : $token->getExpires(), + ]; + $result['raw'] = $this->_rawData; + + return $result; + } +} diff --git a/src/Auth/Social/Mapper/Facebook.php b/src/Auth/Social/Mapper/Facebook.php new file mode 100755 index 000000000..0a2193984 --- /dev/null +++ b/src/Auth/Social/Mapper/Facebook.php @@ -0,0 +1,44 @@ + 'name', + ]; + + /** + * Get avatar url + * @return string + */ + protected function _avatar() + { + return self::FB_GRAPH_BASE_URL . Hash::get($this->_rawData, 'id') . '/picture?type=normal'; + } +} diff --git a/src/Auth/Social/Mapper/Google.php b/src/Auth/Social/Mapper/Google.php new file mode 100755 index 000000000..86e0717b8 --- /dev/null +++ b/src/Auth/Social/Mapper/Google.php @@ -0,0 +1,43 @@ + 'image.url', + 'full_name' => 'displayName', + 'email' => 'emails.0.value', + 'first_name' => 'name.givenName', + 'last_name' => 'name.familyName', + 'bio' => 'aboutMe', + 'link' => 'url' + ]; + + /** + * @return string + */ + protected function _link() + { + return Hash::get($this->_rawData, $this->_mapFields['link']) ?: '#'; + } +} diff --git a/src/Auth/Social/Mapper/Instagram.php b/src/Auth/Social/Mapper/Instagram.php new file mode 100755 index 000000000..267890f44 --- /dev/null +++ b/src/Auth/Social/Mapper/Instagram.php @@ -0,0 +1,43 @@ + 'full_name', + 'avatar' => 'profile_picture', + ]; + + /** + * @return string + */ + protected function _link() + { + return self::INSTAGRAM_BASE_URL . Hash::get($this->_rawData, $this->_mapFields['username']); + } +} diff --git a/src/Auth/Social/Mapper/LinkedIn.php b/src/Auth/Social/Mapper/LinkedIn.php new file mode 100755 index 000000000..6edd447eb --- /dev/null +++ b/src/Auth/Social/Mapper/LinkedIn.php @@ -0,0 +1,28 @@ + 'pictureUrl', + 'first_name' => 'firstName', + 'last_name' => 'lastName', + 'email' => 'emailAddress', + 'bio' => 'headline', + 'link' => 'publicProfileUrl' + ]; +} diff --git a/src/Auth/Social/Mapper/Twitter.php b/src/Auth/Social/Mapper/Twitter.php new file mode 100755 index 000000000..cf2bfe37b --- /dev/null +++ b/src/Auth/Social/Mapper/Twitter.php @@ -0,0 +1,51 @@ + 'uid', + 'username' => 'nickname', + 'full_name' => 'name', + 'first_name' => 'firstName', + 'last_name' => 'lastName', + 'email' => 'email', + 'avatar' => 'imageUrl', + 'bio' => 'description', + 'validated' => 'validated' + ]; + + /** + * @return string + */ + protected function _link() + { + return self::TWITTER_BASE_URL . Hash::get($this->_rawData, $this->_mapFields['username']); + } +} diff --git a/src/Auth/Social/Util/SocialUtils.php b/src/Auth/Social/Util/SocialUtils.php new file mode 100755 index 000000000..9fedd1779 --- /dev/null +++ b/src/Auth/Social/Util/SocialUtils.php @@ -0,0 +1,34 @@ +getShortName(); + } +} diff --git a/src/Auth/SocialAuthenticate.php b/src/Auth/SocialAuthenticate.php new file mode 100755 index 000000000..a18c7e5e9 --- /dev/null +++ b/src/Auth/SocialAuthenticate.php @@ -0,0 +1,431 @@ +normalizeConfig(array_merge($config, $oauthConfig)); + parent::__construct($registry, $config); + } + + /** + * Normalizes providers' configuration. + * + * @param array $config Array of config to normalize. + * @return array + * @throws \Exception + */ + public function normalizeConfig(array $config) + { + $config = Hash::merge((array)Configure::read('OAuth2'), $config); + + if (empty($config['providers'])) { + throw new MissingProviderConfigurationException(); + } + + array_walk($config['providers'], [$this, '_normalizeConfig'], $config); + return $config; + } + + /** + * Callback to loop through config values. + * + * @param array $config Configuration. + * @param string $alias Provider's alias (key) in configuration. + * @param array $parent Parent configuration. + * @return void + */ + protected function _normalizeConfig(&$config, $alias, $parent) + { + unset($parent['providers']); + + $defaults = [ + 'className' => null, + 'options' => [], + 'collaborators' => [], + 'mapFields' => [], + ] + $parent + $this->_defaultConfig; + + $config = array_intersect_key($config, $defaults); + $config += $defaults; + + array_walk($config, [$this, '_validateConfig']); + + foreach (['options', 'collaborators'] as $key) { + if (empty($parent[$key]) || empty($config[$key])) { + continue; + } + + $config[$key] = array_merge($parent[$key], $config[$key]); + } + } + + /** + * Validates the configuration. + * + * @param mixed $value Value. + * @param string $key Key. + * @return void + * @throws CakeDC\Users\Auth\Exception\InvalidProviderException + * @throws CakeDC\Users\Auth\Exception\InvalidSettingsException + */ + protected function _validateConfig(&$value, $key) + { + if ($key === 'className' && !class_exists($value)) { + throw new InvalidProviderException([$value]); + } elseif (!is_array($value) && in_array($key, ['options', 'collaborators'])) { + throw new InvalidSettingsException([$key]); + } + } + + /** + * Get the controller associated with the collection. + * + * @return Controller instance + */ + protected function _getController() + { + return $this->_registry->getController(); + } + + /** + * Get a user based on information in the request. + * + * @param \Cake\Network\Request $request Request object. + * @param \Cake\Network\Response $response Response object. + * @return bool + * @throws \RuntimeException If the `CakeDC/Users/OAuth2.newUser` event is missing or returns empty. + */ + public function authenticate(Request $request, Response $response) + { + return $this->getUser($request); + } + + /** + * Authenticates with OAuth2 provider by getting an access token and + * retrieving the authorized user's profile data. + * + * @param \Cake\Network\Request $request Request object. + * @return array|bool + */ + protected function _authenticate(Request $request) + { + if (!$this->_validate($request)) { + return false; + } + + $provider = $this->provider($request); + $code = $request->query('code'); + + try { + $token = $provider->getAccessToken('authorization_code', compact('code')); + return compact('token') + $provider->getResourceOwner($token)->toArray(); + } catch (\Exception $e) { + return false; + } + } + + /** + * Validates OAuth2 request. + * + * @param \Cake\Network\Request $request Request object. + * @return bool + */ + protected function _validate(Request $request) + { + if (!array_key_exists('code', $request->query) || !$this->provider($request)) { + return false; + } + + $session = $request->session(); + $sessionKey = 'oauth2state'; + $state = $request->query('state'); + + if ($this->config('options.state') && + (!$state || $state !== $session->read($sessionKey))) { + $session->delete($sessionKey); + return false; + } + + return true; + } + + /** + * Maps raw provider's user profile data to local user's data schema. + * + * @param array $data Raw user data. + * @return array + */ + protected function _map($data) + { + if (!$map = $this->config('mapFields')) { + return $data; + } + + foreach ($map as $dst => $src) { + $data[$dst] = $data[$src]; + unset($data[$src]); + } + + return $data; + } + + /** + * Handles unauthenticated access attempts. Will automatically forward to the + * requested provider's authorization URL to let the user grant access to the + * application. + * + * @param \Cake\Network\Request $request Request object. + * @param \Cake\Network\Response $response Response object. + * @return \Cake\Network\Response|null + */ + public function unauthenticated(Request $request, Response $response) + { + $provider = $this->provider($request); + if (empty($provider) || !empty($request->query['code'])) { + return null; + } + + if ($this->config('options.state')) { + $request->session()->write('oauth2state', $provider->getState()); + } + + $response->location($provider->getAuthorizationUrl()); + return $response; + } + + /** + * Returns the `$request`-ed provider. + * + * @param \Cake\Network\Request $request Current HTTP request. + * @return \League\Oauth2\Client\Provider\GenericProvider|false + */ + public function provider(Request $request) + { + if (!$alias = $request->param('provider')) { + return false; + } + + if (empty($this->_provider)) { + $this->_provider = $this->_getProvider($alias); + } + + return $this->_provider; + } + + /** + * Instantiates provider object. + * + * @param string $alias of the provider. + * @return \League\Oauth2\Client\Provider\GenericProvider + */ + protected function _getProvider($alias) + { + if (!$config = $this->config('providers.' . $alias)) { + return false; + } + + $this->config($config); + + if (is_object($config) && $config instanceof AbstractProvider) { + return $config; + } + + $class = $config['className']; + return new $class($config['options'], $config['collaborators']); + } + + /** + * Find or create local user + * + * @param array $data data + * @return array|bool|mixed + * @throws MissingEmailException + */ + protected function _touch(array $data) + { + try { + if (empty($data['provider']) && !empty($this->_provider)) { + $data['provider'] = SocialUtils::getProvider($this->_provider); + } + $user = $this->_socialLogin($data); + } catch (UserNotActiveException $ex) { + $exception = $ex; + } catch (AccountNotActiveException $ex) { + $exception = $ex; + } catch (MissingEmailException $ex) { + $exception = $ex; + } + if (!empty($exception)) { + $args = ['exception' => $exception, 'rawData' => $data]; + $event = new Event(UsersAuthComponent::EVENT_FAILED_SOCIAL_LOGIN, $args); + $event = EventManager::instance()->dispatch($event); + if (method_exists($this->_getController(), 'failedSocialLogin')) { + $this->_getController()->failedSocialLogin($exception, $data, true); + } + return $event->result; + } + + if (!empty($user->username)) { + $user = $this->_findUser($user->username); + } + return $user; + } + + /** + * Get a user based on information in the request. + * + * @param \Cake\Network\Request $request Request object. + * @return mixed Either false or an array of user information + * @throws \RuntimeException If the `CakeDC/Users/OAuth2.newUser` event is missing or returns empty. + */ + public function getUser(Request $request) + { + $data = $request->session()->read(Configure::read('Users.Key.Session.social')); + $requestDataEmail = $request->data('email'); + if (!empty($data) && (!empty($data['email']) || !empty($requestDataEmail))) { + if (!empty($requestDataEmail)) { + $data['email'] = $requestDataEmail; + } + $user = $data; + $request->session()->delete(Configure::read('Users.Key.Session.social')); + } else { + if (empty($data) && !$rawData = $this->_authenticate($request)) { + return false; + } + if (empty($rawData)) { + $rawData = $data; + } + + $provider = $this->_getProviderName($request); + $user = $this->_mapUser($provider, $rawData); + if ($user['provider'] === SocialAccountsTable::PROVIDER_TWITTER) { + $request->session()->write(Configure::read('Users.Key.Session.social'), $user); + } + } + + if (!$user || !$this->config('userModel')) { + return false; + } + + if (!$result = $this->_touch($user)) { + return false; + } + + if ($request->session()->check(Configure::read('Users.Key.Session.social'))) { + $request->session()->delete(Configure::read('Users.Key.Session.social')); + } + return $result; + } + + /** + * Get the provider name based on the request or on the provider set. + * + * @param \Cake\Network\Request $request Request object. + * @return mixed Either false or an array of user information + */ + protected function _getProviderName($request = null) + { + $provider = false; + if (!is_null($this->_provider)) { + $provider = SocialUtils::getProvider($this->_provider); + } elseif (!empty($request)) { + $provider = ucfirst($request->param('provider')); + } + return $provider; + } + + /** + * Get the provider name based on the request or on the provider set. + * + * @param string $provider Provider name. + * @param array $data User data + * @throws MissingProviderException + * @return mixed Either false or an array of user information + */ + protected function _mapUser($provider, $data) + { + if (empty($provider)) { + throw new MissingProviderException(__d('Users', "Provider cannot be empty")); + } + $providerMapperClass = "\\CakeDC\\Users\\Auth\\Social\\Mapper\\$provider"; + $providerMapper = new $providerMapperClass($data); + $user = $providerMapper(); + $user['provider'] = $provider; + return $user; + } + + /** + * @param mixed $data data + * @return mixed + */ + protected function _socialLogin($data) + { + $options = [ + 'use_email' => Configure::read('Users.Email.required'), + 'validate_email' => Configure::read('Users.Email.validate'), + 'token_expiration' => Configure::read('Users.Token.expiration') + ]; + + $userModel = Configure::read('Users.table'); + $User = TableRegistry::get($userModel); + $user = $User->socialLogin($data, $options); + return $user; + } +} diff --git a/src/Auth/SuperuserAuthorize.php b/src/Auth/SuperuserAuthorize.php new file mode 100755 index 000000000..3a37f24c4 --- /dev/null +++ b/src/Auth/SuperuserAuthorize.php @@ -0,0 +1,53 @@ + 'is_superuser', + ]; + + /** + * Check if the user is superuser + * + * @param type $user User information object. + * @param Request $request Cake request object. + * @return bool + */ + public function authorize($user, Request $request) + { + $user = (array)$user; + $superuserField = $this->config('superuser_field'); + if (Hash::check($user, $superuserField)) { + return (bool)Hash::get($user, $superuserField); + } + + return false; + } +} diff --git a/src/Controller/AppController.php b/src/Controller/AppController.php new file mode 100755 index 000000000..6aa6f5872 --- /dev/null +++ b/src/Controller/AppController.php @@ -0,0 +1,33 @@ +loadComponent('Security'); + $this->loadComponent('Csrf'); + $this->loadComponent('CakeDC/Users.UsersAuth'); + } +} diff --git a/src/Controller/Component/RememberMeComponent.php b/src/Controller/Component/RememberMeComponent.php new file mode 100755 index 000000000..488fa272f --- /dev/null +++ b/src/Controller/Component/RememberMeComponent.php @@ -0,0 +1,152 @@ +_cookieName = Configure::read('Users.RememberMe.Cookie.name'); + $this->_validateConfig(); + $this->setCookieOptions(); + $this->_attachEvents(); + } + + /** + * Validate component config + * + * @throws InvalidArgumentException + * @return void + */ + protected function _validateConfig() + { + if (mb_strlen(Security::salt(), '8bit') < 32) { + throw new InvalidArgumentException( + __d('Users', 'Invalid app salt, app salt must be at least 256 bits (32 bytes) long') + ); + } + } + + /** + * Attach the afterLogin and beforeLogount events + * + * @return void + */ + protected function _attachEvents() + { + $eventManager = $this->_registry->getController()->eventManager(); + $eventManager->on(UsersAuthComponent::EVENT_AFTER_LOGIN, [], [$this, 'setLoginCookie']); + $eventManager->on(UsersAuthComponent::EVENT_BEFORE_LOGOUT, [], [$this, 'destroy']); + } + + /** + * Sets cookie configuration options + * + * @return void + */ + public function setCookieOptions() + { + $cookieConfig = Configure::read('Users.RememberMe.Cookie.Config'); + $this->Cookie->configKey($this->_cookieName, $cookieConfig); + } + + /** + * Sets the login cookie that handles the remember me feature + * + * @param Event $event event + * @return void + */ + public function setLoginCookie(Event $event) + { + $user['id'] = $this->Auth->user('id'); + if (empty($user)) { + return; + } + $user['user_agent'] = $this->request->header('User-Agent'); + $this->Cookie->write($this->_cookieName, $user); + } + + /** + * Destroys the remember me cookie + * + * @param Event $event event + * @return void + */ + public function destroy(Event $event) + { + if ($this->Cookie->check($this->_cookieName)) { + $this->Cookie->delete($this->_cookieName); + } + } + + /** + * Reads the stored cookie and auto login the user if present + * + * @param Event $event event + * @return mixed + */ + public function beforeFilter(Event $event) + { + $user = $this->Auth->user(); + if (!empty($user) || $this->request->is(['post', 'put']) || $this->request->action === 'logout' || $this->request->session()->check(Configure::read('Users.Key.Session.social')) || $this->request->param('provider')) { + return; + } + + $user = $this->Auth->identify(); + //No user no cookies + if (empty($user)) { + return; + } + $this->Auth->setUser($user); + $event = $this->_registry->getController()->dispatchEvent(UsersAuthComponent::EVENT_AFTER_COOKIE_LOGIN); + if (is_array($event->result)) { + return $this->_registry->getController()->redirect($event->result); + } + $url = $this->Auth->redirectUrl(); + return $this->_registry->getController()->redirect($url); + } +} diff --git a/src/Controller/Component/UsersAuthComponent.php b/src/Controller/Component/UsersAuthComponent.php new file mode 100755 index 000000000..471a828e3 --- /dev/null +++ b/src/Controller/Component/UsersAuthComponent.php @@ -0,0 +1,161 @@ +_validateConfig(); + $this->_initAuth(); + + if (Configure::read('Users.Social.login')) { + $this->_loadSocialLogin(); + } + if (Configure::read('Users.RememberMe.active')) { + $this->_loadRememberMe(); + } + + $this->_attachPermissionChecker(); + } + + /** + * Load Social Auth object + * + * @return void + */ + protected function _loadSocialLogin() + { + $this->_registry->getController()->Auth->config('authenticate', [ + 'CakeDC/Users.Social' + ], true); + } + + /** + * Load RememberMe component and Auth objects + * + * @return void + */ + protected function _loadRememberMe() + { + $this->_registry->getController()->loadComponent('CakeDC/Users.RememberMe'); + } + + /** + * Attach the isUrlAuthorized event to allow using the Auth authorize from the UserHelper + * + * @return void + */ + protected function _attachPermissionChecker() + { + $this->_registry->getController()->eventManager()->on(self::EVENT_IS_AUTHORIZED, [], [$this, 'isUrlAuthorized']); + } + + /** + * Initialize the AuthComponent and configure allowed actions + * + * @return void + */ + protected function _initAuth() + { + if (Configure::read('Users.auth')) { + //initialize Auth + $this->_registry->getController()->loadComponent('Auth', Configure::read('Auth')); + } + + $this->_registry->getController()->Auth->allow([ + 'register', + 'validateEmail', + 'resendTokenValidation', + 'login', + 'twitterLogin', + 'socialEmail', + 'resetPassword', + 'requestResetPassword', + 'changePassword', + 'endpoint', + 'authenticated' + ]); + } + + /** + * Check if a given url is authorized + * + * @param Event $event event + * + * @return bool + */ + public function isUrlAuthorized(Event $event) + { + $user = $this->_registry->getController()->Auth->user(); + if (empty($user)) { + return false; + } + $url = Hash::get((array)$event->data, 'url'); + if (empty($url)) { + return false; + } + + if (is_array($url)) { + $requestUrl = Router::reverse($url); + $requestParams = Router::parse($requestUrl); + } else { + $requestParams = Router::parse($url); + $requestUrl = $url; + } + $request = new Request($requestUrl); + $request->params = $requestParams; + + $isAuthorized = $this->_registry->getController()->Auth->isAuthorized(null, $request); + return $isAuthorized; + } + + /** + * Validate if the passed configuration makes sense + * + * @throws BadConfigurationException + * @return void + */ + protected function _validateConfig() + { + if (!Configure::read('Users.Email.required') && Configure::read('Users.Email.validate')) { + $message = __d('Users', 'You can\'t enable email validation workflow if use_email is false'); + throw new BadConfigurationException($message); + } + } +} diff --git a/src/Controller/SocialAccountsController.php b/src/Controller/SocialAccountsController.php new file mode 100755 index 000000000..5606f204b --- /dev/null +++ b/src/Controller/SocialAccountsController.php @@ -0,0 +1,92 @@ +Auth->allow(['validateAccount', 'resendValidation']); + } + + /** + * Validates social account + * + * @param string $provider provider + * @param string $reference reference + * @param string $token token + * @return Response + */ + public function validateAccount($provider, $reference, $token) + { + try { + $result = $this->SocialAccounts->validateAccount($provider, $reference, $token); + if ($result) { + $this->Flash->success(__d('Users', 'Account validated successfully')); + } else { + $this->Flash->error(__d('Users', 'Account could not be validated')); + } + } catch (RecordNotFoundException $exception) { + $this->Flash->error(__d('Users', 'Invalid token and/or social account')); + } catch (AccountAlreadyActiveException $exception) { + $this->Flash->error(__d('Users', 'SocialAccount already active')); + } catch (Exception $exception) { + $this->Flash->error(__d('Users', 'Social Account could not be validated')); + } + return $this->redirect(['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'login']); + } + + /** + * Resends validation email if required + * + * @param string $provider provider + * @param string $reference reference + * @return Response|void + * @throws \Users\Model\Table\AccountAlreadyActiveException + */ + public function resendValidation($provider, $reference) + { + try { + $result = $this->SocialAccounts->resendValidation($provider, $reference); + if ($result) { + $this->Flash->success(__d('Users', 'Email sent successfully')); + } else { + $this->Flash->error(__d('Users', 'Email could not be sent')); + } + } catch (RecordNotFoundException $exception) { + $this->Flash->error(__d('Users', 'Invalid account')); + } catch (AccountAlreadyActiveException $exception) { + $this->Flash->error(__d('Users', 'Social Account already active')); + } catch (Exception $exception) { + $this->Flash->error(__d('Users', 'Email could not be resent')); + } + return $this->redirect(['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'login']); + } +} diff --git a/src/Controller/Traits/CustomUsersTableTrait.php b/src/Controller/Traits/CustomUsersTableTrait.php new file mode 100755 index 000000000..5a070ac1c --- /dev/null +++ b/src/Controller/Traits/CustomUsersTableTrait.php @@ -0,0 +1,50 @@ +_usersTable instanceof Table) { + return $this->_usersTable; + } + $this->_usersTable = TableRegistry::get(Configure::read('Users.table')); + return $this->_usersTable; + } + + /** + * Set the users table + * + * @param Table $table table + * @return void + */ + public function setUsersTable(Table $table) + { + $this->_usersTable = $table; + } +} diff --git a/src/Controller/Traits/LoginTrait.php b/src/Controller/Traits/LoginTrait.php new file mode 100755 index 000000000..2212b8892 --- /dev/null +++ b/src/Controller/Traits/LoginTrait.php @@ -0,0 +1,231 @@ +autoRender = false; + $server = new Twitter([ + 'identifier' => Configure::read('OAuth.providers.twitter.options.clientId'), + 'secret' => Configure::read('OAuth.providers.twitter.options.clientSecret'), + 'callbackUri' => Configure::read('OAuth.providers.twitter.options.redirectUri'), + ]); + $oauthToken = $this->request->query('oauth_token'); + $oauthVerifier = $this->request->query('oauth_verifier'); + if (!empty($oauthToken) && !empty($oauthVerifier)) { + $temporaryCredentials = $this->request->session()->read('temporary_credentials'); + $tokenCredentials = $server->getTokenCredentials($temporaryCredentials, $oauthToken, $oauthVerifier); + $user = (array)$server->getUserDetails($tokenCredentials); + $user['token'] = [ + 'accessToken' => $tokenCredentials->getIdentifier(), + 'tokenSecret' => $tokenCredentials->getSecret(), + ]; + $this->request->session()->write(Configure::read('Users.Key.Session.social'), $user); + try { + $user = $this->Auth->identify(); + $this->_afterIdentifyUser($user, true); + } catch (UserNotActiveException $ex) { + $exception = $ex; + } catch (AccountNotActiveException $ex) { + $exception = $ex; + } catch (MissingEmailException $ex) { + $exception = $ex; + } + + if (!empty($exception)) { + return $this->failedSocialLogin($exception, $this->request->session()->read(Configure::read('Users.Key.Session.social')), true); + } + } else { + $temporaryCredentials = $server->getTemporaryCredentials(); + $this->request->session()->write('temporary_credentials', $temporaryCredentials); + $url = $server->getAuthorizationUrl($temporaryCredentials); + return $this->redirect($url); + } + } + + /** + * @param Event $event event + * @return void + */ + public function failedSocialLoginListener(Event $event) + { + return $this->failedSocialLogin($event->data['exception'], $event->data['rawData'], true); + } + + /** + * @param mixed $exception exception + * @param mixed $data data + * @param bool|false $flash flash + * @return mixed + */ + public function failedSocialLogin($exception, $data, $flash = false) + { + $msg = __d('Users', 'Issues trying to log in with your social account'); + + if (isset($exception)) { + if ($exception instanceof MissingEmailException) { + if ($flash) { + $this->Flash->success(__d('Users', 'Please enter your email')); + } + $this->request->session()->write(Configure::read('Users.Key.Session.social'), $data); + return $this->redirect(['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'socialEmail']); + } + if ($exception instanceof UserNotActiveException) { + $msg = __d('Users', 'Your user has not been validated yet. Please check your inbox for instructions'); + } elseif ($exception instanceof AccountNotActiveException) { + $msg = __d('Users', 'Your social account has not been validated yet. Please check your inbox for instructions'); + } + } + if ($flash) { + $this->Auth->config('authError', $msg); + $this->Auth->config('flash.params', ['class' => 'success']); + $this->request->session()->delete(Configure::read('Users.Key.Session.social')); + $this->Flash->success(__d('Users', $msg)); + } + return $this->redirect(['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'login']); + } + + /** + * Social login + * + * @throws NotFoundException + * @return array + */ + public function socialLogin() + { + $socialProvider = $this->request->param('provider'); + $socialUser = $this->request->session()->read(Configure::read('Users.Key.Session.social')); + + if (empty($socialProvider) && empty($socialUser)) { + throw new NotFoundException(); + } + $user = $this->Auth->user(); + return $this->_afterIdentifyUser($user, true); + } + + /** + * Login user + * + * @return mixed + */ + public function login() + { + $event = $this->dispatchEvent(UsersAuthComponent::EVENT_BEFORE_LOGIN); + if (is_array($event->result)) { + return $this->_afterIdentifyUser($event->result); + } + if ($event->isStopped()) { + return $this->redirect($event->result); + } + + $socialLogin = $this->_isSocialLogin(); + + if (!$this->request->is('post') && !$socialLogin) { + if ($this->Auth->user()) { + $msg = __d('Users', 'You are already logged in'); + $this->Flash->error($msg); + return $this->redirect($this->referer()); + } + + return; + } + if ($this->request->is('post')) { + $user = $this->Auth->identify(); + return $this->_afterIdentifyUser($user, $socialLogin); + } + } + + /** + * Update remember me and determine redirect url after user identified + * @param array $user user data after identified + * @param bool $socialLogin is social login + * @return array + */ + protected function _afterIdentifyUser($user, $socialLogin = false) + { + if (!empty($user)) { + $this->Auth->setUser($user); + + $event = $this->dispatchEvent(UsersAuthComponent::EVENT_AFTER_LOGIN, ['user' => $user]); + if (is_array($event->result)) { + return $this->redirect($event->result); + } + $url = $this->Auth->redirectUrl(); + return $this->redirect($url); + } else { + if (!$socialLogin) { + $message = __d('Users', 'Username or password is incorrect'); + $this->Flash->error($message, 'default', [], 'auth'); + } + + return $this->redirect(Configure::read('Auth.loginAction')); + } + } + + /** + * Logout + * + * @return type + */ + public function logout() + { + $eventBefore = $this->dispatchEvent(UsersAuthComponent::EVENT_BEFORE_LOGOUT); + if (is_array($eventBefore->result)) { + return $this->redirect($eventBefore->result); + } + + $this->request->session()->destroy(); + $this->Flash->success(__d('Users', 'You\'ve successfully logged out')); + + $eventAfter = $this->dispatchEvent(UsersAuthComponent::EVENT_AFTER_LOGOUT); + if (is_array($eventAfter->result)) { + return $this->redirect($eventAfter->result); + } + + return $this->redirect($this->Auth->logout()); + } + + /** + * Check if we are doing a social login + * + * @return bool true if social login is enabled and we are processing the social login + * data in the request + */ + protected function _isSocialLogin() + { + return Configure::read('Users.Social.login') && + $this->request->session()->check(Configure::read('Users.Key.Session.social')); + } +} diff --git a/src/Controller/Traits/PasswordManagementTrait.php b/src/Controller/Traits/PasswordManagementTrait.php new file mode 100755 index 000000000..b7045ce57 --- /dev/null +++ b/src/Controller/Traits/PasswordManagementTrait.php @@ -0,0 +1,119 @@ +getUsersTable()->newEntity(); + $id = $this->Auth->user('id'); + if (!empty($id)) { + $user->id = $this->Auth->user('id'); + $validatePassword = true; + //@todo add to the documentation: list of routes used + $redirect = Configure::read('Users.Profile.route'); + } else { + $user->id = $this->request->session()->read(Configure::read('Users.Key.Session.resetPasswordUserId')); + $validatePassword = false; + //@todo add to the documentation: list of routes used + $redirect = $this->Auth->config('loginAction'); + } + $this->set('validatePassword', $validatePassword); + if ($this->request->is('post')) { + try { + $user = $this->getUsersTable()->patchEntity($user, $this->request->data(), ['validate' => 'passwordConfirm']); + if ($user->errors()) { + $this->Flash->error(__d('Users', 'Password could not be changed')); + } else { + $user = $this->getUsersTable()->changePassword($user); + if ($user) { + $this->Flash->success(__d('Users', 'Password has been changed successfully')); + return $this->redirect($redirect); + } else { + $this->Flash->error(__d('Users', 'Password could not be changed')); + } + } + } catch (UserNotFoundException $exception) { + $this->Flash->error(__d('Users', 'User was not found')); + } catch (WrongPasswordException $wpe) { + $this->Flash->error(__d('Users', 'The current password does not match')); + } catch (Exception $exception) { + $this->Flash->error(__d('Users', 'Password could not be changed')); + } + } + $this->set(compact('user')); + $this->set('_serialize', ['user']); + } + + /** + * Reset password + * + * @param null $token token data. + * @return void + */ + public function resetPassword($token = null) + { + $this->validate('password', $token); + } + + /** + * Reset password + * + * @return void|\Cake\Network\Response + */ + public function requestResetPassword() + { + $this->set('user', $this->getUsersTable()->newEntity()); + $this->set('_serialize', ['user']); + if (!$this->request->is('post')) { + return; + } + + $reference = $this->request->data('reference'); + try { + $resetUser = $this->getUsersTable()->resetToken($reference, [ + 'expiration' => Configure::read('Users.Token.expiration'), + 'checkActive' => false, + 'sendEmail' => true, + ]); + if ($resetUser) { + $msg = __d('Users', 'Please check your email to continue with password reset process'); + $this->Flash->success($msg); + } else { + $msg = __d('Users', 'The password token could not be generated. Please try again'); + $this->Flash->error($msg); + } + return $this->redirect(['action' => 'login']); + } catch (UserNotFoundException $exception) { + $this->Flash->error(__d('Users', 'User {0} was not found', $reference)); + } catch (Exception $exception) { + $this->Flash->error(__d('Users', 'Token could not be reset')); + } + } +} diff --git a/src/Controller/Traits/ProfileTrait.php b/src/Controller/Traits/ProfileTrait.php new file mode 100755 index 000000000..0d68ac6bc --- /dev/null +++ b/src/Controller/Traits/ProfileTrait.php @@ -0,0 +1,58 @@ +Auth->user('id'); + $isCurrentUser = false; + if (!Configure::read('Users.Profile.viewOthers') || empty($id)) { + $id = $loggedUserId; + } + try { + $appContain = (array)Configure::read('Auth.authenticate.' . AuthComponent::ALL . '.contain'); + $socialContain = Configure::read('Users.Social.login') ? ['SocialAccounts']: []; + $user = $this->getUsersTable()->get($id, [ + 'contain' => array_merge((array)$appContain, (array)$socialContain) + ]); + $this->set('avatarPlaceholder', Configure::read('Users.Avatar.placeholder')); + if ($user->id === $loggedUserId) { + $isCurrentUser = true; + } + } catch (RecordNotFoundException $ex) { + $this->Flash->error(__d('Users', 'User was not found')); + return $this->redirect($this->request->referer()); + } catch (InvalidPrimaryKeyException $ex) { + $this->Flash->error(__d('Users', 'Not authorized, please login first')); + return $this->redirect($this->request->referer()); + } + $this->set(compact('user', 'isCurrentUser')); + $this->set('_serialize', ['user', 'isCurrentUser']); + } +} diff --git a/src/Controller/Traits/ReCaptchaTrait.php b/src/Controller/Traits/ReCaptchaTrait.php new file mode 100755 index 000000000..825e410f8 --- /dev/null +++ b/src/Controller/Traits/ReCaptchaTrait.php @@ -0,0 +1,56 @@ +_getReCaptchaInstance(); + if (!empty($recaptcha)) { + $response = $recaptcha->verify($recaptchaResponse, $clientIp); + $validReCaptcha = $response->isSuccess(); + } + return $validReCaptcha; + } + + /** + * Create reCaptcha instance if enabled in configuration + * + * @return ReCaptcha + */ + protected function _getReCaptchaInstance() + { + $useReCaptcha = (bool)Configure::read('Users.Registration.reCaptcha'); + $reCaptchaSecret = Configure::read('reCaptcha.secret'); + if ($useReCaptcha && !empty($reCaptchaSecret)) { + return new ReCaptcha($reCaptchaSecret); + } + return null; + } +} diff --git a/src/Controller/Traits/RegisterTrait.php b/src/Controller/Traits/RegisterTrait.php new file mode 100755 index 000000000..822618cd9 --- /dev/null +++ b/src/Controller/Traits/RegisterTrait.php @@ -0,0 +1,138 @@ +getUsersTable(); + $user = $usersTable->newEntity(); + $validateEmail = (bool)Configure::read('Users.Email.validate'); + $useTos = (bool)Configure::read('Users.Tos.required'); + $tokenExpiration = Configure::read('Users.Token.expiration'); + $options = [ + 'token_expiration' => $tokenExpiration, + 'validate_email' => $validateEmail, + 'use_tos' => $useTos + ]; + $requestData = $this->request->data; + $event = $this->dispatchEvent(UsersAuthComponent::EVENT_BEFORE_REGISTER, [ + 'usersTable' => $usersTable, + 'options' => $options, + 'userEntity' => $user, + ]); + + if ($event->result instanceof EntityInterface) { + if ($userSaved = $usersTable->register($user, $event->result->toArray(), $options)) { + return $this->_afterRegister($userSaved); + } + } + if ($event->isStopped()) { + return $this->redirect($event->result); + } + + $this->set(compact('user')); + $this->set('_serialize', ['user']); + + if (!$this->request->is('post')) { + return; + } + + $validPost = $this->_validateRegisterPost(); + if (!$validPost) { + $this->Flash->error(__d('Users', 'The reCaptcha could not be validated')); + return; + } + + $userSaved = $usersTable->register($user, $requestData, $options); + if (!$userSaved) { + $this->Flash->error(__d('Users', 'The user could not be saved')); + return; + } + + return $this->_afterRegister($userSaved); + } + + /** + * Check the POST and validate it for registration, for now we check the reCaptcha + * + * @return bool + */ + protected function _validateRegisterPost() + { + if (!Configure::read('Users.Registration.reCaptcha')) { + return true; + } + $validReCaptcha = $this->validateReCaptcha( + $this->request->data('g-recaptcha-response'), + $this->request->clientIp() + ); + return $validReCaptcha; + } + + /** + * Prepare flash messages after registration, and dispatch afterRegister event + * + * @param EntityInterface $userSaved User entity saved + * @return Response + */ + protected function _afterRegister(EntityInterface $userSaved) + { + $validateEmail = (bool)Configure::read('Users.Email.validate'); + $message = __d('Users', 'You have registered successfully, please log in'); + if ($validateEmail) { + $message = __d('Users', 'Please validate your account before log in'); + } + $event = $this->dispatchEvent(UsersAuthComponent::EVENT_AFTER_REGISTER, [ + 'user' => $userSaved + ]); + if ($event->result instanceof Response) { + return $event->result; + } + $this->Flash->success($message); + return $this->redirect(['action' => 'login']); + } + + /** + * Validate an email + * + * @param string $token token + * @return void + */ + public function validateEmail($token = null) + { + $this->validate('email', $token); + } +} diff --git a/src/Controller/Traits/SimpleCrudTrait.php b/src/Controller/Traits/SimpleCrudTrait.php new file mode 100755 index 000000000..63fd75b72 --- /dev/null +++ b/src/Controller/Traits/SimpleCrudTrait.php @@ -0,0 +1,134 @@ +loadModel(); + $tableAlias = $table->alias(); + $this->set($tableAlias, $this->paginate($table)); + $this->set('tableAlias', $tableAlias); + $this->set('_serialize', [$tableAlias, 'tableAlias']); + } + + /** + * View method + * + * @param string|null $id User id. + * @return void + * @throws NotFoundException When record not found. + */ + public function view($id = null) + { + $table = $this->loadModel(); + $tableAlias = $table->alias(); + $entity = $table->get($id, [ + 'contain' => [] + ]); + $this->set($tableAlias, $entity); + $this->set('tableAlias', $tableAlias); + $this->set('_serialize', [$tableAlias, 'tableAlias']); + } + + /** + * Add method + * + * @return void Redirects on successful add, renders view otherwise. + */ + public function add() + { + $table = $this->loadModel(); + $tableAlias = $table->alias(); + $entity = $table->newEntity(); + $this->set($tableAlias, $entity); + $this->set('tableAlias', $tableAlias); + $this->set('_serialize', [$tableAlias, 'tableAlias']); + if (!$this->request->is('post')) { + return; + } + $entity = $table->patchEntity($entity, $this->request->data); + $singular = Inflector::singularize(Inflector::humanize($tableAlias)); + if ($table->save($entity)) { + $this->Flash->success(__d('Users', 'The {0} has been saved', $singular)); + return $this->redirect(['action' => 'index']); + } + $this->Flash->error(__d('Users', 'The {0} could not be saved', $singular)); + } + + /** + * Edit method + * + * @param string|null $id User id. + * @return void Redirects on successful edit, renders view otherwise. + * @throws NotFoundException When record not found. + */ + public function edit($id = null) + { + $table = $this->loadModel(); + $tableAlias = $table->alias(); + $entity = $table->get($id, [ + 'contain' => [] + ]); + $this->set($tableAlias, $entity); + $this->set('tableAlias', $tableAlias); + $this->set('_serialize', [$tableAlias, 'tableAlias']); + if (!$this->request->is(['patch', 'post', 'put'])) { + return; + } + $entity = $table->patchEntity($entity, $this->request->data); + $singular = Inflector::singularize(Inflector::humanize($tableAlias)); + if ($table->save($entity)) { + $this->Flash->success(__d('Users', 'The {0} has been saved', $singular)); + return $this->redirect(['action' => 'index']); + } + $this->Flash->error(__d('Users', 'The {0} could not be saved', $singular)); + } + + /** + * Delete method + * + * @param string|null $id User id. + * @return Response Redirects to index. + * @throws NotFoundException When record not found. + */ + public function delete($id = null) + { + $this->request->allowMethod(['post', 'delete']); + $table = $this->loadModel(); + $tableAlias = $table->alias(); + $entity = $table->get($id, [ + 'contain' => [] + ]); + $singular = Inflector::singularize(Inflector::humanize($tableAlias)); + if ($table->delete($entity)) { + $this->Flash->success(__d('Users', 'The {0} has been deleted', $singular)); + } else { + $this->Flash->error(__d('Users', 'The {0} could not be deleted', $singular)); + } + return $this->redirect(['action' => 'index']); + } +} diff --git a/src/Controller/Traits/SocialTrait.php b/src/Controller/Traits/SocialTrait.php new file mode 100755 index 000000000..05a90f957 --- /dev/null +++ b/src/Controller/Traits/SocialTrait.php @@ -0,0 +1,46 @@ +request->session()->check(Configure::read('Users.Key.Session.social'))) { + throw new NotFoundException(); + } + $this->request->session()->delete('Flash.auth'); + + if ($this->request->is('post')) { + $validPost = $this->_validateRegisterPost(); + if (!$validPost) { + $this->Flash->error(__d('Users', 'The reCaptcha could not be validated')); + return; + } + $user = $this->Auth->identify(); + return $this->_afterIdentifyUser($user, true); + } + } +} diff --git a/src/Controller/Traits/UserValidationTrait.php b/src/Controller/Traits/UserValidationTrait.php new file mode 100755 index 000000000..74836e61a --- /dev/null +++ b/src/Controller/Traits/UserValidationTrait.php @@ -0,0 +1,105 @@ +getUsersTable()->validate($token, 'activateUser'); + if ($result) { + $this->Flash->success(__d('Users', 'User account validated successfully')); + } else { + $this->Flash->error(__d('Users', 'User account could not be validated')); + } + } catch (UserAlreadyActiveException $exception) { + $this->Flash->error(__d('Users', 'User already active')); + } + break; + case 'password': + $result = $this->getUsersTable()->validate($token); + if (!empty($result)) { + $this->Flash->success(__d('Users', 'Reset password token was validated successfully')); + $this->request->session()->write(Configure::read('Users.Key.Session.resetPasswordUserId'), $result->id); + return $this->redirect(['action' => 'changePassword']); + } else { + $this->Flash->error(__d('Users', 'Reset password token could not be validated')); + } + break; + default: + $this->Flash->error(__d('Users', 'Invalid validation type')); + } + } catch (UserNotFoundException $ex) { + $this->Flash->error(__d('Users', 'Invalid token or user account already validated')); + } catch (TokenExpiredException $ex) { + $this->Flash->error(__d('Users', 'Token already expired')); + } + + return $this->redirect(['action' => 'login']); + } + + /** + * Resend Token validation + * + * @return mixed + */ + public function resendTokenValidation() + { + $this->set('user', $this->getUsersTable()->newEntity()); + $this->set('_serialize', ['user']); + if (!$this->request->is('post')) { + return; + } + $reference = $this->request->data('reference'); + try { + if ($this->getUsersTable()->resetToken($reference, [ + 'expiration' => Configure::read('Users.Token.expiration'), + 'checkActive' => true, + 'sendEmail' => true, + 'emailTemplate' => 'CakeDC/Users.validation' + ])) { + $this->Flash->success(__d('Users', 'Token has been reset successfully. Please check your email.')); + } else { + $this->Flash->error(__d('Users', 'Token could not be reset')); + } + return $this->redirect(['action' => 'login']); + } catch (UserNotFoundException $ex) { + $this->Flash->error(__d('Users', 'User {0} was not found', $reference)); + } catch (UserAlreadyActiveException $ex) { + $this->Flash->error(__d('Users', 'User {0} is already active', $reference)); + } catch (Exception $ex) { + $this->Flash->error(__d('Users', 'Token could not be reset')); + } + } +} diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php new file mode 100755 index 000000000..45cd15827 --- /dev/null +++ b/src/Controller/UsersController.php @@ -0,0 +1,48 @@ +getMailer( + 'CakeDC/Users.Users', + $this->_getEmailInstance($email) + ) + ->send('validation', [$user, __d('Users', 'Your account validation link')]); + } + + /** + * Send the reset password email + * + * @param EntityInterface $user User entity + * @param Email $email instance, if null the default email configuration with the + * @param string $template email template + * Users.validation template will be used, so set a ->template() if you pass an Email + * instance + * @return array email send result + */ + public function sendResetPasswordEmail(EntityInterface $user, Email $email = null, $template = 'CakeDC/Users.reset_password') + { + $this + ->getMailer( + 'CakeDC/Users.Users', + $this->_getEmailInstance($email) + ) + ->send('resetPassword', [$user, $template]); + } + + /** + * Send social validation email to the user + * + * @param EntityInterface $socialAccount social account + * @param EntityInterface $user user + * @param Email $email Email instance or null to use 'default' configuration + * @return mixed + */ + public function sendSocialValidationEmail(EntityInterface $socialAccount, EntityInterface $user, Email $email = null) + { + if (empty($email)) { + $template = 'CakeDC/Users.social_account_validation'; + } else { + $template = $email->template()['template']; + } + $this + ->getMailer( + 'CakeDC/Users.Users', + $this->_getEmailInstance($email) + ) + ->send('socialAccountValidation', [$user, $socialAccount, $template]); + } + + /** + * Get or initialize the email instance. Used for mocking. + * + * @param Email $email if email provided, we'll use the instance instead of creating a new one + * @return Email + */ + protected function _getEmailInstance(Email $email = null) + { + if ($email === null) { + $email = new Email('default'); + $email->emailFormat('both'); + } + return $email; + } +} diff --git a/src/Exception/AccountAlreadyActiveException.php b/src/Exception/AccountAlreadyActiveException.php new file mode 100755 index 000000000..273c4de05 --- /dev/null +++ b/src/Exception/AccountAlreadyActiveException.php @@ -0,0 +1,18 @@ +\n" +"Language-Team: CakeDC \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" + +#: Auth/SimpleRbacAuthorize.php:132 +msgid "Missing configuration file: \"config/{0}.php\". Using default permissions" +msgstr "" + +#: Controller/SocialAccountsController.php:52 +msgid "Account validated successfully" +msgstr "" + +#: Controller/SocialAccountsController.php:54 +msgid "Account could not be validated" +msgstr "" + +#: Controller/SocialAccountsController.php:57 +msgid "Invalid token and/or social account" +msgstr "" + +#: Controller/SocialAccountsController.php:59 +msgid "SocialAccount already active" +msgstr "" + +#: Controller/SocialAccountsController.php:61 +msgid "Social Account could not be validated" +msgstr "" + +#: Controller/SocialAccountsController.php:79 +msgid "Email sent successfully" +msgstr "" + +#: Controller/SocialAccountsController.php:81 +msgid "Email could not be sent" +msgstr "" + +#: Controller/SocialAccountsController.php:84 +msgid "Invalid account" +msgstr "" + +#: Controller/SocialAccountsController.php:86 +msgid "Social Account already active" +msgstr "" + +#: Controller/SocialAccountsController.php:88 +msgid "Email could not be resent" +msgstr "" + +#: Controller/Component/RememberMeComponent.php:70 +msgid "Invalid app salt, app salt must be at least 256 bits (32 bytes) long" +msgstr "" + +#: Controller/Component/UsersAuthComponent.php:156 +msgid "You can't enable email validation workflow if use_email is false" +msgstr "" + +#: Controller/Traits/LoginTrait.php:55 +msgid "The social account is not active. Please check your email for instructions. {0}" +msgstr "" + +#: Controller/Traits/LoginTrait.php:58 +#: Template/Users/social_email.ctp:16 +msgid "Please enter your email" +msgstr "" + +#: Controller/Traits/LoginTrait.php:82 +msgid "Username or password is incorrect" +msgstr "" + +#: Controller/Traits/LoginTrait.php:93 +msgid "There was an error associating your social network account" +msgstr "" + +#: Controller/Traits/LoginTrait.php:113 +msgid "You've successfully logged out" +msgstr "" + +#: Controller/Traits/PasswordManagementTrait.php:53 +msgid "Password has been changed successfully" +msgstr "" + +#: Controller/Traits/PasswordManagementTrait.php:56;63 +msgid "Password could not be changed" +msgstr "" + +#: Controller/Traits/PasswordManagementTrait.php:59 +#: Controller/Traits/ProfileTrait.php:46 +msgid "User was not found" +msgstr "" + +#: Controller/Traits/PasswordManagementTrait.php:61 +msgid "The current password does not match" +msgstr "" + +#: Controller/Traits/PasswordManagementTrait.php:93 +msgid "Please check your email to continue with password reset process" +msgstr "" + +#: Controller/Traits/PasswordManagementTrait.php:96 +msgid "The password token could not be generated. Please try again" +msgstr "" + +#: Controller/Traits/PasswordManagementTrait.php:101 +#: Controller/Traits/UserValidationTrait.php:94 +msgid "User {0} was not found" +msgstr "" + +#: Controller/Traits/PasswordManagementTrait.php:103 +#: Controller/Traits/UserValidationTrait.php:90;98 +msgid "Token could not be reset" +msgstr "" + +#: Controller/Traits/RegisterTrait.php:66 +msgid "The user could not be saved" +msgstr "" + +#: Controller/Traits/RegisterTrait.php:82 +msgid "You have registered successfully, please log in" +msgstr "" + +#: Controller/Traits/RegisterTrait.php:84 +msgid "Please validate your account before log in" +msgstr "" + +#: Controller/Traits/SimpleCrudTrait.php:75;103 +msgid "The {0} has been saved" +msgstr "" + +#: Controller/Traits/SimpleCrudTrait.php:78;106 +msgid "The {0} could not be saved" +msgstr "" + +#: Controller/Traits/SimpleCrudTrait.php:125 +msgid "The {0} has been deleted" +msgstr "" + +#: Controller/Traits/SimpleCrudTrait.php:127 +msgid "The {0} could not be deleted" +msgstr "" + +#: Controller/Traits/UserValidationTrait.php:43 +msgid "User account validated successfully" +msgstr "" + +#: Controller/Traits/UserValidationTrait.php:45 +msgid "User account could not be validated" +msgstr "" + +#: Controller/Traits/UserValidationTrait.php:48 +msgid "User already active" +msgstr "" + +#: Controller/Traits/UserValidationTrait.php:54 +msgid "Reset password token was validated successfully" +msgstr "" + +#: Controller/Traits/UserValidationTrait.php:58 +msgid "Reset password token could not be validated" +msgstr "" + +#: Controller/Traits/UserValidationTrait.php:65 +msgid "Invalid token and/or email" +msgstr "" + +#: Controller/Traits/UserValidationTrait.php:67 +msgid "Token already expired" +msgstr "" + +#: Controller/Traits/UserValidationTrait.php:69 +msgid "Invalid validation type" +msgstr "" + +#: Controller/Traits/UserValidationTrait.php:88 +msgid "Token has been reset successfully. Please check your email." +msgstr "" + +#: Controller/Traits/UserValidationTrait.php:96 +msgid "User {0} is already active" +msgstr "" + +#: Model/Table/SocialAccountsTable.php:208 +msgid "{0}Your social account validation link" +msgstr "" + +#: Model/Table/SocialAccountsTable.php:235;263 +msgid "Account already validated" +msgstr "" + +#: Model/Table/SocialAccountsTable.php:238;266 +msgid "Account not found for the given token and email." +msgstr "" + +#: Model/Table/UsersTable.php:84 +msgid "Your password does not match your confirm password. Please try again" +msgstr "" + +#: Model/Table/UsersTable.php:178 +msgid "Username already exists" +msgstr "" + +#: Model/Table/UsersTable.php:184 +msgid "Email already exists" +msgstr "" + +#: Model/Table/UsersTable.php:213 +msgid "The \"tos\" property is not present" +msgstr "" + +#: Model/Table/UsersTable.php:271 +msgid "Token has already expired user with no token" +msgstr "" + +#: Model/Table/UsersTable.php:274 +msgid "User not found for the given token and email." +msgstr "" + +#: Model/Table/UsersTable.php:341 +msgid "User not found" +msgstr "" + +#: Model/Table/UsersTable.php:368 +msgid "+ {0} secs" +msgstr "" + +#: Model/Table/UsersTable.php:420 +msgid "Unable to login user with reference {0}" +msgstr "" + +#: Model/Table/UsersTable.php:453 +msgid "Email not present" +msgstr "" + +#: Model/Table/UsersTable.php:541 +msgid "{0}Your account validation link" +msgstr "" + +#: Model/Table/UsersTable.php:561 +msgid "{0}Your reset password link" +msgstr "" + +#: Model/Table/UsersTable.php:585 +msgid "The old password does not match" +msgstr "" + +#: Template/Email/html/reset_password.ctp:21 +#: Template/Email/html/social_account_validation.ctp:14 +#: Template/Email/html/validation.ctp:21 +#: Template/Email/text/reset_password.ctp:20 +#: Template/Email/text/social_account_validation.ctp:22 +#: Template/Email/text/validation.ctp:20 +msgid "Hi {0}" +msgstr "" + +#: Template/Email/html/reset_password.ctp:24 +msgid "Reset your password here" +msgstr "" + +#: Template/Email/html/reset_password.ctp:27 +#: Template/Email/html/social_account_validation.ctp:32 +#: Template/Email/html/validation.ctp:27 +msgid "If the link is not correctly displayed, please copy the following address in your web browser {0}" +msgstr "" + +#: Template/Email/html/reset_password.ctp:30 +#: Template/Email/html/social_account_validation.ctp:35 +#: Template/Email/html/validation.ctp:30 +#: Template/Email/text/reset_password.ctp:24 +#: Template/Email/text/social_account_validation.ctp:26 +#: Template/Email/text/validation.ctp:24 +msgid "Thank you" +msgstr "" + +#: Template/Email/html/social_account_validation.ctp:18 +msgid "Activate your social login here" +msgstr "" + +#: Template/Email/html/validation.ctp:24 +msgid "Activate your account here" +msgstr "" + +#: Template/Email/text/reset_password.ctp:22 +#: Template/Email/text/validation.ctp:22 +msgid "Please copy the following address in your web browser {0}" +msgstr "" + +#: Template/Email/text/social_account_validation.ctp:24 +msgid "Please copy the following address in your web browser to activate your social login {0}" +msgstr "" + +#: Template/Users/add.ctp:13 +#: Template/Users/edit.ctp:13 +#: Template/Users/index.ctp:13;26 +#: Template/Users/view.ctp:13;77 +msgid "Actions" +msgstr "" + +#: Template/Users/add.ctp:15 +#: Template/Users/edit.ctp:21 +#: Template/Users/view.ctp:17 +msgid "List Users" +msgstr "" + +#: Template/Users/add.ctp:16 +#: Template/Users/edit.ctp:22 +#: Template/Users/view.ctp:19 +msgid "List Accounts" +msgstr "" + +#: Template/Users/add.ctp:22 +#: Template/Users/register.ctp:15 +msgid "Add User" +msgstr "" + +#: Template/Users/add.ctp:32 +#: Template/Users/change_password.ctp:13 +#: Template/Users/edit.ctp:42 +#: Template/Users/register.ctp:26 +#: Template/Users/request_reset_password.ctp:8 +#: Template/Users/resend_token_validation.ctp:20 +#: Template/Users/social_email.ctp:19 +msgid "Submit" +msgstr "" + +#: Template/Users/change_password.ctp:5 +msgid "Please enter the new password" +msgstr "" + +#: Template/Users/change_password.ctp:7 +msgid "Current password" +msgstr "" + +#: Template/Users/edit.ctp:16 +#: Template/Users/index.ctp:40 +#: Template/Users/view.ctp:99 +msgid "Delete" +msgstr "" + +#: Template/Users/edit.ctp:18 +#: Template/Users/index.ctp:40 +#: Template/Users/view.ctp:16;99 +msgid "Are you sure you want to delete # {0}?" +msgstr "" + +#: Template/Users/edit.ctp:28 +#: Template/Users/view.ctp:15 +msgid "Edit User" +msgstr "" + +#: Template/Users/index.ctp:15 +msgid "New {0}" +msgstr "" + +#: Template/Users/index.ctp:37 +#: Template/Users/view.ctp:95 +msgid "View" +msgstr "" + +#: Template/Users/index.ctp:38 +msgid "Change password" +msgstr "" + +#: Template/Users/index.ctp:39 +#: Template/Users/view.ctp:97 +msgid "Edit" +msgstr "" + +#: Template/Users/index.ctp:49 +msgid "previous" +msgstr "" + +#: Template/Users/index.ctp:51 +msgid "next" +msgstr "" + +#: Template/Users/login.ctp:19 +msgid "Please enter your username and password" +msgstr "" + +#: Template/Users/login.ctp:26 +msgid "Remember me" +msgstr "" + +#: Template/Users/login.ctp:49 +msgid "Login" +msgstr "" + +#: Template/Users/profile.ctp:18 +msgid "{0} {1}" +msgstr "" + +#: Template/Users/profile.ctp:23 +msgid "Change Password" +msgstr "" + +#: Template/Users/profile.ctp:26 +#: Template/Users/view.ctp:28;68 +msgid "Username" +msgstr "" + +#: Template/Users/profile.ctp:28 +#: Template/Users/view.ctp:30 +msgid "Email" +msgstr "" + +#: Template/Users/profile.ctp:33 +msgid "Social Accounts" +msgstr "" + +#: Template/Users/profile.ctp:37 +#: Template/Users/view.ctp:70 +msgid "Avatar" +msgstr "" + +#: Template/Users/profile.ctp:38 +#: Template/Users/view.ctp:67 +msgid "Provider" +msgstr "" + +#: Template/Users/profile.ctp:39 +msgid "Link" +msgstr "" + +#: Template/Users/register.ctp:23 +msgid "Accept TOS conditions?" +msgstr "" + +#: Template/Users/request_reset_password.ctp:5 +msgid "Please enter your email to reset your password" +msgstr "" + +#: Template/Users/resend_token_validation.ctp:15 +msgid "Resend Validation email" +msgstr "" + +#: Template/Users/resend_token_validation.ctp:17 +msgid "Email or username" +msgstr "" + +#: Template/Users/view.ctp:16 +msgid "Delete User" +msgstr "" + +#: Template/Users/view.ctp:18 +msgid "New User" +msgstr "" + +#: Template/Users/view.ctp:26;65 +msgid "Id" +msgstr "" + +#: Template/Users/view.ctp:32 +msgid "First Name" +msgstr "" + +#: Template/Users/view.ctp:34 +msgid "Last Name" +msgstr "" + +#: Template/Users/view.ctp:36;71 +msgid "Token" +msgstr "" + +#: Template/Users/view.ctp:38 +msgid "Api Token" +msgstr "" + +#: Template/Users/view.ctp:42;73 +msgid "Active" +msgstr "" + +#: Template/Users/view.ctp:46;72 +msgid "Token Expires" +msgstr "" + +#: Template/Users/view.ctp:48 +msgid "Activation Date" +msgstr "" + +#: Template/Users/view.ctp:50 +msgid "Tos Date" +msgstr "" + +#: Template/Users/view.ctp:52;75 +msgid "Created" +msgstr "" + +#: Template/Users/view.ctp:54;76 +msgid "Modified" +msgstr "" + +#: Template/Users/view.ctp:61 +msgid "Related Accounts" +msgstr "" + +#: Template/Users/view.ctp:66 +msgid "User Id" +msgstr "" + +#: Template/Users/view.ctp:69 +msgid "Reference" +msgstr "" + +#: Template/Users/view.ctp:74 +msgid "Data" +msgstr "" + +#: View/Helper/UserHelper.php:43 +msgid "Sign in with Facebook" +msgstr "" + +#: View/Helper/UserHelper.php:57 +msgid "Sign in with Twitter" +msgstr "" + +#: View/Helper/UserHelper.php:71 +msgid "Logout" +msgstr "" + +#: View/Helper/UserHelper.php:109 +msgid "Welcome, {0}" +msgstr "" + diff --git a/src/Locale/es/Users.mo b/src/Locale/es/Users.mo new file mode 100755 index 000000000..3cdcb7eb7 Binary files /dev/null and b/src/Locale/es/Users.mo differ diff --git a/src/Locale/es/Users.po b/src/Locale/es/Users.po new file mode 100755 index 000000000..88cbcdae2 --- /dev/null +++ b/src/Locale/es/Users.po @@ -0,0 +1,540 @@ +# LANGUAGE translation of CakePHP Application +# Copyright 2010 - 2015, Cake Development Corporation (http://cakedc.com) +# +msgid "" +msgstr "" +"Project-Id-Version: CakeDC Users\n" +"POT-Creation-Date: 2016-03-07 20:35+0000\n" +"PO-Revision-Date: 2016-03-09 21:06+0000\n" +"Language-Team: CakeDC \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 1.8.7\n" +"Last-Translator: \n" +"Language: es_ES\n" +"X-Poedit-SourceCharset: UTF-8\n" + +#: Auth/SimpleRbacAuthorize.php:132 +msgid "" +"Missing configuration file: \"config/{0}.php\". Using default permissions" +msgstr "" +"Falta el archivo de configuración: \"config/{0}.php\". Utilizando permisos " +"predeterminados" + +#: Controller/SocialAccountsController.php:52 +msgid "Account validated successfully" +msgstr "Cuenta validada correctamente" + +#: Controller/SocialAccountsController.php:54 +msgid "Account could not be validated" +msgstr "No se pudo validar la cuenta" + +#: Controller/SocialAccountsController.php:57 +msgid "Invalid token and/or social account" +msgstr "Token o cuenta social incorrecta" + +#: Controller/SocialAccountsController.php:59 +msgid "SocialAccount already active" +msgstr "Cuenta social ya activa" + +#: Controller/SocialAccountsController.php:61 +msgid "Social Account could not be validated" +msgstr "No se pudo validar la cuenta social " + +#: Controller/SocialAccountsController.php:79 +msgid "Email sent successfully" +msgstr "Email enviado correctamente" + +#: Controller/SocialAccountsController.php:81 +msgid "Email could not be sent" +msgstr "No se puede enviar el email" + +#: Controller/SocialAccountsController.php:84 +msgid "Invalid account" +msgstr "Cuenta inválida" + +#: Controller/SocialAccountsController.php:86 +msgid "Social Account already active" +msgstr "Cuenta social ya activa" + +#: Controller/SocialAccountsController.php:88 +msgid "Email could not be resent" +msgstr "No se puede reenviar el Email" + +#: Controller/Component/RememberMeComponent.php:70 +msgid "Invalid app salt, app salt must be at least 256 bits (32 bytes) long" +msgstr "App salt inválido, debe contener al menos 256 bits (32 bytes)" + +#: Controller/Component/UsersAuthComponent.php:156 +msgid "You can't enable email validation workflow if use_email is false" +msgstr "" +"No es posible activar el flujo de trabajo para la validación de email si " +"use_mail es falso" + +#: Controller/Traits/LoginTrait.php:55 +msgid "" +"The social account is not active. Please check your email for instructions. " +"{0}" +msgstr "" +"La cuenta social está inactiva. Por favor, compruebe las instrucciones en su " +"correo. {0}" + +#: Controller/Traits/LoginTrait.php:58 Template/Users/social_email.ctp:16 +msgid "Please enter your email" +msgstr "Por favor, introduzca su email" + +#: Controller/Traits/LoginTrait.php:82 +msgid "Username or password is incorrect" +msgstr "Usuario o contraseña incorrecta" + +#: Controller/Traits/LoginTrait.php:93 +msgid "There was an error associating your social network account" +msgstr "Hubo un error al asociar su cuenta de la red social" + +#: Controller/Traits/LoginTrait.php:113 +msgid "You've successfully logged out" +msgstr "Ha cerrado sesión correctamente" + +#: Controller/Traits/PasswordManagementTrait.php:53 +msgid "Password has been changed successfully" +msgstr "Contraseña cambiada correctamente" + +#: Controller/Traits/PasswordManagementTrait.php:56;63 +msgid "Password could not be changed" +msgstr "No es posible cambiar la contraseña" + +#: Controller/Traits/PasswordManagementTrait.php:59 +#: Controller/Traits/ProfileTrait.php:46 +msgid "User was not found" +msgstr "Usuario no encontrado" + +#: Controller/Traits/PasswordManagementTrait.php:61 +msgid "The current password does not match" +msgstr "La contraseña actual no coincide" + +#: Controller/Traits/PasswordManagementTrait.php:93 +msgid "Please check your email to continue with password reset process" +msgstr "" +"Por favor, compruebe su correo para continuar con el proceso de " +"restablecimiento de contraseña" + +#: Controller/Traits/PasswordManagementTrait.php:96 +msgid "The password token could not be generated. Please try again" +msgstr "" +"No se pudo generar el token de contraseña. Por favor, inténtelo de nuevo" + +#: Controller/Traits/PasswordManagementTrait.php:101 +#: Controller/Traits/UserValidationTrait.php:94 +msgid "User {0} was not found" +msgstr "Usuario {0} no encontrado" + +#: Controller/Traits/PasswordManagementTrait.php:103 +#: Controller/Traits/UserValidationTrait.php:90;98 +msgid "Token could not be reset" +msgstr "No se puede restablecer el token" + +#: Controller/Traits/RegisterTrait.php:66 +msgid "The user could not be saved" +msgstr "No se ha podido guardar el usuario" + +#: Controller/Traits/RegisterTrait.php:82 +msgid "You have registered successfully, please log in" +msgstr "Se ha registrado correctamente, por favor ingrese" + +#: Controller/Traits/RegisterTrait.php:84 +msgid "Please validate your account before log in" +msgstr "Por favor, valide su cuenta antes de ingresar" + +#: Controller/Traits/SimpleCrudTrait.php:75;103 +msgid "The {0} has been saved" +msgstr "El {0} ha sido guardado" + +#: Controller/Traits/SimpleCrudTrait.php:78;106 +msgid "The {0} could not be saved" +msgstr "El {0} no ha podido guardarse" + +#: Controller/Traits/SimpleCrudTrait.php:125 +msgid "The {0} has been deleted" +msgstr "El {0} ha sido eliminado" + +#: Controller/Traits/SimpleCrudTrait.php:127 +msgid "The {0} could not be deleted" +msgstr "El {0} no ha podido eliminarse" + +#: Controller/Traits/UserValidationTrait.php:43 +msgid "User account validated successfully" +msgstr "Cuenta de usuario validada correctamente" + +#: Controller/Traits/UserValidationTrait.php:45 +msgid "User account could not be validated" +msgstr "No se pudo validar la cuenta de usuario" + +#: Controller/Traits/UserValidationTrait.php:48 +msgid "User already active" +msgstr "Usuario ya activo" + +#: Controller/Traits/UserValidationTrait.php:54 +msgid "Reset password token was validated successfully" +msgstr "Restablecimiento del token de contraseña validado correctamente" + +#: Controller/Traits/UserValidationTrait.php:58 +msgid "Reset password token could not be validated" +msgstr "Restablecimiento del token de contraseña no pudo validarse" + +#: Controller/Traits/UserValidationTrait.php:65 +msgid "Invalid token and/or email" +msgstr "Token y/o email inválido" + +#: Controller/Traits/UserValidationTrait.php:67 +msgid "Token already expired" +msgstr "Token ya expirado" + +#: Controller/Traits/UserValidationTrait.php:69 +msgid "Invalid validation type" +msgstr "Tipo de validación inválido" + +#: Controller/Traits/UserValidationTrait.php:88 +msgid "Token has been reset successfully. Please check your email." +msgstr "" +"Se ha restablecido el token correctamente. Por favor compruebe su email." + +#: Controller/Traits/UserValidationTrait.php:96 +msgid "User {0} is already active" +msgstr "El usuario {0} ya está activo" + +#: Model/Table/SocialAccountsTable.php:208 +msgid "{0}Your social account validation link" +msgstr "{0} Enlace de validación de su cuenta social" + +#: Model/Table/SocialAccountsTable.php:235;263 +msgid "Account already validated" +msgstr "Cuenta ya activada" + +#: Model/Table/SocialAccountsTable.php:238;266 +msgid "Account not found for the given token and email." +msgstr "Cuenta no encontrada para el token y email proporcionado" + +#: Model/Table/UsersTable.php:84 +msgid "Your password does not match your confirm password. Please try again" +msgstr "" +"Su contraseña y la comprobación no concuerdan. Por favor inténtelo de nuevo" + +#: Model/Table/UsersTable.php:178 +msgid "Username already exists" +msgstr "Nombre de usuario ya existente" + +#: Model/Table/UsersTable.php:184 +msgid "Email already exists" +msgstr "Email ya existente" + +#: Model/Table/UsersTable.php:213 +msgid "The \"tos\" property is not present" +msgstr "La propiedad \"tos\" no está presente" + +#: Model/Table/UsersTable.php:271 +msgid "Token has already expired user with no token" +msgstr "El token ha expirado usuario sin token" + +#: Model/Table/UsersTable.php:274 +msgid "User not found for the given token and email." +msgstr "Usuario no encontrado para el token y email proporcionado" + +#: Model/Table/UsersTable.php:341 +msgid "User not found" +msgstr "Usuario no encontrado" + +#: Model/Table/UsersTable.php:368 +msgid "+ {0} secs" +msgstr "+ {0} secs" + +#: Model/Table/UsersTable.php:420 +msgid "Unable to login user with reference {0}" +msgstr "No se puede iniciar sesión con el usuario con referencia {0}" + +#: Model/Table/UsersTable.php:453 +msgid "Email not present" +msgstr "No se encuentra el email" + +#: Model/Table/UsersTable.php:541 +msgid "{0}Your account validation link" +msgstr "{0} Enlace para validar su cuenta" + +#: Model/Table/UsersTable.php:561 +msgid "{0}Your reset password link" +msgstr "{0} Enlace para restablecer su contraseña" + +#: Model/Table/UsersTable.php:585 +msgid "The old password does not match" +msgstr "La antigua contraseña no coincide" + +#: Template/Email/html/reset_password.ctp:21 +#: Template/Email/html/social_account_validation.ctp:14 +#: Template/Email/html/validation.ctp:21 +#: Template/Email/text/reset_password.ctp:20 +#: Template/Email/text/social_account_validation.ctp:22 +#: Template/Email/text/validation.ctp:20 +msgid "Hi {0}" +msgstr "Hola {0}" + +#: Template/Email/html/reset_password.ctp:24 +msgid "Reset your password here" +msgstr "Restablezca su contraseña aquí" + +#: Template/Email/html/reset_password.ctp:27 +#: Template/Email/html/social_account_validation.ctp:32 +#: Template/Email/html/validation.ctp:27 +msgid "" +"If the link is not correctly displayed, please copy the following address in " +"your web browser {0}" +msgstr "" +"Si el enlace no se muestra correctamente, por favor copie la siguiente " +"dirección en su navegador web {0}" + +#: Template/Email/html/reset_password.ctp:30 +#: Template/Email/html/social_account_validation.ctp:35 +#: Template/Email/html/validation.ctp:30 +#: Template/Email/text/reset_password.ctp:24 +#: Template/Email/text/social_account_validation.ctp:26 +#: Template/Email/text/validation.ctp:24 +msgid "Thank you" +msgstr "Gracias" + +#: Template/Email/html/social_account_validation.ctp:18 +msgid "Activate your social login here" +msgstr "Active su acceso social aquí" + +#: Template/Email/html/validation.ctp:24 +msgid "Activate your account here" +msgstr "Active su cuenta aquí" + +#: Template/Email/text/reset_password.ctp:22 +#: Template/Email/text/validation.ctp:22 +msgid "Please copy the following address in your web browser {0}" +msgstr "Por favor copie la siguiente dirección en su navegador web {0}" + +#: Template/Email/text/social_account_validation.ctp:24 +msgid "" +"Please copy the following address in your web browser to activate your " +"social login {0}" +msgstr "" +"Por favor copie la siguiente dirección en su navegador web para activar su " +"acceso social {0}" + +#: Template/Users/add.ctp:13 Template/Users/edit.ctp:13 +#: Template/Users/index.ctp:13;26 Template/Users/view.ctp:13;77 +msgid "Actions" +msgstr "Acciones" + +#: Template/Users/add.ctp:15 Template/Users/edit.ctp:21 +#: Template/Users/view.ctp:17 +msgid "List Users" +msgstr "Listar Usuarios" + +#: Template/Users/add.ctp:16 Template/Users/edit.ctp:22 +#: Template/Users/view.ctp:19 +msgid "List Accounts" +msgstr "Listar Cuentas" + +#: Template/Users/add.ctp:22 Template/Users/register.ctp:15 +msgid "Add User" +msgstr "Añadir Usuario" + +#: Template/Users/add.ctp:32 Template/Users/change_password.ctp:13 +#: Template/Users/edit.ctp:42 Template/Users/register.ctp:26 +#: Template/Users/request_reset_password.ctp:8 +#: Template/Users/resend_token_validation.ctp:20 +#: Template/Users/social_email.ctp:19 +msgid "Submit" +msgstr "Enviar" + +#: Template/Users/change_password.ctp:5 +msgid "Please enter the new password" +msgstr "Por favor introduzca la nueva contraseña" + +#: Template/Users/change_password.ctp:7 +msgid "Current password" +msgstr "Contraseña actual" + +#: Template/Users/edit.ctp:16 Template/Users/index.ctp:40 +#: Template/Users/view.ctp:99 +msgid "Delete" +msgstr "Eliminar" + +#: Template/Users/edit.ctp:18 Template/Users/index.ctp:40 +#: Template/Users/view.ctp:16;99 +msgid "Are you sure you want to delete # {0}?" +msgstr "¿Está seguro que quiere eliminar # {0}?" + +#: Template/Users/edit.ctp:28 Template/Users/view.ctp:15 +msgid "Edit User" +msgstr "Editar Usuario" + +#: Template/Users/index.ctp:15 +msgid "New {0}" +msgstr "Nuevo {0}" + +#: Template/Users/index.ctp:37 Template/Users/view.ctp:95 +msgid "View" +msgstr "Ver" + +#: Template/Users/index.ctp:38 +msgid "Change password" +msgstr "Cambiar contraseña" + +#: Template/Users/index.ctp:39 Template/Users/view.ctp:97 +msgid "Edit" +msgstr "Editar" + +#: Template/Users/index.ctp:49 +msgid "previous" +msgstr "anterior" + +#: Template/Users/index.ctp:51 +msgid "next" +msgstr "siguiente" + +#: Template/Users/login.ctp:19 +msgid "Please enter your username and password" +msgstr "Por favor introduzca su usuario y contraseña" + +#: Template/Users/login.ctp:26 +msgid "Remember me" +msgstr "Recordarme" + +#: Template/Users/login.ctp:49 +msgid "Login" +msgstr "Ingresar" + +#: Template/Users/profile.ctp:18 +msgid "{0} {1}" +msgstr "{0} {1}" + +#: Template/Users/profile.ctp:23 +msgid "Change Password" +msgstr "Cambiar contraseña" + +#: Template/Users/profile.ctp:26 Template/Users/view.ctp:28;68 +msgid "Username" +msgstr "Usuario" + +#: Template/Users/profile.ctp:28 Template/Users/view.ctp:30 +msgid "Email" +msgstr "Email" + +#: Template/Users/profile.ctp:33 +msgid "Social Accounts" +msgstr "Cuentas sociales" + +#: Template/Users/profile.ctp:37 Template/Users/view.ctp:70 +msgid "Avatar" +msgstr "Avatar" + +#: Template/Users/profile.ctp:38 Template/Users/view.ctp:67 +msgid "Provider" +msgstr "Proveedor" + +#: Template/Users/profile.ctp:39 +msgid "Link" +msgstr "Enlace" + +#: Template/Users/register.ctp:23 +msgid "Accept TOS conditions?" +msgstr "¿Acepta las condiciones del servicios?" + +#: Template/Users/request_reset_password.ctp:5 +msgid "Please enter your email to reset your password" +msgstr "Por favor introduzca su email para restablecer su contraseña" + +#: Template/Users/resend_token_validation.ctp:15 +msgid "Resend Validation email" +msgstr "Reenviar email de validación" + +#: Template/Users/resend_token_validation.ctp:17 +msgid "Email or username" +msgstr "Email o usuario" + +#: Template/Users/view.ctp:16 +msgid "Delete User" +msgstr "Eliminar Usuario" + +#: Template/Users/view.ctp:18 +msgid "New User" +msgstr "Nuevo Usuario" + +#: Template/Users/view.ctp:26;65 +msgid "Id" +msgstr "Id" + +#: Template/Users/view.ctp:32 +msgid "First Name" +msgstr "Nombre" + +#: Template/Users/view.ctp:34 +msgid "Last Name" +msgstr "Apellidos" + +#: Template/Users/view.ctp:36;71 +msgid "Token" +msgstr "Token" + +#: Template/Users/view.ctp:38 +msgid "Api Token" +msgstr "Api Token" + +#: Template/Users/view.ctp:42;73 +msgid "Active" +msgstr "Activo" + +#: Template/Users/view.ctp:46;72 +msgid "Token Expires" +msgstr "Token caduca" + +#: Template/Users/view.ctp:48 +msgid "Activation Date" +msgstr "Fecha de activación" + +#: Template/Users/view.ctp:50 +msgid "Tos Date" +msgstr "Fecha Tos" + +#: Template/Users/view.ctp:52;75 +msgid "Created" +msgstr "Creado" + +#: Template/Users/view.ctp:54;76 +msgid "Modified" +msgstr "Modificado" + +#: Template/Users/view.ctp:61 +msgid "Related Accounts" +msgstr "Cuentas relacionadas" + +#: Template/Users/view.ctp:66 +msgid "User Id" +msgstr "Id Usuario" + +#: Template/Users/view.ctp:69 +msgid "Reference" +msgstr "Referencia" + +#: Template/Users/view.ctp:74 +msgid "Data" +msgstr "Datos" + +#: View/Helper/UserHelper.php:43 +msgid "Sign in with Facebook" +msgstr "Ingresar con Facebook" + +#: View/Helper/UserHelper.php:57 +msgid "Sign in with Twitter" +msgstr "Ingresar con Twitter" + +#: View/Helper/UserHelper.php:71 +msgid "Logout" +msgstr "Salir" + +#: View/Helper/UserHelper.php:109 +msgid "Welcome, {0}" +msgstr "Bienvenido, {0}" diff --git a/src/Locale/sv/Users.mo b/src/Locale/sv/Users.mo new file mode 100755 index 000000000..07925b46d Binary files /dev/null and b/src/Locale/sv/Users.mo differ diff --git a/src/Locale/sv/Users.po b/src/Locale/sv/Users.po new file mode 100755 index 000000000..b13668079 --- /dev/null +++ b/src/Locale/sv/Users.po @@ -0,0 +1,723 @@ +# LANGUAGE translation of CakePHP Application +# Copyright 2010 - 2015, Cake Development Corporation (http://cakedc.com) +# +msgid "" +msgstr "" +"Project-Id-Version: Users\n" +"POT-Creation-Date: 2016-02-18 14:10+0100\n" +"PO-Revision-Date: 2016-02-18 14:24+0100\n" +"Last-Translator: Ulrik Södergren \n" +"Language-Team: CakeDC \n" +"Language: sv\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 1.8.7\n" + +#: Auth/SimpleRbacAuthorize.php:133 +msgid "" +"Missing configuration file: \"config/{0}.php\". Using default permissions" +msgstr "" +"Saknad konfigurationsfil: \"config / {0} .php\". Använder " +"standardbehörigheter" + +#: Auth/SocialAuthenticate.php:153 +msgid "Provider cannot be empty" +msgstr "Leverantör kan inte vara tomt" + +#: Controller/SocialAccountsController.php:52 +msgid "Account validated successfully" +msgstr "Kontot har validerats" + +#: Controller/SocialAccountsController.php:54 +msgid "Account could not be validated" +msgstr "Konto kunde inte valideras" + +#: Controller/SocialAccountsController.php:57 +msgid "Invalid token and/or social account" +msgstr "Ogiltig token och / eller social konto" + +#: Controller/SocialAccountsController.php:59 +msgid "SocialAccount already active" +msgstr "SocialAccount redan aktivt" + +#: Controller/SocialAccountsController.php:61 +msgid "Social Account could not be validated" +msgstr "SocialAccount kunde inte valideras" + +#: Controller/SocialAccountsController.php:79 +msgid "Email sent successfully" +msgstr "Epost har skickats." + +#: Controller/SocialAccountsController.php:81 +msgid "Email could not be sent" +msgstr "Epsot kunde inte skickas" + +#: Controller/SocialAccountsController.php:84 +msgid "Invalid account" +msgstr "Ogiltigt konto" + +#: Controller/SocialAccountsController.php:86 +msgid "Social Account already active" +msgstr "SocialAccount redan aktivt" + +#: Controller/SocialAccountsController.php:88 +msgid "Email could not be resent" +msgstr "E-post kan inte skickas om" + +#: Controller/Component/RememberMeComponent.php:69 +msgid "Invalid app salt, app salt must be at least 256 bits (32 bytes) long" +msgstr "" +"Ogiltig app salt, måste app salt vara åtminstone 256 bitar (32 byte) långt" + +#: Controller/Component/UsersAuthComponent.php:157 +msgid "You can't enable email validation workflow if use_email is false" +msgstr "" +"Du kan inte aktivera epost-validering arbetsflöde om use_email är falskt" + +#: Controller/Traits/LoginTrait.php:92 +msgid "Issues trying to log in with your social account" +msgstr "Problem vid inloggning med ditt sociala konto" + +#: Controller/Traits/LoginTrait.php:96 Template/Users/social_email.ctp:16 +msgid "Please enter your email" +msgstr "Lägg till din epost" + +#: Controller/Traits/LoginTrait.php:102 +msgid "" +"Your user has not been validated yet. Please check your inbox for " +"instructions" +msgstr "" +"Ditt konto har inte verifierats ännu. Vänligen kontrollera din inbox för " +"instruktioner" + +#: Controller/Traits/LoginTrait.php:104 +msgid "" +"Your social account has not been validated yet. Please check your inbox for " +"instructions" +msgstr "" +"Din sociala hänsyn har inte validerats ännu. Vänligen kontrollera din inbox " +"för instruktioner" + +#: Controller/Traits/LoginTrait.php:177 +msgid "Username or password is incorrect" +msgstr "Användarnamn eller lösenord är felaktigt" + +#: Controller/Traits/LoginTrait.php:198 +msgid "You've successfully logged out" +msgstr "Du har loggats ut" + +#: Controller/Traits/PasswordManagementTrait.php:53 +msgid "Password has been changed successfully" +msgstr "Lösenordsbytet lyckades!" + +#: Controller/Traits/PasswordManagementTrait.php:56;63 +msgid "Password could not be changed" +msgstr "Lösenordet kunde inte ändras" + +#: Controller/Traits/PasswordManagementTrait.php:59 +#: Controller/Traits/ProfileTrait.php:49 +msgid "User was not found" +msgstr "Användaren hittades inte." + +#: Controller/Traits/PasswordManagementTrait.php:61 +msgid "The current password does not match" +msgstr "Den nuvarande lösenord matchar inte" + +#: Controller/Traits/PasswordManagementTrait.php:102 +msgid "Please check your email to continue with password reset process" +msgstr "Kontrollera din epost för att fortsätta lösenordsåterställningen" + +#: Controller/Traits/PasswordManagementTrait.php:105 Shell/UsersShell.php:266 +msgid "The password token could not be generated. Please try again" +msgstr "Lösenordstoken kunde inte skapas. Var god försök igen" + +#: Controller/Traits/PasswordManagementTrait.php:110 +#: Controller/Traits/UserValidationTrait.php:98 +msgid "User {0} was not found" +msgstr "Användaren {0} hittades inte" + +#: Controller/Traits/PasswordManagementTrait.php:112 +#: Controller/Traits/UserValidationTrait.php:94;102 +msgid "Token could not be reset" +msgstr "Token kunde inte återställas" + +#: Controller/Traits/ProfileTrait.php:52 +msgid "Not authorized, please login first" +msgstr "Inte auktoriserad, vänligen logga in först" + +#: Controller/Traits/RegisterTrait.php:74 Controller/Traits/SocialTrait.php:38 +msgid "The reCaptcha could not be validated" +msgstr "reCAPTCHA angavs inte korrekt" + +#: Controller/Traits/RegisterTrait.php:80 +msgid "The user could not be saved" +msgstr "Användaren kunde inte sparas" + +#: Controller/Traits/RegisterTrait.php:113 +msgid "You have registered successfully, please log in" +msgstr "Din registrering är klar, vänligen logga in" + +#: Controller/Traits/RegisterTrait.php:115 +msgid "Please validate your account before log in" +msgstr "Vänligen validera ditt konto innan inloggning" + +#: Controller/Traits/SimpleCrudTrait.php:76;105 +msgid "The {0} has been saved" +msgstr "{0} har sparats" + +#: Controller/Traits/SimpleCrudTrait.php:79;108 +msgid "The {0} could not be saved" +msgstr "{0} kunde inte sparas" + +#: Controller/Traits/SimpleCrudTrait.php:128 +msgid "The {0} has been deleted" +msgstr "{0} har tagits bort" + +#: Controller/Traits/SimpleCrudTrait.php:130 +msgid "The {0} could not be deleted" +msgstr "{0} kunde inte tas bort" + +#: Controller/Traits/UserValidationTrait.php:42 +msgid "User account validated successfully" +msgstr "Användarkontot har validerats" + +#: Controller/Traits/UserValidationTrait.php:44 +msgid "User account could not be validated" +msgstr "Användarkontot kunde inte valideras" + +#: Controller/Traits/UserValidationTrait.php:47 +msgid "User already active" +msgstr "Användaren redan aktiv" + +#: Controller/Traits/UserValidationTrait.php:53 +msgid "Reset password token was validated successfully" +msgstr "Återställning av lösenord lyckades." + +#: Controller/Traits/UserValidationTrait.php:57 +msgid "Reset password token could not be validated" +msgstr "Token för att återställa lösenord kunde inte valideras" + +#: Controller/Traits/UserValidationTrait.php:61 +msgid "Invalid validation type" +msgstr "Ogiltig valideringsmetod" + +#: Controller/Traits/UserValidationTrait.php:64 +msgid "Invalid token or user account already validated" +msgstr "Ogiltig token eller så har användaren konto redan validerats" + +#: Controller/Traits/UserValidationTrait.php:66 +msgid "Token already expired" +msgstr "Token redan löpt ut" + +#: Controller/Traits/UserValidationTrait.php:92 +msgid "Token has been reset successfully. Please check your email." +msgstr "Token har återställts. Kontrollera din e-post." + +#: Controller/Traits/UserValidationTrait.php:100 +msgid "User {0} is already active" +msgstr "Användaren {0} är redan aktiv" + +#: Model/Behavior/PasswordBehavior.php:42 +msgid "Reference cannot be null" +msgstr "Referens får inte vara null" + +#: Model/Behavior/PasswordBehavior.php:47 +msgid "Token expiration cannot be empty" +msgstr "Giltighet för token kan inte vara tom" + +#: Model/Behavior/PasswordBehavior.php:53 +msgid "User not found" +msgstr "Användaren hittades inte" + +#: Model/Behavior/PasswordBehavior.php:95 +msgid "{0}Your reset password link" +msgstr "{0} Din länk för att återställa lösenord" + +#: Model/Behavior/PasswordBehavior.php:119 +msgid "The old password does not match" +msgstr "Det gamla lösenordet stämmer inte" + +#: Model/Behavior/RegisterBehavior.php:65 +msgid "Your account validation link" +msgstr "Din länk för att validera kontot" + +#: Model/Behavior/RegisterBehavior.php:86 +msgid "User not found for the given token and email." +msgstr "Hittade ingen användare som matchar angivet token eller epost" + +#: Model/Behavior/RegisterBehavior.php:89 +msgid "Token has already expired user with no token" +msgstr "Token har redan gått ut eller användare utan token" + +#: Model/Behavior/RegisterBehavior.php:108 +msgid "User account already validated" +msgstr "Kontot är redan aktiverat!" + +#: Model/Behavior/SocialAccountBehavior.php:82 +msgid "{0}Your social account validation link" +msgstr "{0} Din sociala konto svalideringslänk" + +#: Model/Behavior/SocialAccountBehavior.php:109;136 +msgid "Account already validated" +msgstr "Kontot är redan aktiverat!" + +#: Model/Behavior/SocialAccountBehavior.php:112;139 +msgid "Account not found for the given token and email." +msgstr "Kontot hittades inte för givet token och e-post." + +#: Model/Behavior/SocialBehavior.php:56 +msgid "Unable to login user with reference {0}" +msgstr "Det går inte att logga in användaren med refrens {0}" + +#: Model/Behavior/SocialBehavior.php:98 +msgid "Email not present" +msgstr "Epost saknas" + +#: Model/Table/UsersTable.php:80 +msgid "Your password does not match your confirm password. Please try again" +msgstr "Dina lösenord matchar inte. Vänligen försök igen." + +#: Model/Table/UsersTable.php:158 +msgid "Username already exists" +msgstr "Användarnamnet är upptaget" + +#: Model/Table/UsersTable.php:164 +msgid "Email already exists" +msgstr "E-postadressen finns redan" + +#: Shell/UsersShell.php:54 +msgid "Utilities for CakeDC Users Plugin" +msgstr "Verktyg för CakeDC User Plugin" + +#: Shell/UsersShell.php:55 +msgid "Activate an specific user" +msgstr "Aktivera en specifik användare" + +#: Shell/UsersShell.php:56 +msgid "Add a new superadmin user for testing purposes" +msgstr "Lägga till en ny superadmin användare för teständamål" + +#: Shell/UsersShell.php:57 +msgid "Add a new user" +msgstr "Ny användare" + +#: Shell/UsersShell.php:58 +msgid "Change the role for an specific user" +msgstr "Ändra rollen för en specifik användare" + +#: Shell/UsersShell.php:59 +msgid "Deactivate an specific user" +msgstr "Inaktivare en specifik användare" + +#: Shell/UsersShell.php:60 +msgid "Delete an specific user" +msgstr "Ta bort en specifik användare" + +#: Shell/UsersShell.php:61 +msgid "Reset the password via email" +msgstr "Återställ lösenord via e-post" + +#: Shell/UsersShell.php:62 +msgid "Reset the password for all users" +msgstr "Återställ lösenord för alla användare" + +#: Shell/UsersShell.php:63 +msgid "Reset the password for an specific user" +msgstr "Återställ lösenordet för en specifik användare" + +#: Shell/UsersShell.php:97 +msgid "User added:" +msgstr "Användare tillagd:" + +#: Shell/UsersShell.php:98;126 +msgid "Id: {0}" +msgstr "Id: {0}" + +#: Shell/UsersShell.php:99;127 +msgid "Username: {0}" +msgstr "Användarnamn: {0}" + +#: Shell/UsersShell.php:100;128 +msgid "Email: {0}" +msgstr "Epost: {0}" + +#: Shell/UsersShell.php:101;129 +msgid "Password: {0}" +msgstr "Lösenord: {0}" + +#: Shell/UsersShell.php:125 +msgid "Superuser added:" +msgstr "Superanvändare tillagd:" + +#: Shell/UsersShell.php:131 +msgid "Superuser could not be added:" +msgstr "Superanvändaren kunde inte läggas till:" + +#: Shell/UsersShell.php:134 +msgid "Field: {0} Error: {1}" +msgstr "Fält: {0} fel: {1}" + +#: Shell/UsersShell.php:152;178 +msgid "Please enter a password." +msgstr "Ange ett lösenord." + +#: Shell/UsersShell.php:156 +msgid "Password changed for all users" +msgstr "Lösenordet ändrat för alla användare" + +#: Shell/UsersShell.php:157;185 +msgid "New password: {0}" +msgstr "Nytt lösenord: {0}" + +#: Shell/UsersShell.php:175;203;281;321 +msgid "Please enter a username." +msgstr "Ange ett användarnamn" + +#: Shell/UsersShell.php:184 +msgid "Password changed for user: {0}" +msgstr "Lösenordet ändrat för användare: {0}" + +#: Shell/UsersShell.php:206 +msgid "Please enter a role." +msgstr "Ange en roll." + +#: Shell/UsersShell.php:212 +msgid "Role changed for user: {0}" +msgstr "Rollen har ändrats för användare: {0}" + +#: Shell/UsersShell.php:213 +msgid "New role: {0}" +msgstr "Ny roll: {0}" + +#: Shell/UsersShell.php:228 +msgid "User was activated: {0}" +msgstr "Användaren aktiverades: {0}" + +#: Shell/UsersShell.php:243 +msgid "User was de-activated: {0}" +msgstr "Användaren avaktiverad: {0}" + +#: Shell/UsersShell.php:255 +msgid "Please enter a username or email." +msgstr "Skriv användarnamn eller epost" + +#: Shell/UsersShell.php:263 +msgid "" +"Please ask the user to check the email to continue with password reset " +"process" +msgstr "" +"Be användaren kontrollera sin epost för att fortsätta " +"lösenordsåterställningen" + +#: Shell/UsersShell.php:300 +msgid "The user was not found." +msgstr "Användaren hittades inte." + +#: Shell/UsersShell.php:327 +msgid "The user {0} was deleted successfully" +msgstr "Användaren {0} har tagits bort" + +#: Shell/UsersShell.php:329 +msgid "The user {0} was not deleted. Please try again" +msgstr "Användaren {0} raderades ej. Var god försök igen" + +#: Template/Email/html/reset_password.ctp:21 +#: Template/Email/html/social_account_validation.ctp:14 +#: Template/Email/html/validation.ctp:21 +#: Template/Email/text/reset_password.ctp:20 +#: Template/Email/text/social_account_validation.ctp:22 +#: Template/Email/text/validation.ctp:20 +msgid "Hi {0}" +msgstr "Hej {0}" + +#: Template/Email/html/reset_password.ctp:24 +msgid "Reset your password here" +msgstr "Återställ ditt lösenord här" + +#: Template/Email/html/reset_password.ctp:27 +#: Template/Email/html/social_account_validation.ctp:32 +msgid "" +"If the link is not correcly displayed, please copy the following address in " +"your web browser {0}" +msgstr "" +"Om länken inte visas korrekt, vänligen kopiera följande adress till " +"webbläsaren {0}" + +#: Template/Email/html/reset_password.ctp:30 +#: Template/Email/html/social_account_validation.ctp:35 +#: Template/Email/html/validation.ctp:30 +#: Template/Email/text/reset_password.ctp:24 +#: Template/Email/text/social_account_validation.ctp:26 +#: Template/Email/text/validation.ctp:24 +msgid "Thank you" +msgstr "Tack" + +#: Template/Email/html/social_account_validation.ctp:18 +msgid "Activate your social login here" +msgstr "Aktivera ditt sociala konto här" + +#: Template/Email/html/validation.ctp:24 +msgid "Activate your account here" +msgstr "Aktivera ditt konto här" + +#: Template/Email/html/validation.ctp:27 +msgid "" +"If the link is not correctly displayed, please copy the following address in " +"your web browser {0}" +msgstr "" +"Om länken inte visas korrekt, vänligen kopiera följande adress till din " +"webbläsare {0}" + +#: Template/Email/text/reset_password.ctp:22 +#: Template/Email/text/validation.ctp:22 +msgid "Please copy the following address in your web browser {0}" +msgstr "Kopiera följande adress till webbläsaren {0}" + +#: Template/Email/text/social_account_validation.ctp:24 +msgid "" +"Please copy the following address in your web browser to activate your " +"social login {0}" +msgstr "" +"Kopiera följande adress till din webbläsare för att aktivera din sociala " +"inloggning {0}" + +#: Template/Users/add.ctp:13 Template/Users/edit.ctp:13 +#: Template/Users/index.ctp:13;26 Template/Users/view.ctp:13;77 +msgid "Actions" +msgstr "Resurser" + +#: Template/Users/add.ctp:15 Template/Users/edit.ctp:21 +#: Template/Users/view.ctp:17 +msgid "List Users" +msgstr "Lista användare" + +#: Template/Users/add.ctp:16 Template/Users/edit.ctp:22 +#: Template/Users/view.ctp:19 +msgid "List Accounts" +msgstr "Lista konton" + +#: Template/Users/add.ctp:22 Template/Users/register.ctp:15 +msgid "Add User" +msgstr "Ny användare" + +#: Template/Users/add.ctp:32 Template/Users/change_password.ctp:17 +#: Template/Users/edit.ctp:42 Template/Users/register.ctp:27 +#: Template/Users/request_reset_password.ctp:8 +#: Template/Users/resend_token_validation.ctp:20 +#: Template/Users/social_email.ctp:20 +msgid "Submit" +msgstr "Skicka" + +#: Template/Users/change_password.ctp:5 +msgid "Please enter the new password" +msgstr "Ange ditt nya lösenord" + +#: Template/Users/change_password.ctp:10 +msgid "Current password" +msgstr "Nuvarande lösenord" + +#: Template/Users/edit.ctp:16 Template/Users/index.ctp:40 +#: Template/Users/view.ctp:99 +msgid "Delete" +msgstr "Radera" + +#: Template/Users/edit.ctp:18 Template/Users/index.ctp:40 +#: Template/Users/view.ctp:16;99 +msgid "Are you sure you want to delete # {0}?" +msgstr "Är du säker på att du vill radera{0}" + +#: Template/Users/edit.ctp:28 Template/Users/view.ctp:15 +msgid "Edit User" +msgstr "Redigera användare" + +#: Template/Users/index.ctp:15 +msgid "New {0}" +msgstr "Nytt {0}" + +#: Template/Users/index.ctp:37 Template/Users/view.ctp:95 +msgid "View" +msgstr "Visa" + +#: Template/Users/index.ctp:38 +msgid "Change password" +msgstr "Byt lösenord" + +#: Template/Users/index.ctp:39 Template/Users/view.ctp:97 +msgid "Edit" +msgstr "Ändra" + +#: Template/Users/index.ctp:49 +msgid "previous" +msgstr "föregående" + +#: Template/Users/index.ctp:51 +msgid "next" +msgstr "nästa" + +#: Template/Users/login.ctp:19 +msgid "Please enter your username and password" +msgstr "Ange ditt användarnamn och lösenord" + +#: Template/Users/login.ctp:26 +msgid "Remember me" +msgstr "Kom ihåg mig" + +#: Template/Users/login.ctp:55 +msgid "Login" +msgstr "Loggin" + +#: Template/Users/profile.ctp:18 View/Helper/UserHelper.php:63 +msgid "{0} {1}" +msgstr "{0} {1}" + +#: Template/Users/profile.ctp:24 +msgid "Change Password" +msgstr "Ändra Lösenord" + +#: Template/Users/profile.ctp:27 Template/Users/view.ctp:28;68 +msgid "Username" +msgstr "Användarnamn" + +#: Template/Users/profile.ctp:29 Template/Users/view.ctp:30 +msgid "Email" +msgstr "Epost" + +#: Template/Users/profile.ctp:34 +msgid "Social Accounts" +msgstr "Sociala konton" + +#: Template/Users/profile.ctp:38 Template/Users/view.ctp:70 +msgid "Avatar" +msgstr "Profilbild" + +#: Template/Users/profile.ctp:39 Template/Users/view.ctp:67 +msgid "Provider" +msgstr "Utförare" + +#: Template/Users/profile.ctp:40 +msgid "Link" +msgstr "Länk" + +#: Template/Users/profile.ctp:47 +msgid "Link to {0}" +msgstr "Länk till {0}" + +#: Template/Users/register.ctp:23 +msgid "Accept TOS conditions?" +msgstr "Jag accepterar villkoren" + +#: Template/Users/request_reset_password.ctp:5 +msgid "Please enter your email to reset your password" +msgstr "Ange din e-postadress för att återställa ditt lösenord" + +#: Template/Users/resend_token_validation.ctp:15 +msgid "Resend Validation email" +msgstr "Skicka nytt bekräftelsemejl" + +#: Template/Users/resend_token_validation.ctp:17 +msgid "Email or username" +msgstr "E-post/Användarnamn" + +#: Template/Users/view.ctp:16 +msgid "Delete User" +msgstr "Ta bort användare" + +#: Template/Users/view.ctp:18 +msgid "New User" +msgstr "Ny användare" + +#: Template/Users/view.ctp:26;65 +msgid "Id" +msgstr "Id" + +#: Template/Users/view.ctp:32 +msgid "First Name" +msgstr "Förnamn" + +#: Template/Users/view.ctp:34 +msgid "Last Name" +msgstr "Efternamn" + +#: Template/Users/view.ctp:36;71 +msgid "Token" +msgstr "Token" + +#: Template/Users/view.ctp:38 +msgid "Api Token" +msgstr "Api-token" + +#: Template/Users/view.ctp:42;73 +msgid "Active" +msgstr "Aktivt" + +#: Template/Users/view.ctp:46;72 +msgid "Token Expires" +msgstr "Token förfaller" + +#: Template/Users/view.ctp:48 +msgid "Activation Date" +msgstr "Aktiveringsdatum" + +#: Template/Users/view.ctp:50 +msgid "Tos Date" +msgstr "Tos datum" + +#: Template/Users/view.ctp:52;75 +msgid "Created" +msgstr "Skapad" + +#: Template/Users/view.ctp:54;76 +msgid "Modified" +msgstr "Ändrad" + +#: Template/Users/view.ctp:61 +msgid "Related Accounts" +msgstr "relaterade konton" + +#: Template/Users/view.ctp:66 +msgid "User Id" +msgstr "Användar-ID" + +#: Template/Users/view.ctp:69 +msgid "Reference" +msgstr "Referens" + +#: Template/Users/view.ctp:74 +msgid "Data" +msgstr "Data" + +#: View/Helper/UserHelper.php:62 +msgid "fa fa-{0}" +msgstr "fa fa-{0}" + +#: View/Helper/UserHelper.php:64 +msgid "btn btn-social btn-{0} " +msgstr "btn btn-social btn-{0} " + +#: View/Helper/UserHelper.php:77 +msgid "Logout" +msgstr "Logga ut" + +#: View/Helper/UserHelper.php:115 +msgid "Welcome, {0}" +msgstr "Välkommen, {0}" + +#~ msgid "There was an error associating your social network account" +#~ msgstr "Det gick inte att associera ditt sociala nätverkskonto" + +#~ msgid "Invalid token and/or email" +#~ msgstr "Ogiltig token och / eller epost" + +#~ msgid "The \"tos\" property is not present" +#~ msgstr "\"tos\" fältet saknas" + +#~ msgid "+ {0} secs" +#~ msgstr "{0} sek" + +#~ msgid "Sign in with Facebook" +#~ msgstr "Logga in med Facebook" + +#~ msgid "Sign in with Twitter" +#~ msgstr "Logga in med Twitter" diff --git a/src/Mailer/UsersMailer.php b/src/Mailer/UsersMailer.php new file mode 100755 index 000000000..5fa1beffc --- /dev/null +++ b/src/Mailer/UsersMailer.php @@ -0,0 +1,85 @@ +hiddenProperties(['password', 'token_expires', 'api_token']); + + $this + ->to($user['email']) + ->subject($firstName . $subject) + ->viewVars($user->toArray()) + ->template($template); + } + + /** + * Send the reset password email to the user + * + * @param EntityInterface $user User entity + * @param string $template string, note the first_name of the user will be prepended if exists + * + * @return array email send result + */ + protected function resetPassword(EntityInterface $user, $template = 'CakeDC/Users.reset_password') + { + $firstName = isset($user['first_name'])? $user['first_name'] . ', ' : ''; + $subject = __d('Users', '{0}Your reset password link', $firstName); + $user->hiddenProperties(['password', 'token_expires', 'api_token']); + + $this + ->to($user['email']) + ->subject($subject) + ->viewVars($user->toArray()) + ->template($template); + } + + /** + * Send account validation email to the user + * + * @param EntityInterface $user User entity + * @param EntityInterface $socialAccount SocialAccount entity + * @param string $template string, note the first_name of the user will be prepended if exists + * + * @return array email send result + */ + protected function socialAccountValidation(EntityInterface $user, EntityInterface $socialAccount, $template = 'CakeDC/Users.social_account_validation') + { + $firstName = isset($user['first_name'])? $user['first_name'] . ', ' : ''; + //note: we control the space after the username in the previous line + $subject = __d('Users', '{0}Your social account validation link', $firstName); + $this + ->to($user['email']) + ->subject($subject) + ->viewVars(compact('user', 'socialAccount')) + ->template($template); + } +} diff --git a/src/Model/Behavior/Behavior.php b/src/Model/Behavior/Behavior.php new file mode 100755 index 000000000..f80b0f250 --- /dev/null +++ b/src/Model/Behavior/Behavior.php @@ -0,0 +1,59 @@ +updateToken($tokenExpiration); + } else { + $user['active'] = true; + $user['activation_date'] = new Time(); + } + + return $user; + } + + /** + * Remove user token for validation + * + * @param EntityInterface $user user object. + * @return EntityInterface + */ + protected function _removeValidationToken(EntityInterface $user) + { + $user->token = null; + $user->token_expires = null; + $result = $this->_table->save($user); + + return $result; + } +} diff --git a/src/Model/Behavior/PasswordBehavior.php b/src/Model/Behavior/PasswordBehavior.php new file mode 100755 index 000000000..4f13207f7 --- /dev/null +++ b/src/Model/Behavior/PasswordBehavior.php @@ -0,0 +1,118 @@ +Email = new EmailSender(); + } + /** + * Resets user token + * + * @param string $reference User username or email + * @param array $options checkActive, sendEmail, expiration + * + * @return string + * @throws InvalidArgumentException + * @throws UserNotFoundException + * @throws UserAlreadyActiveException + */ + public function resetToken($reference, array $options = []) + { + if (empty($reference)) { + throw new InvalidArgumentException(__d('Users', "Reference cannot be null")); + } + + $expiration = Hash::get($options, 'expiration'); + if (empty($expiration)) { + throw new InvalidArgumentException(__d('Users', "Token expiration cannot be empty")); + } + + $user = $this->_getUser($reference); + + if (empty($user)) { + throw new UserNotFoundException(__d('Users', "User not found")); + } + if (Hash::get($options, 'checkActive')) { + if ($user->active) { + throw new UserAlreadyActiveException("User account already validated"); + } + $user->active = false; + $user->activation_date = null; + } + $user->updateToken($expiration); + $saveResult = $this->_table->save($user); + $template = !empty($options['emailTemplate']) ? $options['emailTemplate'] : 'CakeDC/Users.reset_password'; + if (Hash::get($options, 'sendEmail')) { + $this->Email->sendResetPasswordEmail($saveResult, null, $template); + } + return $saveResult; + } + + /** + * Get the user by email or username + * + * @param string $reference reference could be either an email or username + * @return mixed user entity if found + */ + protected function _getUser($reference) + { + return $this->_table->findAllByUsernameOrEmail($reference, $reference)->first(); + } + + /** + * Change password method + * + * @param EntityInterface $user user data. + * @throws WrongPasswordException + * @return mixed + */ + public function changePassword(EntityInterface $user) + { + $currentUser = $this->_table->get($user->id, [ + 'contain' => [] + ]); + + if (!empty($user->current_password)) { + if (!$user->checkPassword($user->current_password, $currentUser->password)) { + throw new WrongPasswordException(__d('Users', 'The old password does not match')); + } + } + $user = $this->_table->save($user); + if (!empty($user)) { + $user = $this->_removeValidationToken($user); + } + return $user; + } +} diff --git a/src/Model/Behavior/RegisterBehavior.php b/src/Model/Behavior/RegisterBehavior.php new file mode 100755 index 000000000..36bc66688 --- /dev/null +++ b/src/Model/Behavior/RegisterBehavior.php @@ -0,0 +1,189 @@ +validateEmail = (bool)Configure::read('Users.Email.validate'); + $this->useTos = (bool)Configure::read('Users.Tos.required'); + $this->Email = new EmailSender(); + } + + /** + * Registers an user. + * + * @param EntityInterface $user User information + * @param array $data User information + * @param array $options ['tokenExpiration] + * @return bool|EntityInterface + * @throws InvalidArgumentException + */ + public function register($user, $data, $options) + { + $validateEmail = Hash::get($options, 'validate_email'); + $tokenExpiration = Hash::get($options, 'token_expiration'); + $emailClass = Hash::get($options, 'email_class'); + $user = $this->_table->patchEntity($user, $data, ['validate' => Hash::get($options, 'validator') ?: $this->_getValidators($options)]); + $user->validated = false; + //@todo move updateActive to afterSave? + $user = $this->_updateActive($user, $validateEmail, $tokenExpiration); + $this->_table->isValidateEmail = $validateEmail; + $userSaved = $this->_table->save($user); + if ($userSaved && $validateEmail) { + $this->Email->sendValidationEmail($user, $emailClass); + } + return $userSaved; + } + + /** + * Validates token and return user + * + * @param type $token toke to be validated. + * @param null $callback function that will be returned. + * @throws TokenExpiredException when token has expired. + * @throws UserNotFoundException when user isn't found. + * @return User $user + */ + public function validate($token, $callback = null) + { + $user = $this->_table->find() + ->select(['token_expires', 'id', 'active', 'token']) + ->where(['token' => $token]) + ->first(); + if (empty($user)) { + throw new UserNotFoundException(__d('Users', "User not found for the given token and email.")); + } + if ($user->tokenExpired()) { + throw new TokenExpiredException(__d('Users', "Token has already expired user with no token")); + } + if (!method_exists($this, $callback)) { + return $user; + } + + return $this->_table->{$callback}($user); + } + + /** + * Activates an user + * + * @param EntityInterface $user user object. + * @return mixed User entity or bool false if the user could not be activated + * @throws UserAlreadyActiveException + */ + public function activateUser(EntityInterface $user) + { + if ($user->active) { + throw new UserAlreadyActiveException(__d('Users', "User account already validated")); + } + $user->activation_date = new DateTime(); + $user->token_expires = null; + $user->active = true; + $result = $this->_table->save($user); + + return $result; + } + + /** + * buildValidator + * + * @param Event $event event + * @param Validator $validator validator + * @param string $name name + * @return Validator + */ + public function buildValidator(Event $event, Validator $validator, $name) + { + if ($name === 'default') { + return $this->_emailValidator($validator, $this->validateEmail); + } + } + + /** + * Email validator + * + * @param Validator $validator Validator instance. + * @param bool $validateEmail true when email needs to be required + * @return Validator + */ + protected function _emailValidator(Validator $validator, $validateEmail) + { + $this->validateEmail = $validateEmail; + $validator + ->add('email', 'valid', ['rule' => 'email']) + ->notEmpty('email', 'This field is required', function ($context) { + return $this->validateEmail; + }); + return $validator; + } + + /** + * Tos validator + * + * @param Validator $validator Validator instance. + * @return Validator + */ + protected function _tosValidator(Validator $validator) + { + $validator + ->requirePresence('tos', 'create') + ->notEmpty('tos'); + return $validator; + } + + /** + * Returns the list of validators + * + * @param array $options Array of options ['validate_email' => true/false, 'use_tos' => true/false] + * @return Validator + */ + protected function _getValidators($options) + { + $validateEmail = Hash::get($options, 'validate_email'); + $useTos = Hash::get($options, 'use_tos'); + + $validator = $this->_table->validationDefault(new Validator()); + $validator = $this->_table->validationRegister($validator); + if ($useTos) { + $validator = $this->_tosValidator($validator); + } + + if ($validateEmail) { + $validator = $this->_emailValidator($validator, $validateEmail); + } + return $validator; + } +} diff --git a/src/Model/Behavior/SocialAccountBehavior.php b/src/Model/Behavior/SocialAccountBehavior.php new file mode 100755 index 000000000..81a009213 --- /dev/null +++ b/src/Model/Behavior/SocialAccountBehavior.php @@ -0,0 +1,149 @@ +_table->belongsTo('Users', [ + 'foreignKey' => 'user_id', + 'joinType' => 'INNER', + 'className' => Configure::read('Users.table') + ]); + $this->Email = new EmailSender(); + } + + /** + * After save callback + * + * @param Event $event event + * @param Entity $entity entity + * @param ArrayObject $options options + * @return mixed + */ + public function afterSave(Event $event, Entity $entity, $options) + { + if ($entity->active) { + return true; + } + $user = $this->_table->Users->find()->where(['Users.id' => $entity->user_id, 'Users.active' => true])->first(); + if (empty($user)) { + return true; + } + return $this->sendSocialValidationEmail($entity, $user); + } + + /** + * Send social validation email to the user + * + * @param EntityInterface $socialAccount social account + * @param EntityInterface $user user + * @param Email $email Email instance or null to use 'default' configuration + * @return mixed + */ + public function sendSocialValidationEmail(EntityInterface $socialAccount, EntityInterface $user, Email $email = null) + { + $this->Email = new EmailSender(); + $this->Email->sendSocialValidationEmail($socialAccount, $user, $email); + } + + /** + * Validates the social account + * + * @param string $provider provider + * @param string $reference reference + * @param string $token token + * @throws RecordNotFoundException + * @throws AccountAlreadyActiveException + * @return User + */ + public function validateAccount($provider, $reference, $token) + { + $socialAccount = $this->_table->find() + ->select(['id', 'provider', 'reference', 'active', 'token']) + ->where(['provider' => $provider, 'reference' => $reference]) + ->first(); + + if (!empty($socialAccount) && $socialAccount->token === $token) { + if ($socialAccount->active) { + throw new AccountAlreadyActiveException(__d('Users', "Account already validated")); + } + } else { + throw new RecordNotFoundException(__d('Users', "Account not found for the given token and email.")); + } + + return $this->_activateAccount($socialAccount); + } + + /** + * Validates the social account + * + * @param string $provider provider + * @param string $reference reference + * @throws RecordNotFoundException + * @throws AccountAlreadyActiveException + * @return User + */ + public function resendValidation($provider, $reference) + { + $socialAccount = $this->_table->find() + ->where(['provider' => $provider, 'reference' => $reference]) + ->contain('Users') + ->first(); + + if (!empty($socialAccount)) { + if ($socialAccount->active) { + throw new AccountAlreadyActiveException(__d('Users', "Account already validated")); + } + } else { + throw new RecordNotFoundException(__d('Users', "Account not found for the given token and email.")); + } + + return $this->sendSocialValidationEmail($socialAccount, $socialAccount->user); + } + + /** + * Activates an account + * + * @param Account $socialAccount social account + * @return EntityInterface + */ + protected function _activateAccount($socialAccount) + { + $socialAccount->active = true; + $result = $this->_table->save($socialAccount); + return $result; + } +} diff --git a/src/Model/Behavior/SocialBehavior.php b/src/Model/Behavior/SocialBehavior.php new file mode 100755 index 000000000..48adafda4 --- /dev/null +++ b/src/Model/Behavior/SocialBehavior.php @@ -0,0 +1,222 @@ +_table->SocialAccounts->find() + ->where(['SocialAccounts.reference' => $reference, 'SocialAccounts.provider' => $data['provider']]) + ->contain(['Users']) + ->first(); + if (empty($existingAccount->user)) { + $user = $this->_createSocialUser($data, $options); + if (!empty($user->social_accounts[0])) { + $existingAccount = $user->social_accounts[0]; + } else { + //@todo: what if we don't have a social account after createSocialUser? + throw new InvalidArgumentException(__d('Users', 'Unable to login user with reference {0}', $reference)); + } + } else { + $user = $existingAccount->user; + } + if (!empty($existingAccount)) { + if ($existingAccount->active) { + if ($user->active) { + return $user; + } else { + throw new UserNotActiveException([ + $existingAccount->provider, + $existingAccount->$user + ]); + } + + } else { + throw new AccountNotActiveException([ + $existingAccount->provider, + $existingAccount->reference + ]); + } + } + return false; + } + + /** + * Creates social user, populate the user data based on the social login data first and save it + * + * @param array $data Array social user. + * @param array $options Array option data. + * @throws MissingEmailException + * @return bool|EntityInterface|mixed result of the save operation + */ + protected function _createSocialUser($data, $options = []) + { + $useEmail = Hash::get($options, 'use_email'); + $validateEmail = Hash::get($options, 'validate_email'); + $tokenExpiration = Hash::get($options, 'token_expiration'); + $existingUser = null; + $email = Hash::get($data, 'email'); + if ($useEmail && empty($email)) { + throw new MissingEmailException(__d('Users', 'Email not present')); + } else { + $existingUser = $this->_table->find() + ->where([$this->_table->alias() . '.email' => $email]) + ->first(); + } + + $user = $this->_populateUser($data, $existingUser, $useEmail, $validateEmail, $tokenExpiration); + $this->_table->isValidateEmail = $validateEmail; + $result = $this->_table->save($user); + return $result; + } + + /** + * Build new user entity either by using an existing user or extracting the data from the social login + * data to create a new one + * + * @param array $data Array social login. + * @param EntityInterface $existingUser user data. + * @param string $useEmail email to use. + * @param string $validateEmail email to validate. + * @param string $tokenExpiration token_expires data. + * @return EntityInterface + * @todo refactor + */ + protected function _populateUser($data, $existingUser, $useEmail, $validateEmail, $tokenExpiration) + { + $accountData['username'] = Hash::get($data, 'username'); + $accountData['reference'] = Hash::get($data, 'id'); + $accountData['avatar'] = Hash::get($data, 'avatar'); + $accountData['link'] = Hash::get($data, 'link'); + + $accountData['avatar'] = str_replace('normal', 'square', $accountData['avatar']); + $accountData['description'] = Hash::get($data, 'bio'); + $accountData['token'] = Hash::get($data, 'credentials.token'); + $accountData['token_secret'] = Hash::get($data, 'credentials.secret'); + $expires = Hash::get($data, 'credentials.expires'); + if (!empty($expires)) { + $expiresTime = new DateTime(); + $accountData['token_expires'] = $expiresTime->setTimestamp($expires)->format('Y-m-d H:i:s'); + } else { + $accountData['token_expires'] = null; + } + $accountData['data'] = serialize(Hash::get($data, 'raw')); + $accountData['active'] = true; + + $dataValidated = Hash::get($data, 'validated'); + + if (empty($existingUser)) { + $firstName = Hash::get($data, 'first_name'); + $lastName = Hash::get($data, 'last_name'); + if (!empty($firstName) && !empty($lastName)) { + $userData['first_name'] = $firstName; + $userData['last_name'] = $lastName; + } else { + $name = explode(' ', Hash::get($data, 'full_name')); + $userData['first_name'] = Hash::get($name, 0); + array_shift($name); + $userData['last_name'] = implode(' ', $name); + } + $userData['username'] = Hash::get($data, 'username'); + $username = Hash::get($userData, 'username'); + if (empty($username)) { + $dataEmail = Hash::get($data, 'email'); + if (!empty($dataEmail)) { + $email = explode('@', $dataEmail); + $userData['username'] = Hash::get($email, 0); + } else { + $firstName = Hash::get($userData, 'first_name'); + $lastName = Hash::get($userData, 'last_name'); + $userData['username'] = strtolower($firstName . $lastName); + $userData['username'] = preg_replace('/[^A-Za-z0-9]/i', '', Hash::get($userData, 'username')); + } + } + $userData['username'] = $this->generateUniqueUsername(Hash::get($userData, 'username')); + if ($useEmail) { + $userData['email'] = Hash::get($data, 'email'); + if (empty($dataValidated)) { + $accountData['active'] = false; + } + } + + $userData['password'] = $this->randomString(); + $userData['avatar'] = Hash::get($data, 'avatar'); + $userData['validated'] = !empty($dataValidated); + $userData['tos_date'] = date("Y-m-d H:i:s"); + $userData['gender'] = Hash::get($data, 'gender'); + $userData['social_accounts'][] = $accountData; + + $user = $this->_table->newEntity($userData); + $user = $this->_updateActive($user, false, $tokenExpiration); + } else { + if ($useEmail && empty($dataValidated)) { + $accountData['active'] = false; + } + $user = $existingUser; + } + $socialAccount = $this->_table->SocialAccounts->newEntity($accountData); + //ensure provider is present in Entity + $socialAccount['provider'] = Hash::get($data, 'provider'); + $user['social_accounts'] = [$socialAccount]; + return $user; + } + + /** + * Checks if username exists and generate a new one + * + * @param string $username username data. + * @return string + */ + public function generateUniqueUsername($username) + { + $i = 0; + while (true) { + $existingUsername = $this->_table->find()->where([$this->_table->alias() . '.username' => $username])->count(); + if ($existingUsername > 0) { + $username = $username . $i; + $i++; + continue; + } + break; + } + return $username; + } +} diff --git a/src/Model/Entity/SocialAccount.php b/src/Model/Entity/SocialAccount.php new file mode 100755 index 000000000..dbf4709a1 --- /dev/null +++ b/src/Model/Entity/SocialAccount.php @@ -0,0 +1,41 @@ + true, + 'id' => false, + ]; + + /** + * Fields that are excluded from JSON an array versions of the entity. + * + * @var array + */ + protected $_hidden = [ + 'token', + 'token_secret', + 'token_expires', + ]; +} diff --git a/src/Model/Entity/User.php b/src/Model/Entity/User.php new file mode 100755 index 000000000..b8a57751a --- /dev/null +++ b/src/Model/Entity/User.php @@ -0,0 +1,165 @@ + true, + 'id' => false, + 'is_superuser' => false, + 'role' => false, + ]; + + /** + * Fields that are excluded from JSON an array versions of the entity. + * + * @var array + */ + protected $_hidden = [ + 'password', + 'token', + 'token_expires', + 'api_token', + ]; + + /** + * @param string $password password that will be set. + * @return bool|string + */ + protected function _setPassword($password) + { + return $this->hashPassword($password); + } + + /** + * @param string $password password that will be confirm. + * @return bool|string + */ + protected function _setConfirmPassword($password) + { + return $this->hashPassword($password); + } + + /** + * @param string $tos tos option. It will be set the tos_date + * @return bool + */ + protected function _setTos($tos) + { + if ((bool)$tos === true) { + $this->set('tos_date', new DateTime()); + } + return $tos; + } + + /** + * Hash a password using the configured password hasher, + * use DefaultPasswordHasher if no one was configured + * + * @param string $password password to be hashed + * @return mixed + */ + public function hashPassword($password) + { + $PasswordHasher = $this->getPasswordHasher(); + return $PasswordHasher->hash($password); + } + + /** + * Return the configured Password Hasher + * + * @return mixed + */ + public function getPasswordHasher() + { + $passwordHasher = Configure::read('Users.passwordHasher'); + if (!class_exists($passwordHasher)) { + $passwordHasher = '\Cake\Auth\DefaultPasswordHasher'; + } + return new $passwordHasher; + } + + /** + * Checks if a password is correctly hashed + * + * @param string $password password that will be check. + * @param string $hashedPassword hash used to check password. + * @return bool + */ + public function checkPassword($password, $hashedPassword) + { + $PasswordHasher = $this->getPasswordHasher(); + return $PasswordHasher->check($password, $hashedPassword); + } + + /** + * Returns if the token has already expired + * + * @return bool + */ + public function tokenExpired() + { + if (empty($this->token_expires)) { + return true; + } + + $tokenExpiresTime = $this->token_expires; + if (is_object($this->token_expires)) { + $tokenExpiresTime = $this->token_expires->format("Y-m-d H:i"); + } + + return strtotime($tokenExpiresTime) < strtotime("now"); + } + + /** + * Getter for user avatar + * + * @return string|null avatar + */ + protected function _getAvatar() + { + $avatar = null; + if (!empty($this->_properties['social_accounts'][0])) { + $avatar = $this->_properties['social_accounts'][0]['avatar']; + } + return $avatar; + } + + /** + * Generate token_expires and token in a user + * @param string $tokenExpiration new token_expires user. + * + * @return void + */ + public function updateToken($tokenExpiration) + { + $expires = new DateTime(); + $expires->modify("+ $tokenExpiration secs"); + $this->token_expires = $expires; + $this->token = str_replace('-', '', Text::uuid()); + } +} diff --git a/src/Model/Table/SocialAccountsTable.php b/src/Model/Table/SocialAccountsTable.php new file mode 100755 index 000000000..5e86bb046 --- /dev/null +++ b/src/Model/Table/SocialAccountsTable.php @@ -0,0 +1,118 @@ +table('social_accounts'); + $this->displayField('id'); + $this->primaryKey('id'); + $this->addBehavior('Timestamp'); + $this->addBehavior('CakeDC/Users.SocialAccount'); + } + + /** + * Default validation rules. + * + * @param Validator $validator Validator instance. + * @return Validator + */ + public function validationDefault(Validator $validator) + { + $validator + ->add('id', 'valid', ['rule' => 'uuid']) + ->allowEmpty('id', 'create'); + + $validator + ->requirePresence('provider', 'create') + ->notEmpty('provider'); + + $validator + ->allowEmpty('username'); + + $validator + ->requirePresence('reference', 'create') + ->notEmpty('reference'); + + $validator + ->requirePresence('link', 'create') + ->notEmpty('reference'); + + $validator + ->allowEmpty('avatar'); + + $validator + ->allowEmpty('description'); + + $validator + ->requirePresence('token', 'create') + ->notEmpty('token'); + + $validator + ->allowEmpty('token_secret'); + + $validator + ->add('token_expires', 'valid', ['rule' => 'datetime']) + ->allowEmpty('token_expires'); + + $validator + ->add('active', 'valid', ['rule' => 'boolean']) + ->requirePresence('active', 'create') + ->notEmpty('active'); + + $validator + ->requirePresence('data', 'create') + ->notEmpty('data'); + + return $validator; + } + + /** + * Returns a rules checker object that will be used for validating + * application integrity. + * + * @param RulesChecker $rules The rules object to be modified. + * @return RulesChecker + */ + public function buildRules(RulesChecker $rules) + { + $rules->add($rules->existsIn(['user_id'], 'Users')); + return $rules; + } +} diff --git a/src/Model/Table/UsersTable.php b/src/Model/Table/UsersTable.php new file mode 100755 index 000000000..a34a6fb93 --- /dev/null +++ b/src/Model/Table/UsersTable.php @@ -0,0 +1,170 @@ +table('users'); + $this->displayField('username'); + $this->primaryKey('id'); + $this->addBehavior('Timestamp'); + $this->addBehavior('CakeDC/Users.Register'); + $this->addBehavior('CakeDC/Users.Password'); + $this->addBehavior('CakeDC/Users.Social'); + $this->hasMany('SocialAccounts', [ + 'foreignKey' => 'user_id', + 'className' => 'CakeDC/Users.SocialAccounts' + ]); + } + + /** + * Adds some rules for password confirm + * @param Validator $validator Cake validator object. + * @return Validator + */ + public function validationPasswordConfirm(Validator $validator) + { + $validator + ->requirePresence('password_confirm', 'create') + ->notEmpty('password_confirm'); + + $validator->add('password', 'custom', [ + 'rule' => function ($value, $context) { + $confirm = Hash::get($context, 'data.password_confirm'); + if (!is_null($confirm) && $value != $confirm) { + return false; + } + return true; + }, + 'message' => __d('Users', 'Your password does not match your confirm password. Please try again'), + 'on' => ['create', 'update'], + 'allowEmpty' => false + ]); + + return $validator; + } + + /** + * Default validation rules. + * + * @param Validator $validator Validator instance. + * @return Validator + */ + public function validationDefault(Validator $validator) + { + $validator + ->allowEmpty('id', 'create'); + + $validator + ->requirePresence('username', 'create') + ->notEmpty('username'); + + $validator + ->requirePresence('password', 'create') + ->notEmpty('password'); + + $validator + ->allowEmpty('first_name'); + + $validator + ->allowEmpty('last_name'); + + $validator + ->allowEmpty('token'); + + $validator + ->add('token_expires', 'valid', ['rule' => 'datetime']) + ->allowEmpty('token_expires'); + + $validator + ->allowEmpty('api_token'); + + $validator + ->add('activation_date', 'valid', ['rule' => 'datetime']) + ->allowEmpty('activation_date'); + + $validator + ->add('tos_date', 'valid', ['rule' => 'datetime']) + ->allowEmpty('tos_date'); + + return $validator; + } + + /** + * Wrapper for all validation rules for register + * @param Validator $validator Cake validator object. + * + * @return Validator + */ + public function validationRegister(Validator $validator) + { + $validator = $this->validationDefault($validator); + $validator = $this->validationPasswordConfirm($validator); + return $validator; + } + + /** + * Returns a rules checker object that will be used for validating + * application integrity. + * + * @param RulesChecker $rules The rules object to be modified. + * @return RulesChecker + */ + public function buildRules(RulesChecker $rules) + { + $rules->add($rules->isUnique(['username']), '_isUnique', [ + 'errorField' => 'username', + 'message' => __d('Users', 'Username already exists') + ]); + + if ($this->isValidateEmail) { + $rules->add($rules->isUnique(['email']), '_isUnique', [ + 'errorField' => 'email', + 'message' => __d('Users', 'Email already exists') + ]); + } + + return $rules; + } +} diff --git a/src/Shell/UsersShell.php b/src/Shell/UsersShell.php new file mode 100755 index 000000000..908bfd502 --- /dev/null +++ b/src/Shell/UsersShell.php @@ -0,0 +1,366 @@ +Users = $this->loadModel(Configure::read('Users.table')); + } + + /** + * + * @return OptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + $parser->description(__d('Users', 'Utilities for CakeDC Users Plugin')) + ->addSubcommand('activateUser')->description(__d('Users', 'Activate an specific user')) + ->addSubcommand('addSuperuser')->description(__d('Users', 'Add a new superadmin user for testing purposes')) + ->addSubcommand('addUser')->description(__d('Users', 'Add a new user')) + ->addSubcommand('changeRole')->description(__d('Users', 'Change the role for an specific user')) + ->addSubcommand('deactivateUser')->description(__d('Users', 'Deactivate an specific user')) + ->addSubcommand('deleteUser')->description(__d('Users', 'Delete an specific user')) + ->addSubcommand('passwordEmail')->description(__d('Users', 'Reset the password via email')) + ->addSubcommand('resetAllPasswords')->description(__d('Users', 'Reset the password for all users')) + ->addSubcommand('resetPassword')->description(__d('Users', 'Reset the password for an specific user')) + ->addOptions([ + 'username' => ['short' => 'u', 'help' => 'The username for the new user'], + 'password' => ['short' => 'p', 'help' => 'The password for the new user'], + 'email' => ['short' => 'e', 'help' => 'The email for the new user'] + ]); + return $parser; + } + + /** + * Add a new user + * + * @return void + */ + public function addUser() + { + $username = (empty($this->params['username']) ? + $this->_generateRandomUsername() : $this->params['username']); + + $username = $this->Users->generateUniqueUsername($username); + $password = (empty($this->params['password']) ? + $this->_generateRandomPassword() : $this->params['password']); + $email = (empty($this->params['email']) ? $username . '@example.com' : $this->params['email']); + $user = [ + 'username' => $username, + 'email' => $email, + 'password' => $password, + 'active' => 1, + ]; + + $userEntity = $this->Users->newEntity($user); + $userEntity->is_superuser = true; + $userEntity->role = 'user'; + $savedUser = $this->Users->save($userEntity); + $this->out(__d('Users', 'User added:')); + $this->out(__d('Users', 'Id: {0}', $savedUser->id)); + $this->out(__d('Users', 'Username: {0}', $username)); + $this->out(__d('Users', 'Email: {0}', $savedUser->email)); + $this->out(__d('Users', 'Password: {0}', $password)); + } + + /** + * Add a new superadmin user + * + * @return void + */ + public function addSuperuser() + { + $username = $this->Users->generateUniqueUsername('superadmin'); + $password = $this->_generateRandomPassword(); + $user = [ + 'username' => $username, + 'email' => $username . '@example.com', + 'password' => $password, + 'active' => 1, + ]; + + $userEntity = $this->Users->newEntity($user); + $userEntity->is_superuser = true; + $userEntity->role = 'superuser'; + $savedUser = $this->Users->save($userEntity); + if (!empty($savedUser)) { + $this->out(__d('Users', 'Superuser added:')); + $this->out(__d('Users', 'Id: {0}', $savedUser->id)); + $this->out(__d('Users', 'Username: {0}', $username)); + $this->out(__d('Users', 'Email: {0}', $savedUser->email)); + $this->out(__d('Users', 'Password: {0}', $password)); + } else { + $this->out(__d('Users', 'Superuser could not be added:')); + + collection($userEntity->errors())->each(function ($error, $field) { + $this->out(__d('Users', 'Field: {0} Error: {1}', $field, implode(',', $error))); + }); + } + } + + /** + * Reset password for all user + * + * Arguments: + * + * - Password to be set + * + * @return void + */ + public function resetAllPasswords() + { + $password = Hash::get($this->args, 0); + if (empty($password)) { + $this->error(__d('Users', 'Please enter a password.')); + } + $hashedPassword = $this->_generatedHashedPassword($password); + $this->Users->updateAll(['password' => $hashedPassword], ['id IS NOT NULL']); + $this->out(__d('Users', 'Password changed for all users')); + $this->out(__d('Users', 'New password: {0}', $password)); + } + + /** + * Reset password for a user + * + * Arguments: + * + * - Username + * - Password to be set + * + * @return void + */ + public function resetPassword() + { + $username = Hash::get($this->args, 0); + $password = Hash::get($this->args, 1); + if (empty($username)) { + $this->error(__d('Users', 'Please enter a username.')); + } + if (empty($password)) { + $this->error(__d('Users', 'Please enter a password.')); + } + $data = [ + 'password' => $password + ]; + $this->_updateUser($username, $data); + $this->out(__d('Users', 'Password changed for user: {0}', $username)); + $this->out(__d('Users', 'New password: {0}', $password)); + } + + /** + * Change role for a user + * + * Arguments: + * + * - Username + * - Role to be set + * + * @return void + */ + public function changeRole() + { + $username = Hash::get($this->args, 0); + $role = Hash::get($this->args, 1); + if (empty($username)) { + $this->error(__d('Users', 'Please enter a username.')); + } + if (empty($role)) { + $this->error(__d('Users', 'Please enter a role.')); + } + $data = [ + 'role' => $role + ]; + $savedUser = $this->_updateUser($username, $data); + $this->out(__d('Users', 'Role changed for user: {0}', $username)); + $this->out(__d('Users', 'New role: {0}', $savedUser->role)); + } + + /** + * Activate an specific user + * + * Arguments: + * + * - Username + * + * @return void + */ + public function activateUser() + { + $user = $this->_changeUserActive(true); + $this->out(__d('Users', 'User was activated: {0}', $user->username)); + } + + /** + * De-activate an specific user + * + * Arguments: + * + * - Username + * + * @return void + */ + public function deactivateUser() + { + $user = $this->_changeUserActive(false); + $this->out(__d('Users', 'User was de-activated: {0}', $user->username)); + } + + /** + * Reset password via email for user + * + * @return void + */ + public function passwordEmail() + { + $reference = Hash::get($this->args, 0); + if (empty($reference)) { + $this->error(__d('Users', 'Please enter a username or email.')); + } + $resetUser = $this->Users->resetToken($reference, [ + 'expiration' => Configure::read('Users.Token.expiration'), + 'checkActive' => false, + 'sendEmail' => true, + ]); + if ($resetUser) { + $msg = __d('Users', 'Please ask the user to check the email to continue with password reset process'); + $this->out($msg); + } else { + $msg = __d('Users', 'The password token could not be generated. Please try again'); + $this->error($msg); + } + } + + /** + * Change user active field + * + * @param bool $active active value + * @return bool + */ + protected function _changeUserActive($active) + { + $username = Hash::get($this->args, 0); + if (empty($username)) { + $this->error(__d('Users', 'Please enter a username.')); + } + $data = [ + 'active' => $active + ]; + return $this->_updateUser($username, $data); + } + + /** + * Update user by username + * + * @param string $username username + * @param array $data data + * @return bool + */ + protected function _updateUser($username, $data) + { + $user = $this->Users->find()->where(['username' => $username])->first(); + if (empty($user)) { + $this->error(__d('Users', 'The user was not found.')); + } + $user = $this->Users->patchEntity($user, $data); + collection($data)->filter(function ($value, $field) use ($user) { + return !$user->accessible($field); + })->each(function ($value, $field) use (&$user) { + $user->{$field} = $value; + }); + $savedUser = $this->Users->save($user); + return $savedUser; + } + + /** + * Delete an specific user and associated social accounts + * + * @return void + */ + public function deleteUser() + { + $username = Hash::get($this->args, 0); + if (empty($username)) { + $this->error(__d('Users', 'Please enter a username.')); + } + $user = $this->Users->find()->where(['username' => $username])->first(); + if (isset($this->Users->SocialAccounts)) { + $this->Users->SocialAccounts->deleteAll(['user_id' => $user->id]); + } + $deleteUser = $this->Users->delete($user); + if (!$deleteUser) { + $this->error(__d('Users', 'The user {0} was not deleted. Please try again', $username)); + } + $this->out(__d('Users', 'The user {0} was deleted successfully', $username)); + } + + /** + * Generates a random password. + * + * @return string + */ + protected function _generateRandomPassword() + { + return str_replace('-', '', Text::uuid()); + } + + /** + * Generates a random username based on a list of preexisting ones. + * + * @return string + */ + protected function _generateRandomUsername() + { + return $this->_usernameSeed[array_rand($this->_usernameSeed)]; + } + + /** + * Hash a password + * + * @param string $password password + * @return string + */ + protected function _generatedHashedPassword($password) + { + return (new User)->hashPassword($password); + } + //add filters LIKE in username and email to some tasks + // --force to ignore "you are about to do X to Y users" +} diff --git a/src/Template/Email/html/reset_password.ctp b/src/Template/Email/html/reset_password.ctp new file mode 100755 index 000000000..10feeba1f --- /dev/null +++ b/src/Template/Email/html/reset_password.ctp @@ -0,0 +1,31 @@ + true, + 'plugin' => 'CakeDC/Users', + 'controller' => 'Users', + 'action' => 'resetPassword', + isset($token) ? $token : '' +]; +?> +

+, +

+

+ Html->link(__d('Users', 'Reset your password here'), $activationUrl) ?> +

+

+ Url->build($activationUrl)) ?> +

+

+ , +

diff --git a/src/Template/Email/html/social_account_validation.ctp b/src/Template/Email/html/social_account_validation.ctp new file mode 100755 index 000000000..f4357c898 --- /dev/null +++ b/src/Template/Email/html/social_account_validation.ctp @@ -0,0 +1,36 @@ + + +

+, +

+

+ true, + 'plugin' => 'CakeDC/Users', + 'controller' => 'SocialAccounts', + 'action' => 'validateAccount', + $socialAccount['provider'], + $socialAccount['reference'], + $socialAccount['token'], + ]; + echo $this->Html->link($text, $activationUrl); + ?> +

+

+ Url->build($activationUrl)) ?> +

+

+ , +

diff --git a/src/Template/Email/html/validation.ctp b/src/Template/Email/html/validation.ctp new file mode 100755 index 000000000..df1dc3fe2 --- /dev/null +++ b/src/Template/Email/html/validation.ctp @@ -0,0 +1,31 @@ + true, + 'plugin' => 'CakeDC/Users', + 'controller' => 'Users', + 'action' => 'validateEmail', + isset($token) ? $token : '' +]; +?> +

+, +

+

+ Html->link(__d('Users', 'Activate your account here'), $activationUrl) ?> +

+

+ Url->build($activationUrl)) ?> +

+

+ , +

diff --git a/src/Template/Email/text/reset_password.ctp b/src/Template/Email/text/reset_password.ctp new file mode 100755 index 000000000..c2f38948e --- /dev/null +++ b/src/Template/Email/text/reset_password.ctp @@ -0,0 +1,25 @@ + true, + 'plugin' => 'CakeDC/Users', + 'controller' => 'Users', + 'action' => 'resetPassword', + isset($token) ? $token : '' +]; +?> +, + +Url->build($activationUrl)) ?> + +, + diff --git a/src/Template/Email/text/social_account_validation.ctp b/src/Template/Email/text/social_account_validation.ctp new file mode 100755 index 000000000..e2a7503d7 --- /dev/null +++ b/src/Template/Email/text/social_account_validation.ctp @@ -0,0 +1,27 @@ + true, + 'plugin' => 'CakeDC/Users', + 'controller' => 'SocialAccounts', + 'action' => 'validateAccount', + $socialAccount['provider'], + $socialAccount['reference'], + $socialAccount['token'], + ]; +?> +, + +Url->build($activationUrl)) ?> + +, + diff --git a/src/Template/Email/text/validation.ctp b/src/Template/Email/text/validation.ctp new file mode 100755 index 000000000..9bc3deb34 --- /dev/null +++ b/src/Template/Email/text/validation.ctp @@ -0,0 +1,25 @@ + true, + 'plugin' => 'CakeDC/Users', + 'controller' => 'Users', + 'action' => 'validateEmail', + isset($token) ? $token : '' +]; +?> +, + +Url->build($activationUrl)) ?> + +, + diff --git a/src/Template/Layout/Email/html/default.ctp b/src/Template/Layout/Email/html/default.ctp new file mode 100755 index 000000000..a9cd65b22 --- /dev/null +++ b/src/Template/Layout/Email/html/default.ctp @@ -0,0 +1,25 @@ + + + + + + <?= $this->fetch('title') ?> + + + fetch('content') ?> + + diff --git a/src/Template/Layout/Email/text/default.ctp b/src/Template/Layout/Email/text/default.ctp new file mode 100755 index 000000000..871dcfb48 --- /dev/null +++ b/src/Template/Layout/Email/text/default.ctp @@ -0,0 +1,16 @@ + +fetch('content') ?> diff --git a/src/Template/Layout/default.ctp b/src/Template/Layout/default.ctp new file mode 100755 index 000000000..225659c15 --- /dev/null +++ b/src/Template/Layout/default.ctp @@ -0,0 +1,33 @@ + + + + + Html->charset() ?> + + Html->meta('icon') ?> + Html->css('/admin/css/base') ?> + Html->css('/admin/css/cake') ?> + fetch('meta') ?> + fetch('css') ?> + fetch('script') ?> + + <?= $this->fetch('title') ?> + + + fetch('content') ?> + + diff --git a/src/Template/Users/add.ctp b/src/Template/Users/add.ctp new file mode 100755 index 000000000..29a1dfb61 --- /dev/null +++ b/src/Template/Users/add.ctp @@ -0,0 +1,34 @@ + +
+

+
    +
  • Html->link(__d('Users', 'List Users'), ['action' => 'index']) ?>
  • +
  • Html->link(__d('Users', 'List Accounts'), ['controller' => 'Accounts', 'action' => 'index']) ?>
  • +
+
+
+ Form->create($Users); ?> +
+ + Form->input('username'); + echo $this->Form->input('email'); + echo $this->Form->input('password'); + echo $this->Form->input('first_name'); + echo $this->Form->input('last_name'); + echo $this->Form->input('active', ['type' => 'checkbox']); + ?> +
+ Form->button(__d('Users', 'Submit')) ?> + Form->end() ?> +
diff --git a/src/Template/Users/change_password.ctp b/src/Template/Users/change_password.ctp new file mode 100755 index 000000000..129b1b87a --- /dev/null +++ b/src/Template/Users/change_password.ctp @@ -0,0 +1,19 @@ +
+ Flash->render('auth') ?> + Form->create($user) ?> +
+ + + Form->input('current_password', [ + 'type' => 'password', + 'required' => true, + 'label' => __d('Users', 'Current password')]); + ?> + + Form->input('password'); ?> + Form->input('password_confirm', ['type' => 'password', 'required' => true]); ?> + +
+ Form->button(__d('Users', 'Submit')); ?> + Form->end() ?> +
\ No newline at end of file diff --git a/src/Template/Users/edit.ctp b/src/Template/Users/edit.ctp new file mode 100755 index 000000000..550b124fe --- /dev/null +++ b/src/Template/Users/edit.ctp @@ -0,0 +1,44 @@ + +
+

+
    +
  • Form->postLink( + __d('Users', 'Delete'), + ['action' => 'delete', $Users->id], + ['confirm' => __d('Users', 'Are you sure you want to delete # {0}?', $Users->id)] + ) + ?>
  • +
  • Html->link(__d('Users', 'List Users'), ['action' => 'index']) ?>
  • +
  • Html->link(__d('Users', 'List Accounts'), ['controller' => 'Accounts', 'action' => 'index']) ?>
  • +
+
+
+ Form->create($Users); ?> +
+ + Form->input('username'); + echo $this->Form->input('email'); + echo $this->Form->input('first_name'); + echo $this->Form->input('last_name'); + echo $this->Form->input('token'); + echo $this->Form->input('token_expires'); + echo $this->Form->input('api_token'); + echo $this->Form->input('activation_date'); + echo $this->Form->input('tos_date'); + echo $this->Form->input('active'); + ?> +
+ Form->button(__d('Users', 'Submit')) ?> + Form->end() ?> +
diff --git a/src/Template/Users/index.ctp b/src/Template/Users/index.ctp new file mode 100755 index 000000000..500b381a7 --- /dev/null +++ b/src/Template/Users/index.ctp @@ -0,0 +1,55 @@ + +
+

+
    +
  • Html->link(__d('Users', 'New {0}', $tableAlias), ['action' => 'add']) ?>
  • +
+
+
+ + + + + + + + + + + + + + + + + + + + + + +
Paginator->sort('username') ?>Paginator->sort('email') ?>Paginator->sort('first_name') ?>Paginator->sort('last_name') ?>
username) ?>email) ?>first_name) ?>last_name) ?> + Html->link(__d('Users', 'View'), ['action' => 'view', $user->id]) ?> + Html->link(__d('Users', 'Change password'), ['action' => 'changePassword', $user->id]) ?> + Html->link(__d('Users', 'Edit'), ['action' => 'edit', $user->id]) ?> + Form->postLink(__d('Users', 'Delete'), ['action' => 'delete', $user->id], ['confirm' => __d('Users', 'Are you sure you want to delete # {0}?', $user->id)]) ?> +
+
+
    + Paginator->prev('< ' . __d('Users', 'previous')) ?> + Paginator->numbers() ?> + Paginator->next(__d('Users', 'next') . ' >') ?> +
+

Paginator->counter() ?>

+
+
diff --git a/src/Template/Users/login.ctp b/src/Template/Users/login.ctp new file mode 100755 index 000000000..e62eb2711 --- /dev/null +++ b/src/Template/Users/login.ctp @@ -0,0 +1,35 @@ + +
+ Flash->render('auth') ?> + Form->create() ?> +
+ + Form->input('username', ['required' => true]) ?> + Form->input('password', ['required' => true]) ?> + Form->input(Configure::read('Users.Key.Data.rememberMe'), [ + 'type' => 'checkbox', + 'label' => __d('Users', 'Remember me'), + 'checked' => 'checked' + ]); + } + ?> +
+ User->socialLoginList()); ?> + Form->button(__d('Users', 'Login')); ?> + Form->end() ?> +
diff --git a/src/Template/Users/profile.ctp b/src/Template/Users/profile.ctp new file mode 100755 index 000000000..59091709d --- /dev/null +++ b/src/Template/Users/profile.ctp @@ -0,0 +1,74 @@ + +
+

Html->image(empty($user->avatar) ? $avatarPlaceholder : $user->avatar, ['width' => '180', 'height' => '180']); ?>

+

+ Html->tag( + 'span', + __d('Users', '{0} {1}', $user->first_name, $user->last_name), + ['class' => 'full_name'] + ) + ?> +

+ + Html->link(__d('Users', 'Change Password'), ['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'changePassword']); ?> +
+
+
+

username) ?>

+
+

email) ?>

+ social_accounts)): + ?> +
+ + + + + + + + + + social_accounts as $socialAccount): + $escapedUsername = h($socialAccount->username); + $linkText = empty($escapedUsername) ? __d('Users', 'Link to {0}', h($socialAccount->provider)) : h($socialAccount->username) + ?> + + + + + + + +
Html->image( + $socialAccount->avatar, + ['width' => '90', 'height' => '90'] + ) ?> + provider) ?>Html->link( + $linkText, + $socialAccount->link, + ['target' => '_blank'] + ) ?>
+ +
+
+
diff --git a/src/Template/Users/register.ctp b/src/Template/Users/register.ctp new file mode 100755 index 000000000..f217de2f4 --- /dev/null +++ b/src/Template/Users/register.ctp @@ -0,0 +1,32 @@ + +
+ Form->create($user); ?> +
+ + Form->input('username'); + echo $this->Form->input('email'); + echo $this->Form->input('password'); + echo $this->Form->input('password_confirm', ['type' => 'password']); + echo $this->Form->input('first_name'); + echo $this->Form->input('last_name'); + if (Configure::read('Users.Tos.required')) { + echo $this->Form->input('tos', ['type' => 'checkbox', 'label' => __d('Users', 'Accept TOS conditions?'), 'required' => true]); + } + echo $this->User->addReCaptcha(); + ?> +
+ Form->button(__d('Users', 'Submit')) ?> + Form->end() ?> +
diff --git a/src/Template/Users/request_reset_password.ctp b/src/Template/Users/request_reset_password.ctp new file mode 100755 index 000000000..c8559a63c --- /dev/null +++ b/src/Template/Users/request_reset_password.ctp @@ -0,0 +1,10 @@ +
+ Flash->render('auth') ?> + Form->create('User') ?> +
+ + Form->input('reference') ?> +
+ Form->button(__d('Users', 'Submit')); ?> + Form->end() ?> +
diff --git a/src/Template/Users/resend_token_validation.ctp b/src/Template/Users/resend_token_validation.ctp new file mode 100755 index 000000000..c64c38684 --- /dev/null +++ b/src/Template/Users/resend_token_validation.ctp @@ -0,0 +1,22 @@ + +
+ Form->create($user); ?> +
+ + Form->input('reference', ['label' => __d('Users', 'Email or username')]); + ?> +
+ Form->button(__d('Users', 'Submit')) ?> + Form->end() ?> +
diff --git a/src/Template/Users/social_email.ctp b/src/Template/Users/social_email.ctp new file mode 100755 index 000000000..4ac0f9071 --- /dev/null +++ b/src/Template/Users/social_email.ctp @@ -0,0 +1,22 @@ + +
+ Flash->render() ?> + Form->create('User') ?> +
+ + Form->input('email') ?> +
+ User->addReCaptcha(); ?> + Form->button(__d('Users', 'Submit')); ?> + Form->end() ?> +
diff --git a/src/Template/Users/view.ctp b/src/Template/Users/view.ctp new file mode 100755 index 000000000..17df81728 --- /dev/null +++ b/src/Template/Users/view.ctp @@ -0,0 +1,108 @@ + +
+

+
    +
  • Html->link(__d('Users', 'Edit User'), ['action' => 'edit', $Users->id]) ?>
  • +
  • Form->postLink(__d('Users', 'Delete User'), ['action' => 'delete', $Users->id], ['confirm' => __d('Users', 'Are you sure you want to delete # {0}?', $Users->id)]) ?>
  • +
  • Html->link(__d('Users', 'List Users'), ['action' => 'index']) ?>
  • +
  • Html->link(__d('Users', 'New User'), ['action' => 'add']) ?>
  • +
  • Html->link(__d('Users', 'List Accounts'), ['controller' => 'Accounts', 'action' => 'index']) ?>
  • +
+
+
+

id) ?>

+
+
+
+

id) ?>

+
+

username) ?>

+
+

email) ?>

+
+

first_name) ?>

+
+

last_name) ?>

+
+

token) ?>

+
+

api_token) ?>

+
+
+
+

Number->format($Users->active) ?>

+
+
+
+

token_expires) ?>

+
+

activation_date) ?>

+
+

tos_date) ?>

+
+

created) ?>

+
+

modified) ?>

+
+
+
+ diff --git a/src/Traits/RandomStringTrait.php b/src/Traits/RandomStringTrait.php new file mode 100755 index 000000000..470aea671 --- /dev/null +++ b/src/Traits/RandomStringTrait.php @@ -0,0 +1,29 @@ +addReCaptchaScript(); + } + } + + /** + * Social login link + * + * @param string $name name + * @param array $options options + * @return string + */ + public function socialLogin($name, $options = []) + { + if (empty($options['label'])) { + $options['label'] = 'Sign in with'; + } + $icon = $this->Html->tag('i', '', [ + 'class' => __d('Users', 'fa fa-{0}', strtolower($name)), + ]); + $providerTitle = __d('Users', '{0} {1}', Hash::get($options, 'label'), Inflector::camelize($name)); + $providerClass = __d('Users', 'btn btn-social btn-{0} ' . Hash::get($options, 'class') ?: '', strtolower($name)); + return $this->Html->link($icon . $providerTitle, "/auth/$name", [ + 'escape' => false, 'class' => $providerClass + ]); + } + + /** + * All available Social Login Icons + * + * @return array Links to Social Login Urls + */ + public function socialLoginList() + { + if (!Configure::read('Users.Social.login')) { + return []; + } + $outProviders = []; + $providers = Configure::read('OAuth.providers'); + foreach ($providers as $provider => $options) { + if (!empty($options['options']['redirectUri']) && + !empty($options['options']['clientId']) && + !empty($options['options']['clientSecret'])) { + $outProviders[] = $this->socialLogin($provider); + } + } + + return $outProviders; + } + + /** + * Logout link + * + * @param null $message logout message info. + * @param array $options Array with option data. + * @return string + */ + public function logout($message = null, $options = []) + { + return $this->Html->link(empty($message) ? __d('Users', 'Logout') : $message, [ + 'plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'logout' + ], $options); + } + + /** + * Generate a link if the target url is authorized for the logged in user + * + * @param type $title link's title. + * @param type $url url that the user is making request. + * @param array $options Array with option data. + * @return string + */ + public function link($title, $url = null, array $options = []) + { + if ($this->isAuthorized($url)) { + $linkOptions = $options; + unset($linkOptions['before'], $linkOptions['after']); + return Hash::get($options, 'before') . $this->Html->link($title, $url, $linkOptions) . Hash::get($options, 'after'); + } + + return false; + } + + /** + * Retunrs true if the target url is authorized for the logged in user + * + * @param type $url url that the user is making request. + * @return bool + */ + public function isAuthorized($url = null) + { + $event = new Event(UsersAuthComponent::EVENT_IS_AUTHORIZED, $this, ['url' => $url]); + $result = $this->_View->eventManager()->dispatch($event); + return $result->result; + } + + /** + * Welcome display + * @return mixed + */ + public function welcome() + { + $userId = $this->request->session()->read('Auth.User.id'); + if (empty($userId)) { + return; + } + + $profileUrl = Configure::read('Users.Profile.route'); + $label = __d('Users', 'Welcome, {0}', $this->Html->link($this->request->session()->read('Auth.User.first_name'), $profileUrl)); + return $this->Html->tag('span', $label, ['class' => 'welcome']); + } + + /** + * Add reCaptcha script + * @return void + */ + public function addReCaptchaScript() + { + $this->Html->script('https://www.google.com/recaptcha/api.js', [ + 'block' => 'script', + ]); + } + + /** + * Add reCaptcha to the form + * @return mixed + */ + public function addReCaptcha() + { + if (!Configure::read('Users.Registration.reCaptcha')) { + return false; + } + if (!Configure::read('reCaptcha.key')) { + return $this->Html->tag('p', __d('Users', 'reCaptcha is not configured! Please configure reCaptcha.key or set Users.Registration.reCaptcha to false')); + } + $this->Form->unlockField('g-recaptcha-response'); + return $this->Html->tag('div', '', [ + 'class' => 'g-recaptcha', + 'data-sitekey' => Configure::read('reCaptcha.key') + ]); + } +} diff --git a/tests/App/Controller/AppController.php b/tests/App/Controller/AppController.php new file mode 100755 index 000000000..5899a4e41 --- /dev/null +++ b/tests/App/Controller/AppController.php @@ -0,0 +1,24 @@ +loadComponent('Flash'); + // $this->loadComponent('CakeDC/Users.UsersAuth'); + $this->loadComponent('RequestHandler'); + } +} diff --git a/tests/Fixture/PostsFixture.php b/tests/Fixture/PostsFixture.php new file mode 100755 index 000000000..fadeb7179 --- /dev/null +++ b/tests/Fixture/PostsFixture.php @@ -0,0 +1,60 @@ + ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], + 'title' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'user_id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + '_constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []], + ], + '_options' => [ + 'engine' => 'InnoDB', + 'collation' => 'utf8_general_ci' + ], + ]; + // @codingStandardsIgnoreEnd + + /** + * Records + * + * @var array + */ + public $records = [ + [ + 'id' => '00000000-0000-0000-0000-000000000001', + 'title' => 'post-1', + 'user_id' => '00000000-0000-0000-0000-000000000001', + ], + [ + 'id' => '00000000-0000-0000-0000-000000000002', + 'title' => 'post-2', + 'user_id' => '00000000-0000-0000-0000-000000000002', + ], + ]; +} diff --git a/tests/Fixture/PostsUsersFixture.php b/tests/Fixture/PostsUsersFixture.php new file mode 100755 index 000000000..f4cea2af2 --- /dev/null +++ b/tests/Fixture/PostsUsersFixture.php @@ -0,0 +1,60 @@ + ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], + 'user_id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'post_id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + '_constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []], + ], + '_options' => [ + 'engine' => 'InnoDB', + 'collation' => 'utf8_general_ci' + ], + ]; + // @codingStandardsIgnoreEnd + + /** + * Records + * + * @var array + */ + public $records = [ + [ + 'id' => '00000000-0000-0000-0000-000000000011', + 'user_id' => '00000000-0000-0000-0000-000000000001', + 'post_id' => '00000000-0000-0000-0000-000000000001', + ], + [ + 'id' => '00000000-0000-0000-0000-000000000012', + 'user_id' => '00000000-0000-0000-0000-000000000002', + 'post_id' => '00000000-0000-0000-0000-000000000002', + ], + ]; +} diff --git a/tests/Fixture/SocialAccountsFixture.php b/tests/Fixture/SocialAccountsFixture.php new file mode 100755 index 000000000..7e014fa58 --- /dev/null +++ b/tests/Fixture/SocialAccountsFixture.php @@ -0,0 +1,131 @@ + ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], + 'user_id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'provider' => ['type' => 'string', 'length' => 255, 'unsigned' => false, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'autoIncrement' => null], + 'username' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'reference' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'avatar' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'description' => ['type' => 'text', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], + 'token' => ['type' => 'string', 'length' => 500, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'token_secret' => ['type' => 'string', 'length' => 500, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'token_expires' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], + 'active' => ['type' => 'boolean', 'length' => null, 'null' => false, 'default' => true, 'comment' => '', 'precision' => null], + 'data' => ['type' => 'text', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], + 'created' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], + 'modified' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], + '_constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []], + ], + '_options' => [ + 'engine' => 'InnoDB', + 'collation' => 'utf8_general_ci' + ], + ]; + // @codingStandardsIgnoreEnd + + /** + * Records + * + * @var array + */ + public $records = [ + [ + 'id' => '00000000-0000-0000-0000-000000000001', + 'user_id' => '00000000-0000-0000-0000-000000000001', + 'provider' => 'Facebook', + 'username' => 'user-1-fb', + 'reference' => 'reference-1-1234', + 'avatar' => 'Lorem ipsum dolor sit amet', + 'description' => 'Lorem ipsum dolor sit amet, aliquet feugiat. Convallis morbi fringilla gravida, phasellus feugiat dapibus velit nunc, pulvinar eget sollicitudin venenatis cum nullam, vivamus ut a sed, mollitia lectus. Nulla vestibulum massa neque ut et, id hendrerit sit, feugiat in taciti enim proin nibh, tempor dignissim, rhoncus duis vestibulum nunc mattis convallis.', + 'token' => 'token-1234', + 'token_secret' => 'Lorem ipsum dolor sit amet', + 'token_expires' => '2015-05-22 21:52:44', + 'active' => false, + 'data' => '', + 'created' => '2015-05-22 21:52:44', + 'modified' => '2015-05-22 21:52:44' + ], + [ + 'id' => '00000000-0000-0000-0000-000000000002', + 'user_id' => '00000000-0000-0000-0000-000000000001', + 'provider' => 'Twitter', + 'username' => 'user-1-tw', + 'reference' => 'reference-1-1234', + 'avatar' => 'Lorem ipsum dolor sit amet', + 'description' => '', + 'token' => 'token-1234', + 'token_secret' => 'Lorem ipsum dolor sit amet', + 'token_expires' => '2015-05-22 21:52:44', + 'active' => true, + 'data' => '', + 'created' => '2015-05-22 21:52:44', + 'modified' => '2015-05-22 21:52:44' + ], + [ + 'id' => '00000000-0000-0000-0000-000000000003', + 'user_id' => '00000000-0000-0000-0000-000000000002', + 'provider' => 'Facebook', + 'username' => 'user-2-fb', + 'reference' => 'reference-2-1', + 'avatar' => 'Lorem ipsum dolor sit amet', + 'description' => '', + 'token' => 'token-reference-2-1', + 'token_secret' => 'Lorem ipsum dolor sit amet', + 'token_expires' => '2015-05-22 21:52:44', + 'active' => true, + 'data' => '', + 'created' => '2015-05-22 21:52:44', + 'modified' => '2015-05-22 21:52:44' + ], + [ + 'id' => '00000000-0000-0000-0000-000000000004', + 'user_id' => '00000000-0000-0000-0000-000000000003', + 'provider' => 'Twitter', + 'username' => 'user-2-tw', + 'reference' => 'reference-2-2', + 'avatar' => 'Lorem ipsum dolor sit amet', + 'description' => '', + 'token' => 'token-reference-2-2', + 'token_secret' => 'Lorem ipsum dolor sit amet', + 'token_expires' => '2015-05-22 21:52:44', + 'active' => false, + 'data' => '', + 'created' => '2015-05-22 21:52:44', + 'modified' => '2015-05-22 21:52:44' + ], + [ + 'id' => '00000000-0000-0000-0000-000000000005', + 'user_id' => '00000000-0000-0000-0000-000000000004', + 'provider' => 'Twitter', + 'username' => 'user-2-tw', + 'reference' => 'reference-2-2', + 'avatar' => 'Lorem ipsum dolor sit amet', + 'description' => '', + 'token' => 'token-reference-2-2', + 'token_secret' => 'Lorem ipsum dolor sit amet', + 'token_expires' => '2015-05-22 21:52:44', + 'active' => false, + 'data' => '', + 'created' => '2015-05-22 21:52:44', + 'modified' => '2015-05-22 21:52:44' + ], + ]; +} diff --git a/tests/Fixture/UsersFixture.php b/tests/Fixture/UsersFixture.php new file mode 100755 index 000000000..4521f5270 --- /dev/null +++ b/tests/Fixture/UsersFixture.php @@ -0,0 +1,243 @@ + ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], + 'username' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'email' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'password' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'first_name' => ['type' => 'string', 'length' => 50, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'last_name' => ['type' => 'string', 'length' => 50, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'token' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'token_expires' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], + 'api_token' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'activation_date' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], + 'tos_date' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], + 'active' => ['type' => 'boolean', 'length' => null, 'null' => false, 'default' => true, 'comment' => '', 'precision' => null], + 'is_superuser' => ['type' => 'boolean', 'length' => null, 'unsigned' => false, 'null' => false, 'default' => false, 'comment' => '', 'precision' => null, 'autoIncrement' => null], + 'role' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => 'user', 'comment' => '', 'precision' => null, 'fixed' => null], + 'created' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], + 'modified' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], + '_constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []], + ], + '_options' => [ + 'engine' => 'InnoDB', + 'collation' => 'utf8_general_ci' + ], + ]; + // @codingStandardsIgnoreEnd + + /** + * Records + * + * @var array + */ + public $records = [ + [ + 'id' => '00000000-0000-0000-0000-000000000001', + 'username' => 'user-1', + 'email' => 'user-1@test.com', + 'password' => '12345', + 'first_name' => 'first1', + 'last_name' => 'last1', + 'token' => 'ae93ddbe32664ce7927cf0c5c5a5e59d', + 'token_expires' => '2035-06-24 17:33:54', + 'api_token' => 'yyy', + 'activation_date' => '2015-06-24 17:33:54', + 'tos_date' => '2015-06-24 17:33:54', + 'active' => false, + 'is_superuser' => true, + 'role' => 'admin', + 'created' => '2015-06-24 17:33:54', + 'modified' => '2015-06-24 17:33:54' + ], + [ + 'id' => '00000000-0000-0000-0000-000000000002', + 'username' => 'user-2', + 'email' => 'user-2@test.com', + 'password' => '12345', + 'first_name' => 'user', + 'last_name' => 'second', + 'token' => '6614f65816754310a5f0553436dd89e9', + 'token_expires' => '2015-06-24 17:33:54', + 'api_token' => 'xxx', + 'activation_date' => '2015-06-24 17:33:54', + 'tos_date' => '2015-06-24 17:33:54', + 'active' => true, + 'is_superuser' => true, + 'role' => 'admin', + 'created' => '2015-06-24 17:33:54', + 'modified' => '2015-06-24 17:33:54' + ], + [ + 'id' => '00000000-0000-0000-0000-000000000003', + 'username' => 'user-3', + 'email' => 'user-3@test.com', + 'password' => '12345', + 'first_name' => 'user', + 'last_name' => 'third', + 'token' => 'token-3', + 'token_expires' => '2030-06-20 17:33:54', + 'api_token' => 'xxx', + 'activation_date' => '2015-06-24 17:33:54', + 'tos_date' => '2015-06-24 17:33:54', + 'active' => false, + 'is_superuser' => true, + 'role' => 'admin', + 'created' => '2015-06-24 17:33:54', + 'modified' => '2015-06-24 17:33:54' + ], + [ + 'id' => '00000000-0000-0000-0000-000000000004', + 'username' => 'user-4', + 'email' => '4@example.com', + 'password' => 'Lorem ipsum dolor sit amet', + 'first_name' => 'FirstName4', + 'last_name' => 'Lorem ipsum dolor sit amet', + 'token' => 'token-4', + 'token_expires' => '2030-06-24 17:33:54', + 'api_token' => 'Lorem ipsum dolor sit amet', + 'activation_date' => '2015-06-24 17:33:54', + 'tos_date' => '2015-06-24 17:33:54', + 'active' => true, + 'is_superuser' => false, + 'role' => 'Lorem ipsum dolor sit amet', + 'created' => '2015-06-24 17:33:54', + 'modified' => '2015-06-24 17:33:54' + ], + [ + 'id' => '00000000-0000-0000-0000-000000000005', + 'username' => 'user-5', + 'email' => 'test@example.com', + 'password' => '12345', + 'first_name' => 'first-user-5', + 'last_name' => 'firts name 5', + 'token' => 'token-5', + 'token_expires' => '2015-06-24 17:33:54', + 'api_token' => '', + 'activation_date' => '2015-06-24 17:33:54', + 'tos_date' => '2015-06-24 17:33:54', + 'active' => true, + 'is_superuser' => false, + 'role' => 'user', + 'created' => '2015-06-24 17:33:54', + 'modified' => '2015-06-24 17:33:54' + ], + [ + 'id' => '00000000-0000-0000-0000-000000000006', + 'username' => 'Lorem ipsum dolor sit amet', + 'email' => 'Lorem ipsum dolor sit amet', + 'password' => 'Lorem ipsum dolor sit amet', + 'first_name' => 'Lorem ipsum dolor sit amet', + 'last_name' => 'Lorem ipsum dolor sit amet', + 'token' => 'Lorem ipsum dolor sit amet', + 'token_expires' => '2015-06-24 17:33:54', + 'api_token' => 'Lorem ipsum dolor sit amet', + 'activation_date' => '2015-06-24 17:33:54', + 'tos_date' => '2015-06-24 17:33:54', + 'active' => true, + 'is_superuser' => false, + 'role' => 'Lorem ipsum dolor sit amet', + 'created' => '2015-06-24 17:33:54', + 'modified' => '2015-06-24 17:33:54' + ], + [ + 'id' => '00000000-0000-0000-0000-000000000007', + 'username' => 'Lorem ipsum dolor sit amet', + 'email' => 'Lorem ipsum dolor sit amet', + 'password' => 'Lorem ipsum dolor sit amet', + 'first_name' => 'Lorem ipsum dolor sit amet', + 'last_name' => 'Lorem ipsum dolor sit amet', + 'token' => 'Lorem ipsum dolor sit amet', + 'token_expires' => '2015-06-24 17:33:54', + 'api_token' => 'Lorem ipsum dolor sit amet', + 'activation_date' => '2015-06-24 17:33:54', + 'tos_date' => '2015-06-24 17:33:54', + 'active' => true, + 'is_superuser' => false, + 'role' => 'Lorem ipsum dolor sit amet', + 'created' => '2015-06-24 17:33:54', + 'modified' => '2015-06-24 17:33:54' + ], + [ + 'id' => '00000000-0000-0000-0000-000000000008', + 'username' => 'Lorem ipsum dolor sit amet', + 'email' => 'Lorem ipsum dolor sit amet', + 'password' => 'Lorem ipsum dolor sit amet', + 'first_name' => 'Lorem ipsum dolor sit amet', + 'last_name' => 'Lorem ipsum dolor sit amet', + 'token' => 'Lorem ipsum dolor sit amet', + 'token_expires' => '2015-06-24 17:33:54', + 'api_token' => 'Lorem ipsum dolor sit amet', + 'activation_date' => '2015-06-24 17:33:54', + 'tos_date' => '2015-06-24 17:33:54', + 'active' => true, + 'is_superuser' => false, + 'role' => 'Lorem ipsum dolor sit amet', + 'created' => '2015-06-24 17:33:54', + 'modified' => '2015-06-24 17:33:54' + ], + [ + 'id' => '00000000-0000-0000-0000-000000000009', + 'username' => 'Lorem ipsum dolor sit amet', + 'email' => 'Lorem ipsum dolor sit amet', + 'password' => 'Lorem ipsum dolor sit amet', + 'first_name' => 'Lorem ipsum dolor sit amet', + 'last_name' => 'Lorem ipsum dolor sit amet', + 'token' => 'Lorem ipsum dolor sit amet', + 'token_expires' => '2015-06-24 17:33:54', + 'api_token' => 'Lorem ipsum dolor sit amet', + 'activation_date' => '2015-06-24 17:33:54', + 'tos_date' => '2015-06-24 17:33:54', + 'active' => true, + 'is_superuser' => false, + 'role' => 'Lorem ipsum dolor sit amet', + 'created' => '2015-06-24 17:33:54', + 'modified' => '2015-06-24 17:33:54' + ], + [ + 'id' => '00000000-0000-0000-0000-000000000010', + 'username' => 'Lorem ipsum dolor sit amet', + 'email' => 'Lorem ipsum dolor sit amet', + 'password' => 'Lorem ipsum dolor sit amet', + 'first_name' => 'Lorem ipsum dolor sit amet', + 'last_name' => 'Lorem ipsum dolor sit amet', + 'token' => 'Lorem ipsum dolor sit amet', + 'token_expires' => '2015-06-24 17:33:54', + 'api_token' => 'Lorem ipsum dolor sit amet', + 'activation_date' => '2015-06-24 17:33:54', + 'tos_date' => '2015-06-24 17:33:54', + 'active' => true, + 'is_superuser' => false, + 'role' => 'Lorem ipsum dolor sit amet', + 'created' => '2015-06-24 17:33:54', + 'modified' => '2015-06-24 17:33:54' + ], + ]; +} diff --git a/tests/TestCase/Auth/ApiKeyAuthenticateTest.php b/tests/TestCase/Auth/ApiKeyAuthenticateTest.php new file mode 100755 index 000000000..3008c98ce --- /dev/null +++ b/tests/TestCase/Auth/ApiKeyAuthenticateTest.php @@ -0,0 +1,168 @@ +getMock( + 'Cake\Controller\Controller', + null, + [$request, $response] + ); + $registry = new ComponentRegistry($controller); + $this->apiKey = new ApiKeyAuthenticate($registry, ['require_ssl' => false]); + } + + /** + * Tears down the fixture, for example, closes a network connection. + * This method is called after a test is executed. + */ + public function tearDown() + { + unset($this->apiKey, $this->controller); + } + + /** + * test + * + * @return void + */ + public function testAuthenticateHappy() + { + $request = new Request('/?api_key=yyy'); + $result = $this->apiKey->authenticate($request, new Response()); + $this->assertEquals('user-1', $result['username']); + } + + /** + * test + * + * @return void + */ + public function testAuthenticateFail() + { + $request = new Request('/'); + $result = $this->apiKey->authenticate($request, new Response()); + $this->assertFalse($result); + + $request = new Request('/?api_key=none'); + $result = $this->apiKey->authenticate($request, new Response()); + $this->assertFalse($result); + + $request = new Request('/?api_key='); + $result = $this->apiKey->authenticate($request, new Response()); + $this->assertFalse($result); + } + + /** + * test + * + * @expectedException \OutOfBoundsException + * @expectedExceptionMessage Type wrong is not valid + * + */ + public function testAuthenticateWrongType() + { + $this->apiKey->config('type', 'wrong'); + $request = new Request('/'); + $this->apiKey->authenticate($request, new Response()); + } + + /** + * test + * + * @expectedException \Cake\Network\Exception\ForbiddenException + * @expectedExceptionMessage SSL is required for ApiKey Authentication + * + */ + public function testAuthenticateRequireSSL() + { + $this->apiKey->config('require_ssl', true); + $request = new Request('/?api_key=test'); + $this->apiKey->authenticate($request, new Response()); + } + + /** + * test + * + */ + public function testAuthenticateRequireSSLNoKey() + { + $this->apiKey->config('require_ssl', true); + $request = new Request('/'); + $this->assertFalse($this->apiKey->authenticate($request, new Response())); + } + + + /** + * test + * + * @return void + */ + public function testHeaderHappy() + { + $request = $this->getMockBuilder('\Cake\Network\Request') + ->setMethods(['header']) + ->getMock(); + $request->expects($this->once()) + ->method('header') + ->with('api_key') + ->will($this->returnValue('yyy')); + $this->apiKey->config('type', 'header'); + $result = $this->apiKey->authenticate($request, new Response()); + $this->assertEquals('user-1', $result['username']); + } + + /** + * test + * + * @return void + */ + public function testAuthenticateHeaderFail() + { + $request = $this->getMockBuilder('\Cake\Network\Request') + ->setMethods(['header']) + ->getMock(); + $request->expects($this->once()) + ->method('header') + ->with('api_key') + ->will($this->returnValue('wrong')); + $this->apiKey->config('type', 'header'); + $result = $this->apiKey->authenticate($request, new Response()); + $this->assertFalse($result); + } +} diff --git a/tests/TestCase/Auth/RememberMeAuthenticateTest.php b/tests/TestCase/Auth/RememberMeAuthenticateTest.php new file mode 100755 index 000000000..150e00f37 --- /dev/null +++ b/tests/TestCase/Auth/RememberMeAuthenticateTest.php @@ -0,0 +1,173 @@ +controller = $this->getMock( + 'Cake\Controller\Controller', + null, + [$request, $response] + ); + $registry = new ComponentRegistry($this->controller); + $this->rememberMe = new RememberMeAuthenticate($registry); + } + + /** + * Tears down the fixture, for example, closes a network connection. + * This method is called after a test is executed. + */ + public function tearDown() + { + unset($this->rememberMe, $this->controller); + } + + /** + * test + * + * @return void + */ + public function testAuthenticateHappy() + { + $request = new Request('/'); + $request->env('HTTP_USER_AGENT', 'user-agent'); + $mockCookie = $this->getMockBuilder('Cake\Controller\Component\CookieComponent') + ->disableOriginalConstructor() + ->setMethods(['check', 'read']) + ->getMock(); + $mockCookie + ->expects($this->once()) + ->method('read') + ->with('remember_me') + ->will($this->returnValue([ + 'id' => '00000000-0000-0000-0000-000000000001', + 'user_agent' => 'user-agent' + ])); + $registry = new ComponentRegistry($this->controller); + $registry->set('Cookie', $mockCookie); + $this->rememberMe = new RememberMeAuthenticate($registry); + $result = $this->rememberMe->authenticate($request, new Response()); + $this->assertEquals('user-1', $result['username']); + } + + /** + * test + * + * @return void + */ + public function testAuthenticateBadUser() + { + $request = new Request('/'); + $request->env('HTTP_USER_AGENT', 'user-agent'); + $mockCookie = $this->getMockBuilder('Cake\Controller\Component\CookieComponent') + ->disableOriginalConstructor() + ->setMethods(['check', 'read']) + ->getMock(); + $mockCookie + ->expects($this->once()) + ->method('read') + ->with('remember_me') + ->will($this->returnValue([ + //bad-user + 'id' => '00000000-0000-0000-0000-000000000000', + 'user_agent' => 'user-agent' + ])); + $registry = new ComponentRegistry($this->controller); + $registry->set('Cookie', $mockCookie); + $this->rememberMe = new RememberMeAuthenticate($registry); + $result = $this->rememberMe->authenticate($request, new Response()); + $this->assertFalse($result); + } + + + /** + * test + * + * @return void + */ + public function testAuthenticateBadAgent() + { + $request = new Request('/'); + $request->env('HTTP_USER_AGENT', 'user-agent'); + $mockCookie = $this->getMockBuilder('Cake\Controller\Component\CookieComponent') + ->disableOriginalConstructor() + ->setMethods(['check', 'read']) + ->getMock(); + $mockCookie + ->expects($this->once()) + ->method('read') + ->with('remember_me') + ->will($this->returnValue([ + 'id' => '00000000-0000-0000-0000-000000000001', + 'user_agent' => 'bad-agent' + ])); + $registry = new ComponentRegistry($this->controller); + $registry->set('Cookie', $mockCookie); + $this->rememberMe = new RememberMeAuthenticate($registry); + $result = $this->rememberMe->authenticate($request, new Response()); + $this->assertFalse($result); + } + + /** + * test + * + * @return void + */ + public function testAuthenticateNoCookie() + { + $request = new Request('/'); + $request->env('HTTP_USER_AGENT', 'user-agent'); + $mockCookie = $this->getMockBuilder('Cake\Controller\Component\CookieComponent') + ->disableOriginalConstructor() + ->setMethods(['check', 'read']) + ->getMock(); + $mockCookie + ->expects($this->once()) + ->method('read') + ->with('remember_me') + ->will($this->returnValue(null)); + + $registry = new ComponentRegistry($this->controller); + $registry->set('Cookie', $mockCookie); + $this->rememberMe = new RememberMeAuthenticate($registry); + $result = $this->rememberMe->authenticate($request, new Response()); + $this->assertFalse($result); + } +} diff --git a/tests/TestCase/Auth/Rules/OwnerTest.php b/tests/TestCase/Auth/Rules/OwnerTest.php new file mode 100755 index 000000000..eac550ec8 --- /dev/null +++ b/tests/TestCase/Auth/Rules/OwnerTest.php @@ -0,0 +1,263 @@ +Owner = new Owner(); + $this->request = $this->getMockBuilder('\Cake\Network\Request') + ->getMock(); + } + + /** + * Tears down the fixture, for example, closes a network connection. + * This method is called after a test is executed. + */ + public function tearDown() + { + unset($this->Owner); + } + + /** + * test + * + * @return void + */ + public function testAllowed() + { + $this->request->params = [ + 'plugin' => 'CakeDC/Users', + 'controller' => 'Posts', + 'pass' => ['00000000-0000-0000-0000-000000000001'] + ]; + $user = [ + 'id' => '00000000-0000-0000-0000-000000000001', + ]; + $this->assertTrue($this->Owner->allowed($user, 'user', $this->request)); + } + + /** + * test + * + * @return void + */ + public function testAllowedUsingTableAlias() + { + $this->Owner = new Owner([ + 'table' => 'Posts' + ]); + $this->request->params = [ + 'pass' => ['00000000-0000-0000-0000-000000000001'] + ]; + $user = [ + 'id' => '00000000-0000-0000-0000-000000000001', + ]; + $this->assertTrue($this->Owner->allowed($user, 'user', $this->request)); + } + + /** + * test + * + * @return void + */ + public function testAllowedUsingTableInstance() + { + $this->Owner = new Owner([ + 'table' => TableRegistry::get('CakeDC/Users.Posts'), + ]); + $this->request->params = [ + 'pass' => ['00000000-0000-0000-0000-000000000001'] + ]; + $user = [ + 'id' => '00000000-0000-0000-0000-000000000001', + ]; + $this->assertTrue($this->Owner->allowed($user, 'user', $this->request)); + } + + /** + * test + * + * @return void + * @expectedException \OutOfBoundsException + * @expectedExceptionMessage Table alias is empty, please define a table alias, we could not extract a default table from the request + */ + public function testAllowedShouldThrowExceptionBecauseEmptyAliasFromRequest() + { + $this->request->params = [ + 'pass' => ['00000000-0000-0000-0000-000000000001'] + ]; + $user = [ + 'id' => '00000000-0000-0000-0000-000000000001', + ]; + $this->Owner->allowed($user, 'user', $this->request); + } + + /** + * test + * + * @return void + * @expectedException \OutOfBoundsException + * @expectedExceptionMessage Missing column column_not_found in table Posts while checking ownership permissions for user 00000000-0000-0000-0000-000000000001 + */ + public function testAllowedShouldThrowExceptionBecauseForeignKeyNotPresentInTable() + { + $this->Owner = new Owner([ + 'table' => TableRegistry::get('CakeDC/Users.Posts'), + 'ownerForeignKey' => 'column_not_found', + ]); + $this->request->params = [ + 'pass' => ['00000000-0000-0000-0000-000000000001'] + ]; + $user = [ + 'id' => '00000000-0000-0000-0000-000000000001', + ]; + $this->Owner->allowed($user, 'user', $this->request); + } + + /** + * test + * + * @return void + */ + public function testNotAllowedBecauseNotOwner() + { + $this->request->params = [ + 'plugin' => 'CakeDC/Users', + 'controller' => 'Posts', + 'pass' => ['00000000-0000-0000-0000-000000000002'] + ]; + $user = [ + 'id' => '00000000-0000-0000-0000-000000000001', + ]; + $this->assertFalse($this->Owner->allowed($user, 'user', $this->request)); + } + + /** + * test + * + * @return void + */ + public function testNotAllowedBecauseUserNotFound() + { + $this->request->params = [ + 'plugin' => 'CakeDC/Users', + 'controller' => 'Posts', + 'pass' => ['00000000-0000-0000-0000-000000000002'] + ]; + $user = [ + 'id' => '99999999-0000-0000-0000-000000000000', + ]; + $this->assertFalse($this->Owner->allowed($user, 'user', $this->request)); + } + + /** + * test + * + * @return void + */ + public function testNotAllowedBecausePostNotFound() + { + $this->request->params = [ + 'plugin' => 'CakeDC/Users', + 'controller' => 'Posts', + 'pass' => ['99999999-0000-0000-0000-000000000000'] //not found + ]; + $user = [ + 'id' => '00000000-0000-0000-0000-000000000001', + ]; + $this->assertFalse($this->Owner->allowed($user, 'user', $this->request)); + } + + /** + * test + * + * @return void + * @expectedException \OutOfBoundsException + * @expectedExceptionMessage Missing column user_id in table NoDefaultTable while checking ownership permissions for user 00000000-0000-0000-0000-000000000001 + */ + public function testNotAllowedBecauseNoDefaultTable() + { + $this->request->params = [ + 'plugin' => 'CakeDC/Users', + 'controller' => 'NoDefaultTable', + 'pass' => ['00000000-0000-0000-0000-000000000001'] + ]; + $user = [ + 'id' => '00000000-0000-0000-0000-000000000001', + ]; + $this->assertFalse($this->Owner->allowed($user, 'user', $this->request)); + } + + /** + * Test using the Owner rule in a belongsToMany association + * Posts belongsToMany Users + * @return void + */ + public function testAllowedBelongsToMany() + { + $this->Owner = new Owner([ + 'table' => 'PostsUsers', + 'id' => 'post_id', + ]); + $this->request->params = [ + 'plugin' => 'CakeDC/Users', + 'controller' => 'IsNotUsed', + 'pass' => ['00000000-0000-0000-0000-000000000001'] + ]; + $user = [ + 'id' => '00000000-0000-0000-0000-000000000001', + ]; + $this->assertTrue($this->Owner->allowed($user, 'user', $this->request)); + } + + /** + * Test using the Owner rule in a belongsToMany association + * Posts belongsToMany Users + * @return void + */ + public function testNotAllowedBelongsToMany() + { + $this->Owner = new Owner([ + 'table' => 'PostsUsers', + 'id' => 'post_id', + ]); + $this->request->params = [ + 'plugin' => 'CakeDC/Users', + 'controller' => 'IsNotUsed', + 'pass' => ['00000000-0000-0000-0000-000000000002'] + ]; + $user = [ + 'id' => '00000000-0000-0000-0000-000000000001', + ]; + $this->assertFalse($this->Owner->allowed($user, 'user', $this->request)); + } +} diff --git a/tests/TestCase/Auth/SimpleRbacAuthorizeTest.php b/tests/TestCase/Auth/SimpleRbacAuthorizeTest.php new file mode 100755 index 000000000..7f84f7878 --- /dev/null +++ b/tests/TestCase/Auth/SimpleRbacAuthorizeTest.php @@ -0,0 +1,692 @@ + 'admin', + 'plugin' => '*', + 'controller' => '*', + 'action' => '*', + ], + //specific actions allowed for the user role in Users plugin + [ + 'role' => 'user', + 'plugin' => 'CakeDC/Users', + 'controller' => 'Users', + 'action' => ['profile', 'logout'], + ], + //all roles allowed to Pages/display + [ + 'role' => '*', + 'plugin' => null, + 'controller' => ['Pages'], + 'action' => ['display'], + ], + ]; + + + /** + * Sets up the fixture, for example, opens a network connection. + * This method is called before a test is executed. + */ + public function setUp() + { + $request = new Request(); + $response = new Response(); + + $this->controller = $this->getMock( + 'Cake\Controller\Controller', + null, + [$request, $response] + ); + $this->registry = new ComponentRegistry($this->controller); + } + + /** + * Tears down the fixture, for example, closes a network connection. + * This method is called after a test is executed. + */ + public function tearDown() + { + unset($this->simpleRbacAuthorize, $this->controller); + } + + /** + * @covers CakeDC\Users\Auth\SimpleRbacAuthorize::__construct + */ + public function testConstruct() + { + //don't autoload config + $this->simpleRbacAuthorize = new SimpleRbacAuthorize($this->registry, ['autoload_config' => false]); + $this->assertEmpty($this->simpleRbacAuthorize->config('permissions')); + } + + /** + * test + * + * @return void + */ + public function testLoadPermissions() + { + $this->simpleRbacAuthorize = $this->getMockBuilder('CakeDC\Users\Auth\SimpleRbacAuthorize') + ->disableOriginalConstructor() + ->getMock(); + $reflectedClass = new ReflectionClass($this->simpleRbacAuthorize); + $loadPermissions = $reflectedClass->getMethod('_loadPermissions'); + $loadPermissions->setAccessible(true); + $permissions = $loadPermissions->invoke($this->simpleRbacAuthorize, 'missing'); + $this->assertEquals($this->defaultPermissions, $permissions); + } + + /** + * @covers CakeDC\Users\Auth\SimpleRbacAuthorize::__construct + */ + public function testConstructMissingPermissionsFile() + { + $this->simpleRbacAuthorize = $this->getMock( + 'CakeDC\Users\Auth\SimpleRbacAuthorize', + null, + [$this->registry, ['autoload_config' => 'does-not-exist']] + ); + //we should have the default permissions + $this->assertEquals($this->defaultPermissions, $this->simpleRbacAuthorize->config('permissions')); + } + + protected function assertConstructorPermissions($instance, $config, $permissions) + { + $reflectedClass = new ReflectionClass($instance); + $constructor = $reflectedClass->getConstructor(); + $constructor->invoke($this->simpleRbacAuthorize, $this->registry, $config); + + //we should have the default permissions + $resultPermissions = $this->simpleRbacAuthorize->config('permissions'); + $this->assertEquals($permissions, $resultPermissions); + } + + /** + * @covers CakeDC\Users\Auth\SimpleRbacAuthorize::__construct + */ + public function testConstructPermissionsFileHappy() + { + $permissions = [[ + 'controller' => 'Test', + 'action' => 'test' + ]]; + $className = 'CakeDC\Users\Auth\SimpleRbacAuthorize'; + $this->simpleRbacAuthorize = $this->getMockBuilder($className) + ->setMethods(['_loadPermissions']) + ->disableOriginalConstructor() + ->getMock(); + $this->simpleRbacAuthorize + ->expects($this->once()) + ->method('_loadPermissions') + ->with('permissions-happy') + ->will($this->returnValue($permissions)); + $this->assertConstructorPermissions($className, ['autoload_config' => 'permissions-happy'], $permissions); + } + + protected function preparePermissions($permissions) + { + $className = 'CakeDC\Users\Auth\SimpleRbacAuthorize'; + $simpleRbacAuthorize = $this->getMockBuilder($className) + ->setMethods(['_loadPermissions']) + ->disableOriginalConstructor() + ->getMock(); + $simpleRbacAuthorize->config('permissions', $permissions); + return $simpleRbacAuthorize; + } + + /** + * @dataProvider providerAuthorize + */ + public function testAuthorize($permissions, $user, $requestParams, $expected, $msg = null) + { + $this->simpleRbacAuthorize = $this->preparePermissions($permissions); + $request = new Request(); + $request->plugin = Hash::get($requestParams, 'plugin'); + $request->controller = $requestParams['controller']; + $request->action = $requestParams['action']; + $prefix = Hash::get($requestParams, 'prefix'); + if ($prefix) { + $request->params = ['prefix' => $prefix]; + } + + $result = $this->simpleRbacAuthorize->authorize($user, $request); + $this->assertSame($expected, $result, $msg); + } + + public function providerAuthorize() + { + return [ + 'happy-strict-all' => [ + //permissions + [[ + 'plugin' => 'Tests', + 'role' => 'test', + 'controller' => 'Tests', + 'action' => 'test', + 'allowed' => true, + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'test' + ], + //expected + true + ], + 'happy-strict-all-deny' => [ + //permissions + [[ + 'plugin' => 'Tests', + 'role' => 'test', + 'controller' => 'Tests', + 'action' => 'test', + 'allowed' => false, + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'test' + ], + //expected + false + ], + 'happy-plugin-null-allowed-null' => [ + //permissions + [[ + 'role' => 'test', + 'controller' => 'Tests', + 'action' => 'test', + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => null, + 'controller' => 'Tests', + 'action' => 'test' + ], + //expected + true + ], + 'happy-plugin-asterisk' => [ + //permissions + [[ + 'plugin' => '*', + 'role' => 'test', + 'controller' => 'Tests', + 'action' => 'test', + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Any', + 'controller' => 'Tests', + 'action' => 'test' + ], + //expected + true + ], + 'happy-plugin-asterisk-main-app' => [ + //permissions + [[ + 'plugin' => '*', + 'role' => 'test', + 'controller' => 'Tests', + 'action' => 'test', + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => null, + 'controller' => 'Tests', + 'action' => 'test' + ], + //expected + true + ], + 'happy-role-asterisk' => [ + //permissions + [[ + 'role' => '*', + 'controller' => 'Tests', + 'action' => 'test', + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'any-role', + ], + //request + [ + 'plugin' => null, + 'controller' => 'Tests', + 'action' => 'test' + ], + //expected + true + ], + 'happy-controller-asterisk' => [ + //permissions + [[ + 'plugin' => 'Tests', + 'role' => 'test', + 'controller' => '*', + 'action' => 'test', + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'test' + ], + //expected + true + ], + 'happy-action-asterisk' => [ + //permissions + [[ + 'plugin' => 'Tests', + 'role' => 'test', + 'controller' => 'Tests', + 'action' => '*', + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'any' + ], + //expected + true + ], + 'happy-some-asterisk-allowed' => [ + //permissions + [[ + 'plugin' => '*', + 'role' => 'test', + 'controller' => '*', + 'action' => '*', + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'any' + ], + //expected + true + ], + 'happy-some-asterisk-deny' => [ + //permissions + [[ + 'plugin' => '*', + 'role' => 'test', + 'controller' => '*', + 'action' => '*', + 'allowed' => false, + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'any' + ], + //expected + false + ], + 'all-deny' => [ + //permissions + [[ + 'plugin' => '*', + 'role' => '*', + 'controller' => '*', + 'action' => '*', + 'allowed' => false, + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Any', + 'controller' => 'Any', + 'action' => 'any' + ], + //expected + false + ], + 'dasherized' => [ + //permissions + [[ + 'plugin' => 'Tests', + 'role' => 'test', + 'controller' => 'TestTests', + 'action' => 'TestAction', + 'allowed' => true, + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'tests', + 'controller' => 'test-tests', + 'action' => 'test-action' + ], + //expected + true + ], + 'happy-array' => [ + //permissions + [[ + 'plugin' => ['Tests'], + 'role' => ['test'], + 'controller' => ['Tests'], + 'action' => ['one', 'two'], + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'one' + ], + //expected + true + ], + 'happy-array' => [ + //permissions + [[ + 'plugin' => ['Tests'], + 'role' => ['test'], + 'controller' => ['Tests'], + 'action' => ['one', 'two'], + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'three' + ], + //expected + false + ], + 'happy-callback-check-params' => [ + //permissions + [[ + 'plugin' => ['Tests'], + 'role' => ['test'], + 'controller' => ['Tests'], + 'action' => ['one', 'two'], + 'allowed' => function ($user, $role, $request) { + return $user['id'] === 1 && $role = 'test' && $request->plugin == 'Tests'; + } + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'one' + ], + //expected + true + ], + 'happy-callback-deny' => [ + //permissions + [[ + 'plugin' => ['*'], + 'role' => ['test'], + 'controller' => ['Tests'], + 'action' => ['one', 'two'], + 'allowed' => function ($user, $role, $request) { + return false; + } + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'one' + ], + //expected + false + ], + 'happy-prefix' => [ + //permissions + [[ + 'role' => ['test'], + 'prefix' => ['admin'], + 'controller' => ['Tests'], + 'action' => ['one', 'two'], + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'prefix' => 'admin', + 'controller' => 'Tests', + 'action' => 'one' + ], + //expected + true + ], + 'deny-prefix' => [ + //permissions + [[ + 'role' => ['test'], + 'prefix' => ['admin'], + 'controller' => ['Tests'], + 'action' => ['one', 'two'], + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'controller' => 'Tests', + 'action' => 'one' + ], + //expected + false + ], + 'star-prefix' => [ + //permissions + [[ + 'role' => ['test'], + 'prefix' => '*', + 'controller' => ['Tests'], + 'action' => ['one', 'two'], + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'prefix' => 'admin', + 'controller' => 'Tests', + 'action' => 'one' + ], + //expected + true + ], + 'array-prefix' => [ + //permissions + [[ + 'role' => ['test'], + 'prefix' => ['one', 'admin'], + 'controller' => '*', + 'action' => '*', + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'prefix' => 'admin', + 'controller' => 'Tests', + 'action' => 'one' + ], + //expected + true + ], + 'array-prefix' => [ + //permissions + [ + [ + 'role' => ['test'], + 'prefix' => ['one', 'admin'], + 'controller' => '*', + 'action' => 'one', + 'allowed' => false, + ], + [ + 'role' => ['test'], + 'prefix' => ['one', 'admin'], + 'controller' => '*', + 'action' => '*', + ], + ], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'prefix' => 'admin', + 'controller' => 'Tests', + 'action' => 'one' + ], + //expected + false + ], + ]; + } +} diff --git a/tests/TestCase/Auth/SocialAuthenticateTest.php b/tests/TestCase/Auth/SocialAuthenticateTest.php new file mode 100755 index 000000000..babfae221 --- /dev/null +++ b/tests/TestCase/Auth/SocialAuthenticateTest.php @@ -0,0 +1,468 @@ +Table = TableRegistry::get('CakeDC/Users.Users'); + + $this->Token = $this->getMockBuilder('AccessToken') + ->setMethods(['getToken', 'getExpires']) + ->disableOriginalConstructor() + ->getMock(); + + $this->controller = $this->getMock( + 'Cake\Controller\Controller', + ['failedSocialLogin'], + [$request, $response] + ); + + $this->Request = $request; + $this->SocialAuthenticate = $this->_getSocialAuthenticateMockMethods(['_authenticate', '_getProviderName', + '_mapUser', '_socialLogin', 'dispatchEvent', '_validateConfig', '_getController']); + + $this->SocialAuthenticate->expects($this->any()) + ->method('_getController') + ->will($this->returnValue($this->controller)); + } + + /** + * Tears down the fixture, for example, closes a network connection. + * This method is called after a test is executed. + */ + public function tearDown() + { + unset($this->SocialAuthenticate, $this->controller); + } + + protected function _getSocialAuthenticateMock() + { + return $this->getMockBuilder('CakeDC\Users\Auth\SocialAuthenticate') + ->disableOriginalConstructor() + ->getMock(); + } + + protected function _getSocialAuthenticateMockMethods($methods) + { + return $this->getMockBuilder('CakeDC\Users\Auth\SocialAuthenticate') + ->disableOriginalConstructor() + ->setMethods($methods) + ->getMock(); + } + + /** + * Test getUser + * + * @dataProvider providerGetUser + */ + public function testGetUserAuth($rawData, $mapper) + { + $this->SocialAuthenticate->expects($this->once()) + ->method('_authenticate') + ->with($this->Request) + ->will($this->returnValue($rawData)); + + $this->SocialAuthenticate->expects($this->once()) + ->method('_getProviderName') + ->will($this->returnValue('facebook')); + + $this->SocialAuthenticate->expects($this->once()) + ->method('_mapUser') + ->will($this->returnValue($mapper)); + + $user = $this->Table->get('00000000-0000-0000-0000-000000000002'); + $this->SocialAuthenticate->expects($this->once()) + ->method('_socialLogin') + ->will($this->returnValue($user)); + + + $result = $this->SocialAuthenticate->getUser($this->Request); + $this->assertTrue($result['active']); + $this->assertEquals('00000000-0000-0000-0000-000000000002', $result['id']); + } + + /** + * Provider for getUser test method + * + */ + public function providerGetUser() + { + return [ + [ + 'rawData' => [ + 'token' => 'token', + 'id' => 'reference-2-1', + 'name' => 'User S', + 'first_name' => 'user', + 'last_name' => 'second', + 'email' => 'userSecond@example.com', + 'cover' => [ + 'id' => 'reference-2-1' + ], + 'gender' => 'female', + 'locale' => 'en_US', + 'link' => 'link', + ], + 'mappedData' => [ + 'id' => 'reference-2-1', + 'username' => null, + 'full_name' => 'User S', + 'first_name' => 'user', + 'last_name' => 'second', + 'email' => 'userSecond@example.com', + 'link' => 'link', + 'bio' => null, + 'locale' => 'en_US', + 'validated' => true, + 'credentials' => [ + 'token' => 'token', + 'secret' => null, + 'expires' => 1458423682 + ], + 'raw' => [ + + ], + 'provider' => 'Facebook' + ], + ] + + ]; + } + + /** + * Test getUser + * + */ + public function testGetUserSessionData() + { + $user = ['username' => 'username', 'email' => 'myemail@test.com']; + $this->SocialAuthenticate = $this->_getSocialAuthenticateMockMethods(['_authenticate', + '_getProviderName', '_mapUser', '_touch', '_validateConfig' ]); + + $session = $this->getMock('Cake\Network\Session', ['read', 'delete']); + $session->expects($this->once()) + ->method('read') + ->with('Users.social') + ->will($this->returnValue($user)); + + $session->expects($this->once()) + ->method('delete') + ->with('Users.social'); + + $this->Request = $this->getMock('Cake\Network\Request', ['session']); + $this->Request->expects($this->any()) + ->method('session') + ->will($this->returnValue($session)); + + $this->SocialAuthenticate->expects($this->once()) + ->method('_touch') + ->will($this->returnValue($user)); + + $this->SocialAuthenticate->getUser($this->Request); + } + + /** + * Test getUser + * + * @dataProvider providerGetUser + */ + public function testGetUserNotEmailProvided($rawData, $mapper) + { + $this->SocialAuthenticate->expects($this->once()) + ->method('_authenticate') + ->with($this->Request) + ->will($this->returnValue($rawData)); + + $this->SocialAuthenticate->expects($this->once()) + ->method('_getProviderName') + ->will($this->returnValue('facebook')); + + $this->SocialAuthenticate->expects($this->once()) + ->method('_mapUser') + ->will($this->returnValue($mapper)); + + $this->SocialAuthenticate->expects($this->once()) + ->method('_socialLogin') + ->will($this->throwException(new MissingEmailException('missing email'))); + + $this->controller->expects($this->once()) + ->method('failedSocialLogin'); + + $this->SocialAuthenticate->getUser($this->Request); + } + + /** + * Test getUser + * + * @dataProvider providerGetUser + */ + public function testGetUserNotActive($rawData, $mapper) + { + $this->SocialAuthenticate->expects($this->once()) + ->method('_authenticate') + ->with($this->Request) + ->will($this->returnValue($rawData)); + + $this->SocialAuthenticate->expects($this->once()) + ->method('_getProviderName') + ->will($this->returnValue('facebook')); + + $this->SocialAuthenticate->expects($this->once()) + ->method('_mapUser') + ->will($this->returnValue($mapper)); + + $this->SocialAuthenticate->expects($this->once()) + ->method('_socialLogin') + ->will($this->throwException(new UserNotActiveException('user not active'))); + + $this->SocialAuthenticate->getUser($this->Request); + } + + /** + * Test getUser + * + * @dataProvider providerGetUser + */ + public function testGetUserNotActiveAccount($rawData, $mapper) + { + $this->SocialAuthenticate->expects($this->once()) + ->method('_authenticate') + ->with($this->Request) + ->will($this->returnValue($rawData)); + + $this->SocialAuthenticate->expects($this->once()) + ->method('_getProviderName') + ->will($this->returnValue('facebook')); + + $this->SocialAuthenticate->expects($this->once()) + ->method('_mapUser') + ->will($this->returnValue($mapper)); + + $this->SocialAuthenticate->expects($this->once()) + ->method('_socialLogin') + ->will($this->throwException(new AccountNotActiveException('user not active'))); + + $this->SocialAuthenticate->getUser($this->Request); + } + + /** + * Test getUser + * + * @dataProvider providerTwitter + */ + public function testGetUserNotEmailProvidedTwitter($rawData, $mapper) + { + $this->SocialAuthenticate->expects($this->once()) + ->method('_authenticate') + ->with($this->Request) + ->will($this->returnValue($rawData)); + + $this->SocialAuthenticate->expects($this->once()) + ->method('_getProviderName') + ->will($this->returnValue('twitter')); + + $this->SocialAuthenticate->expects($this->once()) + ->method('_mapUser') + ->will($this->returnValue($mapper)); + + $this->SocialAuthenticate->expects($this->once()) + ->method('_socialLogin') + ->will($this->throwException(new MissingEmailException('missing email'))); + + $this->SocialAuthenticate->getUser($this->Request); + } + + /** + * Provider for getUser test method + * + */ + public function providerTwitter() + { + return [ + [ + 'rawData' => [ + 'token' => 'token', + 'id' => 'reference-2-1', + 'name' => 'User S', + 'first_name' => 'user', + 'last_name' => 'second', + 'email' => 'userSecond@example.com', + 'cover' => [ + 'id' => 'reference-2-1' + ], + 'gender' => 'female', + 'locale' => 'en_US', + 'link' => 'link', + ], + 'mappedData' => [ + 'id' => 'reference-2-1', + 'username' => null, + 'full_name' => 'User S', + 'first_name' => 'user', + 'last_name' => 'second', + 'email' => 'userSecond@example.com', + 'link' => 'link', + 'bio' => null, + 'locale' => 'en_US', + 'validated' => true, + 'credentials' => [ + 'token' => 'token', + 'secret' => null, + 'expires' => 1458423682 + ], + 'raw' => [ + + ], + 'provider' => 'Twitter' + ], + ] + + ]; + } + + /** + * Test _socialLogin + * + * @dataProvider providerMapper + */ + public function testSocialLogin() + { + $this->SocialAuthenticate = $this->_getSocialAuthenticateMock(); + + $reflectedClass = new ReflectionClass($this->SocialAuthenticate); + $socialLogin = $reflectedClass->getMethod('_socialLogin'); + $socialLogin->setAccessible(true); + $data = [ + 'id' => 'reference-2-1', + 'provider' => 'Facebook' + ]; + $result = $socialLogin->invoke($this->SocialAuthenticate, $data); + $this->assertEquals($result->id, '00000000-0000-0000-0000-000000000002'); + $this->assertTrue($result->active); + } + + /** + * Test _mapUser + * + * @dataProvider providerMapper + */ + public function testMapUser($data, $mappedData) + { + $data['token'] = $this->Token; + $this->SocialAuthenticate = $this->_getSocialAuthenticateMock(); + + $reflectedClass = new ReflectionClass($this->SocialAuthenticate); + $mapUser = $reflectedClass->getMethod('_mapUser'); + $mapUser->setAccessible(true); + + $this->Token->expects($this->once()) + ->method('getToken') + ->will($this->returnValue('token')); + + $this->Token->expects($this->once()) + ->method('getExpires') + ->will($this->returnValue(1458510952)); + + $result = $mapUser->invoke($this->SocialAuthenticate, 'Facebook', $data); + unset($result['raw']); + $this->assertEquals($result, $mappedData); + } + + /** + * Provider for _mapUser test method + * + */ + public function providerMapper() + { + return [ + [ + 'rawData' => [ + 'id' => 'my-facebook-id', + 'name' => 'My name.', + 'first_name' => 'My first name', + 'last_name' => 'My lastname.', + 'email' => 'myemail@example.com', + 'gender' => 'female', + 'locale' => 'en_US', + 'link' => 'https://www.facebook.com/app_scoped_user_id/my-facebook-id/', + ], + 'mappedData' => [ + 'id' => 'my-facebook-id', + 'username' => null, + 'full_name' => 'My name.', + 'first_name' => 'My first name', + 'last_name' => 'My lastname.', + 'email' => 'myemail@example.com', + 'avatar' => 'https://graph.facebook.com/my-facebook-id/picture?type=normal', + 'gender' => 'female', + 'link' => 'https://www.facebook.com/app_scoped_user_id/my-facebook-id/', + 'bio' => null, + 'locale' => 'en_US', + 'validated' => true, + 'credentials' => [ + 'token' => 'token', + 'secret' => null, + 'expires' => (int)1458510952 + ], + 'provider' => 'Facebook' + ], + ] + + ]; + } + + /** + * Test _mapUser + * + * @expectedException CakeDC\Users\Exception\MissingProviderException + */ + public function testMapUserException() + { + $data = []; + $this->SocialAuthenticate = $this->_getSocialAuthenticateMock(); + + $reflectedClass = new ReflectionClass($this->SocialAuthenticate); + $mapUser = $reflectedClass->getMethod('_mapUser'); + $mapUser->setAccessible(true); + $mapUser->invoke($this->SocialAuthenticate, null, $data); + } +} diff --git a/tests/TestCase/Auth/SuperuserAuthorizeTest.php b/tests/TestCase/Auth/SuperuserAuthorizeTest.php new file mode 100755 index 000000000..bde73f341 --- /dev/null +++ b/tests/TestCase/Auth/SuperuserAuthorizeTest.php @@ -0,0 +1,93 @@ +controller = $this->getMock( + 'Cake\Controller\Controller', + null, + [$request, $response] + ); + $registry = new ComponentRegistry($this->controller); + $this->superuserAuthorize = new SuperuserAuthorize($registry); + } + + /** + * Tears down the fixture, for example, closes a network connection. + * This method is called after a test is executed. + */ + public function tearDown() + { + unset($this->superuserAuthorize, $this->controller); + } + + /** + * @covers CakeDC\Users\Auth\SuperuserAuthorize::authorize + */ + public function testAuthorizeIsSuperuser() + { + $user = [ + 'is_superuser' => true, + ]; + $request = new Request(); + $result = $this->superuserAuthorize->authorize($user, $request); + $this->assertTrue($result); + } + + /** + * @covers CakeDC\Users\Auth\SuperuserAuthorize::authorize + */ + public function testAuthorizeIsNotSuperuser() + { + $user = [ + 'is_superuser' => false, + ]; + $request = new Request(); + $result = $this->superuserAuthorize->authorize($user, $request); + $this->assertFalse($result); + } + + /** + * @covers CakeDC\Users\Auth\SuperuserAuthorize::authorize + */ + public function testAuthorizeWeirdUser() + { + $request = new Request(); + $user = 'non array'; + $result = $this->superuserAuthorize->authorize($user, $request); + $this->assertFalse($result); + } +} diff --git a/tests/TestCase/Controller/Component/RememberMeComponentTest.php b/tests/TestCase/Controller/Component/RememberMeComponentTest.php new file mode 100755 index 000000000..701bdf587 --- /dev/null +++ b/tests/TestCase/Controller/Component/RememberMeComponentTest.php @@ -0,0 +1,201 @@ +request = new Request('controller_posts/index'); + $this->request->params['pass'] = []; + $this->controller = $this->getMockBuilder('Cake\Controller\Controller') + ->setMethods(['redirect']) + ->setConstructorArgs([$this->request]) + ->getMock(); + $this->registry = new ComponentRegistry($this->controller); + $this->rememberMeComponent = new RememberMeComponent($this->registry, []); + } + + /** + * tearDown method + * + * @return void + */ + public function tearDown() + { + unset($this->rememberMeComponent); + + parent::tearDown(); + } + + /** + * Test initialize method + * + * @return void + */ + public function testInitialize() + { + $cookieOptions = [ + 'expires' => '1 month', + 'httpOnly' => true, + 'path' => '', + 'domain' => '', + 'secure' => false, + 'key' => '2a20bac195a9eb2e28f05b7ac7090afe599365a8fe480b7d8a5ce0f79687346e', + 'encryption' => 'aes', + 'enabled' => false + ]; + $this->assertEquals($cookieOptions, $this->rememberMeComponent->Cookie->configKey('remember_me')); + } + + /** + * Test initialize method + * + * @return void + */ + public function testInitializeException() + { + $salt = Security::salt(); + Security::salt('too small'); + try { + $this->rememberMeComponent = new RememberMeComponent($this->registry, []); + } catch (InvalidArgumentException $ex) { + $this->assertEquals('Invalid app salt, app salt must be at least 256 bits (32 bytes) long', $ex->getMessage()); + } + + Security::salt($salt); + } + + /** + * Test + * + * @return void + */ + public function testSetLoginCookie() + { + $event = new Event('event'); + $this->rememberMeComponent->Auth = $this->getMockBuilder('Cake\Controller\Component\AuthComponent') + ->setMethods(['user']) + ->disableOriginalConstructor() + ->getMock(); + $this->rememberMeComponent->Auth->expects($this->once()) + ->method('user') + ->with('id') + ->will($this->returnValue(1)); + $this->rememberMeComponent->Cookie = $this->getMockBuilder('Cake\Controller\Component\CookieComponent') + ->setMethods(['write']) + ->disableOriginalConstructor() + ->getMock(); + $this->rememberMeComponent->request = (new Request('/'))->env('HTTP_USER_AGENT', 'user-agent'); + $this->rememberMeComponent->Cookie->expects($this->once()) + ->method('write') + ->with('remember_me', ['id' => 1, 'user_agent' => 'user-agent']); + $this->rememberMeComponent->setLoginCookie($event); + } + + /** + * Test + * + * @return void + */ + public function testBeforeFilter() + { + $event = new Event('event'); + $this->rememberMeComponent->Auth = $this->getMockBuilder('Cake\Controller\Component\AuthComponent') + ->setMethods(['user', 'identify', 'setUser', 'redirectUrl']) + ->disableOriginalConstructor() + ->getMock(); + $this->rememberMeComponent->Auth->expects($this->once()) + ->method('user'); + $user = ['id' => 1]; + $this->rememberMeComponent->Auth->expects($this->once()) + ->method('identify') + ->will($this->returnValue($user)); + $this->rememberMeComponent->Auth->expects($this->once()) + ->method('setUser') + ->with($user); + $this->rememberMeComponent->Auth->expects($this->once()) + ->method('redirectUrl') + ->will($this->returnValue('/login')); + $this->controller->expects($this->once()) + ->method('redirect') + ->with('/login'); + $this->rememberMeComponent->beforeFilter($event); + } + + /** + * Test + * + * @return void + */ + public function testBeforeFilterNotIdentified() + { + $event = new Event('event'); + $this->rememberMeComponent->Auth = $this->getMockBuilder('Cake\Controller\Component\AuthComponent') + ->setMethods(['user', 'identify', 'setUser', 'redirectUrl']) + ->disableOriginalConstructor() + ->getMock(); + $this->rememberMeComponent->Auth->expects($this->at(0)) + ->method('user'); + $this->rememberMeComponent->Auth->expects($this->at(1)) + ->method('identify'); + + $this->assertNull($this->rememberMeComponent->beforeFilter($event)); + } + + /** + * Test + * + * @return void + */ + public function testBeforeFilterUserLoggedIn() + { + $event = new Event('event'); + $this->rememberMeComponent->Auth = $this->getMockBuilder('Cake\Controller\Component\AuthComponent') + ->setMethods(['user', 'identify', 'setUser', 'redirectUrl']) + ->disableOriginalConstructor() + ->getMock(); + $this->rememberMeComponent->Auth->expects($this->once()) + ->method('user') + ->will($this->returnValue([ + 'id' => 1, + ])); + $this->assertNull($this->rememberMeComponent->beforeFilter($event)); + } +} diff --git a/tests/TestCase/Controller/Component/UsersAuthComponentTest.php b/tests/TestCase/Controller/Component/UsersAuthComponentTest.php new file mode 100755 index 000000000..3138bb384 --- /dev/null +++ b/tests/TestCase/Controller/Component/UsersAuthComponentTest.php @@ -0,0 +1,222 @@ +backupUsersConfig = Configure::read('Users'); + + Router::reload(); + Plugin::routes('CakeDC/Users'); + Router::connect('/route/*', [ + 'plugin' => 'CakeDC/Users', + 'controller' => 'Users', + 'action' => 'requestResetPassword' + ]); + Security::salt('YJfIxfs2guVoUubWDYhG93b0qyJfIxfs2guwvniR2G0FgaC9mi'); + Configure::write('App.namespace', 'Users'); + $this->request = $this->getMock('Cake\Network\Request', ['is', 'method']); + $this->request->expects($this->any())->method('is')->will($this->returnValue(true)); + $this->response = $this->getMock('Cake\Network\Response', ['stop']); + $this->Controller = new Controller($this->request, $this->response); + $this->Registry = $this->Controller->components(); + $this->Controller->UsersAuth = new UsersAuthComponent($this->Registry); + } + + /** + * tearDown method + * + * @return void + */ + public function tearDown() + { + parent::tearDown(); + + $_SESSION = []; + unset($this->Controller, $this->UsersAuth); + Configure::write('Users', $this->backupUsersConfig); + } + + /** + * Test initialize + * + */ + public function testInitialize() + { + $this->Registry->unload('Auth'); + $this->Controller->UsersAuth = new UsersAuthComponent($this->Registry); + $this->assertInstanceOf('CakeDC\Users\Controller\Component\UsersAuthComponent', $this->Controller->UsersAuth); + } + + /** + * Test initialize with not rememberMe component needed + * + */ + public function testInitializeNoRequiredRememberMe() + { + Configure::write('Users.RememberMe.active', false); + $class = 'CakeDC\Users\Controller\Component\UsersAuthComponent'; + $this->Controller->UsersAuth = $this->getMockBuilder($class) + ->setMethods(['_loadRememberMe', '_initAuth', '_loadSocialLogin', '_attachPermissionChecker']) + ->disableOriginalConstructor() + ->getMock(); + $this->Controller->UsersAuth->expects($this->once()) + ->method('_initAuth'); + $this->Controller->UsersAuth->expects($this->never()) + ->method('_loadRememberMe'); + $this->Controller->UsersAuth->initialize([]); + } + + /** + * test + * + * @return void + */ + public function testIsUrlAuthorizedUserNotLoggedIn() + { + $event = new Event('event'); + $this->Controller->Auth = $this->getMockBuilder('Cake\Controller\Component\AuthComponent') + ->setMethods(['user', 'isAuthorized']) + ->disableOriginalConstructor() + ->getMock(); + $this->Controller->Auth->expects($this->once()) + ->method('user') + ->will($this->returnValue(false)); + $result = $this->Controller->UsersAuth->isUrlAuthorized($event); + $this->assertFalse($result); + } + + /** + * test + * + * @return void + */ + public function testIsUrlAuthorizedNoUrl() + { + $event = new Event('event'); + $this->Controller->Auth = $this->getMockBuilder('Cake\Controller\Component\AuthComponent') + ->setMethods(['user', 'isAuthorized']) + ->disableOriginalConstructor() + ->getMock(); + $this->Controller->Auth->expects($this->once()) + ->method('user') + ->will($this->returnValue(['id' => 1])); + $result = $this->Controller->UsersAuth->isUrlAuthorized($event); + $this->assertFalse($result); + } + + /** + * test + * + * @return void + */ + public function testIsUrlAuthorizedUrlString() + { + $event = new Event('event'); + $event->data = [ + 'url' => '/route', + ]; + $this->Controller->Auth = $this->getMockBuilder('Cake\Controller\Component\AuthComponent') + ->setMethods(['user', 'isAuthorized']) + ->disableOriginalConstructor() + ->getMock(); + $this->Controller->Auth->expects($this->once()) + ->method('user') + ->will($this->returnValue(['id' => 1])); + $request = new Request('/route'); + $request->params = [ + 'plugin' => 'CakeDC/Users', + 'controller' => 'Users', + 'action' => 'requestResetPassword', + 'pass' => [], + ]; + $this->Controller->Auth->expects($this->once()) + ->method('isAuthorized') + ->with(null, $request) + ->will($this->returnValue(true)); + $result = $this->Controller->UsersAuth->isUrlAuthorized($event); + $this->assertTrue($result); + } + + /** + * test + * + * @return void + */ + public function testIsUrlAuthorizedUrlArray() + { + $event = new Event('event'); + $event->data = [ + 'url' => [ + 'plugin' => 'CakeDC/Users', + 'controller' => 'Users', + 'action' => 'requestResetPassword', + 'pass-one' + ], + ]; + $this->Controller->Auth = $this->getMockBuilder('Cake\Controller\Component\AuthComponent') + ->setMethods(['user', 'isAuthorized']) + ->disableOriginalConstructor() + ->getMock(); + $this->Controller->Auth->expects($this->once()) + ->method('user') + ->will($this->returnValue(['id' => 1])); + $request = new Request('/route/pass-one'); + $request->params = [ + 'plugin' => 'CakeDC/Users', + 'controller' => 'Users', + 'action' => 'requestResetPassword', + 'pass' => ['pass-one'], + ]; + $this->Controller->Auth->expects($this->once()) + ->method('isAuthorized') + ->with(null, $request) + ->will($this->returnValue(true)); + $result = $this->Controller->UsersAuth->isUrlAuthorized($event); + $this->assertTrue($result); + } +} diff --git a/tests/TestCase/Controller/SocialAccountsControllerTest.php b/tests/TestCase/Controller/SocialAccountsControllerTest.php new file mode 100755 index 000000000..7b269638a --- /dev/null +++ b/tests/TestCase/Controller/SocialAccountsControllerTest.php @@ -0,0 +1,202 @@ +configOpauth = Configure::read('Opauth'); + $this->configRememberMe = Configure::read('Users.RememberMe.active'); + Configure::write('Opauth', null); + Configure::write('Users.RememberMe.active', false); + + Email::configTransport('test', [ + 'className' => 'Debug' + ]); + $this->configEmail = Email::config('default'); + Email::config('default', [ + 'transport' => 'test', + 'from' => 'cakedc@example.com' + ]); + + $request = new Request('/users/users/index'); + $request->params['plugin'] = 'CakeDC/Users'; + + $this->Controller = $this->getMockBuilder('CakeDC\Users\Controller\SocialAccountsController') + ->setMethods(['redirect', 'render']) + ->setConstructorArgs([$request, null, 'SocialAccounts']) + ->getMock(); + $this->Controller->SocialAccounts = $this->getMockForModel('CakeDC\Users.SocialAccounts', ['sendSocialValidationEmail'], [ + 'className' => 'CakeDC\Users\Model\Table\SocialAccountsTable' + ]); + } + + /** + * tearDown + * + * @return void + */ + public function tearDown() + { + Email::drop('default'); + Email::dropTransport('test'); + Email::config('default', $this->configEmail); + + Configure::write('Opauth', $this->configOpauth); + Configure::write('Users.RememberMe.active', $this->configRememberMe); + + parent::tearDown(); + } + + /** + * test + * + * @return void + */ + public function testValidateAccountHappy() + { + $this->Controller->expects($this->once()) + ->method('redirect') + ->with(['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'login']); + $this->Controller->validateAccount('Facebook', 'reference-1-1234', 'token-1234'); + $this->assertEquals('Account validated successfully', $this->Controller->request->session()->read('Flash.flash.0.message')); + } + + /** + * test + * + * @return void + */ + public function testValidateAccountInvalidToken() + { + $this->Controller->expects($this->once()) + ->method('redirect') + ->with(['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'login']); + $this->Controller->validateAccount('Facebook', 'reference-1-1234', 'token-not-found'); + $this->assertEquals('Invalid token and/or social account', $this->Controller->request->session()->read('Flash.flash.0.message')); + } + + /** + * test + * + * @return void + */ + public function testValidateAccountAlreadyActive() + { + $this->Controller->expects($this->once()) + ->method('redirect') + ->with(['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'login']); + $this->Controller->validateAccount('Twitter', 'reference-1-1234', 'token-1234'); + $this->assertEquals('SocialAccount already active', $this->Controller->request->session()->read('Flash.flash.0.message')); + } + + /** + * test + * + * @return void + */ + public function testResendValidationHappy() + { + $behaviorMock = $this->getMockBuilder('CakeDC\Users\Model\Behavior\SocialAccountBehavior') + ->setMethods(['sendSocialValidationEmail']) + ->setConstructorArgs([$this->Controller->SocialAccounts]) + ->getMock(); + $this->Controller->SocialAccounts->behaviors()->set('SocialAccount', $behaviorMock); + $behaviorMock->expects($this->once()) + ->method('sendSocialValidationEmail') + ->will($this->returnValue(true)); + $this->Controller->expects($this->once()) + ->method('redirect') + ->with(['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'login']); + + $this->Controller->resendValidation('Facebook', 'reference-1-1234'); + $this->assertEquals('Email sent successfully', $this->Controller->request->session()->read('Flash.flash.0.message')); + } + + /** + * test + * + * @return void + */ + public function testResendValidationEmailError() + { + $behaviorMock = $this->getMockBuilder('CakeDC\Users\Model\Behavior\SocialAccountBehavior') + ->setMethods(['sendSocialValidationEmail']) + ->setConstructorArgs([$this->Controller->SocialAccounts]) + ->getMock(); + $this->Controller->SocialAccounts->behaviors()->set('SocialAccount', $behaviorMock); + $behaviorMock->expects($this->once()) + ->method('sendSocialValidationEmail') + ->will($this->returnValue(false)); + $this->Controller->expects($this->once()) + ->method('redirect') + ->with(['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'login']); + + $this->Controller->resendValidation('Facebook', 'reference-1-1234'); + $this->assertEquals('Email could not be sent', $this->Controller->request->session()->read('Flash.flash.0.message')); + } + + /** + * test + * + * @return void + */ + public function testResendValidationInvalid() + { + $this->Controller->expects($this->once()) + ->method('redirect') + ->with(['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'login']); + $this->Controller->resendValidation('Facebook', 'reference-invalid'); + $this->assertEquals('Invalid account', $this->Controller->request->session()->read('Flash.flash.0.message')); + } + + /** + * test + * + * @return void + */ + public function testResendValidationAlreadyActive() + { + $this->Controller->expects($this->once()) + ->method('redirect') + ->with(['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'login']); + $this->Controller->validateAccount('Twitter', 'reference-1-1234', 'token-1234'); + $this->assertEquals('SocialAccount already active', $this->Controller->request->session()->read('Flash.flash.0.message')); + } +} diff --git a/tests/TestCase/Controller/Traits/BaseTraitTest.php b/tests/TestCase/Controller/Traits/BaseTraitTest.php new file mode 100755 index 000000000..131d58aa5 --- /dev/null +++ b/tests/TestCase/Controller/Traits/BaseTraitTest.php @@ -0,0 +1,188 @@ +traitMockMethods)); + $this->table = TableRegistry::get('CakeDC/Users.Users'); + try { + $this->Trait = $this->getMockBuilder($this->traitClassName) + ->setMethods($traitMockMethods) + ->getMockForTrait(); + $this->Trait->expects($this->any()) + ->method('getUsersTable') + ->will($this->returnValue($this->table)); + } catch (PHPUnit_Framework_MockObject_RuntimeException $ex) { + debug($ex); + $this->fail("Unit tests extending BaseTraitTest should declare the trait class name in the \$traitClassName variable before calling setUp()"); + } + + if ($this->mockDefaultEmail) { + Email::configTransport('test', [ + 'className' => 'Debug' + ]); + $this->configEmail = Email::config('default'); + Email::config('default', [ + 'transport' => 'test', + 'from' => 'cakedc@example.com' + ]); + } + } + + /** + * tearDown + * + * @return void + */ + public function tearDown() + { + unset($this->table, $this->Trait); + if ($this->mockDefaultEmail) { + Email::drop('default'); + Email::dropTransport('test'); + Email::config('default', $this->configEmail); + } + parent::tearDown(); + } + + /** + * mock request for GET + * + * @return void + */ + protected function _mockRequestGet() + { + $this->Trait->request = $this->getMockBuilder('Cake\Network\Request') + ->setMethods(['is', 'referer']) + ->getMock(); + $this->Trait->request->expects($this->any()) + ->method('is') + ->with('post') + ->will($this->returnValue(false)); + } + + /** + * mock Flash Component + * + * @return void + */ + protected function _mockFlash() + { + $this->Trait->Flash = $this->getMockBuilder('Cake\Controller\Component\FlashComponent') + ->setMethods(['error', 'success']) + ->disableOriginalConstructor() + ->getMock(); + } + + /** + * mock Request for POST, is and allow methods + * + * @param mixed $with used in with + * @return void + */ + protected function _mockRequestPost($with = 'post') + { + $this->Trait->request = $this->getMockBuilder('Cake\Network\Request') + ->setMethods(['is', 'data']) + ->getMock(); + $this->Trait->request->expects($this->any()) + ->method('is') + ->with($with) + ->will($this->returnValue(true)); + } + + /** + * Mock Auth and retur user id 1 + * + * @return void + */ + protected function _mockAuthLoggedIn() + { + $this->Trait->Auth = $this->getMockBuilder('Cake\Controller\Component\AuthComponent') + ->setMethods(['user', 'identify', 'setUser', 'redirectUrl']) + ->disableOriginalConstructor() + ->getMock(); + $user = [ + 'id' => '00000000-0000-0000-0000-000000000001', + 'password' => '12345', + ]; + $this->Trait->Auth->expects($this->any()) + ->method('identify') + ->will($this->returnValue($user)); + $this->Trait->Auth->expects($this->any()) + ->method('user') + ->with('id') + ->will($this->returnValue('00000000-0000-0000-0000-000000000001')); + } + + /** + * Mock the Auth component + * + * @return void + */ + protected function _mockAuth() + { + $this->Trait->Auth = $this->getMockBuilder('Cake\Controller\Component\AuthComponent') + ->setMethods(['user', 'identify', 'setUser', 'redirectUrl']) + ->disableOriginalConstructor() + ->getMock(); + } + + /** + * mock utility + * + * @param Event $event event + * @return void + */ + protected function _mockDispatchEvent(Event $event = null) + { + if (is_null($event)) { + $event = new Event('cool-name-here'); + } + $this->Trait->expects($this->any()) + ->method('dispatchEvent') + ->will($this->returnValue($event)); + } +} diff --git a/tests/TestCase/Controller/Traits/CustomUsersTableTraitTest.php b/tests/TestCase/Controller/Traits/CustomUsersTableTraitTest.php new file mode 100755 index 000000000..90f142699 --- /dev/null +++ b/tests/TestCase/Controller/Traits/CustomUsersTableTraitTest.php @@ -0,0 +1,42 @@ +controller = $this->getMock( + 'Cake\Controller\Controller', + ['header', 'redirect', 'render', '_stop'] + ); + $this->controller->Trait = $this->getMockForTrait('CakeDC\Users\Controller\Traits\CustomUsersTableTrait'); + } + + public function tearDown() + { + parent::tearDown(); + } + + public function testGetUsersTable() + { + $table = $this->controller->Trait->getUsersTable(); + $this->assertEquals('CakeDC/Users.Users', $table->registryAlias()); + $newTable = new Table(); + $this->controller->Trait->setUsersTable($newTable); + $this->assertSame($newTable, $this->controller->Trait->getUsersTable()); + } +} diff --git a/tests/TestCase/Controller/Traits/LoginTraitTest.php b/tests/TestCase/Controller/Traits/LoginTraitTest.php new file mode 100755 index 000000000..e60b6db8d --- /dev/null +++ b/tests/TestCase/Controller/Traits/LoginTraitTest.php @@ -0,0 +1,389 @@ +traitClassName = 'CakeDC\Users\Controller\Traits\LoginTrait'; + $this->traitMockMethods = ['dispatchEvent', 'isStopped', 'redirect', 'getUsersTable', 'set']; + + parent::setUp(); + $request = new Request(); + $this->Trait = $this->getMockBuilder('CakeDC\Users\Controller\Traits\LoginTrait') + ->setMethods(['dispatchEvent', 'redirect']) + ->getMockForTrait(); + + $this->Trait->Auth = $this->getMockBuilder('Cake\Controller\Component\AuthComponent') + ->setMethods(['config']) + ->disableOriginalConstructor() + ->getMock(); + + $this->Trait->request = $request; + } + + /** + * tearDown + * + * @return void + */ + public function tearDown() + { + parent::tearDown(); + } + + /** + * test + * + * @return void + */ + public function testLoginHappy() + { + $this->_mockDispatchEvent(new Event('event')); + $this->Trait->request = $this->getMockBuilder('Cake\Network\Request') + ->setMethods(['is']) + ->getMock(); + $this->Trait->request->expects($this->any()) + ->method('is') + ->with('post') + ->will($this->returnValue(true)); + $this->Trait->Auth = $this->getMockBuilder('Cake\Controller\Component\AuthComponent') + ->setMethods(['user', 'identify', 'setUser', 'redirectUrl']) + ->disableOriginalConstructor() + ->getMock(); + $user = [ + 'id' => 1, + ]; + $redirectLoginOK = '/'; + $this->Trait->Auth->expects($this->at(0)) + ->method('identify') + ->will($this->returnValue($user)); + $this->Trait->Auth->expects($this->at(1)) + ->method('setUser') + ->with($user); + $this->Trait->Auth->expects($this->at(2)) + ->method('redirectUrl') + ->will($this->returnValue($redirectLoginOK)); + $this->Trait->expects($this->once()) + ->method('redirect') + ->with($redirectLoginOK); + $this->Trait->login(); + } + + /** + * test + * + * @return void + */ + public function testAfterIdentifyEmptyUser() + { + $this->_mockDispatchEvent(new Event('event')); + $this->Trait->request = $this->getMockBuilder('Cake\Network\Request') + ->setMethods(['is']) + ->getMock(); + $this->Trait->request->expects($this->any()) + ->method('is') + ->with('post') + ->will($this->returnValue(true)); + $this->Trait->Auth = $this->getMockBuilder('Cake\Controller\Component\AuthComponent') + ->setMethods(['user', 'identify', 'setUser', 'redirectUrl']) + ->disableOriginalConstructor() + ->getMock(); + $user = []; + $this->Trait->Auth->expects($this->once()) + ->method('identify') + ->will($this->returnValue($user)); + $this->Trait->Flash = $this->getMockBuilder('Cake\Controller\Component\FlashComponent') + ->setMethods(['error']) + ->disableOriginalConstructor() + ->getMock(); + $this->Trait->Flash->expects($this->once()) + ->method('error') + ->with('Username or password is incorrect', 'default', [], 'auth'); + $this->Trait->login(); + } + + /** + * test + * + * @return void + */ + public function testAfterIdentifyEmptyUserSocialLogin() + { + $this->Trait = $this->getMockBuilder('CakeDC\Users\Controller\Traits\LoginTrait') + ->setMethods(['dispatchEvent', 'redirect', '_isSocialLogin']) + ->getMockForTrait(); + $this->Trait->expects($this->any()) + ->method('_isSocialLogin') + ->will($this->returnValue(true)); + $this->_mockDispatchEvent(new Event('event')); + $this->Trait->request = $this->getMockBuilder('Cake\Network\Request') + ->setMethods(['is']) + ->getMock(); + $this->Trait->Auth = $this->getMockBuilder('Cake\Controller\Component\AuthComponent') + ->setMethods(['user', 'identify', 'setUser', 'redirectUrl']) + ->disableOriginalConstructor() + ->getMock(); + + $this->Trait->login(); + } + + /** + * test + * + * @return void + */ + public function testLoginBeforeLoginReturningArray() + { + $user = [ + 'id' => 1 + ]; + $event = new Event('event'); + $event->result = $user; + $this->Trait->expects($this->at(0)) + ->method('dispatchEvent') + ->with(UsersAuthComponent::EVENT_BEFORE_LOGIN) + ->will($this->returnValue($event)); + $this->Trait->expects($this->at(1)) + ->method('dispatchEvent') + ->with(UsersAuthComponent::EVENT_AFTER_LOGIN) + ->will($this->returnValue(new Event('name'))); + $this->Trait->Auth = $this->getMockBuilder('Cake\Controller\Component\AuthComponent') + ->setMethods(['setUser', 'redirectUrl']) + ->disableOriginalConstructor() + ->getMock(); + $redirectLoginOK = '/'; + $this->Trait->Auth->expects($this->once()) + ->method('setUser') + ->with($user); + $this->Trait->Auth->expects($this->once()) + ->method('redirectUrl') + ->will($this->returnValue($redirectLoginOK)); + $this->Trait->expects($this->once()) + ->method('redirect') + ->with($redirectLoginOK); + $this->Trait->login(); + } + + /** + * test + * + * @return void + */ + public function testLoginBeforeLoginReturningStoppedEvent() + { + $event = new Event('event'); + $event->result = '/'; + $event->stopPropagation(); + $this->Trait->expects($this->at(0)) + ->method('dispatchEvent') + ->with(UsersAuthComponent::EVENT_BEFORE_LOGIN) + ->will($this->returnValue($event)); + $this->Trait->expects($this->once()) + ->method('redirect') + ->with('/'); + $this->Trait->login(); + } + + /** + * test + * + * @return void + */ + public function testLoginGet() + { + $this->_mockDispatchEvent(new Event('event')); + $socialLogin = Configure::read('Users.Social.login'); + Configure::write('Users.Social.login', false); + $this->Trait->Auth = $this->getMockBuilder('Cake\Controller\Component\AuthComponent') + ->setMethods(['user']) + ->disableOriginalConstructor() + ->getMock(); + $this->Trait->request = $this->getMockBuilder('Cake\Network\Request') + ->setMethods(['is']) + ->disableOriginalConstructor() + ->getMock(); + $this->Trait->request->expects($this->once()) + ->method('is') + ->with('post') + ->will($this->returnValue(false)); + $this->Trait->login(); + Configure::write('Users.Social.login', $socialLogin); + } + + /** + * test + * + * @return void + */ + public function testLogout() + { + $this->_mockDispatchEvent(new Event('event')); + $this->Trait->Auth = $this->getMockBuilder('Cake\Controller\Component\AuthComponent') + ->setMethods(['logout']) + ->disableOriginalConstructor() + ->getMock(); + $redirectLogoutOK = '/'; + $this->Trait->Auth->expects($this->once()) + ->method('logout') + ->will($this->returnValue($redirectLogoutOK)); + $this->Trait->expects($this->once()) + ->method('redirect') + ->with($redirectLogoutOK); + $this->Trait->Flash = $this->getMockBuilder('Cake\Controller\Component\FlashComponent') + ->setMethods(['success']) + ->disableOriginalConstructor() + ->getMock(); + $this->Trait->Flash->expects($this->once()) + ->method('success') + ->with('You\'ve successfully logged out'); + $this->Trait->logout(); + } + + /** + * test + * + * @return void + */ + public function testFailedSocialLoginMissingEmail() + { + $event = new Entity(); + $event->data = [ + 'exception' => new MissingEmailException('Email not present'), + 'rawData' => [ + 'id' => 11111, + 'username' => 'user-1' + ] + ]; + $this->_mockFlash(); + $this->_mockRequestGet(); + $this->Trait->Flash->expects($this->once()) + ->method('success') + ->with('Please enter your email'); + + $this->Trait->expects($this->once()) + ->method('redirect') + ->with(['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'socialEmail']); + + $this->Trait->failedSocialLogin($event->data['exception'], $event->data['rawData'], true); + } + + /** + * test + * + * @return void + */ + public function testFailedSocialUserNotActive() + { + $event = new Entity(); + $event->data = [ + 'exception' => new UserNotActiveException('Facebook user-1'), + 'rawData' => [ + 'id' => 111111, + 'username' => 'user-1' + ] + ]; + $this->_mockFlash(); + $this->_mockRequestGet(); + $this->Trait->Flash->expects($this->once()) + ->method('success') + ->with('Your user has not been validated yet. Please check your inbox for instructions'); + + $this->Trait->expects($this->once()) + ->method('redirect') + ->with(['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'login']); + + $this->Trait->Auth->expects($this->at(0)) + ->method('config') + ->with('authError', 'Your user has not been validated yet. Please check your inbox for instructions'); + + $this->Trait->Auth->expects($this->at(1)) + ->method('config') + ->with('flash.params', ['class' => 'success']); + + $this->Trait->failedSocialLogin($event->data['exception'], $event->data['rawData'], true); + } + + /** + * test + * + * @return void + */ + public function testFailedSocialUserAccountNotActive() + { + $event = new Entity(); + $event->data = [ + 'exception' => new AccountNotActiveException('Facebook user-1'), + 'rawData' => [ + 'id' => 111111, + 'username' => 'user-1' + ] + ]; + $this->_mockFlash(); + $this->_mockRequestGet(); + $this->Trait->Flash->expects($this->once()) + ->method('success') + ->with('Your social account has not been validated yet. Please check your inbox for instructions'); + + $this->Trait->expects($this->once()) + ->method('redirect') + ->with(['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'login']); + + $this->Trait->failedSocialLogin($event->data['exception'], $event->data['rawData'], true); + } + + + /** + * test + * + * @return void + */ + public function testFailedSocialUserAccount() + { + $event = new Entity(); + $event->data = [ + 'rawData' => [ + 'id' => 111111, + 'username' => 'user-1' + ] + ]; + $this->_mockFlash(); + $this->_mockRequestGet(); + $this->Trait->Flash->expects($this->once()) + ->method('success') + ->with('Issues trying to log in with your social account'); + + $this->Trait->expects($this->once()) + ->method('redirect') + ->with(['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'login']); + + $this->Trait->failedSocialLogin(null, $event->data['rawData'], true); + } +} diff --git a/tests/TestCase/Controller/Traits/PasswordManagementTraitTest.php b/tests/TestCase/Controller/Traits/PasswordManagementTraitTest.php new file mode 100755 index 000000000..3c64ebb75 --- /dev/null +++ b/tests/TestCase/Controller/Traits/PasswordManagementTraitTest.php @@ -0,0 +1,160 @@ +traitClassName = 'CakeDC\Users\Controller\Traits\PasswordManagementTrait'; + $this->traitMockMethods = ['set', 'redirect', 'validate']; + parent::setUp(); + } + + /** + * tearDown + * + * @return void + */ + public function tearDown() + { + parent::tearDown(); + } + + /** + * test + * + * @return void + */ + public function testChangePasswordHappy() + { + $this->assertEquals('12345', $this->table->get('00000000-0000-0000-0000-000000000001')->password); + $this->_mockRequestPost(); + $this->_mockAuthLoggedIn(); + $this->_mockFlash(); + $this->Trait->request->expects($this->once()) + ->method('data') + ->will($this->returnValue([ + 'password' => 'new', + 'password_confirm' => 'new', + ])); + $this->Trait->expects($this->once()) + ->method('redirect') + ->with(['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'profile']); + $this->Trait->Flash->expects($this->any()) + ->method('success') + ->with('Password has been changed successfully'); + $this->Trait->changePassword(); + $hasher = PasswordHasherFactory::build('Default'); + $this->assertTrue($hasher->check('new', $this->table->get('00000000-0000-0000-0000-000000000001')->password)); + } + + /** + * test + * + * @return void + */ + public function testChangePasswordGetLoggedIn() + { + $this->_mockRequestGet(); + $this->_mockAuthLoggedIn(); + $this->Trait->expects($this->any()) + ->method('set') + ->will($this->returnCallback(function ($param1, $param2 = null) { + if ($param1 === 'validatePassword') { + TestCase::assertEquals($param2, true); + } + })); + $this->Trait->changePassword(); + } + + /** + * test + * + * @return void + */ + public function testChangePasswordGetNotLoggedIn() + { + $this->_mockRequestGet(); + $this->_mockAuth(); + $this->Trait->expects($this->any()) + ->method('set') + ->will($this->returnCallback(function ($param1, $param2 = null) { + if ($param1 === 'validatePassword') { + TestCase::assertEquals($param2, false); + } + })); + $this->Trait->changePassword(); + } + + /** + * test + * + * @return void + */ + public function testResetPassword() + { + $token = 'token'; + $this->Trait->expects($this->once()) + ->method('validate') + ->with('password', $token); + $this->Trait->resetPassword($token); + } + + /** + * test + * + * @return void + */ + public function testRequestResetPasswordGet() + { + $this->assertEquals('ae93ddbe32664ce7927cf0c5c5a5e59d', $this->table->get('00000000-0000-0000-0000-000000000001')->token); + $this->_mockRequestGet(); + $this->_mockFlash(); + $this->Trait->request->expects($this->never()) + ->method('data'); + $this->Trait->requestResetPassword(); + } + + /** + * test + * + * @return void + */ + public function testRequestPasswordHappy() + { + $this->assertEquals('ae93ddbe32664ce7927cf0c5c5a5e59d', $this->table->get('00000000-0000-0000-0000-000000000001')->token); + $this->_mockRequestPost(); + $this->_mockAuthLoggedIn(); + $this->_mockFlash(); + $reference = 'user-1'; + $this->Trait->request->expects($this->once()) + ->method('data') + ->with('reference') + ->will($this->returnValue($reference)); + $this->Trait->Flash->expects($this->any()) + ->method('success') + ->with('Password has been changed successfully'); + $this->Trait->requestResetPassword(); + $this->assertNotEquals('xxx', $this->table->get('00000000-0000-0000-0000-000000000001')->token); + } +} diff --git a/tests/TestCase/Controller/Traits/ProfileTraitTest.php b/tests/TestCase/Controller/Traits/ProfileTraitTest.php new file mode 100755 index 000000000..c88a45cb1 --- /dev/null +++ b/tests/TestCase/Controller/Traits/ProfileTraitTest.php @@ -0,0 +1,113 @@ +traitClassName = 'CakeDC\Users\Controller\Traits\ProfileTrait'; + $this->traitMockMethods = ['set', 'getUsersTable', 'redirect', 'validate']; + parent::setUp(); + } + + /** + * test + * + * @return void + */ + public function testProfileGetNotLoggedInUserNotFound() + { + $userId = '00000000-0000-0000-0000-000000000000'; //not found + $this->_mockRequestGet(); + $this->_mockAuth(); + $this->_mockFlash(); + $this->Trait->Flash->expects($this->once()) + ->method('error') + ->with('User was not found'); + $this->Trait->profile($userId); + } + + /** + * test + * + * @return void + */ + public function testProfileGetLoggedInUserNotFound() + { + $userId = '00000000-0000-0000-0000-000000000000'; //not found + $this->_mockRequestGet(); + $this->_mockAuthLoggedIn(); + $this->_mockFlash(); + $this->Trait->Flash->expects($this->once()) + ->method('error') + ->with('User was not found'); + $this->Trait->profile($userId); + } + + /** + * test + * + * @return void + */ + public function testProfileGetNotLoggedInEmptyId() + { + $this->_mockRequestGet(); + $this->_mockAuth(); + $this->_mockFlash(); + $this->Trait->Flash->expects($this->once()) + ->method('error') + ->with('Not authorized, please login first'); + $this->Trait->profile(); + } + + /** + * test + * + * @return void + */ + public function testProfileGetLoggedInMyProfile() + { + $this->_mockRequestGet(); + $this->_mockAuthLoggedIn(); + $this->_mockFlash(); + $this->Trait->expects($this->any()) + ->method('set') + ->will($this->returnCallback(function ($param1, $param2 = null) { + if ($param1 === 'avatarPlaceholder') { + BaseTraitTest::assertEquals('CakeDC/Users.avatar_placeholder.png', $param2); + } elseif (is_array($param1)) { + BaseTraitTest::assertEquals('user-1', $param1['user']->username); + } + })); + $this->Trait->profile(); + } +} diff --git a/tests/TestCase/Controller/Traits/RecaptchaTraitTest.php b/tests/TestCase/Controller/Traits/RecaptchaTraitTest.php new file mode 100755 index 000000000..655e83c18 --- /dev/null +++ b/tests/TestCase/Controller/Traits/RecaptchaTraitTest.php @@ -0,0 +1,84 @@ +Trait = $this->getMockBuilder('CakeDC\Users\Controller\Traits\ReCaptchaTrait') + ->setMethods(['_getReCaptchaInstance']) + ->getMockForTrait(); + } + + /** + * tearDown callback + * + * @return void + */ + public function tearDown() + { + parent::tearDown(); + } + + /** + * testValidateValidReCaptcha + * + * @return void + */ + public function testValidateValidReCaptcha() + { + $ReCaptcha = $this->getMock('ReCaptcha\ReCaptcha', ['verify'], [], '', false); + $Response = $this->getMock('ReCaptcha\Response', ['isSuccess'], [], '', false); + $Response->expects($this->any()) + ->method('isSuccess') + ->will($this->returnValue(true)); + $ReCaptcha->expects($this->any()) + ->method('verify') + ->with('value') + ->will($this->returnValue($Response)); + $this->Trait->expects($this->any()) + ->method('_getReCaptchaInstance') + ->will($this->returnValue($ReCaptcha)); + $this->Trait->validateReCaptcha('value', '255.255.255.255'); + } + + /** + * testValidateInvalidReCaptcha + * + * @return void + */ + public function testValidateInvalidReCaptcha() + { + $ReCaptcha = $this->getMock('ReCaptcha\ReCaptcha', ['verify'], [], '', false); + $Response = $this->getMock('ReCaptcha\Response', ['isSuccess'], [], '', false); + $Response->expects($this->any()) + ->method('isSuccess') + ->will($this->returnValue(false)); + $ReCaptcha->expects($this->any()) + ->method('verify') + ->with('invalid') + ->will($this->returnValue($Response)); + $this->Trait->expects($this->any()) + ->method('_getReCaptchaInstance') + ->will($this->returnValue($ReCaptcha)); + $this->Trait->validateReCaptcha('invalid', '255.255.255.255'); + } +} diff --git a/tests/TestCase/Controller/Traits/RegisterTraitTest.php b/tests/TestCase/Controller/Traits/RegisterTraitTest.php new file mode 100755 index 000000000..0ea8e721e --- /dev/null +++ b/tests/TestCase/Controller/Traits/RegisterTraitTest.php @@ -0,0 +1,235 @@ +traitClassName = 'CakeDC\Users\Controller\Traits\RegisterTrait'; + $this->traitMockMethods = ['validate', 'dispatchEvent', 'set', 'validateReCaptcha', 'redirect']; + $this->mockDefaultEmail = true; + parent::setUp(); + + Plugin::routes('CakeDC/Users'); + } + + /** + * tearDown + * + * @return void + */ + public function tearDown() + { + parent::tearDown(); + } + + /** + * test + * + * @return void + */ + public function testValidateEmail() + { + $token = 'token'; + $this->Trait->expects($this->once()) + ->method('validate') + ->with('email', $token); + $this->Trait->validateEmail($token); + } + + /** + * test + * + * @return void + */ + public function testRegisterHappy() + { + $this->assertEquals(0, $this->table->find()->where(['username' => 'testRegistration'])->count()); + $this->_mockRequestPost(); + $this->_mockAuth(); + $this->_mockFlash(); + $this->_mockDispatchEvent(); + $this->Trait->Flash->expects($this->once()) + ->method('success') + ->with('Please validate your account before log in'); + $this->Trait->expects($this->once()) + ->method('validateRecaptcha') + ->will($this->returnValue(true)); + $this->Trait->expects($this->once()) + ->method('redirect') + ->with(['action' => 'login']); + $this->Trait->request->data = [ + 'username' => 'testRegistration', + 'password' => 'password', + 'email' => 'test-registration@example.com', + 'password_confirm' => 'password', + 'tos' => 1 + ]; + + $this->Trait->register(); + + $this->assertEquals(1, $this->table->find()->where(['username' => 'testRegistration'])->count()); + } + + /** + * test + * + * @return void + */ + public function testRegisterValidationErrors() + { + $this->assertEquals(0, $this->table->find()->where(['username' => 'testRegistration'])->count()); + $this->_mockRequestPost(); + $this->_mockAuth(); + $this->_mockFlash(); + $this->_mockDispatchEvent(); + $this->Trait->Flash->expects($this->once()) + ->method('error') + ->with('The user could not be saved'); + $this->Trait->expects($this->once()) + ->method('validateRecaptcha') + ->will($this->returnValue(true)); + $this->Trait->expects($this->never()) + ->method('redirect'); + $this->Trait->request->data = [ + 'username' => 'testRegistration', + 'password' => 'password', + 'email' => 'test-registration@example.com', + 'password_confirm' => 'not-matching', + 'tos' => 1 + ]; + + $this->Trait->register(); + + $this->assertEquals(0, $this->table->find()->where(['username' => 'testRegistration'])->count()); + } + + /** + * test + * + * @return void + */ + public function testRegisterRecaptchaNotValid() + { + $this->assertEquals(0, $this->table->find()->where(['username' => 'testRegistration'])->count()); + $this->_mockRequestPost(); + $this->_mockAuth(); + $this->_mockFlash(); + $this->_mockDispatchEvent(); + $this->Trait->Flash->expects($this->once()) + ->method('error') + ->with('The reCaptcha could not be validated'); + $this->Trait->expects($this->once()) + ->method('validateRecaptcha') + ->will($this->returnValue(false)); + $this->Trait->request->data = [ + 'username' => 'testRegistration', + 'password' => 'password', + 'email' => 'test-registration@example.com', + 'password_confirm' => 'password', + 'tos' => 1 + ]; + + $this->Trait->register(); + + $this->assertEquals(0, $this->table->find()->where(['username' => 'testRegistration'])->count()); + } + + /** + * test + * + * @return void + */ + public function testRegisterGet() + { + $this->assertEquals(0, $this->table->find()->where(['username' => 'testRegistration'])->count()); + $this->_mockRequestGet(); + $this->_mockAuth(); + $this->_mockFlash(); + $this->_mockDispatchEvent(); + $this->Trait->Flash->expects($this->never()) + ->method('success'); + $this->Trait->expects($this->never()) + ->method('validateRecaptcha'); + $this->Trait->expects($this->never()) + ->method('redirect'); + $this->Trait->register(); + + $this->assertEquals(0, $this->table->find()->where(['username' => 'testRegistration'])->count()); + } + + /** + * test + * + * @return void + */ + public function testRegisterRecaptchaDisabled() + { + $recaptcha = Configure::read('Users.Registration.reCaptcha'); + Configure::write('Users.Registration.reCaptcha', false); + $this->assertEquals(0, $this->table->find()->where(['username' => 'testRegistration'])->count()); + $this->_mockRequestPost(); + $this->_mockAuth(); + $this->_mockFlash(); + $this->_mockDispatchEvent(); + $this->Trait->Flash->expects($this->once()) + ->method('success') + ->with('Please validate your account before log in'); + $this->Trait->expects($this->never()) + ->method('validateRecaptcha'); + $this->Trait->expects($this->once()) + ->method('redirect') + ->with(['action' => 'login']); + $this->Trait->request->data = [ + 'username' => 'testRegistration', + 'password' => 'password', + 'email' => 'test-registration@example.com', + 'password_confirm' => 'password', + 'tos' => 1 + ]; + + $this->Trait->register(); + + $this->assertEquals(1, $this->table->find()->where(['username' => 'testRegistration'])->count()); + Configure::write('Users.Registration.reCaptcha', $recaptcha); + } + + /** + * test + * + * @return void + * @expectedException Cake\Network\Exception\NotFoundException + */ + public function testRegisterNotEnabled() + { + $active = Configure::read('Users.Registration.active'); + Configure::write('Users.Registration.active', false); + $this->_mockRequestPost(); + $this->_mockAuth(); + $this->_mockFlash(); + $this->_mockDispatchEvent(); + $this->Trait->register(); + Configure::write('Users.Registration.active', $active); + } +} diff --git a/tests/TestCase/Controller/Traits/SimpleCrudTraitTest.php b/tests/TestCase/Controller/Traits/SimpleCrudTraitTest.php new file mode 100755 index 000000000..2e807ebda --- /dev/null +++ b/tests/TestCase/Controller/Traits/SimpleCrudTraitTest.php @@ -0,0 +1,279 @@ +traitClassName = 'CakeDC\Users\Controller\Traits\SimpleCrudTrait'; + $this->traitMockMethods = ['dispatchEvent', 'isStopped', 'redirect', 'getUsersTable', 'set', 'loadModel', 'paginate']; + parent::setUp(); + $viewVarsContainer = $this; + $this->Trait->expects($this->any()) + ->method('set') + ->will($this->returnCallback(function ($param1, $param2 = null) use ($viewVarsContainer) { + $viewVarsContainer->viewVars[$param1] = $param2; + })); + $this->Trait->expects($this->once()) + ->method('loadModel') + ->will($this->returnValue($this->table)); + } + + /** + * tearDown + * + * @return void + */ + public function tearDown() + { + $this->viewVars = null; + parent::tearDown(); + } + + /** + * test + * + * @return void + */ + public function testIndex() + { + $this->Trait->expects($this->once()) + ->method('paginate') + ->with($this->table) + ->will($this->returnValue([])); + $this->Trait->index(); + $expected = [ + 'Users' => [], + 'tableAlias' => 'Users', + '_serialize' => [ + 'Users', + 'tableAlias' + ] + ]; + $this->assertSame($expected, $this->viewVars); + } + + /** + * test + * + * @return void + */ + public function testView() + { + $id = '00000000-0000-0000-0000-000000000001'; + $this->Trait->view($id); + $expected = [ + 'Users' => $this->table->get($id), + 'tableAlias' => 'Users', + '_serialize' => [ + 'Users', + 'tableAlias' + ] + ]; + $this->assertEquals($expected, $this->viewVars); + } + + /** + * test + * + * @return void + * @expectedException Cake\Datasource\Exception\RecordNotFoundException + */ + public function testViewNotFound() + { + $this->Trait->view('00000000-0000-0000-0000-000000000000'); + } + + /** + * test + * + * @return void + * @expectedException Cake\Datasource\Exception\InvalidPrimaryKeyException + */ + public function testViewInvalidPK() + { + $this->Trait->view(); + } + + /** + * test + * + * @return void + */ + public function testAddGet() + { + $this->_mockRequestGet(); + $this->Trait->add(); + $expected = [ + 'Users' => $this->table->newEntity(), + 'tableAlias' => 'Users', + '_serialize' => [ + 'Users', + 'tableAlias' + ] + ]; + $this->assertEquals($expected, $this->viewVars); + } + + /** + * test + * + * @return void + */ + public function testAddPostHappy() + { + $this->assertSame(0, $this->table->find()->where(['username' => 'testuser'])->count()); + $this->_mockRequestPost(); + $this->_mockFlash(); + $this->Trait->request->data = [ + 'username' => 'testuser', + 'email' => 'testuser@test.com', + 'password' => 'password', + 'first_name' => 'test', + 'last_name' => 'user', + ]; + + $this->Trait->Flash->expects($this->once()) + ->method('success') + ->with('The User has been saved'); + + $this->Trait->add(); + + $this->assertSame(1, $this->table->find()->where(['username' => 'testuser'])->count()); + } + + /** + * test + * + * @return void + */ + public function testAddPostErrors() + { + $this->assertSame(0, $this->table->find()->where(['username' => 'testuser'])->count()); + $this->_mockRequestPost(); + $this->_mockFlash(); + $this->Trait->request->data = [ + 'username' => 'testuser', + 'email' => 'testuser@test.com', + 'first_name' => 'test', + 'last_name' => 'user', + ]; + + $this->Trait->Flash->expects($this->once()) + ->method('error') + ->with('The User could not be saved'); + + $this->Trait->add(); + + $this->assertSame(0, $this->table->find()->where(['username' => 'testuser'])->count()); + } + + /** + * test + * + * @return void + */ + public function testEditPostHappy() + { + $this->assertEquals('user-1@test.com', $this->table->get('00000000-0000-0000-0000-000000000001')->email); + $this->_mockRequestPost(['patch', 'post', 'put']); + $this->_mockFlash(); + $this->Trait->request->data = [ + 'email' => 'newtestuser@test.com', + ]; + + $this->Trait->Flash->expects($this->once()) + ->method('success') + ->with('The User has been saved'); + + $this->Trait->edit('00000000-0000-0000-0000-000000000001'); + + $this->assertEquals('newtestuser@test.com', $this->table->get('00000000-0000-0000-0000-000000000001')->email); + } + + /** + * test + * + * @return void + */ + public function testEditPostErrors() + { + $this->assertEquals('user-1@test.com', $this->table->get('00000000-0000-0000-0000-000000000001')->email); + $this->_mockRequestPost(['patch', 'post', 'put']); + $this->_mockFlash(); + $this->Trait->request->data = [ + 'email' => 'not-an-email', + ]; + + $this->Trait->Flash->expects($this->once()) + ->method('error') + ->with('The User could not be saved'); + + $this->Trait->edit('00000000-0000-0000-0000-000000000001'); + + $this->assertEquals('user-1@test.com', $this->table->get('00000000-0000-0000-0000-000000000001')->email); + } + + /** + * test + * + * @return void + * @expectedException Cake\Datasource\Exception\RecordNotFoundException + */ + public function testDeleteHappy() + { + $this->assertNotEmpty($this->table->get('00000000-0000-0000-0000-000000000001')); + $this->_mockRequestPost(); + $this->Trait->request->expects($this->any()) + ->method('allow') + ->with(['post', 'delete']) + ->will($this->returnValue(true)); + + $this->_mockFlash(); + $this->Trait->Flash->expects($this->once()) + ->method('success') + ->with('The User has been deleted'); + + $this->Trait->delete('00000000-0000-0000-0000-000000000001'); + + $this->table->get('00000000-0000-0000-0000-000000000001'); + } + + /** + * test + * + * @return void + * @expectedException Cake\Datasource\Exception\RecordNotFoundException + */ + public function testDeleteNotFound() + { + $this->assertNotEmpty($this->table->get('00000000-0000-0000-0000-000000000001')); + $this->_mockRequestPost(); + $this->Trait->request->expects($this->any()) + ->method('allow') + ->with(['post', 'delete']) + ->will($this->returnValue(true)); + + $this->Trait->delete('00000000-0000-0000-0000-000000000000'); + } +} diff --git a/tests/TestCase/Controller/Traits/SocialTraitTest.php b/tests/TestCase/Controller/Traits/SocialTraitTest.php new file mode 100755 index 000000000..e5e02bbf5 --- /dev/null +++ b/tests/TestCase/Controller/Traits/SocialTraitTest.php @@ -0,0 +1,164 @@ +controller = $this->getMock( + 'Cake\Controller\Controller', + ['header', 'redirect', 'render', '_stop'] + ); + $this->controller->Trait = $this->getMockForTrait( + 'CakeDC\Users\Controller\Traits\SocialTrait', + [], + '', + true, + true, + true, + ['_getOpauthInstance', 'redirect', '_generateOpauthCompleteUrl', '_afterIdentifyUser', '_validateRegisterPost'] + ); + } + + public function tearDown() + { + parent::tearDown(); + } + + /** + * Test socialEmail + * + */ + public function testSocialEmail() + { + $session = $this->getMock('Cake\Network\Session', ['check', 'delete']); + $session->expects($this->at(0)) + ->method('check') + ->with('Users.social') + ->will($this->returnValue('social_key')); + + $session->expects($this->at(1)) + ->method('delete') + ->with('Flash.auth'); + + $this->controller->Trait->request = $this->getMock('Cake\Network\Request', ['session']); + $this->controller->Trait->request->expects($this->any()) + ->method('session') + ->will($this->returnValue($session)); + + $this->controller->Trait->socialEmail(); + } + + /** + * Test socialEmail + * + * @expectedException \Cake\Network\Exception\NotFoundException + */ + public function testSocialEmailInvalid() + { + $session = $this->getMock('Cake\Network\Session', ['check']); + $session->expects($this->once()) + ->method('check') + ->with('Users.social') + ->will($this->returnValue(null)); + + $this->controller->Trait->request = $this->getMock('Cake\Network\Request', ['session']); + $this->controller->Trait->request->expects($this->once()) + ->method('session') + ->will($this->returnValue($session)); + + $this->controller->Trait->socialEmail(); + } + + public function testSocialEmailPostValidateFalse() + { + $session = $this->getMock('Cake\Network\Session', ['check', 'delete']); + $session->expects($this->any()) + ->method('check') + ->with('Users.social') + ->will($this->returnValue(true)); + + $session->expects($this->once()) + ->method('delete') + ->with('Flash.auth'); + + $this->controller->Trait->request = $this->getMock('Cake\Network\Request', ['session', 'is']); + $this->controller->Trait->request->expects($this->any()) + ->method('session') + ->will($this->returnValue($session)); + + $this->controller->Trait->request->expects($this->once()) + ->method('is') + ->with('post') + ->will($this->returnValue(true)); + + $this->controller->Trait->expects($this->once()) + ->method('_validateRegisterPost') + ->will($this->returnValue(false)); + + $this->controller->Trait->Flash = $this->getMockBuilder('Cake\Controller\Component\FlashComponent') + ->setMethods(['error']) + ->disableOriginalConstructor() + ->getMock(); + + $this->controller->Trait->Flash->expects($this->once()) + ->method('error') + ->with('The reCaptcha could not be validated'); + + $this->controller->Trait->socialEmail(); + } + + public function testSocialEmailPostValidateTrue() + { + $session = $this->getMock('Cake\Network\Session', ['check', 'delete']); + $session->expects($this->any()) + ->method('check') + ->with('Users.social') + ->will($this->returnValue(true)); + + $session->expects($this->once()) + ->method('delete') + ->with('Flash.auth'); + + $this->controller->Trait->request = $this->getMock('Cake\Network\Request', ['session', 'is']); + $this->controller->Trait->request->expects($this->any()) + ->method('session') + ->will($this->returnValue($session)); + + $this->controller->Trait->request->expects($this->once()) + ->method('is') + ->with('post') + ->will($this->returnValue(true)); + + $this->controller->Trait->expects($this->once()) + ->method('_validateRegisterPost') + ->will($this->returnValue(true)); + + $this->controller->Trait->Auth = $this->getMockBuilder('Cake\Controller\Component\AuthComponent') + ->setMethods(['identify']) + ->disableOriginalConstructor() + ->getMock(); + + $this->controller->Trait->Auth->expects($this->once()) + ->method('identify'); + + $this->controller->Trait->expects($this->once()) + ->method('_afterIdentifyUser'); + + $this->controller->Trait->socialEmail(); + } +} diff --git a/tests/TestCase/Controller/Traits/UserValidationTraitTest.php b/tests/TestCase/Controller/Traits/UserValidationTraitTest.php new file mode 100755 index 000000000..1a9d18a9b --- /dev/null +++ b/tests/TestCase/Controller/Traits/UserValidationTraitTest.php @@ -0,0 +1,194 @@ +traitClassName = 'CakeDC\Users\Controller\Traits\UserValidationTrait'; + $this->traitMockMethods = ['dispatchEvent', 'isStopped', 'redirect', 'getUsersTable', 'set']; + $this->mockDefaultEmail = true; + parent::setUp(); + } + + /** + * test + * + * @return void + */ + public function testValidateHappyEmail() + { + $this->_mockFlash(); + $user = $this->table->findByToken('token-3')->first(); + $this->assertFalse($user->active); + $this->Trait->Flash->expects($this->once()) + ->method('success') + ->with('User account validated successfully'); + $this->Trait->expects($this->once()) + ->method('redirect') + ->with(['action' => 'login']); + $this->Trait->validate('email', 'token-3'); + $user = $this->table->findById($user->id)->first(); + $this->assertTrue($user->active); + } + + /** + * test + * + * @return void + */ + public function testValidateUserNotFound() + { + $this->_mockFlash(); + $this->Trait->Flash->expects($this->once()) + ->method('error') + ->with('Invalid token or user account already validated'); + $this->Trait->expects($this->once()) + ->method('redirect') + ->with(['action' => 'login']); + $this->Trait->validate('email', 'not-found'); + } + + /** + * test + * + * @return void + */ + public function testValidateTokenExpired() + { + $this->_mockFlash(); + $this->Trait->Flash->expects($this->once()) + ->method('error') + ->with('Token already expired'); + $this->Trait->expects($this->once()) + ->method('redirect') + ->with(['action' => 'login']); + $this->Trait->validate('email', '6614f65816754310a5f0553436dd89e9'); + } + + /** + * test + * + * @return void + */ + public function testValidateInvalidOp() + { + $this->_mockFlash(); + $this->Trait->Flash->expects($this->once()) + ->method('error') + ->with('Invalid validation type'); + $this->Trait->expects($this->once()) + ->method('redirect') + ->with(['action' => 'login']); + $this->Trait->validate('invalid-op', '6614f65816754310a5f0553436dd89e9'); + } + + /** + * test + * + * @return void + */ + public function testValidateHappyPassword() + { + $this->_mockRequestGet(); + $this->_mockFlash(); + $user = $this->table->findByToken('token-4')->first(); + $this->assertTrue($user->active); + $this->Trait->Flash->expects($this->once()) + ->method('success') + ->with('Reset password token was validated successfully'); + $this->Trait->expects($this->once()) + ->method('redirect') + ->with(['action' => 'changePassword']); + $this->Trait->validate('password', 'token-4'); + $user = $this->table->findById($user->id)->first(); + $this->assertTrue($user->active); + } + + /** + * test + * + * @return void + */ + public function testResendTokenValidationHappy() + { + $this->_mockRequestPost(); + $this->_mockFlash(); + $this->Trait->request->expects($this->once()) + ->method('data') + ->with('reference') + ->will($this->returnValue('user-3')); + + $this->Trait->Flash->expects($this->once()) + ->method('success') + ->with('Token has been reset successfully. Please check your email.'); + $this->Trait->expects($this->once()) + ->method('redirect') + ->with(['action' => 'login']); + $this->Trait->resendTokenValidation(); + } + + /** + * test + * + * @return void + */ + public function testResendTokenValidationAlreadyActive() + { + $this->_mockRequestPost(); + $this->_mockFlash(); + $this->Trait->request->expects($this->once()) + ->method('data') + ->with('reference') + ->will($this->returnValue('user-4')); + + $this->Trait->Flash->expects($this->once()) + ->method('error') + ->with('User user-4 is already active'); + $this->Trait->expects($this->never()) + ->method('redirect') + ->with(['action' => 'login']); + $this->Trait->resendTokenValidation(); + } + + /** + * test + * + * @return void + */ + public function testResendTokenValidationNotFound() + { + $this->_mockRequestPost(); + $this->_mockFlash(); + $this->Trait->request->expects($this->once()) + ->method('data') + ->with('reference') + ->will($this->returnValue('not-found')); + + $this->Trait->Flash->expects($this->once()) + ->method('error') + ->with('User not-found was not found'); + $this->Trait->expects($this->never()) + ->method('redirect') + ->with(['action' => 'login']); + $this->Trait->resendTokenValidation(); + } +} diff --git a/tests/TestCase/Email/EmailSenderTest.php b/tests/TestCase/Email/EmailSenderTest.php new file mode 100755 index 000000000..a08755b5b --- /dev/null +++ b/tests/TestCase/Email/EmailSenderTest.php @@ -0,0 +1,161 @@ +EmailSender = $this->getMockBuilder('CakeDC\Users\Email\EmailSender') + ->setMethods(['_getEmailInstance', 'getMailer']) + ->getMock(); + + $this->UserMailer = $this->getMockBuilder('CakeDC\Users\Mailer\UserMailer') + ->setMethods(['send']) + ->getMock(); + + $this->fullBaseBackup = Router::fullBaseUrl(); + Router::fullBaseUrl('http://users.test'); + + Email::configTransport('test', [ + 'className' => 'Debug' + ]); + } + + /** + * tearDown + * + * @return void + */ + public function tearDown() + { + Email::drop('default'); + Email::dropTransport('test'); + parent::tearDown(); + } + + /** + * test sendValidationEmail + * + * @return void + */ + public function testSendEmailValidation() + { + $table = TableRegistry::get('CakeDC/Users.Users'); + $user = $table->newEntity([ + 'first_name' => 'FirstName', + 'email' => 'test@example.com', + 'token' => '12345' + ]); + + $email = new Email([ + 'from' => 'test@example.com', + 'transport' => 'test', + 'emailFormat' => 'both', + ]); + + $this->EmailSender->expects($this->once()) + ->method('getMailer') + ->with('CakeDC/Users.Users') + ->will($this->returnValue($this->UserMailer)); + + $this->UserMailer->expects($this->once()) + ->method('send') + ->with('validation', [$user, 'Your account validation link']); + + $this->EmailSender->sendValidationEmail($user, $email); + } + + /** + * test sendResetPasswordEmail + * + * @return void + */ + public function testSendResetPasswordEmailMailer() + { + $table = TableRegistry::get('CakeDC/Users.Users'); + $user = $table->newEntity([ + 'first_name' => 'FirstName', + 'email' => 'test@example.com', + 'token' => '12345' + ]); + + $email = new Email([ + 'from' => 'test@example.com', + 'transport' => 'test', + 'template' => 'CakeDC/Users.reset_password', + 'emailFormat' => 'both', + ]); + + $this->EmailSender->expects($this->once()) + ->method('getMailer') + ->with('CakeDC/Users.Users') + ->will($this->returnValue($this->UserMailer)); + + $this->UserMailer->expects($this->once()) + ->method('send') + ->with('resetPassword', [$user, 'CakeDC/Users.reset_password']); + + $this->EmailSender->sendResetPasswordEmail($user, $email); + } + + /** + * test sendSocialValidationEmail + * + * @return void + */ + public function testSendSocialValidationEmailMailer() + { + $this->Table = TableRegistry::get('CakeDC/Users.SocialAccounts'); + $user = $this->Table->find()->contain('Users')->first(); + $email = new Email([ + 'from' => 'test@example.com', + 'transport' => 'test', + 'template' => 'CakeDC/Users.my_template', + 'emailFormat' => 'both', + ]); + + $this->EmailSender->expects($this->once()) + ->method('getMailer') + ->with('CakeDC/Users.Users') + ->will($this->returnValue($this->UserMailer)); + + $this->UserMailer->expects($this->once()) + ->method('send') + ->with('socialAccountValidation', [$user->user, $user, 'CakeDC/Users.my_template']); + + $this->EmailSender->sendSocialValidationEmail($user, $user->user, $email); + } +} diff --git a/tests/TestCase/Mailer/UsersMailerTest.php b/tests/TestCase/Mailer/UsersMailerTest.php new file mode 100755 index 000000000..1aae78b62 --- /dev/null +++ b/tests/TestCase/Mailer/UsersMailerTest.php @@ -0,0 +1,221 @@ +Email = $this->getMockBuilder('Cake\Mailer\Email') + ->setMethods(['to', 'subject', 'viewVars', 'template']) + ->getMock(); + + $this->UsersMailer = $this->getMockBuilder('CakeDC\Users\Mailer\UsersMailer') + ->setConstructorArgs([$this->Email]) + ->setMethods(['to', 'subject', 'viewVars', 'template']) + ->getMock(); + } + + /** + * tearDown + * + * @return void + */ + public function tearDown() + { + unset($this->UsersMailer); + unset($this->Email); + parent::tearDown(); + } + + /** + * test sendValidationEmail + * + * @return void + */ + public function testValidation() + { + $table = TableRegistry::get('CakeDC/Users.Users'); + $data = [ + 'first_name' => 'FirstName', + 'email' => 'test@example.com', + 'token' => '12345' + ]; + $user = $table->newEntity($data); + $this->UsersMailer->expects($this->once()) + ->method('to') + ->with($user['email']) + ->will($this->returnValue($this->Email)); + + $this->Email->expects($this->once()) + ->method('subject') + ->with('FirstName, Validate your Account') + ->will($this->returnValue($this->Email)); + + $this->Email->expects($this->once()) + ->method('viewVars') + ->with($data) + ->will($this->returnValue($this->Email)); + + $this->Email->expects($this->once()) + ->method('template') + ->with('CakeDC/Users.validation') + ->will($this->returnValue($this->Email)); + + $this->invokeMethod($this->UsersMailer, 'validation', [$user, 'Validate your Account']); + } + + /** + * test sendValidationEmail including 'template' + * + * @return void + */ + public function testValidationWithTemplate() + { + $table = TableRegistry::get('CakeDC/Users.Users'); + $data = [ + 'first_name' => 'FirstName', + 'email' => 'test@example.com', + 'token' => '12345' + ]; + $user = $table->newEntity($data); + $this->UsersMailer->expects($this->once()) + ->method('to') + ->with($user['email']) + ->will($this->returnValue($this->Email)); + + $this->Email->expects($this->once()) + ->method('subject') + ->with('FirstName, Validate your Account') + ->will($this->returnValue($this->Email)); + + $this->Email->expects($this->once()) + ->method('viewVars') + ->with($data) + ->will($this->returnValue($this->Email)); + + $this->Email->expects($this->once()) + ->method('template') + ->with('myTemplate') + ->will($this->returnValue($this->Email)); + + $this->invokeMethod($this->UsersMailer, 'validation', [$user, 'Validate your Account', 'myTemplate']); + } + + /** + * test SocialAccountValidation + * + * @return void + */ + public function testSocialAccountValidation() + { + $social = TableRegistry::get('CakeDC/Users.SocialAccounts') + ->get('00000000-0000-0000-0000-000000000001', ['contain' => 'Users']); + + $this->UsersMailer->expects($this->once()) + ->method('to') + ->with('user-1@test.com') + ->will($this->returnValue($this->Email)); + + $this->Email->expects($this->once()) + ->method('subject') + ->with('first1, Your social account validation link') + ->will($this->returnValue($this->Email)); + + + $this->Email->expects($this->once()) + ->method('viewVars') + ->with(['user' => $social->user, 'socialAccount' => $social]) + ->will($this->returnValue($this->Email)); + + $this->invokeMethod($this->UsersMailer, 'socialAccountValidation', [$social->user, $social]); + } + + /** + * test sendValidationEmail including 'template' + * + * @return void + */ + public function testResetPassword() + { + $table = TableRegistry::get('CakeDC/Users.Users'); + $data = [ + 'first_name' => 'FirstName', + 'email' => 'test@example.com', + 'token' => '12345' + ]; + $user = $table->newEntity($data); + $this->UsersMailer->expects($this->once()) + ->method('to') + ->with($user['email']) + ->will($this->returnValue($this->Email)); + + $this->Email->expects($this->once()) + ->method('subject') + ->with('FirstName, Your reset password link') + ->will($this->returnValue($this->Email)); + + $this->Email->expects($this->once()) + ->method('viewVars') + ->with($data) + ->will($this->returnValue($this->Email)); + + $this->Email->expects($this->once()) + ->method('template') + ->with('myTemplate') + ->will($this->returnValue($this->Email)); + + + $this->invokeMethod($this->UsersMailer, 'resetPassword', [$user, 'myTemplate']); + } + + /** + * Call protected/private method of a class. + * + * @param object &$object Instantiated object that we will run method on. + * @param string $methodName Method name to call + * @param array $parameters Array of parameters to pass into method. + * + * @return mixed Method return. + */ + public function invokeMethod(&$object, $methodName, $parameters = []) + { + $reflection = new \ReflectionClass(get_class($object)); + $method = $reflection->getMethod($methodName); + $method->setAccessible(true); + + return $method->invokeArgs($object, $parameters); + } +} diff --git a/tests/TestCase/Model/Behavior/PasswordBehaviorTest.php b/tests/TestCase/Model/Behavior/PasswordBehaviorTest.php new file mode 100755 index 000000000..238a7923d --- /dev/null +++ b/tests/TestCase/Model/Behavior/PasswordBehaviorTest.php @@ -0,0 +1,149 @@ +table = TableRegistry::get('CakeDC/Users.Users'); + $this->Behavior = $this->getMockBuilder('CakeDC\Users\Model\Behavior\PasswordBehavior') + ->setMethods(['sendResetPasswordEmail']) + ->setConstructorArgs([$this->table]) + ->getMock(); + $this->Behavior->Email = $this->getMockBuilder('CakeDC\Users\Email\EmailSender') + ->setMethods(['sendResetPasswordEmail']) + ->getMock(); + } + + /** + * tearDown + * + * @return void + */ + public function tearDown() + { + unset($this->table, $this->Behavior); + parent::tearDown(); + } + + + /** + * Test resetToken + * + */ + public function testResetToken() + { + $user = $this->table->findAllByUsername('user-1')->first(); + $token = $user->token; + $this->Behavior->Email->expects($this->never()) + ->method('sendResetPasswordEmail') + ->with($user); + $result = $this->Behavior->resetToken('user-1', [ + 'expiration' => 3600, + 'checkActive' => true, + ]); + $this->assertNotEquals($token, $result->token); + $this->assertEmpty($result->activation_date); + $this->assertFalse($result->active); + } + + /** + * Test resetToken + * + */ + public function testResetTokenSendEmail() + { + $user = $this->table->findAllByUsername('user-1')->first(); + $token = $user->token; + $tokenExpires = $user->token_expires; + $this->Behavior->Email->expects($this->once()) + ->method('sendResetPasswordEmail'); + $result = $this->Behavior->resetToken('user-1', [ + 'expiration' => 3600, + 'checkActive' => true, + 'sendEmail' => true + ]); + $this->assertNotEquals($token, $result->token); + $this->assertNotEquals($tokenExpires, $result->token_expires); + $this->assertEmpty($result->activation_date); + $this->assertFalse($result->active); + } + + /** + * Test resetToken + * + * @expectedException InvalidArgumentException + */ + public function testResetTokenWithNullParams() + { + $this->Behavior->resetToken(null); + } + + /** + * Test resetToken + * + * @expectedException CakeDC\Users\Exception\UserNotFoundException + */ + public function testResetTokenNotExistingUser() + { + $this->Behavior->resetToken('user-not-found', [ + 'expiration' => 3600 + ]); + } + + /** + * Test resetToken + * + * @expectedException CakeDC\Users\Exception\UserAlreadyActiveException + */ + public function testResetTokenUserAlreadyActive() + { + $activeUser = TableRegistry::get('CakeDC/Users.Users')->findAllByUsername('user-4')->first(); + $this->assertTrue($activeUser->active); + $this->table = $this->getMockForModel('CakeDC/Users.Users', ['save']); + $this->table->expects($this->never()) + ->method('save'); + $this->Behavior->expects($this->never()) + ->method('sendResetPasswordEmail'); + $this->Behavior->resetToken('user-4', [ + 'expiration' => 3600, + 'checkActive' => true, + ]); + } +} diff --git a/tests/TestCase/Model/Behavior/RegisterBehaviorTest.php b/tests/TestCase/Model/Behavior/RegisterBehaviorTest.php new file mode 100755 index 000000000..e6d3657b3 --- /dev/null +++ b/tests/TestCase/Model/Behavior/RegisterBehaviorTest.php @@ -0,0 +1,267 @@ +addBehavior('CakeDC/Users/Register.Register'); + $this->Table = $table; + $this->Behavior = $table->behaviors()->Register; + Email::configTransport('test', [ + 'className' => 'Debug' + ]); + $this->Email = new Email(['from' => 'test@example.com', 'transport' => 'test']); + } + + /** + * tearDown + * + * @return void + */ + public function tearDown() + { + unset($this->Table, $this->Behavior); + Email::dropTransport('test'); + parent::tearDown(); + } + + /** + * Test register method + * + * @return void + */ + public function testValidateRegisterNoValidateEmail() + { + $user = [ + 'username' => 'testuser', + 'email' => 'testuser@test.com', + 'password' => 'password', + 'password_confirm' => 'password', + 'first_name' => 'test', + 'last_name' => 'user', + 'tos' => 1 + ]; + $result = $this->Table->register($this->Table->newEntity(), $user, ['token_expiration' => 3600, 'validate_email' => 0, 'email_class' => $this->Email]); + $this->assertTrue($result->active); + } + + /** + * Test register method + * + * @return void + */ + public function testValidateRegisterEmptyUser() + { + $user = []; + $result = $this->Table->register($this->Table->newEntity(), $user, ['token_expiration' => 3600, 'validate_email' => 1, 'email_class' => $this->Email]); + $this->assertFalse($result); + } + + /** + * Test register method + * + * @return void + */ + public function testValidateRegisterValidateEmailAndTos() + { + $user = [ + 'username' => 'testuser', + 'email' => 'testuser@test.com', + 'password' => 'password', + 'password_confirm' => 'password', + 'first_name' => 'test', + 'last_name' => 'user', + 'tos' => 1 + ]; + $result = $this->Table->register($this->Table->newEntity(), $user, ['token_expiration' => 3600, 'validate_email' => 1, 'email_class' => $this->Email]); + $this->assertNotEmpty($result); + $this->assertFalse($result->active); + $this->assertNotEmpty($result->tos_date); + } + + /** + * Test register method + * + * @return void + */ + public function testValidateRegisterValidatorOption() + { + $this->Table = $this->getMockForModel('CakeDC/Users.Users', ['validationCustom', 'patchEntity', 'errors', 'save']); + + $this->Behavior = $this->getMockBuilder('CakeDC\Users\Model\Behavior\RegisterBehavior') + ->setMethods(['getValidators', '_updateActive']) + ->setConstructorArgs([$this->Table]) + ->getMock(); + + $user = [ + 'username' => 'testuser', + 'email' => 'testuser@test.com', + 'password' => 'password', + 'password_confirm' => 'password', + 'first_name' => 'test', + 'last_name' => 'user', + 'tos' => 1 + ]; + + $this->Behavior->expects($this->never()) + ->method('getValidators'); + + $entityUser = $this->Table->newEntity($user); + + $this->Behavior->expects($this->once()) + ->method('_updateActive') + ->will($this->returnValue($entityUser)); + + $this->Table->expects($this->once()) + ->method('patchEntity') + ->with($this->Table->newEntity(), $user, ['validate' => 'custom']) + ->will($this->returnValue($entityUser)); + + $this->Table->expects($this->once()) + ->method('save') + ->with($entityUser) + ->will($this->returnValue($entityUser)); + + $result = $this->Behavior->register($this->Table->newEntity(), $user, ['validator' => 'custom', 'validate_email' => 1, 'email_class' => $this->Email]); + $this->assertNotEmpty($result->tos_date); + } + + /** + * Test register method + * + */ + public function testValidateRegisterTosRequired() + { + $user = [ + 'username' => 'testuser', + 'email' => 'testuser@test.com', + 'password' => 'password', + 'password_confirm' => 'password', + 'first_name' => 'test', + 'last_name' => 'user', + ]; + $result = $this->Table->register($this->Table->newEntity(), $user, ['token_expiration' => 3600, 'validate_email' => 1, 'use_tos' => 1, 'email_class' => $this->Email]); + $this->assertFalse($result); + } + + /** + * Test register method + * + * @return void + */ + public function testValidateRegisterNoTosRequired() + { + $user = [ + 'username' => 'testuser', + 'email' => 'testuser@test.com', + 'password' => 'password', + 'password_confirm' => 'password', + 'first_name' => 'test', + 'last_name' => 'user', + ]; + $result = $this->Table->register($this->Table->newEntity(), $user, ['token_expiration' => 3600, 'validate_email' => 1, 'use_tos' => 0, 'email_class' => $this->Email]); + $this->assertNotEmpty($result); + } + + /** + * Test ActivateUser method + * + * @return void + */ + public function testActivateUser() + { + $user = $this->Table->find()->where(['id' => '00000000-0000-0000-0000-000000000001'])->first(); + $result = $this->Table->activateUser($user); + $this->assertTrue($result->active); + } + + /** + * Test Validate method + * + * @return void + */ + public function testValidate() + { + $result = $this->Table->validate('ae93ddbe32664ce7927cf0c5c5a5e59d', 'activateUser'); + $this->assertTrue($result->active); + $this->assertEmpty($result->token_expires); + } + + /** + * Test Validate method + * + * @return void + * @expectedException CakeDC\Users\Exception\TokenExpiredException + */ + public function testValidateUserWithExpiredToken() + { + $this->Table->validate('token-5', 'activateUser'); + } + + /** + * Test Validate method + * + * @return void + * @expectedException CakeDC\Users\Exception\UserNotFoundException + */ + public function testValidateNotExistingUser() + { + $this->Table->validate('not-existing-token', 'activateUser'); + } + + /** + * Test activateUser method + * + * @return void + */ + public function testActiveUserRemoveValidationToken() + { + $user = $this->Table->find()->where(['id' => '00000000-0000-0000-0000-000000000001'])->first(); + $this->Behavior = $this->getMockBuilder('CakeDC\Users\Model\Behavior\RegisterBehavior') + ->setConstructorArgs([$this->Table]) + ->getMock(); + + $resultValidationToken = $user; + $resultValidationToken->token_expires = null; + $resultValidationToken->token = null; + + $this->Behavior->activateUser($user); + } +} diff --git a/tests/TestCase/Model/Behavior/SocialAccountBehaviorTest.php b/tests/TestCase/Model/Behavior/SocialAccountBehaviorTest.php new file mode 100755 index 000000000..8bfc5b5a5 --- /dev/null +++ b/tests/TestCase/Model/Behavior/SocialAccountBehaviorTest.php @@ -0,0 +1,141 @@ +Table = TableRegistry::get('CakeDC/Users.SocialAccounts'); + $this->Behavior = $this->Table->behaviors()->SocialAccount; + } + + /** + * tearDown + * + * @return void + */ + public function tearDown() + { + unset($this->Table, $this->Behavior, $this->Email); + parent::tearDown(); + } + + /** + * Test validateEmail method + * + * @return void + */ + public function testValidateEmail() + { + $token = 'token-1234'; + $result = $this->Behavior->validateAccount(SocialAccountsTable::PROVIDER_FACEBOOK, 'reference-1-1234', $token); + $this->assertTrue($result->active); + $this->assertEquals($token, $result->token); + } + + /** + * Test validateEmail method + * + * @expectedException \Cake\Datasource\Exception\RecordNotFoundException + */ + public function testValidateEmailInvalidToken() + { + $this->Behavior->validateAccount(1, 'reference-1234', 'invalid-token'); + } + + /** + * Test validateEmail method + * + * @expectedException \Cake\Datasource\Exception\RecordNotFoundException + */ + public function testValidateEmailInvalidUser() + { + $this->Behavior->validateAccount(1, 'invalid-user', 'token-1234'); + } + + /** + * Test validateEmail method + * + * @expectedException CakeDC\Users\Exception\AccountAlreadyActiveException + */ + public function testValidateEmailActiveAccount() + { + $this->Behavior->validateAccount(SocialAccountsTable::PROVIDER_TWITTER, 'reference-1-1234', 'token-1234'); + } + + /** + * testAfterSaveSocialNotActiveUserNotActive + * don't send email, user is not active + * + * @return void + */ + public function testAfterSaveSocialNotActiveUserNotActive() + { + $event = new Event('eventName'); + $entity = $this->Table->find()->first(); + $this->assertTrue($this->Behavior->afterSave($event, $entity, [])); + } + + /** + * testAfterSaveSocialActiveUserActive + * social account is active, don't send email + * + * @return void + */ + public function testAfterSaveSocialActiveUserActive() + { + $event = new Event('eventName'); + $entity = $this->Table->findById('00000000-0000-0000-0000-000000000003')->first(); + $this->assertTrue($this->Behavior->afterSave($event, $entity, [])); + } + + /** + * testAfterSaveSocialActiveUserNotActive + * social account is active, don't send email + * + * @return void + */ + public function testAfterSaveSocialActiveUserNotActive() + { + $event = new Event('eventName'); + $entity = $this->Table->findById('00000000-0000-0000-0000-000000000002')->first(); + $this->assertTrue($this->Behavior->afterSave($event, $entity, [])); + } +} diff --git a/tests/TestCase/Model/Behavior/SocialBehaviorTest.php b/tests/TestCase/Model/Behavior/SocialBehaviorTest.php new file mode 100755 index 000000000..33f700836 --- /dev/null +++ b/tests/TestCase/Model/Behavior/SocialBehaviorTest.php @@ -0,0 +1,356 @@ +Table = $this->getMockForModel('CakeDC/Users.Users', ['save']); + $this->Behavior = $this->getMockBuilder('CakeDC\Users\Model\Behavior\SocialBehavior') + ->setMethods(['randomString', '_updateActive', 'generateUniqueUsername']) + ->setConstructorArgs([$this->Table]) + ->getMock(); + } + + /** + * tearDown + * + * @return void + */ + public function tearDown() + { + unset($this->Table, $this->Behavior, $this->Email); + parent::tearDown(); + } + + /** + * Test socialLogin with facebook and not existing user + * + * @dataProvider providerFacebookSocialLogin + */ + public function testSocialLoginFacebookProvider($data, $options, $dataUser) + { + $user = $this->Table->newEntity($dataUser, ['associated' => ['SocialAccounts']]); + $user->password = '$2y$10$0QzszaIEpW1pYpoKJVf4DeqEAHtg9whiLTX/l3TcHAoOLF1bC9U.6'; + + $this->Behavior->expects($this->once()) + ->method('generateUniqueUsername') + ->with('email') + ->will($this->returnValue('username')); + + $this->Behavior->expects($this->once()) + ->method('randomString') + ->will($this->returnValue('password')); + + $this->Behavior->expects($this->once()) + ->method('_updateActive') + ->will($this->returnValue($user)); + + $this->Table->expects($this->once()) + ->method('save') + ->with($user) + ->will($this->returnValue($user)); + + $result = $this->Behavior->socialLogin($data, $options); + $this->assertEquals($result, $user); + } + + /** + * Provider for socialLogin with facebook and not existing user + * + */ + public function providerFacebookSocialLogin() + { + return [ + 'provider' => [ + 'data' => [ + 'id' => 'facebook-id', + 'username' => null, + 'full_name' => 'Full name', + 'first_name' => 'First name', + 'last_name' => 'Last name', + 'email' => 'email@example.com', + 'raw' => [ + 'id' => '10153521527396318', + 'name' => 'Ful Name.', + 'first_name' => 'First Name', + 'last_name' => 'Last name', + 'email' => 'email@example.com', + 'picture' => [ + 'data' => [ + 'url' => 'data-url' + ] + ] + ], + 'credentials' => [ + 'token' => 'token', + 'secret' => null, + 'expires' => 1458423682 + ], + 'validated' => true, + 'link' => 'facebook-link', + 'provider' => 'Facebook' + ], + 'options' => [ + 'use_email' => true, + 'validate_email' => true, + 'token_expiration' => 3600 + ], + 'result' => [ + 'first_name' => 'First name', + 'last_name' => 'Last name', + 'username' => 'username', + 'email' => 'email@example.com', + 'password' => '$2y$10$oLPxCkKJ1TUCR6xJ1t0Wj.7Fznx49Wn4NZB2aJCmVvRMucaHuNyyO', + 'avatar' => null, + 'tos_date' => '2016-01-20 15:45:09', + 'gender' => null, + 'social_accounts' => [ + [ + 'provider' => 'Facebook', + 'username' => null, + 'reference' => '10153521527396318', + 'avatar' => '', + 'link' => 'facebook-link', + 'description' => null, + 'token' => 'token', + 'token_secret' => null, + 'token_expires' => '2016-03-19 21:41:22', + 'data' => '-', + 'active' => true + ] + ], + 'activation_date' => '2016-01-20 15:45:09', + 'active' => true, + ] + ] + + ]; + } + + /** + * Test socialLogin with facebook with existing and active user + * + * @dataProvider providerFacebookSocialLoginExistingReference + */ + public function testSocialLoginExistingReference($data, $options) + { + $this->Behavior->expects($this->never()) + ->method('generateUniqueUsername'); + + $this->Behavior->expects($this->never()) + ->method('randomString'); + + $this->Behavior->expects($this->never()) + ->method('_updateActive'); + + $result = $this->Behavior->socialLogin($data, $options); + $this->assertEquals($result->id, '00000000-0000-0000-0000-000000000002'); + $this->assertTrue($result->active); + } + + /** + * Provider for socialLogin with facebook with existing and active user + * + */ + public function providerFacebookSocialLoginExistingReference() + { + return [ + 'provider' => [ + 'data' => [ + 'id' => 'reference-2-1', + 'provider' => 'Facebook' + ], + 'options' => [ + 'use_email' => true, + 'validate_email' => true, + 'token_expiration' => 3600 + ], + ] + + ]; + } + + /** + * Test socialLogin with existing and active user and not active social account + * + * @expectedException CakeDC\Users\Exception\AccountNotActiveException + * @dataProvider providerSocialLoginExistingAndNotActiveAccount + */ + public function testSocialLoginExistingNotActiveReference($data, $options) + { + $this->Behavior->expects($this->never()) + ->method('generateUniqueUsername'); + + $this->Behavior->expects($this->never()) + ->method('randomString'); + + $this->Behavior->expects($this->never()) + ->method('_updateActive'); + $this->Behavior->socialLogin($data, $options); + } + + /** + * Provider for socialLogin with existing and active user and not active social account + * + */ + public function providerSocialLoginExistingAndNotActiveAccount() + { + return [ + 'provider' => [ + 'data' => [ + 'id' => 'reference-1-1234', + 'provider' => 'Facebook' + ], + 'options' => [ + 'use_email' => true, + 'validate_email' => true, + 'token_expiration' => 3600 + ], + ] + + ]; + } + + /** + * Test socialLogin with existing and active account but not active user + * + * @expectedException CakeDC\Users\Exception\UserNotActiveException + * @dataProvider providerSocialLoginExistingAccountNotActiveUser + */ + public function testSocialLoginExistingReferenceNotActiveUser($data, $options) + { + $this->Behavior->expects($this->never()) + ->method('generateUniqueUsername'); + + $this->Behavior->expects($this->never()) + ->method('randomString'); + + $this->Behavior->expects($this->never()) + ->method('_updateActive'); + $this->Behavior->socialLogin($data, $options); + } + + /** + * Provider for socialLogin with existing and active account but not active user + * + */ + public function providerSocialLoginExistingAccountNotActiveUser() + { + return [ + 'provider' => [ + 'data' => [ + 'id' => 'reference-1-1234', + 'provider' => 'Twitter' + ], + 'options' => [ + 'use_email' => true, + 'validate_email' => true, + 'token_expiration' => 3600 + ], + ] + + ]; + } + + /** + * Test socialLogin with facebook and not existing user + * + * @dataProvider providerFacebookSocialLoginNoEmail + * @expectedException CakeDC\Users\Exception\MissingEmailException + */ + public function testSocialLoginNoEmail($data, $options) + { + $this->Behavior->socialLogin($data, $options); + } + + + /** + * Provider for socialLogin with facebook and not existing user + * + */ + public function providerFacebookSocialLoginNoEmail() + { + return [ + 'provider' => [ + 'data' => [ + 'id' => 'facebook-id', + 'username' => null, + 'full_name' => 'Full name', + 'first_name' => 'First name', + 'last_name' => 'Last name', + 'validated' => true, + 'link' => 'facebook-link', + 'provider' => 'Facebook' + ], + 'options' => [ + 'use_email' => true, + 'validate_email' => true, + 'token_expiration' => 3600 + ], + ] + + ]; + } + + /** + * Test socialLogin with facebook and not existing user + * + * @dataProvider providerGenerateUsername + */ + public function testGenerateUniqueUsername($param, $expected) + { + $this->Behavior = $this->getMockBuilder('CakeDC\Users\Model\Behavior\SocialBehavior') + ->setMethods(['randomString', '_updateActive']) + ->setConstructorArgs([$this->Table]) + ->getMock(); + + $result = $this->Behavior->generateUniqueUsername($param); + $this->assertEquals($expected, $result); + } + + /** + * Provider for socialLogin with facebook and not existing user + * + */ + public function providerGenerateUsername() + { + return [ + ['username', 'username'], + ['user-1', 'user-10'], + ['user-5', 'user-50'] + + ]; + } +} diff --git a/tests/TestCase/Model/Entity/UserTest.php b/tests/TestCase/Model/Entity/UserTest.php new file mode 100755 index 000000000..2788aa8ad --- /dev/null +++ b/tests/TestCase/Model/Entity/UserTest.php @@ -0,0 +1,104 @@ +User = new User(); + } + + /** + * tearDown method + * + * @return void + */ + public function tearDown() + { + unset($this->User); + + parent::tearDown(); + } + + /** + * Test tokenExpired method + * + * @return void + */ + public function testTokenExpiredEmpty() + { + $this->User->token_expires = null; + $isExpired = $this->User->tokenExpired(); + $this->assertTrue($isExpired); + } + + /** + * Test tokenExpired method + * + * @return void + */ + public function testTokenExpiredNotYet() + { + $this->User->token_expires = '-1 day'; + $isExpired = $this->User->tokenExpired(); + $this->assertTrue($isExpired); + } + + /** + * Test tokenExpired method + * + * @return void + */ + public function testTokenExpired() + { + $this->User->token_expires = '+1 day'; + $isExpired = $this->User->tokenExpired(); + $this->assertFalse($isExpired); + } + + public function testPasswordsAreEncrypted() + { + $pw = 'password'; + $this->User->password = $pw; + $this->assertTrue((new DefaultPasswordHasher)->check($pw, $this->User->password)); + } + + public function testConfirmPasswordsAreEncrypted() + { + $pw = 'password'; + $this->User->confirm_password = $pw; + $this->assertTrue((new DefaultPasswordHasher)->check($pw, $this->User->confirm_password)); + } + + public function testCheckPassword() + { + $pw = 'password'; + $this->assertTrue($this->User->checkPassword($pw, (new DefaultPasswordHasher)->hash($pw))); + $this->assertFalse($this->User->checkPassword($pw, 'fail')); + } + + public function testGetAvatar() + { + $this->assertNull($this->User->avatar); + $avatar = 'first-avatar'; + $this->User->social_accounts = [ + ['avatar' => 'first-avatar'], + ['avatar' => 'second-avatar'] + ]; + $this->assertSame($avatar, $this->User->avatar); + } +} diff --git a/tests/TestCase/Model/Table/SocialAccountsTableTest.php b/tests/TestCase/Model/Table/SocialAccountsTableTest.php new file mode 100755 index 000000000..e57156cec --- /dev/null +++ b/tests/TestCase/Model/Table/SocialAccountsTableTest.php @@ -0,0 +1,74 @@ +SocialAccounts = TableRegistry::get('CakeDC/Users.SocialAccounts'); + } + + /** + * tearDown method + * + * @return void + */ + public function tearDown() + { + unset($this->SocialAccounts); + + parent::tearDown(); + } + + public function testValidationHappy() + { + $data = [ + 'provider' => 'Facebook', + 'reference' => 'test-reference', + 'link' => 'test-link', + 'token' => 'test-token', + 'active' => 0, + 'data' => 'test-data', + ]; + $entity = $this->SocialAccounts->newEntity($data); + $this->assertEmpty($entity->errors()); + } +} diff --git a/tests/TestCase/Model/Table/UsersTableTest.php b/tests/TestCase/Model/Table/UsersTableTest.php new file mode 100755 index 000000000..f9522fd60 --- /dev/null +++ b/tests/TestCase/Model/Table/UsersTableTest.php @@ -0,0 +1,314 @@ +Users = TableRegistry::get('CakeDC/Users.Users'); + $this->fullBaseBackup = Router::fullBaseUrl(); + Router::fullBaseUrl('http://users.test'); + Email::configTransport('test', [ + 'className' => 'Debug' + ]); + $this->configEmail = Email::config('default'); + Email::config('default', [ + 'transport' => 'test', + 'from' => 'cakedc@example.com' + ]); + $this->Email = new Email(['from' => 'test@example.com', 'transport' => 'test']); + Plugin::routes('CakeDC/Users'); + } + + /** + * tearDown method + * + * @return void + */ + public function tearDown() + { + unset($this->Users); + Router::fullBaseUrl($this->fullBaseBackup); + Email::drop('default'); + Email::dropTransport('test'); + Email::config('default', $this->configEmail); + + parent::tearDown(); + } + + /** + * Test register method + * + * @return void + */ + public function testValidateRegisterNoValidateEmail() + { + $user = [ + 'username' => 'testuser', + 'email' => 'testuser@test.com', + 'password' => 'password', + 'password_confirm' => 'password', + 'first_name' => 'test', + 'last_name' => 'user', + 'tos' => 1 + ]; + $result = $this->Users->register($this->Users->newEntity(), $user, ['token_expiration' => 3600, 'validate_email' => 0]); + $this->assertTrue($result->active); + } + + /** + * Test register method + * + * @return void + */ + public function testValidateRegisterEmptyUser() + { + $user = []; + $result = $this->Users->register($this->Users->newEntity(), $user, ['token_expiration' => 3600, 'validate_email' => 1]); + $this->assertFalse($result); + } + + /** + * Test register method + * + * @return void + */ + public function testValidateRegisterValidateEmail() + { + $user = [ + 'username' => 'testuser', + 'email' => 'testuser@test.com', + 'password' => 'password', + 'password_confirm' => 'password', + 'first_name' => 'test', + 'last_name' => 'user', + 'tos' => 1 + ]; + $result = $this->Users->register($this->Users->newEntity(), $user, ['token_expiration' => 3600, 'validate_email' => 1]); + $this->assertNotEmpty($result); + $this->assertFalse($result->active); + } + + /** + * test + */ + public function testValidateRegisterTosRequired() + { + $user = [ + 'username' => 'testuser', + 'email' => 'testuser@test.com', + 'password' => 'password', + 'password_confirm' => 'password', + 'first_name' => 'test', + 'last_name' => 'user', + ]; + $userEntity = $this->Users->newEntity(); + $this->Users->register($userEntity, $user, ['token_expiration' => 3600, 'validate_email' => 1, 'use_tos' => 1]); + $this->assertEquals(['tos' => ['_required' => 'This field is required']], $userEntity->errors()); + } + + /** + * Test register method + testValidateRegisterValidateEmail */ + public function testValidateRegisterNoTosRequired() + { + $user = [ + 'username' => 'testuser', + 'email' => 'testuser@test.com', + 'password' => 'password', + 'password_confirm' => 'password', + 'first_name' => 'test', + 'last_name' => 'user', + ]; + $result = $this->Users->register($this->Users->newEntity(), $user, ['token_expiration' => 3600, 'validate_email' => 1, 'use_tos' => 0]); + $this->assertNotEmpty($result); + } + + /** + * Test ActivateUser method + * + * @return void + */ + public function testActivateUser() + { + $user = $this->Users->find()->where(['id' => '00000000-0000-0000-0000-000000000001'])->first(); + $result = $this->Users->activateUser($user); + $this->assertTrue($result->active); + } + + public function testSocialLogin() + { + $data = [ + 'provider' => SocialAccountsTable::PROVIDER_FACEBOOK, + 'email' => 'user-2@test.com', + 'id' => 'reference-2-1', + 'link' => 'link', + 'raw' => [ + 'id' => 'reference-2-1', + 'token' => 'token', + 'first_name' => 'User 2', + 'gender' => 'female', + 'verified' => 1, + 'user_email' => 'user-2@test.com', + 'link' => 'link' + ] + ]; + $options = [ + 'use_email' => 1, + 'validate_email' => 1, + 'token_expiration' => 3600 + ]; + $result = $this->Users->socialLogin($data, $options); + $this->assertEquals('user-2@test.com', $result->email); + $this->assertTrue($result->active); + } + + /** + * Test socialLogin + * + * @expectedException CakeDC\Users\Exception\AccountNotActiveException + */ + public function testSocialLoginInactiveAccount() + { + $data = [ + 'provider' => SocialAccountsTable::PROVIDER_TWITTER, + 'email' => 'hello@test.com', + 'id' => 'reference-2-2', + 'link' => 'link', + 'raw' => [ + 'id' => 'reference-2-2', + 'first_name' => 'User 2', + 'gender' => 'female', + 'verified' => 1, + 'user_email' => 'hello@test.com', + ] + ]; + $options = [ + 'use_email' => 1, + 'validate_email' => 1, + 'token_expiration' => 3600 + ]; + $result = $this->Users->socialLogin($data, $options); + $this->assertEquals('user-2@test.com', $result->email); + $this->assertFalse($result->active); + } + + /** + * Test socialLogin + * + * @expectedException InvalidArgumentException + */ + public function testSocialLoginCreateNewAccountWithNoCredentials() + { + $data = [ + 'provider' => SocialAccountsTable::PROVIDER_TWITTER, + 'email' => 'user@test.com', + 'id' => 'reference-not-existing', + 'link' => 'link', + 'raw' => [ + 'id' => 'reference-not-existing', + 'first_name' => 'Not existing user', + 'gender' => 'male', + 'user_email' => 'user@test.com', + ], + 'credentials' => [], + 'name' => '', + ]; + + $options = [ + 'use_email' => 0, + 'validate_email' => 1, + 'token_expiration' => 3600 + ]; + $result = $this->Users->socialLogin($data, $options); + $this->assertFalse($result); + } + + /** + * Test socialLogin + * + */ + public function testSocialLoginCreateNewAccount() + { + $data = [ + 'provider' => SocialAccountsTable::PROVIDER_TWITTER, + 'email' => 'username@test.com', + 'id' => 'no-existing-reference', + 'link' => 'link', + 'first_name' => 'First Name', + 'last_name' => 'Last Name', + 'raw' => [ + 'id' => 'no-existing-reference', + 'first_name' => 'First Name', + 'last_name' => 'Last Name', + 'gender' => 'male', + 'user_email' => 'user@test.com', + 'twitter' => 'link' + ], + 'info' => [ + 'first_name' => 'First Name', + 'last_name' => 'Last Name', + 'urls' => ['twitter' => 'twitter'] + ], + 'validated' => true, + 'credentials' => [ + 'token' => 'token', + 'token_secret' => 'secret', + 'token_expires' => '' + ], + ]; + + $options = [ + 'use_email' => 0, + 'validate_email' => 0, + 'token_expiration' => 3600 + ]; + $result = $this->Users->socialLogin($data, $options); + $this->assertNotEmpty($result); + $this->assertEquals('no-existing-reference', $result->social_accounts[0]->reference); + $this->assertEquals(1, count($result->social_accounts)); + $this->assertEquals('username', $result->username); + $this->assertEquals('First Name', $result->first_name); + $this->assertEquals('Last Name', $result->last_name); + } +} diff --git a/tests/TestCase/Shell/UsersShellTest.php b/tests/TestCase/Shell/UsersShellTest.php new file mode 100755 index 000000000..e3c4311d7 --- /dev/null +++ b/tests/TestCase/Shell/UsersShellTest.php @@ -0,0 +1,309 @@ +out = new ConsoleOutput(); + $this->io = $this->getMock('Cake\Console\ConsoleIo'); + $this->Users = TableRegistry::get('CakeDC/Users.Users'); + + $this->Shell = $this->getMockBuilder('CakeDC\Users\Shell\UsersShell') + ->setMethods(['in', 'out', '_stop', 'clear', '_usernameSeed', '_generateRandomPassword', + '_generateRandomUsername', '_generatedHashedPassword', 'error', '_updateUser']) + ->setConstructorArgs([$this->io]) + ->getMock(); + + $this->Shell->Users = $this->getMockBuilder('CakeDC\Users\Model\UsersTable') + ->setMethods(['generateUniqueUsername', 'newEntity', 'save', 'updateAll']) + ->getMock(); + + $this->Shell->Command = $this->getMock( + 'Cake\Shell\Task\CommandTask', + ['in', '_stop', 'clear', 'out'], + [$this->io] + ); + } + + /** + * Tear Down + * + * @return void + */ + public function tearDown() + { + parent::tearDown(); + unset($this->Shell); + } + + /** + * Add user test + * Adding user with username, email and password + * + * @return void + */ + public function testAddUser() + { + $user = [ + 'username' => 'yeliparra', + 'password' => '123', + 'email' => 'yeli.parra@gmail.com', + 'active' => 1, + ]; + + $this->Shell->expects($this->never()) + ->method('_generateRandomUsername'); + + $this->Shell->expects($this->never()) + ->method('_generateRandomPassword'); + + $this->Shell->Users->expects($this->once()) + ->method('generateUniqueUsername') + ->with($user['username']) + ->will($this->returnValue($user['username'])); + + $entityUser = $this->Users->newEntity($user); + + $this->Shell->Users->expects($this->once()) + ->method('newEntity') + ->with($user) + ->will($this->returnValue($entityUser)); + + $userSaved = $entityUser; + $userSaved->id = 'my-id'; + + $this->Shell->Users->expects($this->once()) + ->method('save') + ->with($entityUser) + ->will($this->returnValue($userSaved)); + + $this->Shell->runCommand(['addUser', '--username=' . $user['username'], '--password=' . $user['password'], '--email=' . $user['email']]); + } + + /** + * Add user test + * Adding user passing no params + * + * @return void + */ + public function testAddUserWithNoParams() + { + $user = [ + 'username' => 'anakin', + 'password' => 'mypassword', + 'email' => 'anakin@example.com', + 'active' => 1, + ]; + + $this->Shell->Users->expects($this->once()) + ->method('generateUniqueUsername') + ->with($user['username']) + ->will($this->returnValue($user['username'])); + + $this->Shell->expects($this->once()) + ->method('_generateRandomPassword') + ->will($this->returnValue($user['password'])); + + $this->Shell->expects($this->once()) + ->method('_generateRandomUsername') + ->will($this->returnValue($user['username'])); + + $entityUser = $this->Users->newEntity($user); + + $this->Shell->Users->expects($this->once()) + ->method('newEntity') + ->with($user) + ->will($this->returnValue($entityUser)); + + $userSaved = $entityUser; + $userSaved->id = 'my-id'; + + $this->Shell->Users->expects($this->once()) + ->method('save') + ->with($entityUser) + ->will($this->returnValue($userSaved)); + + //TODO: Add assertions with 'out' + + $this->Shell->runCommand(['addUser']); + } + + /** + * Add superadmin user + * + * @return void + */ + public function testAddSuperuser() + { + $this->Shell->Users->expects($this->once()) + ->method('generateUniqueUsername') + ->with('superadmin') + ->will($this->returnValue('superadmin')); + + $this->Shell->expects($this->once()) + ->method('_generateRandomPassword') + ->will($this->returnValue('password')); + + $user = [ + 'username' => 'superadmin', + 'password' => 'password', + 'email' => 'superadmin@example.com', + 'active' => 1, + ]; + $entityUser = $this->Users->newEntity($user); + + $this->Shell->Users->expects($this->once()) + ->method('newEntity') + ->with($user) + ->will($this->returnValue($entityUser)); + + $userSaved = $entityUser; + $userSaved->id = 'my-id'; + $userSaved->is_superuser = true; + $userSaved->role = 'superuser'; + + $this->Shell->Users->expects($this->once()) + ->method('save') + ->with($entityUser) + ->will($this->returnValue($userSaved)); + + $this->Shell->runCommand(['addSuperuser']); + } + + /** + * Reset all passwords + * + * @return void + */ + public function testResetAllPasswords() + { + $this->Shell->expects($this->once()) + ->method('_generatedHashedPassword') + ->will($this->returnValue('hashedPasssword')); + + $this->Shell->Users->expects($this->once()) + ->method('updateAll') + ->with(['password' => 'hashedPasssword'], ['id IS NOT NULL']); + + $this->Shell->runCommand(['resetAllPasswords', '123']); + } + + /** + * Reset all passwords + * + * @return void + */ + public function testResetAllPasswordsNoPassingParams() + { + $this->Shell->expects($this->once()) + ->method('error') + ->with('Please enter a password.'); + + $this->Shell->runCommand(['resetAllPasswords']); + } + + /** + * Reset password + * + * @return void + */ + public function testResetPassword() + { + $user = $this->Users->newEntity(); + $user->username = 'user-1'; + $user->password = 'password'; + + $this->Shell->expects($this->once()) + ->method('_updateUser') + ->will($this->returnValue($user)); + + $this->Shell->runCommand(['resetPassword', 'user-1', 'password']); + } + + /** + * Change role + * + * @return void + */ + public function testChangeRole() + { + $this->Shell = new UsersShell($this->io); + $this->Shell->Users = $this->Users; + $user = $this->Users->get('00000000-0000-0000-0000-000000000001'); + $this->assertSame('admin', $user['role']); + $this->Shell->runCommand(['changeRole', 'user-1', 'another-role']); + $user = $this->Users->get('00000000-0000-0000-0000-000000000001'); + $this->assertSame('another-role', $user['role']); + } + + /** + * Activate user + * + * @return void + */ + public function testActivateUser() + { + $this->Shell = new UsersShell($this->io); + $this->Shell->Users = $this->Users; + $user = $this->Users->get('00000000-0000-0000-0000-000000000001'); + $this->assertFalse($user['active']); + $this->Shell->runCommand(['activateUser', 'user-1']); + $user = $this->Users->get('00000000-0000-0000-0000-000000000001'); + $this->assertTrue($user['active']); + } + + /** + * Delete user + * + * @return void + * @expected + */ + public function testDeleteUser() + { + $this->Shell = new UsersShell($this->io); + $this->Shell->Users = $this->Users; + + $this->assertNotEmpty($this->Users->findById('00000000-0000-0000-0000-000000000001')->first()); + $this->assertNotEmpty($this->Users->SocialAccounts->findByUserId('00000000-0000-0000-0000-000000000001')->toArray()); + $this->Shell->runCommand(['deleteUser', 'user-1']); + $this->assertEmpty($this->Users->findById('00000000-0000-0000-0000-000000000001')->first()); + $this->assertEmpty($this->Users->SocialAccounts->findByUserId('00000000-0000-0000-0000-000000000001')->toArray()); + + $this->assertNotEmpty($this->Users->findById('00000000-0000-0000-0000-000000000005')->first()); + $this->Shell->runCommand(['deleteUser', 'user-5']); + $this->assertEmpty($this->Users->findById('00000000-0000-0000-0000-000000000005')->first()); + } +} diff --git a/tests/TestCase/Traits/RandomStringTraitTest.php b/tests/TestCase/Traits/RandomStringTraitTest.php new file mode 100755 index 000000000..58324c67b --- /dev/null +++ b/tests/TestCase/Traits/RandomStringTraitTest.php @@ -0,0 +1,43 @@ +Trait = $this->getMockForTrait('CakeDC\Users\Traits\RandomStringTrait'); + } + + public function tearDown() + { + parent::tearDown(); + } + + public function testRandomString() + { + $result = $this->Trait->randomString(); + $this->assertEquals(10, strlen($result)); + + $result = $this->Trait->randomString(30); + $this->assertEquals(30, strlen($result)); + + $result = $this->Trait->randomString('-300'); + $this->assertEquals(10, strlen($result)); + + $result = $this->Trait->randomString('text'); + $this->assertEquals(10, strlen($result)); + } +} diff --git a/tests/TestCase/View/Helper/UserHelperTest.php b/tests/TestCase/View/Helper/UserHelperTest.php new file mode 100755 index 000000000..6da81b94f --- /dev/null +++ b/tests/TestCase/View/Helper/UserHelperTest.php @@ -0,0 +1,254 @@ +View = $this->getMock('Cake\View\View', ['append']); + $this->User = new UserHelper($this->View); + $this->request = new Request(); + } + + /** + * tearDown method + * + * @return void + */ + public function tearDown() + { + unset($this->User); + + parent::tearDown(); + } + + /** + * Test twitterLogin + * + * @return void + */ + public function testLogout() + { + $result = $this->User->logout(); + $expected = 'Logout'; + $this->assertEquals($expected, $result); + } + + /** + * Test twitterLogin + * + * @return void + */ + public function testLogoutDifferentMessage() + { + $result = $this->User->logout('Sign Out'); + $expected = 'Sign Out'; + $this->assertEquals($expected, $result); + } + + /** + * Test twitterLogin + * + * @return void + */ + public function testLogoutWithOptions() + { + $result = $this->User->logout('Sign Out', ['class' => 'logout']); + $expected = 'Sign Out'; + $this->assertEquals($expected, $result); + } + + /** + * Test link + * + * @return void + */ + public function testLinkFalse() + { + $link = $this->User->link('title', ['controller' => 'noaccess']); + $this->assertSame(false, $link); + } + + /** + * Test link + * + * @return void + */ + public function testLinkAuthorized() + { + $view = new View(); + $eventManagerMock = $this->getMockBuilder('Cake\Event\EventManager') + ->setMethods(['dispatch']) + ->getMock(); + $view->eventManager($eventManagerMock); + $this->User = new UserHelper($view); + $result = new Event('dispatch-result'); + $result->result = true; + $eventManagerMock->expects($this->once()) + ->method('dispatch') + ->will($this->returnValue($result)); + + $link = $this->User->link('title', '/', ['before' => 'before_', 'after' => '_after', 'class' => 'link-class']); + $this->assertSame('before_title_after', $link); + } + + + /** + * Test isAuthorized + * + * @return void + */ + public function testIsAuthorized() + { + $view = new View(); + $eventManagerMock = $this->getMockBuilder('Cake\Event\EventManager') + ->setMethods(['dispatch']) + ->getMock(); + $view->eventManager($eventManagerMock); + $this->User = new UserHelper($view); + $result = new Event('dispatch-result'); + $result->result = true; + $eventManagerMock->expects($this->once()) + ->method('dispatch') + ->will($this->returnValue($result)); + + $result = $this->User->isAuthorized(['controller' => 'MyController', 'action' => 'myAction']); + $this->assertTrue($result); + } + + + + /** + * Test link + * + * @return void + */ + public function testWelcome() + { + $session = $this->getMock('Cake\Network\Session', ['read']); + $session->expects($this->at(0)) + ->method('read') + ->with('Auth.User.id') + ->will($this->returnValue(2)); + + $session->expects($this->at(1)) + ->method('read') + ->with('Auth.User.first_name') + ->will($this->returnValue('david')); + + $this->User->request = $this->getMock('Cake\Network\Request', ['session']); + $this->User->request->expects($this->any()) + ->method('session') + ->will($this->returnValue($session)); + + $expected = 'Welcome, david'; + $result = $this->User->welcome(); + $this->assertEquals($expected, $result); + } + + /** + * Test link + * + * @return void + */ + public function testWelcomeNotLoggedInUser() + { + $session = $this->getMock('Cake\Network\Session', ['read']); + $session->expects($this->at(0)) + ->method('read') + ->with('Auth.User.id') + ->will($this->returnValue(null)); + + $this->User->request = $this->getMock('Cake\Network\Request', ['session']); + $this->User->request->expects($this->any()) + ->method('session') + ->will($this->returnValue($session)); + + $result = $this->User->welcome(); + $this->assertEmpty($result); + } + + /** + * Test add ReCaptcha field + * + * @return void + */ + public function testAddReCaptcha() + { + $siteKey = Configure::read('reCaptcha.key'); + Configure::write('reCaptcha.key', 'testKey'); + $result = $this->User->addReCaptcha(); + $this->assertEquals('
', $result); + Configure::write('reCaptcha.key', $siteKey); + } + + /** + * Test add ReCaptcha field + * + * @return void + */ + public function testAddReCaptchaEmpty() + { + Configure::write('Users.Registration.reCaptcha', false); + $result = $this->User->addReCaptcha(); + $this->assertFalse($result); + } + + /** + * Test add ReCaptcha field + * + * @return void + */ + public function testAddReCaptchaScript() + { + $this->View->expects($this->at(0)) + ->method('append') + ->with('script', $this->stringContains('https://www.google.com/recaptcha/api.js')); + $this->User->addReCaptchaScript(); + } + + /** + * Test social login link + * + * @return void + */ + public function testSocialLoginLink() + { + $result = $this->User->socialLogin('facebook'); + $this->assertEquals('Sign in with Facebook', $result); + + $result = $this->User->socialLogin('twitter', ['label' => 'Register with']); + $this->assertEquals('Register with Twitter', $result); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100755 index 000000000..e94f7f148 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,103 @@ + 'Users\Test\App']); + +Cake\Core\Configure::write('debug', true); +Cake\Core\Configure::write('App.encoding', 'UTF-8'); + +$TMP = new \Cake\Filesystem\Folder(TMP); +$TMP->create(TMP . 'cache/models', 0777); +$TMP->create(TMP . 'cache/persistent', 0777); +$TMP->create(TMP . 'cache/views', 0777); + +$cache = [ + 'default' => [ + 'engine' => 'File', + ], + '_cake_core_' => [ + 'className' => 'File', + 'prefix' => 'users_myapp_cake_core_', + 'path' => CACHE . 'persistent/', + 'serialize' => true, + 'duration' => '+10 seconds', + ], + '_cake_model_' => [ + 'className' => 'File', + 'prefix' => 'users_app_cake_model_', + 'path' => CACHE . 'models/', + 'serialize' => 'File', + 'duration' => '+10 seconds', + ], +]; + +Cake\Cache\Cache::config($cache); +Cake\Core\Configure::write('Session', [ + 'defaults' => 'php' +]); + +//init router +\Cake\Routing\Router::reload(); + +\Cake\Core\Plugin::load('CakeDC/Users', [ + 'path' => dirname(dirname(__FILE__)) . DS, + 'routes' => true +]); +if (file_exists($root . '/config/bootstrap.php')) { + require $root . '/config/bootstrap.php'; +} + +Cake\Routing\DispatcherFactory::add('Routing'); +Cake\Routing\DispatcherFactory::add('ControllerFactory'); + + +class_alias('CakeDC\Users\Test\App\Controller\AppController', 'App\Controller\AppController'); + +// Ensure default test connection is defined +if (!getenv('db_dsn')) { + putenv('db_dsn=sqlite:///:memory:'); +} + +Cake\Datasource\ConnectionManager::config('test', [ + 'url' => getenv('db_dsn'), + 'timezone' => 'UTC' +]); diff --git a/tests/config/routes.php b/tests/config/routes.php new file mode 100755 index 000000000..1f8d915d6 --- /dev/null +++ b/tests/config/routes.php @@ -0,0 +1,35 @@ + '/users'], function ($routes) { + $routes->fallbacks('DashedRoute'); +}); + +$oauthPath = Configure::read('Opauth.path'); +if (is_array($oauthPath)) { + Router::scope('/auth', function ($routes) use ($oauthPath) { + $routes->connect( + '/*', + $oauthPath + ); + }); +} +Router::connect('/accounts/validate/*', [ + 'plugin' => 'CakeDC/Users', + 'controller' => 'SocialAccounts', + 'action' => 'validate' +]); +Router::connect('/profile/*', ['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'profile']); +Router::connect('/login', ['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'login']); +Router::connect('/logout', ['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'logout']); diff --git a/webroot/empty b/webroot/empty new file mode 100755 index 000000000..e69de29bb diff --git a/webroot/img/avatar_placeholder.png b/webroot/img/avatar_placeholder.png new file mode 100755 index 000000000..5a29e1a2d Binary files /dev/null and b/webroot/img/avatar_placeholder.png differ