diff --git a/CHANGELOG-1.0.md b/CHANGELOG-1.0.md index 861f62eb8..1c0be30a8 100644 --- a/CHANGELOG-1.0.md +++ b/CHANGELOG-1.0.md @@ -3,6 +3,16 @@ CHANGELOG for 1.0.x This changelog references any relevant changes introduced in 1.0 minor versions. +* 1.0.3 (2019-10-23) + * **Issue #230:** Custom field privilege issue + * **Issue #29:** File attachment limit exceed + * **Issue #234:** Agent profile issue while thread added at customer panel + * **Issue #240:** Super admin name is not showing when set via terminal + * **Misc. Updates:** + * Added patch to support previously configured workflows with deprecated events + * Both agents and customers now share a common password reset page (events agent.forgot_password & customer.forgot_password deprecated) + * Updated README.md with link to the official gitter chat for uvdesk/core-framework + * 1.0.1 (2019-10-15) * **Issue #223:** Custom field privilege issue * **Issue #224:** Email template privilege issue diff --git a/Controller/Authentication.php b/Controller/Authentication.php index 40fbd91ae..ca70eccd5 100644 --- a/Controller/Authentication.php +++ b/Controller/Authentication.php @@ -3,12 +3,14 @@ namespace Webkul\UVDesk\CoreFrameworkBundle\Controller; use Symfony\Component\Form\FormError; -use Webkul\UVDesk\CoreFrameworkBundle\Entity\User; use Symfony\Component\HttpFoundation\Request; -use Webkul\UVDesk\CoreFrameworkBundle\Utils\TokenGenerator; +use Symfony\Component\HttpFoundation\Response; +use Webkul\UVDesk\CoreFrameworkBundle\Entity\User; use Symfony\Component\EventDispatcher\GenericEvent; -use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\Form\Extension\Core\Type\EmailType; +use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Webkul\UVDesk\CoreFrameworkBundle\Utils\TokenGenerator; +use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; use Webkul\UVDesk\CoreFrameworkBundle\Workflow\Events as CoreWorkflowEvents; class Authentication extends Controller @@ -32,82 +34,71 @@ public function logout(Request $request) public function forgotPassword(Request $request) { - if (null == $this->get('user.service')->getSessionUser()) { - $entityManager = $this->getDoctrine()->getManager(); + if (null != $this->get('user.service')->getSessionUser()) { + return new Response('How did you land here? :/', 404); + } + + $entityManager = $this->getDoctrine()->getManager(); - if ($request->getMethod() == 'POST') { - $user = new User(); - $form = $this->createFormBuilder($user,['csrf_protection' => false]) - ->add('email',EmailType::class) - ->getForm(); + if ($request->getMethod() == 'POST') { + $user = new User(); + $form = $this->createFormBuilder($user,['csrf_protection' => false]) + ->add('email',EmailType::class) + ->getForm(); - $form->submit(['email' => $request->request->get('forgot_password_form')['email']]); - $form->handleRequest($request); - - if ($form->isValid()) { - $repository = $this->getDoctrine()->getRepository('UVDeskCoreFrameworkBundle:User'); - $user = $entityManager->getRepository('UVDeskCoreFrameworkBundle:User')->findOneBy(array('email' => $form->getData()->getEmail())); - - if ($user && $user->getAgentInstance()) { - // Trigger agent forgot password event - $event = new GenericEvent(CoreWorkflowEvents\Agent\ForgotPassword::getId(), [ - 'entity' => $user, - ]); - - $this->get('event_dispatcher')->dispatch('uvdesk.automation.workflow.execute', $event); - $request->getSession()->getFlashBag()->set('success','Please check your mail for password update.'); + $form->submit(['email' => $request->request->get('forgot_password_form')['email']]); + $form->handleRequest($request); + + if ($form->isValid()) { + $repository = $this->getDoctrine()->getRepository('UVDeskCoreFrameworkBundle:User'); + $user = $entityManager->getRepository('UVDeskCoreFrameworkBundle:User')->findOneByEmail($form->getData()->getEmail()); + + if (!empty($user)) { + // Trigger agent forgot password event + $event = new GenericEvent(CoreWorkflowEvents\UserForgotPassword::getId(), [ + 'entity' => $user, + ]); - return $this->redirect($this->generateUrl('helpdesk_member_update_account_credentials')."/".$form->getData()->getEmail()); - } else { - $request->getSession()->getFlashBag()->set('warning', 'This Email address is not registered with us.'); - } + $this->get('event_dispatcher')->dispatch('uvdesk.automation.workflow.execute', $event); + $request->getSession()->getFlashBag()->set('success', 'Please check your mail for password update.'); + } else { + $request->getSession()->getFlashBag()->set('warning', 'This email address is not registered with us.'); } } - - return $this->render("@UVDeskCoreFramework//forgotPassword.html.twig"); } - - return $this->redirect($this->generateUrl('helpdesk_member_dashboard')); + + return $this->render("@UVDeskCoreFramework//forgotPassword.html.twig"); } - public function updateCredentials($email, $verificationCode) + public function updateCredentials($email, $verificationCode, Request $request, UserPasswordEncoderInterface $encoder) { if (empty($email) || empty($verificationCode)) { - return $this->redirect($this->generateUrl('helpdesk_member_handle_login')); + return new Response('How did you land here? :/', 404); + } else { + $entityManager = $this->getDoctrine()->getManager(); + $user = $entityManager->getRepository('UVDeskCoreFrameworkBundle:User')->findOneByEmail($email); + + if (empty($user) || $user->getVerificationCode() != $verificationCode) { + return new Response('How did you land here? :/', 404); + } } - $entityManager = $this->getDoctrine()->getManager(); - $request = $this->container->get('request_stack')->getCurrentRequest(); - - // Validate request - $user = $entityManager->getRepository('UVDeskCoreFrameworkBundle:User')->findOneByEmail($email); - - if (empty($user) || null == $user->getAgentInstance() || $user->getVerificationCode() != $verificationCode) { - return $this->redirect($this->generateUrl('helpdesk_member_handle_login')); - } - if ($request->getMethod() == 'POST') { $updatedCredentials = $request->request->all(); if ($updatedCredentials['password'] === $updatedCredentials['confirmPassword']) { - $user->setPassword($this->encodePassword($user, $updatedCredentials['password'])); + $user->setPassword($encoder->encodePassword($user, $updatedCredentials['password'])); $user->setVerificationCode(TokenGenerator::generateToken()); $entityManager->persist($user); $entityManager->flush(); $request->getSession()->getFlashBag()->set('success', 'Your password has been updated successfully.'); - return $this->redirect($this->generateUrl('helpdesk_member_handle_login')); } else { - $request->getSession()->getFlashBag()->set('warning', "Password don't match."); + $request->getSession()->getFlashBag()->set('warning', "Please try again. The passwords do not match."); } } - - return $this->render("@UVDeskCoreFramework//resetPassword.html.twig"); - } - protected function encodePassword(User $user, $plainPassword) - { - return $encodedPassword = $this->container->get('security.password_encoder')->encodePassword($user, $plainPassword); + return $this->render("@UVDeskCoreFramework//resetPassword.html.twig"); } } diff --git a/Fixtures/EmailTemplates.php b/Fixtures/EmailTemplates.php index d299f3d44..3d88c675f 100644 --- a/Fixtures/EmailTemplates.php +++ b/Fixtures/EmailTemplates.php @@ -10,15 +10,14 @@ class EmailTemplates extends DoctrineFixture { private static $seeds = [ + CoreEmailTemplates\UserForgotPassword::class, CoreEmailTemplates\Agent\TicketReply::class, CoreEmailTemplates\Agent\TicketCreated::class, CoreEmailTemplates\Agent\AccountCreated::class, - CoreEmailTemplates\Agent\ForgotPassword::class, CoreEmailTemplates\Agent\TicketAssigned::class, CoreEmailTemplates\Customer\TicketReply::class, CoreEmailTemplates\Customer\TicketCreated::class, CoreEmailTemplates\Customer\AccountCreated::class, - CoreEmailTemplates\Customer\ForgotPassword::class, ]; public function load(ObjectManager $entityManager) diff --git a/README.md b/README.md index 56d8570e1..25e78d129 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,12 @@

-[UVDesk Community Edition][1] is an easy-to-use, highly customizable open-source **helpdesk solution** built on top of the reliable [Symfony][2] **PHP framework**, enabling organizations to provide their customers with the best level of support solution possible. - -CoreFrameworkBundle --------------- +

+ Latest Stable Version + Total Downloads + License + connect on gitter +

The standalone **CoreFrameworkBundle** lies at the heart of the [UVDesk Community][1] helpdesk, providing the core essential functionalities and integration tools to easily integrate any other community helpdesk packages, furhter extending the capabilities of the helpdesk system. diff --git a/Repository/ThreadRepository.php b/Repository/ThreadRepository.php index db95849a9..5a97c29c9 100644 --- a/Repository/ThreadRepository.php +++ b/Repository/ThreadRepository.php @@ -137,7 +137,7 @@ public function getAllCustomerThreads($ticketId,\Symfony\Component\HttpFoundatio 'reply' => html_entity_decode($thread['message']), 'source' => $thread['source'], 'threadType' => $thread['threadType'], - 'userType' => 'customer', + 'userType' => $thread['createdBy'], 'formatedCreatedAt' => $userService->getLocalizedFormattedTime($userService->getSessionUser(), $thread['createdAt']), 'timestamp' => $userService->convertToDatetimeTimezoneTimestamp($thread['createdAt']), 'cc' => $thread['cc'], diff --git a/Resources/config/routes/private.yaml b/Resources/config/routes/private.yaml index f77a27ebc..386b9e416 100644 --- a/Resources/config/routes/private.yaml +++ b/Resources/config/routes/private.yaml @@ -7,15 +7,6 @@ helpdesk_member_handle_logout: path: /logout controller: Webkul\UVDesk\CoreFrameworkBundle\Controller\Authentication::logout -helpdesk_member_forgot_account_password: - path: /forgot-password - controller: Webkul\UVDesk\CoreFrameworkBundle\Controller\Authentication::forgotPassword - -helpdesk_member_update_account_credentials: - path: /update-credentials/{email}/{verificationCode} - controller: Webkul\UVDesk\CoreFrameworkBundle\Controller\Authentication::updateCredentials - defaults: { email: '', verificationCode: '' } - # Agent Panel Resources helpdesk_member_dashboard: path: /dashboard diff --git a/Resources/config/routes/public.yaml b/Resources/config/routes/public.yaml index 3c0d16f66..ba7cdb9d0 100644 --- a/Resources/config/routes/public.yaml +++ b/Resources/config/routes/public.yaml @@ -1 +1,12 @@ -# Add public routing resources here ... \ No newline at end of file +# Add public routing resources here ... +helpdesk_forgot_account_password: + path: /{_locale}/forgot-password + controller: Webkul\UVDesk\CoreFrameworkBundle\Controller\Authentication::forgotPassword + requirements: { _locale: '%app_locales%' } + defaults: { _locale: '%locale%' } + +helpdesk_update_account_credentials: + path: /{_locale}/update-credentials/{email}/{verificationCode} + controller: Webkul\UVDesk\CoreFrameworkBundle\Controller\Authentication::updateCredentials + requirements: { _locale: '%app_locales%' } + defaults: { _locale: '%locale%', email: '', verificationCode: '' } diff --git a/Resources/views/Templates/attachment.html.twig b/Resources/views/Templates/attachment.html.twig index ab3893ada..a8a0dce6d 100644 --- a/Resources/views/Templates/attachment.html.twig +++ b/Resources/views/Templates/attachment.html.twig @@ -7,37 +7,85 @@ $(function () { var FileView = Backbone.View.extend({ fileCounter: 0, + max_post_size: {{ max_post_size }}, + max_file_uploads: {{ max_file_uploads }}, + upload_max_filesize: {{ upload_max_filesize }}, el: '.attachment-block', events : { 'click .uv-file-label': 'createFileType', 'change .attachment': 'selectFile', - 'click .uv-added-attachment span': 'removeFile' + 'click .uv-added-attachment span': 'removeFile', + 'click .uv-field-message': 'removeError', }, createFileType: function(e) { - currentElement = Backbone.$(e.currentTarget) + this.removeError(e) + var currentElement = Backbone.$(e.currentTarget), + attachmentBlock = currentElement.parents('.attachment-block') + if (attachmentBlock.children('.uv-added-attachment').length + 1 > this.max_file_uploads) { + attachmentBlock.append(this.getDefaultErrorMessage()) + return; + } this.fileCounter += 1; - currentElement.parents('.attachment-block').append('') + attachmentBlock.append('') $('#file-' + this.fileCounter).find('.attachment').trigger('click') }, labelTemplate: _.template('
'), selectFile: function(e) { - currentElement = Backbone.$(e.currentTarget) + var currentElement = Backbone.$(e.currentTarget); var attachmentBlock = currentElement.parents(".uv-added-attachment"); - if(currentElement.length) { - files = currentElement[0].files; - if(files.length) { + var isError = false; + + if (currentElement.length) { + files = currentElement[0].files; + + if (files.length) { for (var i = 0; i < files.length; i++) { var fileName = files[i].name; + + if (files[i].size > this.upload_max_filesize) { + isError = true; + break; + } + + // Validating Form Size + var formSize = 0 + var formData = new FormData(currentElement.parents('form')[0]) + + for (var pair of formData.entries()) { + if (pair[1] instanceof Blob) { + formSize += pair[1].size + } else { + formSize += pair[1].length + } + } + + if (formSize > this.max_post_size) { + isError = true + } + attachmentBlock.append(this.labelTemplate({'fileName': fileName})); } - } + } + } + + if (isError) { + attachmentBlock.parents('.attachment-block').append(this.getDefaultErrorMessage()) + attachmentBlock.remove() + return } attachmentBlock.show() }, removeFile: function(e) { + this.removeError(e) Backbone.$(e.currentTarget).parents('.uv-added-attachment').remove() - } + }, + getDefaultErrorMessage: function() { + return 'You can send up to ' + Math.floor(this.upload_max_filesize/(1024*1024)) + ' MB in attachments. If you have more than one attachment, they can\'t add up to more than ' + Math.floor(this.max_post_size/(1024*1024)) + ' MB and ' + this.max_file_uploads + ' attachments in total.' + }, + removeError: function(e) { + Backbone.$(e.currentTarget).parents('.attachment-block').find('.uv-field-message').remove() + } }); var fileView = new FileView(); diff --git a/Resources/views/Templates/layout.html.twig b/Resources/views/Templates/layout.html.twig index cbb6f2308..e0b302fca 100644 --- a/Resources/views/Templates/layout.html.twig +++ b/Resources/views/Templates/layout.html.twig @@ -78,7 +78,7 @@ {% set currentUser = user_service.getSessionUser() %} - {% if currentUser is not empty %} + {% if currentUser is not empty and currentUser.getAgentInstance() is not empty %} {% set currentUserDetails = currentUser.getAgentInstance().getPartialDetails() %} {% endif %} diff --git a/Resources/views/login.html.twig b/Resources/views/login.html.twig index 2b2a2cd22..6fb98e060 100644 --- a/Resources/views/login.html.twig +++ b/Resources/views/login.html.twig @@ -40,7 +40,7 @@
- Forgot Password? + Forgot Password?
diff --git a/Services/EmailService.php b/Services/EmailService.php index 28e845105..f2cd52607 100644 --- a/Services/EmailService.php +++ b/Services/EmailService.php @@ -342,7 +342,7 @@ public function getEmailPlaceholderValues(User $user, $userType = 'member') } // Link to update account login credentials - $updateCredentialsURL = $router->generate(('customer' == $userType) ? 'helpdesk_customer_update_account_credentials' : 'helpdesk_member_update_account_credentials', [ + $updateCredentialsURL = $router->generate( 'helpdesk_update_account_credentials', [ 'email' => $user->getEmail(), 'verificationCode' => $user->getVerificationCode(), ], UrlGeneratorInterface::ABSOLUTE_URL); diff --git a/Services/TicketService.php b/Services/TicketService.php index bac2adec2..f6d600b42 100644 --- a/Services/TicketService.php +++ b/Services/TicketService.php @@ -1361,16 +1361,16 @@ public function isTicketAccessGranted(Ticket $ticket, User $user = null, $firewa // @TODO: Take current firewall into consideration (access check on behalf of agent/customer) if (empty($user)) { $user = $this->container->get('user.service')->getSessionUser(); - - if (empty($user)) { - return false; - } } - $agentInstance = $user->getAgentInstance(); - - if (empty($agentInstance)) { + if (empty($user)) { return false; + } else { + $agentInstance = $user->getAgentInstance(); + + if (empty($agentInstance)) { + return false; + } } if ($agentInstance->getSupportRole()->getId() == 3 && in_array($agentInstance->getTicketAccessLevel(), [2, 3, 4])) { diff --git a/Services/UserService.php b/Services/UserService.php index 904ba07bd..a5e69cba4 100644 --- a/Services/UserService.php +++ b/Services/UserService.php @@ -228,7 +228,7 @@ public function createUserInstance($email, $name, SupportRole $role, array $extr public function getAgentPartialDataCollection(Request $request = null) { $queryBuilder = $this->entityManager->createQueryBuilder() - ->select("user.id, user.email, CONCAT(user.firstName, ' ', user.lastName) as name, userInstance.profileImagePath as smallThumbnail") + ->select("user.id, user.email, CONCAT(user.firstName, ' ', COALESCE(user.lastName, '')) as name, userInstance.profileImagePath as smallThumbnail") ->from('UVDeskCoreFrameworkBundle:User', 'user') ->leftJoin('user.userInstance', 'userInstance') ->leftJoin('userInstance.supportRole', 'supportRole') diff --git a/Templates/Email/Resources/Agent/ForgotPassword.php b/Templates/Email/Resources/Agent/ForgotPassword.php deleted file mode 100644 index da16c9167..000000000 --- a/Templates/Email/Resources/Agent/ForgotPassword.php +++ /dev/null @@ -1,68 +0,0 @@ -

-

-

-

-

{%global.companyLogo%}

-

-
-

-

- - Forgot password, this is it!! - -

-

-
-

-

Hi {%user.userName%}, -
-

-

-
-

-

You recently requested to reset your password for your {%global.companyName%} account. Click the link to reset it {%user.forgotPasswordLink%}

-

-
-

-

If you did not request a password reset, please ignore this mail or revert back to let us know.

-
-
-
-

Thanks and Regards

-

{%global.companyName%}

-

-

-MESSAGE; - - public static function getName() - { - return self::$name; - } - - public static function getTemplateType() - { - return self::$type; - } - - public static function getSubject() - { - return self::$subject; - } - - public static function getMessage() - { - return self::$message; - } -} \ No newline at end of file diff --git a/Templates/Email/Resources/Customer/ForgotPassword.php b/Templates/Email/Resources/UserForgotPassword.php similarity index 90% rename from Templates/Email/Resources/Customer/ForgotPassword.php rename to Templates/Email/Resources/UserForgotPassword.php index 789746b36..37b6c2ea9 100644 --- a/Templates/Email/Resources/Customer/ForgotPassword.php +++ b/Templates/Email/Resources/UserForgotPassword.php @@ -1,13 +1,13 @@

diff --git a/Templates/config.yaml b/Templates/config.yaml index d64417502..a3e1f6948 100644 --- a/Templates/config.yaml +++ b/Templates/config.yaml @@ -9,6 +9,12 @@ parameters: uvdesk_site_path.member_prefix: member uvdesk_site_path.knowledgebase_customer_prefix: customer + # File uploads constraints + # @TODO: Set these parameters via compilers + max_post_size: 8388608 + max_file_uploads: 20 + upload_max_filesize: 2097152 + uvdesk: site_url: 'localhost:8000' upload_manager: diff --git a/Templates/twig.yaml b/Templates/twig.yaml index 1fb0aa3c0..d423bf860 100644 --- a/Templates/twig.yaml +++ b/Templates/twig.yaml @@ -3,6 +3,9 @@ twig: default_agent_image_path: '%assets_default_agent_profile_image_path%' default_customer_image_path: '%assets_default_customer_profile_image_path%' default_helpdesk_image_path: '%assets_default_helpdesk_profile_image_path%' + max_post_size: '%max_post_size%' + max_file_uploads: '%max_file_uploads%' + upload_max_filesize: '%upload_max_filesize%' user_service: "@user.service" uvdesk_service: "@uvdesk.service" ticket_service: "@ticket.service" diff --git a/Templates/uvdesk.php b/Templates/uvdesk.php index 6951e4a9b..6a50b5600 100644 --- a/Templates/uvdesk.php +++ b/Templates/uvdesk.php @@ -12,6 +12,12 @@ uvdesk_site_path.member_prefix: member uvdesk_site_path.knowledgebase_customer_prefix: customer + + # File uploads constraints + # @TODO: Set these parameters via compilers + max_post_size: 8388608 + max_file_uploads: 20 + upload_max_filesize: 2097152 uvdesk: site_url: '{{ SITE_URL }}' diff --git a/Workflow/Actions/MailUser.php b/Workflow/Actions/MailUser.php new file mode 100644 index 000000000..b925e7be8 --- /dev/null +++ b/Workflow/Actions/MailUser.php @@ -0,0 +1,63 @@ +get('doctrine.orm.entity_manager'); + + return array_map(function ($emailTemplate) { + return [ + 'id' => $emailTemplate->getId(), + 'name' => $emailTemplate->getName(), + ]; + }, $entityManager->getRepository('UVDeskCoreFrameworkBundle:EmailTemplates')->findAll()); + } + + public static function applyAction(ContainerInterface $container, $entity, $value = null) + { + $entityManager = $container->get('doctrine.orm.entity_manager'); + + switch (true) { + case $entity instanceof CoreEntities\User: + $emailTemplate = $entityManager->getRepository('UVDeskCoreFrameworkBundle:EmailTemplates')->findOneById($value); + + if (empty($emailTemplate)) { + // @TODO: Send default email template + return; + } + + $emailPlaceholders = $container->get('email.service')->getEmailPlaceholderValues($entity); + $subject = $container->get('email.service')->processEmailSubject($emailTemplate->getSubject(), $emailPlaceholders); + $message = $container->get('email.service')->processEmailContent($emailTemplate->getMessage(), $emailPlaceholders); + + $messageId = $container->get('email.service')->sendMail($subject, $message, $entity->getEmail()); + break; + default: + break; + } + } +} diff --git a/Workflow/Events/Agent/ForgotPassword.php b/Workflow/Events/Agent/ForgotPassword.php index f4fbcd707..99785a54c 100644 --- a/Workflow/Events/Agent/ForgotPassword.php +++ b/Workflow/Events/Agent/ForgotPassword.php @@ -3,16 +3,10 @@ namespace Webkul\UVDesk\CoreFrameworkBundle\Workflow\Events\Agent; use Webkul\UVDesk\AutomationBundle\Workflow\FunctionalGroup; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Webkul\UVDesk\AutomationBundle\Workflow\Event as WorkflowEvent; +use Webkul\UVDesk\CoreFrameworkBundle\Workflow\Events\UserForgotPassword as UserForgotPasswordEvent; -class ForgotPassword extends WorkflowEvent +class ForgotPassword extends UserForgotPasswordEvent { - public static function getId() - { - return 'uvdesk.agent.forgot_password'; - } - public static function getDescription() { return 'Agent Forgot Password'; diff --git a/Workflow/Events/Customer/ForgotPassword.php b/Workflow/Events/Customer/ForgotPassword.php index 15359dbd6..dcd70e59e 100644 --- a/Workflow/Events/Customer/ForgotPassword.php +++ b/Workflow/Events/Customer/ForgotPassword.php @@ -3,16 +3,10 @@ namespace Webkul\UVDesk\CoreFrameworkBundle\Workflow\Events\Customer; use Webkul\UVDesk\AutomationBundle\Workflow\FunctionalGroup; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Webkul\UVDesk\AutomationBundle\Workflow\Event as WorkflowEvent; +use Webkul\UVDesk\CoreFrameworkBundle\Workflow\Events\UserForgotPassword as UserForgotPasswordEvent; -class ForgotPassword extends WorkflowEvent +class ForgotPassword extends UserForgotPasswordEvent { - public static function getId() - { - return 'uvdesk.customer.forgot_password'; - } - public static function getDescription() { return 'Customer Forgot Password'; diff --git a/Workflow/Events/UserForgotPassword.php b/Workflow/Events/UserForgotPassword.php new file mode 100644 index 000000000..6fd4e5070 --- /dev/null +++ b/Workflow/Events/UserForgotPassword.php @@ -0,0 +1,25 @@ +