diff --git a/include/ajax.orgs.php b/include/ajax.orgs.php index 9a72c20449470d3c8ce8fa916132e063374fd862..baa7a6351d1a07fe04accbd57c28dfc4e950d8aa 100644 --- a/include/ajax.orgs.php +++ b/include/ajax.orgs.php @@ -169,6 +169,32 @@ class OrgsAjaxAPI extends AjaxController { return $resp; } + function importUsers($org_id) { + global $thisstaff; + + if (!$thisstaff) + Http::response(403, 'Login Required'); + elseif (!($org = Organization::lookup($org_id))) + Http::response(404, 'No such organization'); + + $info = array( + 'title' => 'Import Users', + 'action' => "#orgs/$org_id/import-users", + 'upload_url' => "orgs.php?a=import-users", + ); + + if ($_POST) { + $status = User::importFromPost($_POST['pasted']); + if (is_string($status)) + $info['error'] = $status; + else + Http::response(201, "{\"count\": $status}"); + } + $info += Format::input($_POST); + + include STAFFINC_DIR . 'templates/user-import.tmpl.php'; + } + function addOrg() { $info = array(); diff --git a/include/ajax.users.php b/include/ajax.users.php index 2193abf85b952d82682095be6709e38b49bbf45c..bad2bb7fe65af4c1bf022f5abdef858d77173f1d 100644 --- a/include/ajax.users.php +++ b/include/ajax.users.php @@ -248,6 +248,30 @@ class UsersAjaxAPI extends AjaxController { include(STAFFINC_DIR . 'templates/user-lookup.tmpl.php'); } + function importUsers() { + global $thisstaff; + + if (!$thisstaff) + Http::response(403, 'Login Required'); + + $info = array( + 'title' => 'Import Users', + 'action' => '#users/import', + 'upload_url' => "users.php?do=import-users", + ); + + if ($_POST) { + $status = User::importFromPost($_POST['pasted']); + if (is_string($status)) + $info['error'] = $status; + else + Http::response(201, "{\"count\": $status}"); + } + $info += Format::input($_POST); + + include STAFFINC_DIR . 'templates/user-import.tmpl.php'; + } + function getLookupForm() { return self::_lookupform(); } diff --git a/include/class.user.php b/include/class.user.php index e8ed94158018501dc1e19e8cb19cea06ef551ec3..ad599b5d07d817fa5b094edfd1561260b144afcd 100644 --- a/include/class.user.php +++ b/include/class.user.php @@ -317,6 +317,128 @@ class User extends UserModel { return UserAccount::register($this, $vars, $errors); } + static function importCsv($stream, $defaults=array()) { + //Read the header (if any) + $headers = array('name' => 'Full Name', 'email' => 'Email Address'); + $uform = UserForm::getUserForm(); + $all_fields = $uform->getFields(); + $named_fields = array(); + $has_header = true; + foreach ($all_fields as $f) + if ($f->get('name')) + $named_fields[] = $f; + + if (($data = fgetcsv($stream, 1000, ","))) { + if (Validator::is_email($data[1])) { + $has_header = false; // We don't have an header! + } + else { + $headers = array(); + foreach ($data as $h) { + $found = false; + foreach ($all_fields as $f) { + if (in_array(mb_strtolower($h), array( + mb_strtolower($f->get('name')), mb_strtolower($f->get('label'))))) { + $found = true; + if (!$f->get('name')) + return $h.': Field must have `variable` set to be imported'; + $headers[$f->get('name')] = $f->get('label'); + break; + } + } + if (!$found) { + $has_header = false; + if (count($data) == count($named_fields)) { + // Number of fields in the user form matches the number + // of fields in the data. Assume things line up + $headers = array(); + foreach ($named_fields as $f) + $headers[$f->get('name')] = $f->get('label'); + break; + } + else { + return $h.': Unable to map header to a user field'; + } + } + } + } + } + else + return 'Whoops. Perhaps you meant to send some CSV records'; + + // 'name' and 'email' MUST be in the headers + if (!isset($headers['name']) || !isset($headers['email'])) + return 'CSV file must include `name` and `email` columns'; + + if (!$has_header) + fseek($stream, 0); + + $users = $fields = $keys = array(); + foreach ($headers as $h => $label) { + if (!($f = $uform->getField($h))) + continue; + + $name = $keys[] = $f->get('name'); + $fields[$name] = $f->getImpl(); + } + + // Add default fields (org_id, etc). + foreach ($defaults as $key => $val) { + // Don't apply defaults which are also being imported + if (isset($header[$key])) + unset($defaults[$key]); + $keys[] = $key; + } + + while (($data = fgetcsv($stream, 1000, ",")) !== false) { + if (!count($data)) + // Skip empty rows + continue; + elseif (count($data) != count($headers)) + return 'Bad data. Expected: '.implode(', ', $headers); + // Validate according to field configuration + $i = 0; + foreach ($headers as $h => $label) { + $f = $fields[$h]; + $T = $f->parse($data[$i]); + if ($f->validateEntry($T) && $f->errors()) + return $label.': Invalid data: '.implode(', ', $f->errors()); + // Convert to database format + $data[$i] = $f->to_database($T); + $i++; + } + // Add default fields + foreach ($defaults as $key => $val) + $data[] = $val; + + $users[] = $data; + } + + foreach ($users as $u) { + $vars = array_combine($keys, $u); + if (!static::fromVars($vars)) + return 'Unable to import user: '.print_r($vars, true); + } + + return count($users); + } + + function importFromPost($stuff, $extra=array()) { + if (is_array($stuff) && !$stuff['error']) { + $stream = fopen($stuff['tmp_name'], 'r'); + } + elseif ($stuff) { + $stream = fopen('php://temp', 'w+'); + fwrite($stream, $stuff); + rewind($stream); + } + else { + return 'Unable to parse submitted users'; + } + + return User::importCsv($stream, $extra); + } + function updateInfo($vars, &$errors) { $valid = true; diff --git a/include/staff/templates/user-import.tmpl.php b/include/staff/templates/user-import.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..581f1cb48b3c6e191d14c99df80b7995deb16b95 --- /dev/null +++ b/include/staff/templates/user-import.tmpl.php @@ -0,0 +1,85 @@ +<div id="the-lookup-form"> +<h3><?php echo $info['title']; ?></h3> +<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b> +<hr/> +<?php +if ($info['error']) { + echo sprintf('<p id="msg_error">%s</p>', $info['error']); +} elseif ($info['warn']) { + echo sprintf('<p id="msg_warning">%s</p>', $info['warn']); +} elseif ($info['msg']) { + echo sprintf('<p id="msg_notice">%s</p>', $info['msg']); +} ?> +<ul class="tabs"> + <li><a href="#copy-paste" class="active" + ><i class="icon-edit"></i> Copy Paste</a></li> + <li><a href="#upload" + ><i class="icon-fixed-width icon-cloud-upload"></i> Upload</a></li> +</ul> +<form action="<?php echo $info['action']; ?>" method="post" enctype="multipart/form-data" + onsubmit="javascript: + if ($(this).find('[name=import]').val()) { + $(this).attr('action', '<?php echo $info['upload_url']; ?>'); + $(document).unbind('submit.dialog'); + }"> +<?php echo csrf_token(); +if ($org_id) { ?> + <input type="hidden" name="id" value="<?php echo $org_id; ?>"/> +<?php } ?> + +<div class="tab_content" id="copy-paste" style="margin:5px;"> +<h2 style="margin-bottom:10px">Name and Email</h2> +<p> +Enter one name and email address per line.<br/> +<em>To import more other fields, use the Upload tab.</em> +</p> +<textarea name="pasted" style="display:block;width:100%;height:8em" + placeholder="e.g. John Doe, john.doe@osticket.com"> +<?php echo $info['pasted']; ?> +</textarea> +</div> + +<div class="tab_content" id="upload" style="display:none;margin:5px;"> +<h2 style="margin-bottom:10px">Import a CSV File</h2> +<p> +<em>Use the columns shown in the table below. To add more fields, visit the +Admin Panel -> Manage -> Forms -> <?php echo +UserForm::getUserForm()->get('title'); ?> page to edit the available fields. +Only fields with `variable` defined can be imported.</em> +</p> +<table class="list"><tr> +<?php + $fields = array(); + $data = array( + array('name' => 'John Doe', 'email' => 'john.doe@osticket.com') + ); + foreach (UserForm::getUserForm()->getFields() as $f) + if ($f->get('name')) + $fields[] = $f->get('name'); + foreach ($fields as $f) { ?> + <th><?php echo mb_convert_case($f, MB_CASE_TITLE); ?></th> +<?php } ?> +</tr> +<?php + foreach ($data as $d) { + foreach ($fields as $f) { + ?><td><?php + if (isset($d[$f])) echo $d[$f]; + ?></td><?php + } + } ?> +</tr></table> +<br/> +<input type="file" name="import"/> +</div> + <hr> + <p class="full-width"> + <span class="buttons" style="float:left"> + <input type="reset" value="Reset"> + <input type="button" name="cancel" class="close" value="Cancel"> + </span> + <span class="buttons" style="float:right"> + <input type="submit" value="Import Users"> + </span> + </p> +</form> diff --git a/include/staff/templates/users.tmpl.php b/include/staff/templates/users.tmpl.php index f0be96907b6a7d52333c989a274ed86073cf573e..2e864633b561db33fca6262feccf28e6dc9ea8be 100644 --- a/include/staff/templates/users.tmpl.php +++ b/include/staff/templates/users.tmpl.php @@ -55,7 +55,11 @@ else ?> <div style="width:700px; float:left;"><b><?php echo $showing; ?></b></div> <div style="float:right;text-align:right;padding-right:5px;"> - <b><a href="#orgs/<?php echo $org->getId(); ?>/add-user" class="Icon newstaff add-user">Add New User</a></b></div> + <b><a href="#orgs/<?php echo $org->getId(); ?>/add-user" class="Icon newstaff add-user">Add User</a></b> + | + <b><a href="#orgs/<?php echo $org->getId(); ?>/import-users" class="add-user"> + <i class="icon-cloud-upload icon-large"></i> Import</a></b> +</div> <div class="clear"></div> <br/> <?php @@ -117,10 +121,12 @@ endif; $(function() { $(document).on('click', 'a.add-user', function(e) { e.preventDefault(); - $.userLookup('ajax.php/orgs/<?php echo $org->getId(); ?>/add-user', function (user) { - window.location.href = 'orgs.php?id=<?php echo $org->getId(); ?>' - }); - + $.userLookup('ajax.php/' + $(this).attr('href').substr(1), function (user) { + if (user && user.id) + window.location.href = 'orgs.php?id=<?php echo $org->getId(); ?>' + else + $.pjax({url: window.location.href, container: '#content'}) + }); return false; }); }); diff --git a/include/staff/users.inc.php b/include/staff/users.inc.php index 0439698f0863368fea95201d82cc424b9948a9a0..97e5862fdceff9ed155cfd43f0727c0168cb2c93 100644 --- a/include/staff/users.inc.php +++ b/include/staff/users.inc.php @@ -83,7 +83,10 @@ $query="$select $from $where GROUP BY user.id ORDER BY $order_by LIMIT ".$pageNa </form> </div> <div style="float:right;text-align:right;padding-right:5px;"> - <b><a href="#users/add" class="Icon newstaff add-user">Add New User</a></b></div> + <b><a href="#users/add" class="Icon newstaff popup-dialog">Add User</a></b> + | + <b><a href="#users/import" class="popup-dialog"><i class="icon-cloud-upload icon-large"></i> Import</a></b> +</div> <div class="clear"></div> <?php $showing = $search ? 'Search Results: ' : ''; @@ -167,10 +170,13 @@ $(function() { property: "/bin/true" }); - $(document).on('click', 'a.add-user', function(e) { + $(document).on('click', 'a.popup-dialog', function(e) { e.preventDefault(); - $.userLookup('ajax.php/users/add', function (user) { - window.location.href = 'users.php?id='+user.id; + $.userLookup('ajax.php/' + $(this).attr('href').substr(1), function (user) { + if (user && user.id) + window.location.href = 'users.php?id='+user.id; + else + $.pjax({url: window.location.href, container: '#content'}) }); return false; diff --git a/scp/ajax.php b/scp/ajax.php index d0830bdf84cf81116154d9caf9f29fd0b1272d28..fada24ab53278dfda7acf32ab6c93c4afa59769d 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -78,6 +78,7 @@ $dispatcher = patterns('', url_get('^/lookup/form$', 'getLookupForm'), url_post('^/lookup/form$', 'addUser'), url_get('^/add$', 'addUser'), + url('^/import$', 'importUsers'), url_get('^/select$', 'selectUser'), url_get('^/select/(?P<id>\d+)$', 'selectUser'), url_get('^/select/auth:(?P<bk>\w+):(?P<id>.+)$', 'addRemoteUser'), @@ -107,6 +108,7 @@ $dispatcher = patterns('', url_get('^/(?P<id>\d+)/add-user(?:/(?P<userid>\d+))?$', 'addUser'), url_get('^/(?P<id>\d+)/add-user(?:/auth:(?P<userid>.+))?$', 'addUser', array(true)), url_post('^/(?P<id>\d+)/add-user$', 'addUser'), + url('^/(?P<id>\d+)/import-users$', 'importUsers'), url_get('^/(?P<id>\d+)/delete$', 'delete'), url_delete('^/(?P<id>\d+)/delete$', 'delete') )), diff --git a/scp/orgs.php b/scp/orgs.php index 0ecb8d1892cbed3f27610139a5a6fb0d4e3a53af..bfdc267aeacd56772f5226b49ec9f4c57e543e71 100644 --- a/scp/orgs.php +++ b/scp/orgs.php @@ -14,9 +14,24 @@ **********************************************************************/ require('staff.inc.php'); $org = null; -if ($_REQUEST['id']) - $org = Organization::lookup($_REQUEST['id']); +if ($_REQUEST['id'] || $_REQUEST['org_id']) + $org = Organization::lookup($_REQUEST['org_id'] ?: $_REQUEST['id']); +if ($_POST) { + switch ($_REQUEST['a']) { + case 'import-users': + if (!$org) { + $errors['err'] = 'Organization ID must be specified for import'; + break; + } + $status = User::importFromPost($_FILES['import'] ?: $_POST['pasted'], + array('org_id'=>$org->getId())); + if (is_numeric($status)) + $msg = "Successfully imported $status clients"; + else + $errors['err'] = $status; + } +} $page = $org? 'org-view.inc.php' : 'orgs.inc.php'; $nav->setTabActive('users'); diff --git a/scp/users.php b/scp/users.php index 3020a91f19fceb60517d817382d28a1cdfb1e2e2..547d278ac52e5b3d91e7edd06c6602805d25b725 100644 --- a/scp/users.php +++ b/scp/users.php @@ -18,7 +18,7 @@ if ($_REQUEST['id'] && !($user=User::lookup($_REQUEST['id']))) $errors['err'] = 'Unknown or invalid user ID.'; if ($_POST) { - switch(strtolower($_POST['do'])) { + switch(strtolower($_REQUEST['do'])) { case 'update': if (!$user) { $errors['err']='Unknown or invalid user.'; @@ -66,6 +66,13 @@ if ($_POST) { $errors['err'] = "Coming soon!"; } break; + case 'import-users': + $status = User::importFromPost($_FILES['import'] ?: $_POST['pasted']); + if (is_numeric($status)) + $msg = "Successfully imported $status clients"; + else + $errors['err'] = $status; + break; default: $errors['err'] = 'Unknown action/command'; break;