ChangePasswordCommand.php 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. <?php
  2. /**
  3. * @package Grav\Plugin\Login
  4. *
  5. * @copyright Copyright (C) 2014 - 2017 RocketTheme, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Plugin\Console;
  9. use Grav\Common\Config\Config;
  10. use Grav\Console\ConsoleCommand;
  11. use Grav\Common\Grav;
  12. use Grav\Common\File\CompiledYamlFile;
  13. use Grav\Common\User\User;
  14. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  15. use Symfony\Component\Console\Input\InputOption;
  16. use Symfony\Component\Console\Helper\Helper;
  17. use Symfony\Component\Console\Question\Question;
  18. /**
  19. * Class CleanCommand
  20. *
  21. * @package Grav\Console\Cli
  22. */
  23. class ChangePasswordCommand extends ConsoleCommand
  24. {
  25. /**
  26. * @var array
  27. */
  28. protected $options = [];
  29. /**
  30. * Configure the command
  31. */
  32. protected function configure()
  33. {
  34. $this
  35. ->setName('change-password')
  36. ->setAliases(['edit-password', 'newpass', 'changepass', 'passwd'])
  37. ->addOption(
  38. 'user',
  39. 'u',
  40. InputOption::VALUE_REQUIRED,
  41. 'The username'
  42. )
  43. ->addOption(
  44. 'password',
  45. 'p',
  46. InputOption::VALUE_REQUIRED,
  47. "The password. Note that this option is not recommended because the password will be visible by users listing the processes. You should also make sure the password respects Grav's password policy."
  48. )
  49. ->setDescription('Changes a User Password')
  50. ->setHelp('The <info>change-password</info> changes the password of the specified user. (User must exist)')
  51. ;
  52. }
  53. /**
  54. * @return int|null|void
  55. */
  56. protected function serve()
  57. {
  58. $this->options = [
  59. 'user' => $this->input->getOption('user'),
  60. 'password1' => $this->input->getOption('password')
  61. ];
  62. $this->validateOptions();
  63. $helper = $this->getHelper('question');
  64. $data = [];
  65. $this->output->writeln('<green>Changing User Password</green>');
  66. $this->output->writeln('');
  67. if (!$this->options['user']) {
  68. // Get username and validate
  69. $question = new Question('Enter a <yellow>username</yellow>: ');
  70. $question->setValidator(function ($value) {
  71. return $this->validate('user', $value);
  72. });
  73. $username = $helper->ask($this->input, $this->output, $question);
  74. } else {
  75. $username = $this->options['user'];
  76. }
  77. if (!$this->options['password1']) {
  78. // Get password and validate
  79. $password = $this->askForPassword($helper, 'Enter a <yellow>new password</yellow>: ', function ($password1) use ($helper) {
  80. $this->validate('password1', $password1);
  81. // Since input is hidden when prompting for passwords, the user is asked to repeat the password
  82. return $this->askForPassword($helper, 'Repeat the <yellow>password</yellow>: ', function ($password2) use ($password1) {
  83. return $this->validate('password2', $password2, $password1);
  84. });
  85. });
  86. $data['password'] = $password;
  87. } else {
  88. $data['password'] = $this->options['password1'];
  89. }
  90. // Lowercase the username for the filename
  91. $username = strtolower($username);
  92. /** @var UniformResourceLocator $locator */
  93. $locator = Grav::instance()['locator'];
  94. // Grab the account file and read in the information before setting the file (prevent setting erase)
  95. $oldUserFile = CompiledYamlFile::instance($locator->findResource('account://' . $username . YAML_EXT, true, true));
  96. $oldData = (array)$oldUserFile->content();
  97. //Set the password feild to new password
  98. $oldData['password'] = $data['password'];
  99. // Create user object and save it using oldData (with updated password)
  100. $user = new User($oldData);
  101. $file = CompiledYamlFile::instance($locator->findResource('account://' . $username . YAML_EXT, true, true));
  102. $user->file($file);
  103. $user->save();
  104. $this->output->writeln('');
  105. $this->output->writeln('<green>Success!</green> User <cyan>' . $username . '\'s</cyan> password changed.');
  106. }
  107. /**
  108. *
  109. */
  110. protected function validateOptions()
  111. {
  112. foreach (array_filter($this->options) as $type => $value) {
  113. $this->validate($type, $value);
  114. }
  115. }
  116. /**
  117. * @param $type
  118. * @param $value
  119. * @param string $extra
  120. *
  121. * @return mixed
  122. */
  123. protected function validate($type, $value, $extra = '')
  124. {
  125. /** @var Config $config */
  126. $config = Grav::instance()['config'];
  127. /** @var UniformResourceLocator $locator */
  128. $locator = Grav::instance()['locator'];
  129. $username_regex = '/' . $config->get('system.username_regex') . '/';
  130. $pwd_regex = '/' . $config->get('system.pwd_regex') . '/';
  131. switch ($type) {
  132. case 'user':
  133. if (!preg_match($username_regex, $value)) {
  134. throw new \RuntimeException('Username should be between 3 and 16 characters, including lowercase letters, numbers, underscores, and hyphens. Uppercase letters, spaces, and special characters are not allowed');
  135. }
  136. if (!$locator->findResource('account://' . $value . YAML_EXT)) {
  137. throw new \RuntimeException('Username "' . $value . '" does not exist, please pick another username');
  138. }
  139. break;
  140. case 'password1':
  141. if (!preg_match($pwd_regex, $value)) {
  142. throw new \RuntimeException('Password must contain at least one number and one uppercase and lowercase letter, and at least 8 or more characters');
  143. }
  144. break;
  145. case 'password2':
  146. if (strcmp($value, $extra)) {
  147. throw new \RuntimeException('Passwords did not match.');
  148. }
  149. break;
  150. }
  151. return $value;
  152. }
  153. /**
  154. * Get password and validate.
  155. *
  156. * @param Helper $helper
  157. * @param string $question
  158. * @param callable $validator
  159. *
  160. * @return string
  161. */
  162. protected function askForPassword(Helper $helper, $question, callable $validator)
  163. {
  164. $question = new Question($question);
  165. $question->setValidator($validator);
  166. $question->setHidden(true);
  167. $question->setHiddenFallback(true);
  168. return $helper->ask($this->input, $this->output, $question);
  169. }
  170. }