NewUserCommand.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  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\File\CompiledYamlFile;
  12. use Grav\Common\User\User;
  13. use Grav\Common\Grav;
  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\ChoiceQuestion;
  18. use Symfony\Component\Console\Question\Question;
  19. /**
  20. * Class CleanCommand
  21. *
  22. * @package Grav\Console\Cli
  23. */
  24. class NewUserCommand extends ConsoleCommand
  25. {
  26. /**
  27. * @var array
  28. */
  29. protected $options = [];
  30. /**
  31. * Configure the command
  32. */
  33. protected function configure()
  34. {
  35. $this
  36. ->setName('new-user')
  37. ->setAliases(['add-user', 'newuser'])
  38. ->addOption(
  39. 'user',
  40. 'u',
  41. InputOption::VALUE_REQUIRED,
  42. 'The username'
  43. )
  44. ->addOption(
  45. 'password',
  46. 'p',
  47. InputOption::VALUE_REQUIRED,
  48. "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."
  49. )
  50. ->addOption(
  51. 'email',
  52. 'e',
  53. InputOption::VALUE_REQUIRED,
  54. 'The user email'
  55. )
  56. ->addOption(
  57. 'permissions',
  58. 'P',
  59. InputOption::VALUE_REQUIRED,
  60. 'The user permissions. It can be either `a` for Admin access only, `s` for Site access only and `b` for both Admin and Site access.'
  61. )
  62. ->addOption(
  63. 'fullname',
  64. 'N',
  65. InputOption::VALUE_REQUIRED,
  66. 'The user full name.'
  67. )
  68. ->addOption(
  69. 'title',
  70. 't',
  71. InputOption::VALUE_REQUIRED,
  72. 'The title of the user. Usually used as a subtext. Example: Admin, Collaborator, Developer'
  73. )
  74. ->addOption(
  75. 'state',
  76. 's',
  77. InputOption::VALUE_REQUIRED,
  78. 'The state of the account. Can be either `enabled` or `disabled`. [default: "enabled"]'
  79. )
  80. ->setDescription('Creates a new user')
  81. ->setHelp('The <info>new-user</info> creates a new user file in user/accounts/ folder')
  82. ;
  83. }
  84. /**
  85. * @return int|null|void
  86. */
  87. protected function serve()
  88. {
  89. $this->options = [
  90. 'user' => $this->input->getOption('user'),
  91. 'password1' => $this->input->getOption('password'),
  92. 'email' => $this->input->getOption('email'),
  93. 'permissions' => $this->input->getOption('permissions'),
  94. 'fullname' => $this->input->getOption('fullname'),
  95. 'title' => $this->input->getOption('title'),
  96. 'state' => $this->input->getOption('state')
  97. ];
  98. $this->validateOptions();
  99. $helper = $this->getHelper('question');
  100. $data = [];
  101. $this->output->writeln('<green>Creating new user</green>');
  102. $this->output->writeln('');
  103. if (!$this->options['user']) {
  104. // Get username and validate
  105. $question = new Question('Enter a <yellow>username</yellow>: ', 'admin');
  106. $question->setValidator(function ($value) {
  107. return $this->validate('user', $value);
  108. });
  109. $username = $helper->ask($this->input, $this->output, $question);
  110. } else {
  111. $username = $this->options['user'];
  112. }
  113. if (!$this->options['password1']) {
  114. // Get password and validate
  115. $password = $this->askForPassword($helper, 'Enter a <yellow>password</yellow>: ', function ($password1) use ($helper) {
  116. $this->validate('password1', $password1);
  117. // Since input is hidden when prompting for passwords, the user is asked to repeat the password
  118. return $this->askForPassword($helper, 'Repeat the <yellow>password</yellow>: ', function ($password2) use ($password1) {
  119. return $this->validate('password2', $password2, $password1);
  120. });
  121. });
  122. $data['password'] = $password;
  123. } else {
  124. $data['password'] = $this->options['password1'];
  125. }
  126. if (!$this->options['email']) {
  127. // Get email and validate
  128. $question = new Question('Enter an <yellow>email</yellow>: ');
  129. $question->setValidator(function ($value) {
  130. return $this->validate('email', $value);
  131. });
  132. $data['email'] = $helper->ask($this->input, $this->output, $question);
  133. } else {
  134. $data['email'] = $this->options['email'];
  135. }
  136. if (!$this->options['permissions']) {
  137. // Choose permissions
  138. $question = new ChoiceQuestion(
  139. 'Please choose a set of <yellow>permissions</yellow>:',
  140. array('a' => 'Admin Access', 's' => 'Site Access', 'b' => 'Admin and Site Access'),
  141. 'a'
  142. );
  143. $question->setErrorMessage('Permissions %s is invalid.');
  144. $permissions_choice = $helper->ask($this->input, $this->output, $question);
  145. } else {
  146. $permissions_choice = $this->options['permissions'];
  147. }
  148. switch ($permissions_choice) {
  149. case 'a':
  150. $data['access']['admin'] = ['login' => true, 'super' => true];
  151. break;
  152. case 's':
  153. $data['access']['site'] = ['login' => true];
  154. break;
  155. case 'b':
  156. $data['access']['admin'] = ['login' => true, 'super' => true];
  157. $data['access']['site'] = ['login' => true];
  158. }
  159. if (!$this->options['fullname']) {
  160. // Get fullname
  161. $question = new Question('Enter a <yellow>fullname</yellow>: ');
  162. $question->setValidator(function ($value) {
  163. return $this->validate('fullname', $value);
  164. });
  165. $data['fullname'] = $helper->ask($this->input, $this->output, $question);
  166. } else {
  167. $data['fullname'] = $this->options['fullname'];
  168. }
  169. if (!$this->options['title'] && !count(array_filter($this->options))) {
  170. // Get title
  171. $question = new Question('Enter a <yellow>title</yellow>: ');
  172. $data['title'] = $helper->ask($this->input, $this->output, $question);
  173. } else {
  174. $data['title'] = $this->options['title'];
  175. }
  176. if (!$this->options['state'] && !count(array_filter($this->options))) {
  177. // Choose State
  178. $question = new ChoiceQuestion(
  179. 'Please choose the <yellow>state</yellow> for the account:',
  180. array('enabled' => 'Enabled', 'disabled' => 'Disabled'),
  181. 'enabled'
  182. );
  183. $question->setErrorMessage('State %s is invalid.');
  184. $data['state'] = $helper->ask($this->input, $this->output, $question);
  185. } else {
  186. $data['state'] = $this->options['state'] ?: 'enabled';
  187. }
  188. // Lowercase the username for the filename
  189. $username = strtolower($username);
  190. /** @var UniformResourceLocator $locator */
  191. $locator = Grav::instance()['locator'];
  192. // Create user object and save it
  193. $user = new User($data);
  194. $file = CompiledYamlFile::instance($locator->findResource('account://' . $username . YAML_EXT, true, true));
  195. $user->file($file);
  196. $user->save();
  197. $this->output->writeln('');
  198. $this->output->writeln('<green>Success!</green> User <cyan>' . $username . '</cyan> created.');
  199. }
  200. /**
  201. *
  202. */
  203. protected function validateOptions()
  204. {
  205. foreach (array_filter($this->options) as $type => $value) {
  206. $this->validate($type, $value);
  207. }
  208. }
  209. /**
  210. * @param $type
  211. * @param $value
  212. * @param string $extra
  213. *
  214. * @return mixed
  215. */
  216. protected function validate($type, $value, $extra = '')
  217. {
  218. /** @var Config $config */
  219. $config = Grav::instance()['config'];
  220. /** @var UniformResourceLocator $locator */
  221. $locator = Grav::instance()['locator'];
  222. $username_regex = '/' . $config->get('system.username_regex') . '/';
  223. $pwd_regex = '/' . $config->get('system.pwd_regex') . '/';
  224. switch ($type) {
  225. case 'user':
  226. if (!preg_match($username_regex, $value)) {
  227. 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');
  228. }
  229. if (file_exists($locator->findResource('account://' . $value . YAML_EXT))) {
  230. throw new \RuntimeException('Username "' . $value . '" already exists, please pick another username');
  231. }
  232. break;
  233. case 'password1':
  234. if (!preg_match($pwd_regex, $value)) {
  235. throw new \RuntimeException('Password must contain at least one number and one uppercase and lowercase letter, and at least 8 or more characters');
  236. }
  237. break;
  238. case 'password2':
  239. if (strcmp($value, $extra)) {
  240. throw new \RuntimeException('Passwords did not match.');
  241. }
  242. break;
  243. case 'email':
  244. if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
  245. throw new \RuntimeException('Not a valid email address');
  246. }
  247. break;
  248. case 'permissions':
  249. if (!in_array($value, ['a', 's', 'b'])) {
  250. throw new \RuntimeException('Permissions ' . $value . ' are invalid.');
  251. }
  252. break;
  253. case 'fullname':
  254. if ($value === null || trim($value) === '') {
  255. throw new \RuntimeException('Fullname cannot be empty');
  256. }
  257. break;
  258. case 'state':
  259. if ($value !== 'enabled' && $value !== 'disabled') {
  260. throw new \RuntimeException('State is not valid');
  261. }
  262. break;
  263. }
  264. return $value;
  265. }
  266. /**
  267. * Get password and validate.
  268. *
  269. * @param Helper $helper
  270. * @param string $question
  271. * @param callable $validator
  272. *
  273. * @return string
  274. */
  275. protected function askForPassword(Helper $helper, $question, callable $validator)
  276. {
  277. $question = new Question($question);
  278. $question->setValidator($validator);
  279. $question->setHidden(true);
  280. $question->setHiddenFallback(true);
  281. return $helper->ask($this->input, $this->output, $question);
  282. }
  283. }