From ae1afd2f6529e4d07b18317304e5e2c302d783ce Mon Sep 17 00:00:00 2001
From: Christopher Smith <chris@jalakai.co.uk>
Date: Fri, 2 Aug 2013 17:21:48 +0200
Subject: [PATCH] add csv import functionality to the user manager

---
 lib/plugins/usermanager/admin.php          | 187 +++++++++++++++++++++
 lib/plugins/usermanager/lang/en/import.txt |   9 +
 lib/plugins/usermanager/lang/en/lang.php   |  15 ++
 lib/plugins/usermanager/style.css          |   3 +
 4 files changed, 214 insertions(+)
 create mode 100644 lib/plugins/usermanager/lang/en/import.txt

diff --git a/lib/plugins/usermanager/admin.php b/lib/plugins/usermanager/admin.php
index 72ac47e15..ddf10a115 100644
--- a/lib/plugins/usermanager/admin.php
+++ b/lib/plugins/usermanager/admin.php
@@ -30,6 +30,7 @@ class admin_plugin_usermanager extends DokuWiki_Admin_Plugin {
     var $_edit_user = '';     // set to user selected for editing
     var $_edit_userdata = array();
     var $_disabled = '';      // if disabled set to explanatory string
+    var $_import_failures = array();
 
     /**
      * Constructor
@@ -49,6 +50,11 @@ class admin_plugin_usermanager extends DokuWiki_Admin_Plugin {
           $this->_auth = & $auth;
 
         }
+
+        // attempt to retrieve any import failures from the session
+        if ($_SESSION['import_failures']){
+            $this->_import_failures = $_SESSION['import_failures'];
+        }
     }
 
      /**
@@ -102,6 +108,8 @@ class admin_plugin_usermanager extends DokuWiki_Admin_Plugin {
                           $this->_start = 0;
                           break;
           case "export" : $this->_export(); break;
+          case "import" : $this->_import(); break;
+          case "importfails" : $this->_downloadImportFailures(); break;
         }
 
         $this->_user_total = $this->_auth->canDo('getUserCount') ? $this->_auth->getUserCount($this->_filter) : -1;
@@ -238,6 +246,10 @@ class admin_plugin_usermanager extends DokuWiki_Admin_Plugin {
           ptln("  </div>");
           ptln("</div>");
         }
+
+        if ($this->_auth->canDo('addUser')) {
+          $this->_htmlImportForm();
+        }
         ptln("</div>");
     }
 
@@ -352,6 +364,59 @@ class admin_plugin_usermanager extends DokuWiki_Admin_Plugin {
         }
     }
 
+    function _htmlImportForm($indent=0) {
+        global $ID;
+
+        $failure_download_link = wl($ID,array('do'=>'admin','page'=>'usermanager','fn[importfails]'=>1));
+
+        ptln('<div class="level2 import_users">',$indent);
+        print $this->locale_xhtml('import');
+        ptln('  <form action="'.wl($ID).'" method="post" enctype="multipart/form-data">',$indent);
+        formSecurityToken();
+        ptln('    <label>User list file (csv):  <input type="file" name="import" /></label>',$indent);
+        ptln('    <input type="submit" name="fn[import]" value="'.$this->lang['import'].'" />',$indent);
+        ptln('    <input type="hidden" name="do"    value="admin" />',$indent);
+        ptln('    <input type="hidden" name="page"  value="usermanager" />',$indent);
+
+        $this->_htmlFilterSettings($indent+4);
+        ptln('  </form>',$indent);
+        ptln('</div>');
+
+        // list failures from the previous import
+        if ($this->_import_failures) {
+            $digits = strlen(count($this->_import_failures));
+            ptln('<div class="level3 import_failures">',$indent);
+            ptln('  <h3>Most Recent Import - Failures</h3>');
+            ptln('  <table class="import_failures">',$indent);
+            ptln('    <thead>',$indent);
+            ptln('      <tr>',$indent);
+            ptln('        <th class="line">'.$this->lang['line'].'</th>',$indent);
+            ptln('        <th class="error">'.$this->lang['error'].'</th>',$indent);
+            ptln('        <th class="userid">'.$this->lang['user_id'].'</th>',$indent);
+            ptln('        <th class="username">'.$this->lang['user_name'].'</th>',$indent);
+            ptln('        <th class="usermail">'.$this->lang['user_mail'].'</th>',$indent);
+            ptln('        <th class="usergroups">'.$this->lang['user_groups'].'</th>',$indent);
+            ptln('      </tr>',$indent);
+            ptln('    </thead>',$indent);
+            ptln('    <tbody>',$indent);
+            foreach ($this->_import_failures as $line => $failure) {
+                ptln('      <tr>',$indent);
+                ptln('        <td class="lineno"> '.sprintf('%0'.$digits.'d',$line).' </td>',$indent);
+                ptln('        <td class="error">' .$failure['error'].' </td>', $indent);
+                ptln('        <td class="field userid"> '.hsc($failure['user'][0]).' </td>',$indent);
+                ptln('        <td class="field username"> '.hsc($failure['user'][2]).' </td>',$indent);
+                ptln('        <td class="field usermail"> '.hsc($failure['user'][3]).' </td>',$indent);
+                ptln('        <td class="field usergroups"> '.hsc($failure['user'][4]).' </td>',$indent);
+                ptln('      </tr>',$indent);
+            }
+            ptln('    </tbody>',$indent);
+            ptln('  </table>',$indent);
+            ptln('  <p><a href="'.$failure_download_link.'">Download Failures as CSV for correction</a></p>');
+            ptln('</div>');
+        }
+
+    }
+
     function _addUser(){
         global $INPUT;
         if (!checkSecurityToken()) return false;
@@ -666,4 +731,126 @@ class admin_plugin_usermanager extends DokuWiki_Admin_Plugin {
         fclose($fd);
         die;
     }
+
+    /*
+     * import a file of users in csv format
+     *
+     * csv file should have 4 columns, user_id, full name, email, groups (comma separated)
+     */
+    function _import() {
+        // check we are allowed to add users
+        if (!checkSecurityToken()) return false;
+        if (!$this->_auth->canDo('addUser')) return false;
+
+        // check file uploaded ok.
+        if (empty($_FILES['import']['size']) || !empty($FILES['import']['error']) && is_uploaded_file($FILES['import']['tmp_name'])) {
+            msg($this->lang['import_error_upload'],-1);
+            return false;
+        }
+        // retrieve users from the file
+        $this->_import_failures = array();
+        $import_success_count = 0;
+        $import_fail_count = 0;
+        $line = 0;
+        $fd = fopen($_FILES['import']['tmp_name'],'r');
+        if ($fd) {
+            while($csv = fgets($fd)){
+                $raw = str_getcsv($csv);
+                $error = '';                        // clean out any errors from the previous line
+                // data checks...
+                if (1 == ++$line) {
+                    if ($raw[0] == 'user_id' || $raw[0] == $this->lang['user_id']) continue;    // skip headers
+                }
+                if (count($raw) < 4) {                                        // need at least four fields
+                    $import_fail_count++;
+                    $error = sprintf($this->lang['import_error_fields'], count($raw));
+                    $this->_import_failures[$line] = array('error' => $error, 'user' => $raw, 'orig' => $csv);
+                    continue;
+                }
+                array_splice($raw,1,0,auth_pwgen());                          // splice in a generated password
+                $clean = $this->_cleanImportUser($raw, $error);
+                if ($clean && $this->_addImportUser($clean, $error)) {
+#                    $this->_notifyUser($clean[0],$clean[1]);
+                    $import_success_count++;
+                } else {
+                    $import_fail_count++;
+                    $this->_import_failures[$line] = array('error' => $error, 'user' => $raw, 'orig' => $csv);
+                }
+            }
+            msg(sprintf($this->lang['import_success_count'], ($import_success_count+$import_fail_count), $import_success_count),($import_success_count ? 1 : -1));
+            if ($import_fail_count) {
+                msg(sprintf($this->lang['import_failure_count'], $import_fail_count),-1);
+            }
+        } else {
+            msg($this->lang['import_error_readfail'],-1);
+        }
+
+        // save import failures into the session
+        if (!headers_sent()) {
+          session_start();
+          $_SESSION['import_failures'] = $this->_import_failures;
+          session_write_close();
+        }
+    }
+
+    function _cleanImportUser($candidate, & $error){
+        global $INPUT;
+
+        // kludgy ....
+        $INPUT->set('userid', $candidate[0]);
+        $INPUT->set('userpass', $candidate[1]);
+        $INPUT->set('username', $candidate[2]);
+        $INPUT->set('usermail', $candidate[3]);
+        $INPUT->set('usergroups', $candidate[4]);
+
+        $cleaned = $this->_retrieveUser();
+        list($user,$pass,$name,$mail,$grps) = $cleaned;
+        if (empty($user)) {
+            $error = $this->lang['import_error_baduserid'];
+            return false;
+        }
+
+        // no need to check password, handled elsewhere
+
+        if (!($this->_auth->canDo('modName') xor empty($name))){
+            $error = $this->lang['import_error_badname'];
+            return false;
+        }
+
+        if (!($this->_auth->canDo('modMail') xor empty($mail))){
+            $error = $this->lang['import_error_badmail'];
+            return false;
+        }
+
+        return $cleaned;
+    }
+
+    function _addImportUser($user, & $error){
+        if (!$this->_auth->triggerUserMod('create', $user)) {
+            $error = $this->lang['import_error_create'];
+            return false;
+        }
+
+        return true;
+    }
+
+    function _downloadImportFailures(){
+
+        // ==============================================================================================
+        // GENERATE OUTPUT
+        // normal headers for downloading...
+        header('Content-type: text/csv;charset=utf-8');
+        header('Content-Disposition: attachment; filename="importfails.csv"');
+#       // for debugging assistance, send as text plain to the browser
+#       header('Content-type: text/plain;charset=utf-8');
+
+        // output the csv
+        $fd = fopen('php://output','w');
+        foreach ($this->_import_failures as $line => $fail) {
+            fputs($fd, $fail['orig']);
+        }
+        fclose($fd);
+        die;
+    }
+
 }
diff --git a/lib/plugins/usermanager/lang/en/import.txt b/lib/plugins/usermanager/lang/en/import.txt
new file mode 100644
index 000000000..2087083e0
--- /dev/null
+++ b/lib/plugins/usermanager/lang/en/import.txt
@@ -0,0 +1,9 @@
+===== Bulk User Import =====
+
+Requires a CSV file of users with at least four columns.
+The columns must contain, in order: user-id, full name, email address and groups.
+The CSV fields should be separated by commas (,) and strings delimited by quotation marks ("").  Backslash (\) can be used for escaping.
+For an example of a suitable file, try the "Export Users" function above.  
+Duplicate user-ids will be ignored.
+
+A password will be generated and emailed to each successfully imported user.
diff --git a/lib/plugins/usermanager/lang/en/lang.php b/lib/plugins/usermanager/lang/en/lang.php
index ae0ee1c16..f22d1f805 100644
--- a/lib/plugins/usermanager/lang/en/lang.php
+++ b/lib/plugins/usermanager/lang/en/lang.php
@@ -33,6 +33,9 @@ $lang['clear']       = 'Reset Search Filter';
 $lang['filter']      = 'Filter';
 $lang['export_all']  = 'Export All Users (CSV)';
 $lang['export_filtered'] = 'Export Filtered User list (CSV)';
+$lang['import']      = 'Import New Users';
+$lang['line']        = 'Line no.';
+$lang['error']       = 'Error message';
 
 $lang['summary']     = 'Displaying users %1$d-%2$d of %3$d found. %4$d users total.';
 $lang['nonefound']   = 'No users found. %d users total.';
@@ -58,3 +61,15 @@ $lang['add_fail'] = 'User addition failed';
 $lang['notify_ok'] = 'Notification email sent';
 $lang['notify_fail'] = 'Notification email could not be sent';
 
+// import errors
+$lang['import_success_count'] = 'User Import: %d users found, %d imported successfully.';
+$lang['import_failure_count'] = 'User Import: %d failed. Failures are listed below.';
+$lang['import_error_fields']  = "Insufficient fields, found %d, require 4.";
+$lang['import_error_baduserid'] = "User-id missing";
+$lang['import_error_badname'] = 'Bad name';
+$lang['import_error_badmail'] = 'Bad mail';
+$lang['import_error_upload']  = 'Import Failed. The csv file could not be uploaded or is empty.';
+$lang['import_error_readfail'] = 'Import Failed. Unable to read uploaded file.';
+$lang['import_error_create']  = 'Unable to create the user';
+
+
diff --git a/lib/plugins/usermanager/style.css b/lib/plugins/usermanager/style.css
index ff8e5d9d1..61b6c2c4e 100644
--- a/lib/plugins/usermanager/style.css
+++ b/lib/plugins/usermanager/style.css
@@ -17,4 +17,7 @@
   color: #ccc!important;
   border-color: #ccc!important;
 }
+#user__manager .import_failures {
+	margin-top: 1em;
+}
 /* IE won't understand but doesn't require it */
-- 
GitLab