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.dynamic_forms.php b/include/class.dynamic_forms.php index af36795fafecf615ba9ad47f98cfc4284fb15278..c5b021c12c5437c44deb0a86ff9fe4d1cce9543e 100644 --- a/include/class.dynamic_forms.php +++ b/include/class.dynamic_forms.php @@ -166,6 +166,12 @@ class UserForm extends DynamicForm { static::$instance = static::getUserForm()->instanciate(); return static::$instance; } + + static function getNewInstance() { + $o = static::objects(); + static::$instance = $o[0]->instanciate(); + return static::$instance; + } } class TicketForm extends DynamicForm { @@ -742,7 +748,7 @@ class DynamicFormEntryAnswer extends VerySimpleModel { } function getValue() { - if (!$this->_value) + if (!$this->_value && isset($this->value)) $this->_value = $this->getField()->to_php( $this->get('value'), $this->get('value_id')); return $this->_value; diff --git a/include/class.orm.php b/include/class.orm.php index 4a6102de98027654ee6bcca7286f5df7b72952ef..0e8b3065429ef53e1a1bceb9d82dc99f6bf60ac4 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -36,10 +36,7 @@ class VerySimpleModel { $this->dirty = array(); } - function get($field) { - return $this->ht[$field]; - } - function __get($field) { + function get($field, $default=false) { if (array_key_exists($field, $this->ht)) return $this->ht[$field]; elseif (isset(static::$meta['joins'][$field])) { @@ -50,9 +47,14 @@ class VerySimpleModel { array($j['fkey'][1] => $this->ht[$j['local']])); return $v; } + if (isset($default)) + return $default; throw new OrmException(sprintf('%s: %s: Field not defined', get_class($this), $field)); } + function __get($field) { + return $this->get($field, null); + } function __isset($field) { return array_key_exists($field, $this->ht) @@ -78,7 +80,7 @@ class VerySimpleModel { $this->ht[$field] = $value; // Capture the foreign key id value $field = $j['local']; - $value = $value->{$j['fkey'][1]}; + $value = $value->get($j['fkey'][1]); // Fall through to the standard logic below } // XXX: Fully support or die if updating pk diff --git a/include/class.user.php b/include/class.user.php index f930379b8bc940b213975d32de8f7054f2aee0e4..083368fc6afd91b3147624d057dedd80400eee9b 100644 --- a/include/class.user.php +++ b/include/class.user.php @@ -103,12 +103,13 @@ class UserModel extends VerySimpleModel { return $this->org; } - function setOrganization($org) { + function setOrganization($org, $save=true) { if (!$org instanceof Organization) return false; $this->set('org', $org); - $this->save(); + if ($save) + $this->save(); return true; } @@ -146,8 +147,12 @@ class User extends UserModel { // Try and lookup by email address $user = static::lookupByEmail($vars['email']); if (!$user) { + $name = $vars['name']; + if (!$name) + list($name) = explode('@', $vars['email'], 2); + $user = User::create(array( - 'name'=>$vars['name'], + 'name'=>$name, 'created'=>new SqlFunction('NOW'), 'updated'=>new SqlFunction('NOW'), //XXX: Do plain create once the cause @@ -156,13 +161,20 @@ class User extends UserModel { )); // Is there an organization registered for this domain list($mailbox, $domain) = explode('@', $vars['email'], 2); - if ($org = Organization::forDomain($domain)) - $user->setOrganization($org); - - $user->save(true); - $user->emails->add($user->default_email); - // Attach initial custom fields - $user->addDynamicData($vars); + if (isset($vars['org_id'])) + $user->set('org_id', $vars['org_id']); + elseif ($org = Organization::forDomain($domain)) + $user->setOrganization($org, false); + + try { + $user->save(true); + $user->emails->add($user->default_email); + // Attach initial custom fields + $user->addDynamicData($vars); + } + catch (OrmException $e) { + return null; + } } return $user; @@ -203,7 +215,11 @@ class User extends UserModel { } function getName() { - return new PersonsName($this->name); + if (!$this->name) + list($name) = explode('@', $this->getDefaultEmailAddress(), 2); + else + $name = $this->name; + return new PersonsName($name); } function getUpdateDate() { @@ -240,8 +256,7 @@ class User extends UserModel { } function addDynamicData($data) { - - $uf = UserForm::getInstance(); + $uf = UserForm::getNewInstance(); $uf->setClientId($this->id); foreach ($uf->getFields() as $f) if (isset($data[$f->get('name')])) @@ -255,7 +270,7 @@ class User extends UserModel { if (!isset($this->_entries)) { $this->_entries = DynamicFormEntry::forClient($this->id)->all(); if (!$this->_entries) { - $g = UserForm::getInstance(); + $g = UserForm::getNewInstance(); $g->setClientId($this->id); $g->save(); $this->_entries[] = $g; @@ -306,6 +321,127 @@ 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, ","))) + return 'Whoops. Perhaps you meant to send some CSV records'; + + 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'; + } + } + } + } + + // '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..e02c9dbc8cd90fb1500265770e282fb1ee93e7ce 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: ' : ''; @@ -112,7 +115,12 @@ else if($res && db_num_rows($res)): $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null; while ($row = db_fetch_array($res)) { - $name = new PersonsName($row['name']); + // Default to email address mailbox if no name specified + if (!$row['name']) + list($name) = explode('@', $row['email']); + else + $name = new PersonsName($row['name']); + // Account status if ($row['account_id']) $status = new UserAccountStatus($row['status']); @@ -167,10 +175,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; diff --git a/setup/cli/modules/user.php b/setup/cli/modules/user.php index 3e5602288bebae9876ed14975c398cdf3a65f779..aef84a2123e5ecea6b4d61841516b225d0fc2a60 100644 --- a/setup/cli/modules/user.php +++ b/setup/cli/modules/user.php @@ -19,6 +19,8 @@ class UserManager extends Module { var $options = array( 'file' => array('-f', '--file', 'metavar'=>'path', 'help' => 'File or stream to process'), + 'org' => array('-O', '--org', 'metavar'=>'ORGID', + 'help' => 'Set the organization ID on import'), ); var $stream; @@ -34,27 +36,19 @@ class UserManager extends Module { elseif (!($this->stream = fopen($options['file'], 'rb'))) $this->fail("Unable to open input file [{$options['file']}]"); - //Read the header (if any) - if (($data = fgetcsv($this->stream, 1000, ","))) { - if (Validator::is_email($data[1])) - fseek($this->stream, 0); // We don't have an header! - else; - // TODO: process the header here to figure out the columns - // for now we're assuming Name, Email + $extras = array(); + if ($options['org']) { + if (!($org = Organization::lookup($options['org']))) + $this->fail($options['org'].': Unknown organization ID'); + $extras['org_id'] = $options['org']; } - - while (($data = fgetcsv($this->stream, 1000, ",")) !== FALSE) { - if (!$data[0]) - $this->stderr->write('Invalid data or format: Name - required'); - elseif (!Validator::is_email($data[1])) - $this->stderr->write('Invalid data or format: Valid - email required'); - elseif (!User::fromVars(array('name' => $data[0], 'email' => $data[1]))) - $this->stderr->write('Unable to import user: '.print_r($data, true)); - } - + $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')))