From b8381709462e2477fedda96d91ff37df0570392c Mon Sep 17 00:00:00 2001
From: Jared Hancock <jared@osticket.com>
Date: Thu, 5 Feb 2015 10:45:40 -0600
Subject: [PATCH] cli: user: Add list, activate, lock features

This adds the ability to find, activate, lock, and optionally send a
password-reset email to one ore more users in the help desk.
---
 include/class.user.php             |   6 +-
 setup/cli/modules/class.module.php |  11 +-
 setup/cli/modules/user.php         | 210 ++++++++++++++++++++++++-----
 3 files changed, 192 insertions(+), 35 deletions(-)

diff --git a/include/class.user.php b/include/class.user.php
index 0ccfaf1a7..758b5ef01 100644
--- a/include/class.user.php
+++ b/include/class.user.php
@@ -932,6 +932,10 @@ class UserAccount extends UserAccountModel {
         return static::sendUnlockEmail('registration-client') === true;
     }
 
+    function setPassword($new) {
+        $this->set('passwd', Passwd::hash($vars['passwd1']));
+    }
+
     protected function sendUnlockEmail($template) {
         global $ost, $cfg;
 
@@ -1016,7 +1020,7 @@ class UserAccount extends UserAccountModel {
         $this->set('username', $vars['username']);
 
         if ($vars['passwd1']) {
-            $this->set('passwd', Passwd::hash($vars['passwd1']));
+            $this->setPassword($vars['passwd1']);
             $this->setStatus(UserAccountStatus::CONFIRMED);
         }
 
diff --git a/setup/cli/modules/class.module.php b/setup/cli/modules/class.module.php
index a1647ce3c..e01e507ab 100644
--- a/setup/cli/modules/class.module.php
+++ b/setup/cli/modules/class.module.php
@@ -39,8 +39,14 @@ class Option {
             $value = null;
         elseif ($value)
             $nargs = 1;
-        if ($this->type == 'int')
+        switch ($this->type) {
+        case 'int':
             $value = (int)$value;
+            break;
+        case 'bool':
+            $value = strcasecmp($value, 'true') === 0 || ((int) $value);
+            break;
+        }
         switch ($this->action) {
             case 'store_true':
                 $value = true;
@@ -156,7 +162,7 @@ class Module {
 
         if ($this->arguments) {
             echo "\nArguments:\n";
-            foreach ($this->arguments as $name=>$help)
+            foreach ($this->arguments as $name=>$help) {
                 $extra = '';
                 if (is_array($help)) {
                     if (isset($help['options']) && is_array($help['options'])) {
@@ -169,6 +175,7 @@ class Module {
                 echo $name . "\n    " . wordwrap(
                     preg_replace('/\s+/', ' ', $help), 76, "\n    ")
                         .$extra."\n";
+            }
         }
 
         if ($this->epilog) {
diff --git a/setup/cli/modules/user.php b/setup/cli/modules/user.php
index 2d6d146a9..6fd05f778 100644
--- a/setup/cli/modules/user.php
+++ b/setup/cli/modules/user.php
@@ -9,8 +9,12 @@ class UserManager extends Module {
         'action' => array(
             'help' => 'Action to be performed',
             'options' => array(
-                'import' => 'Import users to the system',
-                'export' => 'Export users from the system',
+                'import' => 'Import users from CSV file',
+                'export' => 'Export users from the system to CSV',
+                'activate' => 'Create or activate an account',
+                'lock' => "Lock a user's account",
+                'set-password' => "Set a user's account password",
+                'list' => 'List users based on search criteria',
             ),
         ),
     );
@@ -21,6 +25,26 @@ class UserManager extends Module {
             'help' => 'File or stream to process'),
         'org' => array('-O', '--org', 'metavar'=>'ORGID',
             'help' => 'Set the organization ID on import'),
+
+        'send-mail' => array('-m', '--send-mail',
+            'help' => 'Send the user an email. Used with `activate` and `set-password`',
+            'default' => false, 'action' => 'store_true'),
+
+        'verbose' => array('-v', '--verbose', 'default'=>false,
+            'action'=>'store_true', 'help' => 'Be more verbose'
+        ),
+
+        // -- Search criteria
+        'account' => array('-A', '--account', 'type'=>'bool', 'metavar'=>'bool',
+            'help' => 'Search for users based on activation status'),
+        'isconfirmed' => array('-C', '--isconfirmed', 'type'=>'bool', 'metavar'=>'bool',
+            'help' => 'Search for users based on confirmation status'),
+        'islocked' => array('-L', '--islocked', 'type'=>'bool', 'metavar'=>'bool',
+            'help' => 'Search for users based on locked status'),
+        'email' => array('-E', '--email',
+            'help' => 'Search by email address'),
+        'id' => array('-U', '--id',
+            'help' => 'Search by user id'),
         );
 
     var $stream;
@@ -30,42 +54,164 @@ class UserManager extends Module {
         Bootstrap::connect();
 
         switch ($args['action']) {
-            case 'import':
-                // Properly detect Macintosh style line endings
-                ini_set('auto_detect_line_endings', true);
-
-                if (!$options['file'])
-                    $this->fail('CSV file to import users from is required!');
-                elseif (!($this->stream = fopen($options['file'], 'rb')))
-                    $this->fail("Unable to open input file [{$options['file']}]");
-
-                $extras = array();
-                if ($options['org']) {
-                    if (!($org = Organization::lookup($options['org'])))
-                        $this->fail($options['org'].': Unknown organization ID');
-                    $extras['org_id'] = $options['org'];
+        case 'import':
+            // Properly detect Macintosh style line endings
+            ini_set('auto_detect_line_endings', true);
+
+            if (!$options['file'])
+                $this->fail('CSV file to import users from is required!');
+            elseif (!($this->stream = fopen($options['file'], 'rb')))
+                $this->fail("Unable to open input file [{$options['file']}]");
+
+            $extras = array();
+            if ($options['org']) {
+                if (!($org = Organization::lookup($options['org'])))
+                    $this->fail($options['org'].': Unknown organization ID');
+                $extras['org_id'] = $options['org'];
+            }
+            $status = User::importCsv($this->stream, $extras);
+            if (is_numeric($status))
+                $this->stderr->write("Successfully imported $status clients\n");
+            else
+                $this->fail($status);
+            break;
+
+        case 'export':
+            $stream = $options['file'] ?: 'php://stdout';
+            if (!($this->stream = fopen($stream, 'c')))
+                $this->fail("Unable to open output file [{$options['file']}]");
+
+            fputcsv($this->stream, array('Name', 'Email'));
+            foreach (User::objects() as $user)
+                fputcsv($this->stream,
+                        array((string) $user->getName(), $user->getEmail()));
+            break;
+
+        case 'activate':
+            $users = $this->getQuerySet($options);
+            foreach ($users as $U) {
+                if ($options['verbose']) {
+                    $this->stderr->write(sprintf(
+                        "Activating %s <%s>\n",
+                        $U->getName(), $U->getDefaultEmail()
+                    ));
                 }
-                $status = User::importCsv($this->stream, $extras);
-                if (is_numeric($status))
-                    $this->stderr->write("Successfully imported $status clients\n");
-                else
-                    $this->fail($status);
+                if (!($account = $U->getAccount())) {
+                    $account = UserAccount::create(array('user' => $U));
+                    $U->account = $account;
+                    $U->save();
+                }
+
+                if ($options['send-mail']) {
+                    global $ost, $cfg;
+                    $ost = osTicket::start();
+                    $cfg = $ost->getConfig();
+
+                    if (($error = $account->sendConfirmEmail()) && $error !== true) {
+                        $this->warn(sprintf('%s: Unable to send email: %s',
+                            $U->getDefaultEmail(), $error->getMessage()
+                        ));
+                    }
+                }
+            }
+
+            break;
+
+        case 'lock':
+            $users = $this->getQuerySet($options);
+            $users->select_related('account');
+            foreach ($users as $U) {
+                if (!($account = $U->getAccount())) {
+                    $this->warn(sprintf(
+                        '%s: User does not have a client account',
+                        $U->getName()
+                    ));
+                }
+                $account->setFlag(UserAccountStatus::LOCKED);
+                $account->save();
+            }
+
+            break;
+
+        case 'list':
+            $users = $this->getQuerySet($options);
+
+            foreach ($users as $U) {
+                $this->stdout->write(sprintf(
+                    "%d %s <%s>%s\n",
+                    $U->id, $U->getName(), $U->getDefaultEmail(),
+                    ($O = $U->getOrganization()) ? " ({$O->getName()})" : ''
+                ));
+            }
+
+            break;
+
+        case 'set-password':
+            $this->stderr->write('Enter new password: ');
+            $ps1 = fgets(STDIN);
+            if (!function_exists('posix_isatty') || !posix_isatty(STDIN)) {
+                $this->stderr->write('Re-enter new password: ');
+                $ps2 = fgets(STDIN);
+
+                if ($ps1 != $ps2)
+                    $this->fail('Passwords do not match');
+            }
+
+            // Account is required
+            $options['account'] = true;
+            $users = $this->getQuerySet($options);
+
+            $updated  = 0;
+            foreach ($users as $U) {
+                $U->account->setPassword($ps1);
+                if ($U->account->save())
+                    $updated++;
+            }
+            $this->stdout->write(sprintf('Updated %d users', $updated));
+            break;
+
+        default:
+            $this->stderr->write('Unknown action!');
+        }
+        @fclose($this->stream);
+    }
+
+    function getQuerySet($options, $requireOne=false) {
+        $users = User::objects();
+        foreach ($options as $O=>$V) {
+            if (!isset($V))
+                continue;
+            switch ($O) {
+            case 'account':
+                $users->filter(array('account__isnull' => !$V));
                 break;
 
-            case 'export':
-                $stream = $options['file'] ?: 'php://stdout';
-                if (!($this->stream = fopen($stream, 'c')))
-                    $this->fail("Unable to open output file [{$options['file']}]");
+            case 'isconfirmed':
+            case 'islocked':
+                $flags = array(
+                    'isconfirmed' => UserAccountStatus::CONFIRMED,
+                    'islocked' => UserAccountStatus::LOCKED,
+                );
+                $Q = new Q(array('account__status__hasbit'=>$flags[$O]));
+                if (!$V)
+                    $Q->negate();
+                $users->filter($Q);
+                break;
 
-                fputcsv($this->stream, array('Name', 'Email'));
-                foreach (User::objects() as $user)
-                    fputcsv($this->stream,
-                            array((string) $user->getName(), $user->getEmail()));
+            case 'org':
+                if (is_numeric($V))
+                    $users->filter(array('org__id'=>$V));
+                else
+                    $users->filter(array('org__name__contains'=>$V));
                 break;
-            default:
-                $this->stderr->write('Unknown action!');
+
+            case 'id':
+                $users->filter(array('id'=>$V));
+                break;
+            }
+
         }
-        @fclose($this->stream);
+        return $users;
     }
 }
 Module::register('user', 'UserManager');
-- 
GitLab