Skip to content
Snippets Groups Projects
Commit e093f9c0 authored by Jared Hancock's avatar Jared Hancock
Browse files

Merge branch 'develop-next' into develop

Conflicts:
	WHATSNEW.md
parents 35ce6531 d1137d2b
Branches
Tags
No related merge requests found
Showing
with 1760 additions and 355 deletions
......@@ -8,3 +8,6 @@ Vagrantfile
# Staging directory used for packaging script
stage
# Ignore packaged plugins and language packs
*.phar
osTicket v1.8.1.2
=================
* All fixes and enhancements from v1.8.0.4, plus *
### Enhancements
* Better detection of email loops (#584, #684)
### Bugs
* Fix selection of the auto-response email for a department (#666)
* Don't require current password when resetting (#671)
* Fix incorrect matchup of collaborators to users (#676)
osTicket v1.8.1.1
=================
* All fixes and enahncements from v1.8.0.4, plus *
### Enhancements
* Add signature to activity notice for staff replies (#605)
* Show company name in the copyright footer (#586)
* Signature is displayed below the staff response box (#609)
### Bugs
* Fix footnotes generated in html2text for same link text but different URLs (5e2f58d)
* Fix processing of emails for existing users (#588)
* Avoid adding an aliased system email address as a collaborator (#604, #627)
* Show current staff / user names where possible (#608)
* Fix display of _forgot my password_ link (#611)
* Export the value of custom fields (not the ID number) (#610)
* Fix saving the backend with file metadata (#595)
* Use the database as a failsafe attachment backend (#594)
* Avoid a crash when sending some mails (#589)
* Fix migrating attachments when upgrading from osTicket 1.6 (#614)
* Email templates ship with the ticket number in the subject line (#644)
* If inline images are stripped from the email, they are not considered attachments (#649, bcbebd0, 35a23be)
* Fix incorrect Content-Id headers generated for inline images (23ce0a0, e37ec74)
* New installs have the `Staff` group enabled (c7130c5)
* Always show the ticket thread when following an email link (17725ca)
### Security and Performance
* Staff can only see closed tickets if they have access via group or primary department (#623, #655)
* Fix incorrect honoring of ban list and over limit settings (#660)
* Keep existing session after login (c4bfb69)
* Fix password reset system (dfaca0d, #664)
osTicket v1.8.0.4
=================
### Enhancements
......@@ -36,6 +80,31 @@ osTicket v1.8.0.4
### Performance and Security
* Staff can only see closed tickets if they have access via group or primary department (#623, #655)
osTicket v1.8.1
===============
*All fixes and enhancements from v1.8.0.3, plus*
### Enhancements
* Ticket filters support matching on email To and Cc fields (#529)
* Popup summary and collaborator list on ticket queue page (#521)
### Bugs
* New ticket by staff adds `recipient` and `staff` context to email templates (#527)
* Forbid password reset for non-local users (#570)
* Allow an administrator to lift the force password change flag (#570)
* Locks are released on logout (#522)
* Text email footnotes are written as [title][#] (7ccbf0c)
* Fix several issues with display and download of attachments (#530)
* Fix sending a reply email if requested not to (#531)
* Only consider collaborators if the receiving system email is identified (#537)
* Do not consider `delivered-to` addresses as collaborators (#544)
* Assume `iso-8859-1` MIME body encoding if not specified (#551)
* Add new features to the storage API to implement Amazon S3 (#515)
### Performance and Security
* Support auditing login attempts (#559)
* Avoid auth strikeouts when not attempting a login (#559, #523)
osTicket v1.8.0.3
=================
### Enhancements
......@@ -75,6 +144,78 @@ osTicket v1.8.0.3
* Reuse SMTP connections where possible (#462)
* Enforce max file size for attachments sent via API (#568)
osTicket v1.8.1-rc1
===================
### Enhancements
* Much better email bounce detection (#461, #474)
* Handle messages forwarded as `message/rfc822` content type (#482)
* [Esc] key cancels popup dialogs (#465)
* Support regex matches in ticket filter (584465c)
### Bugs
* 'Priority' column is included in the ticket queue export (#451)
* Retry queries on MySQL error 1213 (#493)
* Client login email is not case-sensitive (398cbc7)
* Drop silly border on text emails if HTML ticket thread is disabled (439a32a)
* Fix ticket submission error if client is already logged in (#469)
* Fix fetching from more than 10 mail accounts (#468)
* Fix `deploy` command-line application (#450)
* Fix error email on upgrade (#452)
* Ship with a `plugins/` folder (90b0a65)
* Fix file key not replaced in thread body correctly for de-duplicated files (#492)
* Better handling of text and html thread posts (#508)
* Fix clickable links ending with punctuation (#497)
* Fix whitespace mangling of Unicode text with non-breaking-spaces (#502)
* Fix image size set to zero when images are added to drafts (#504)
* Correctly detect php-dom extension (#503)
* Fix delivery issue of emails delivered to group mailboxes (#510)
### Merged from v1.8.0.2
* Log entry for password reset attempts (#435)
osTicket v1.8.1 (Preview)
=========================
### Collaborator Support (CC)
In addition to the ticket owner, other end users can be collaborators on a
ticket. Responses received from them are integrated automatically into the
ticket thread, and emails are sent to all collaborators when new messages and
responses arrive into the system. All collaborators have access to the ticket
via the client portal and are able to log new messages.
### Plugin management system
osTicket supports plugins via a (currently undocumented) simple plugin API and
interface. Plugins can be written and distributed as files or unpacked via ZIP
archives, or distributed via PHP PHAR files. The plugin system is developed in
hopes of adding extensibility to osTicket without significant overhead.
Initially, two "classes" of plugins are supported: authentication, and file
storage.
### Pluggable authentication
Staff members can now be authenticated against a backend other than the
osTicket internal database. Available immediately is integration with LDAP
(RFC-2307) and Microsoft® Active Directory. The initial authentication system
also support user lookups, so when browsing for new users when creating
tickets, your directory server will be queried for users and email addresses.
### Pluggable attachment storage
Attachments can live outside the database again. You can now write or install a
plugin to store your attachments somewhere other than in your database, and
osTicket will use the backend to store and retrieve (or redirect to) your
attachments. We've initially made a plugin available to store attachments on
the filesystem and plan on adding an Amazon S3 plugin very soon.
### Internationalization, Phase 1
Select your default data on installation, and select the language preference,
as a staff member, for the help tips. You can also now select the language of
the email templates when creating a new template. The templates for that
language will be used instead of the English ones where translated versions are
available.
### Minor Enhancements
* Clients can update their profile information on the web portal
* Clients can update ticket details (if enabled)
* Custom ticket-details fields are included in ticket queue exports
osTicket v1.8.0.2
=================
### Enhancements
......
......@@ -39,5 +39,6 @@ $dispatcher = patterns('',
url_get('^help-topic/(?P<id>\d+)$', 'getClientFormsForHelpTopic')
))
);
Signal::send('ajax.client', $dispatcher);
print $dispatcher->resolve($ost->get_path_info());
?>
......@@ -17,18 +17,18 @@
require('secure.inc.php');
require_once(INCLUDE_DIR.'class.attachment.php');
//Basic checks
if(!$thisclient
|| !$_GET['id']
if(!$thisclient
|| !$_GET['id']
|| !$_GET['h']
|| !($attachment=Attachment::lookup($_GET['id']))
|| !($file=$attachment->getFile()))
die('Unknown attachment!');
//Validate session access hash - we want to make sure the link is FRESH! and the user has access to the parent ticket!!
$vhash=md5($attachment->getFileId().session_id().$file->getHash());
if(strcasecmp(trim($_GET['h']),$vhash)
|| !($ticket=$attachment->getTicket())
|| !$ticket->checkClientAccess($thisclient))
$vhash=md5($attachment->getFileId().session_id().strtolower($file->getKey()));
if(strcasecmp(trim($_GET['h']),$vhash)
|| !($ticket=$attachment->getTicket())
|| !$ticket->checkUserAccess($thisclient))
die('Unknown or invalid attachment');
//Download the file..
$file->download();
......
......@@ -89,6 +89,7 @@ class Bootstrap {
define('TICKET_LOCK_TABLE',$prefix.'ticket_lock');
define('TICKET_EVENT_TABLE',$prefix.'ticket_event');
define('TICKET_EMAIL_INFO_TABLE',$prefix.'ticket_email_info');
define('TICKET_COLLABORATOR_TABLE', $prefix.'ticket_collaborator');
define('TICKET_PRIORITY_TABLE',$prefix.'ticket_priority');
define('PRIORITY_TABLE',TICKET_PRIORITY_TABLE);
......@@ -111,6 +112,8 @@ class Bootstrap {
define('FILTER_TABLE', $prefix.'filter');
define('FILTER_RULE_TABLE', $prefix.'filter_rule');
define('PLUGIN_TABLE', $prefix.'plugin');
define('API_KEY_TABLE',$prefix.'api_key');
define('TIMEZONE_TABLE',$prefix.'timezone');
}
......@@ -169,8 +172,8 @@ class Bootstrap {
function loadCode() {
#include required files
require(INCLUDE_DIR.'class.ostsession.php');
require(INCLUDE_DIR.'class.usersession.php');
require(INCLUDE_DIR.'class.signal.php');
require(INCLUDE_DIR.'class.auth.php');
require(INCLUDE_DIR.'class.pagenate.php'); //Pagenate helper!
require(INCLUDE_DIR.'class.log.php');
require(INCLUDE_DIR.'class.crypto.php');
......
......@@ -43,11 +43,9 @@ require_once(INCLUDE_DIR.'class.dept.php');
//clear some vars
$errors=array();
$msg='';
$thisclient=$nav=null;
$nav=null;
//Make sure the user is valid..before doing anything else.
if($_SESSION['_client']['userID'] && $_SESSION['_client']['key'])
$thisclient = new ClientSession($_SESSION['_client']['userID'],$_SESSION['_client']['key']);
$thisclient = UserAuthenticationBackend::getUser();
//is the user logged in?
if($thisclient && $thisclient->getId() && $thisclient->isValid()){
$thisclient->refreshSession();
......
......@@ -102,5 +102,28 @@ class ContentAjaxAPI extends AjaxController {
return $content;
}
function getSignature($type, $id) {
global $thisstaff;
if (!$thisstaff)
Http::response(403, 'Login Required');
switch ($type) {
case 'none':
break;
case 'mine':
echo Format::viewableImages($thisstaff->getSignature());
break;
case 'dept':
if (!($dept = Dept::lookup($id)))
Http::response(404, 'No such department');
echo Format::viewableImages($dept->getSignature());
break;
default:
Http::response(400, 'Unknown signature type');
break;
}
}
}
?>
......@@ -107,7 +107,7 @@ class DraftAjaxAPI extends AjaxController {
return Http::response(500, 'Unable to attach image');
echo JsonDataEncoder::encode(array(
'content_id' => 'cid:'.$f->getHash(),
'content_id' => 'cid:'.$f->getKey(),
'filelink' => sprintf('image.php?h=%s', $f->getDownloadHash())
));
}
......
......@@ -36,7 +36,5 @@ class DynamicFormsAjaxAPI extends AjaxController {
else
$field->save();
}
}
?>
......@@ -31,11 +31,11 @@ class TicketsAjaxAPI extends AjaxController {
$limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25;
$tickets=array();
$sql='SELECT DISTINCT ticketID, email.address AS email'
$sql='SELECT DISTINCT `number`, email.address AS email'
.' FROM '.TICKET_TABLE.' ticket'
.' LEFT JOIN '.USER_TABLE.' user ON user.id = ticket.user_id'
.' LEFT JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id'
.' WHERE ticketID LIKE \''.db_input($_REQUEST['q'], false).'%\'';
.' WHERE `number` LIKE \''.db_input($_REQUEST['q'], false).'%\'';
$sql.=' AND ( staff_id='.db_input($thisstaff->getId());
......@@ -337,123 +337,192 @@ class TicketsAjaxAPI extends AjaxController {
if(!$thisstaff || !($ticket=Ticket::lookup($tid)) || !$ticket->checkStaffAccess($thisstaff))
Http::response(404, 'No such ticket');
$staff=$ticket->getStaff();
$lock=$ticket->getLock();
$error=$msg=$warn=null;
ob_start();
include STAFFINC_DIR . 'templates/ticket-preview.tmpl.php';
$resp = ob_get_contents();
ob_end_clean();
if($lock && $lock->getStaffId()==$thisstaff->getId())
$warn.='&nbsp;<span class="Icon lockedTicket">Ticket is locked by '.$lock->getStaffName().'</span>';
elseif($ticket->isOverdue())
$warn.='&nbsp;<span class="Icon overdueTicket">Marked overdue!</span>';
return $resp;
}
ob_start();
echo sprintf(
'<div style="width:500px; padding: 2px 2px 0 5px;">
<h2>%s</h2><br>',Format::htmlchars($ticket->getSubject()));
if($error)
echo sprintf('<div id="msg_error">%s</div>',$error);
elseif($msg)
echo sprintf('<div id="msg_notice">%s</div>',$msg);
elseif($warn)
echo sprintf('<div id="msg_warning">%s</div>',$warn);
echo '<table border="0" cellspacing="" cellpadding="1" width="100%" class="ticket_info">';
$ticket_state=sprintf('<span>%s</span>',ucfirst($ticket->getStatus()));
if($ticket->isOpen()) {
if($ticket->isOverdue())
$ticket_state.=' &mdash; <span>Overdue</span>';
else
$ticket_state.=sprintf(' &mdash; <span>%s</span>',$ticket->getPriority());
}
function addRemoteCollaborator($tid, $bk, $id) {
global $thisstaff;
echo sprintf('
<tr>
<th width="100">Ticket State:</th>
<td>%s</td>
</tr>
<tr>
<th>Create Date:</th>
<td>%s</td>
</tr>',$ticket_state,
Format::db_datetime($ticket->getCreateDate()));
if($ticket->isClosed()) {
echo sprintf('
<tr>
<th>Close Date:</th>
<td>%s <span class="faded">by %s</span></td>
</tr>',
Format::db_datetime($ticket->getCloseDate()),
($staff?$staff->getName():'staff')
);
} elseif($ticket->getEstDueDate()) {
echo sprintf('
<tr>
<th>Due Date:</th>
<td>%s</td>
</tr>',
Format::db_datetime($ticket->getEstDueDate()));
}
echo '</table>';
if (!($ticket=Ticket::lookup($tid))
|| !$ticket->checkStaffAccess($thisstaff))
Http::response(404, 'No such ticket');
elseif (!$bk || !$id)
Http::response(422, 'Backend and user id required');
elseif (!($backend = StaffAuthenticationBackend::getBackend($bk)))
Http::response(404, 'User not found');
$user_info = $backend->lookup($id);
$form = UserForm::getUserForm()->getForm($user_info);
$info = array();
if (!$user_info)
$info['error'] = 'Unable to find user in directory';
return self::_addcollaborator($ticket, null, $form, $info);
}
//Collaborators utils
function addCollaborator($tid, $uid=0) {
global $thisstaff;
if (!($ticket=Ticket::lookup($tid))
|| !$ticket->checkStaffAccess($thisstaff))
Http::response(404, 'No such ticket');
echo '<hr>
<table border="0" cellspacing="" cellpadding="1" width="100%" class="ticket_info">';
if($ticket->isOpen()) {
echo sprintf('
<tr>
<th width="100">Assigned To:</th>
<td>%s</td>
</tr>',$ticket->isAssigned()?implode('/', $ticket->getAssignees()):' <span class="faded">&mdash; Unassigned &mdash;</span>');
$user = $uid? User::lookup($uid) : null;
//If not a post then assume new collaborator form
if(!$_POST)
return self::_addcollaborator($ticket, $user);
$user = $form = null;
if (isset($_POST['id']) && $_POST['id']) { //Existing user/
$user = User::lookup($_POST['id']);
} else { //We're creating a new user!
$form = UserForm::getUserForm()->getForm($_POST);
$user = User::fromForm($form);
}
$errors = $info = array();
if ($user) {
if ($user->getId() == $ticket->getOwnerId())
$errors['err'] = sprintf('Ticket owner, %s, is a collaborator by default!',
$user->getName());
elseif (($c=$ticket->addCollaborator($user,
array('isactive'=>1), $errors))) {
$note = Format::htmlchars(sprintf('%s <%s> added as a collaborator',
$c->getName(), $c->getEmail()));
$ticket->logNote('New Collaborator Added', $note,
$thisstaff, false);
$info = array('msg' => sprintf('%s added as a collaborator',
$c->getName()));
return self::_collaborators($ticket, $info);
}
}
echo sprintf(
' <tr>
<th width="100">Department:</th>
<td>%s</td>
</tr>
<tr>
<th>Help Topic:</th>
<td>%s</td>
</tr>
<tr>
<th>From:</th>
<td>%s <span class="faded">%s</span></td>
</tr>',
Format::htmlchars($ticket->getDeptName()),
Format::htmlchars($ticket->getHelpTopic()),
Format::htmlchars($ticket->getName()),
$ticket->getEmail());
echo '
</table>';
$options = array();
$options[]=array('action'=>'Thread ('.$ticket->getThreadCount().')','url'=>"tickets.php?id=$tid");
if($ticket->getNumNotes())
$options[]=array('action'=>'Notes ('.$ticket->getNumNotes().')','url'=>"tickets.php?id=$tid#notes");
if($ticket->isOpen())
$options[]=array('action'=>'Reply','url'=>"tickets.php?id=$tid#reply");
if($thisstaff->canAssignTickets())
$options[]=array('action'=>($ticket->isAssigned()?'Reassign':'Assign'),'url'=>"tickets.php?id=$tid#assign");
if($thisstaff->canTransferTickets())
$options[]=array('action'=>'Transfer','url'=>"tickets.php?id=$tid#transfer");
$options[]=array('action'=>'Post Note','url'=>"tickets.php?id=$tid#note");
if($thisstaff->canEditTickets())
$options[]=array('action'=>'Edit Ticket','url'=>"tickets.php?id=$tid&a=edit");
if($options) {
echo '<ul class="tip_menu">';
foreach($options as $option)
echo sprintf('<li><a href="%s">%s</a></li>',$option['url'],$option['action']);
echo '</ul>';
if($errors && $errors['err']) {
$info +=array('error' => $errors['err']);
} else {
$info +=array('error' =>'Unable to add collaborator - try again');
}
echo '</div>';
return self::_addcollaborator($ticket, $user, $form, $info);
}
function updateCollaborator($cid) {
global $thisstaff;
if(!($c=Collaborator::lookup($cid))
|| !($user=$c->getUser())
|| !($ticket=$c->getTicket())
|| !$ticket->checkStaffAccess($thisstaff)
)
Http::response(404, 'Unknown collaborator');
$errors = array();
if(!$user->updateInfo($_POST, $errors))
return self::_collaborator($c ,$user->getForms($_POST), $errors);
$info = array('msg' => sprintf('%s updated successfully',
$c->getName()));
return self::_collaborators($ticket, $info);
}
function viewCollaborator($cid) {
global $thisstaff;
if(!($collaborator=Collaborator::lookup($cid))
|| !($ticket=$collaborator->getTicket())
|| !$ticket->checkStaffAccess($thisstaff))
Http::response(404, 'Unknown collaborator');
return self::_collaborator($collaborator);
}
function showCollaborators($tid) {
global $thisstaff;
if(!($ticket=Ticket::lookup($tid))
|| !$ticket->checkStaffAccess($thisstaff))
Http::response(404, 'No such ticket');
if($ticket->getCollaborators())
return self::_collaborators($ticket);
return self::_addcollaborator($ticket);
}
function previewCollaborators($tid) {
global $thisstaff;
if (!($ticket=Ticket::lookup($tid))
|| !$ticket->checkStaffAccess($thisstaff))
Http::response(404, 'No such ticket');
ob_start();
include STAFFINC_DIR . 'templates/collaborators-preview.tmpl.php';
$resp = ob_get_contents();
ob_end_clean();
return $resp;
}
function _addcollaborator($ticket, $user=null, $form=null, $info=array()) {
$info += array(
'title' => sprintf('Ticket #%s: Add a collaborator', $ticket->getNumber()),
'action' => sprintf('#tickets/%d/add-collaborator', $ticket->getId()),
'onselect' => sprintf('ajax.php/tickets/%d/add-collaborator/', $ticket->getId()),
);
return self::_userlookup($user, $form, $info);
}
function updateCollaborators($tid) {
global $thisstaff;
if(!($ticket=Ticket::lookup($tid))
|| !$ticket->checkStaffAccess($thisstaff))
Http::response(404, 'No such ticket');
$errors = $info = array();
if ($ticket->updateCollaborators($_POST, $errors))
Http::response(201, sprintf('Recipients (%d of %d)',
$ticket->getNumActiveCollaborators(),
$ticket->getNumCollaborators()));
if($errors && $errors['err'])
$info +=array('error' => $errors['err']);
return self::_collaborators($ticket, $info);
}
function _collaborator($collaborator, $form=null, $info=array()) {
$info += array('action' => '#collaborators/'.$collaborator->getId());
$user = $collaborator->getUser();
ob_start();
include(STAFFINC_DIR . 'templates/user.tmpl.php');
$resp = ob_get_contents();
ob_end_clean();
return $resp;
}
function _collaborators($ticket, $info=array()) {
ob_start();
include(STAFFINC_DIR . 'templates/collaborators.tmpl.php');
$resp = ob_get_contents();
ob_end_clean();
......@@ -469,7 +538,7 @@ class TicketsAjaxAPI extends AjaxController {
Http::response(404, 'No such ticket');
if(!($user = $ticket->getOwner()))
if(!($user = User::lookup($ticket->getOwnerId())))
Http::response(404, 'Unknown user');
......@@ -493,7 +562,7 @@ class TicketsAjaxAPI extends AjaxController {
if(!$thisstaff
|| !($ticket=Ticket::lookup($tid))
|| !$ticket->checkStaffAccess($thisstaff)
|| ! ($user = $ticket->getOwner()))
|| !($user = User::lookup($ticket->getOwnerId())))
Http::response(404, 'No such ticket/user');
$errors = array();
......@@ -523,12 +592,17 @@ class TicketsAjaxAPI extends AjaxController {
Http::response(404, 'No such ticket');
$user = $ticket->getOwner();
$user = User::lookup($ticket->getOwnerId());
$info = array(
'title' => sprintf('Change user for ticket #%s', $ticket->getNumber())
);
return self::_userlookup($user, null, $info);
}
function _userlookup($user, $form, $info) {
ob_start();
include(STAFFINC_DIR . 'templates/user-lookup.tmpl.php');
$resp = ob_get_contents();
......@@ -537,6 +611,5 @@ class TicketsAjaxAPI extends AjaxController {
}
}
?>
......@@ -20,8 +20,13 @@ if(!defined('INCLUDE_DIR')) die('!');
require_once(INCLUDE_DIR.'class.i18n.php');
class HelpTipAjaxAPI extends AjaxController {
function getTipsJson($namespace, $lang='en_US') {
global $ost;
function getTipsJson($namespace, $lang=false) {
global $ost, $thisstaff;
if (!$lang)
$lang = ($thisstaff)
? $thisstaff->getLanguage()
: Internationalization::getDefaultLanguage();
$i18n = new Internationalization($lang);
$tips = $i18n->getTemplate("help/tips/$namespace.yaml");
......
......@@ -30,6 +30,17 @@ class UsersAjaxAPI extends AjaxController {
$limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25;
$users=array();
$emails=array();
foreach (StaffAuthenticationBackend::searchUsers($_REQUEST['q']) as $u) {
$name = "{$u['first']} {$u['last']}";
$users[] = array('email' => $u['email'], 'name'=>$name,
'info' => "{$u['email']} - $name (remote)",
'id' => "auth:".$u['id'], "/bin/true" => $_REQUEST['q']);
$emails[] = $u['email'];
}
$remote_emails = ($emails = array_filter($emails))
? ' OR email.address IN ('.implode(',',db_input($emails)).') '
: '';
$escaped = db_input(strtolower($_REQUEST['q']), false);
$sql='SELECT DISTINCT user.id, email.address, name '
......@@ -39,19 +50,25 @@ class UsersAjaxAPI extends AjaxController {
LEFT JOIN '.FORM_ANSWER_TABLE.' value ON (value.entry_id=entry.id) '
.' WHERE email.address LIKE \'%'.$escaped.'%\'
OR user.name LIKE \'%'.$escaped.'%\'
OR value.value LIKE \'%'.$escaped.'%\'
ORDER BY user.created '
OR value.value LIKE \'%'.$escaped.'%\''.$remote_emails
.' ORDER BY user.created '
.' LIMIT '.$limit;
if(($res=db_query($sql)) && db_num_rows($res)){
while(list($id,$email,$name)=db_fetch_row($res)) {
foreach ($users as $i=>$u) {
if ($u['email'] == $email) {
unset($users[$i]);
break;
}
}
$name = Format::htmlchars($name);
$users[] = array('email'=>$email, 'name'=>$name, 'info'=>"$email - $name",
"id" => $id, "/bin/true" => $_REQUEST['q']);
}
}
return $this->json_encode($users);
return $this->json_encode(array_values($users));
}
......@@ -99,27 +116,34 @@ class UsersAjaxAPI extends AjaxController {
function addUser() {
$valid = true;
$form = UserForm::getUserForm()->getForm($_POST);
if (!$form->isValid())
$valid = false;
if (($field=$form->getField('email'))
&& $field->getClean()
&& User::lookup(array('emails__address'=>$field->getClean()))) {
$field->addError('Email is assigned to another user');
$valid = false;
}
if ($valid && ($user = User::fromForm($form->getClean())))
if (($user = User::fromForm($form)))
Http::response(201, $user->to_json());
$info = array('error' =>'Error adding user - try again!');
return self::_lookupform($form, $info);
}
function addRemoteUser($bk, $id) {
global $thisstaff;
if (!$thisstaff)
Http::response(403, 'Login Required');
elseif (!$bk || !$id)
Http::response(422, 'Backend and user id required');
elseif (!($backend = StaffAuthenticationBackend::getBackend($bk)))
Http::response(404, 'User not found');
$user_info = $backend->lookup($id);
$form = UserForm::getUserForm()->getForm($user_info);
$info = array('title' => 'Import Remote User');
if (!$user_info)
$info['error'] = 'Unable to find user in directory';
include(STAFFINC_DIR . 'templates/user-lookup.tmpl.php');
}
function getLookupForm() {
return self::_lookupform();
}
......@@ -151,5 +175,26 @@ class UsersAjaxAPI extends AjaxController {
return $resp;
}
function searchStaff() {
global $thisstaff;
if (!$thisstaff)
Http::response(403, 'Login required for searching');
elseif (!$thisstaff->isAdmin())
Http::response(403,
'Administrative privilege is required for searching');
elseif (!isset($_REQUEST['q']))
Http::response(400, 'Query argument is required');
$users = array();
foreach (StaffAuthenticationBackend::allRegistered() as $ab) {
if (!$ab instanceof AuthDirectorySearch)
continue;
foreach ($ab->search($_REQUEST['q']) as $u)
$users[] = $u;
}
return $this->json_encode($users);
}
}
?>
......@@ -38,8 +38,11 @@ class TicketApiController extends ApiController {
if(!strcasecmp($format, 'email')) {
$supported = array_merge($supported, array('header', 'mid',
'emailId', 'ticketId', 'reply-to', 'reply-to-name',
'in-reply-to', 'references', 'thread-type'));
'emailId', 'to-email-id', 'ticketId', 'reply-to', 'reply-to-name',
'in-reply-to', 'references', 'thread-type',
'recipients' => array('*' => array('name', 'email', 'source'))
));
$supported['attachments']['*'][] = 'cid';
}
......@@ -103,7 +106,7 @@ class TicketApiController extends ApiController {
if(!$ticket)
return $this->exerr(500, "Unable to create new ticket: unknown error");
$this->response(201, $ticket->getExtId());
$this->response(201, $ticket->getNumber());
}
/* private helper functions */
......
......@@ -337,20 +337,12 @@ class ApiXmlDataParser extends XmlDataParser {
}
if (isset($value['encoding']))
$value['body'] = Format::utf8encode($value['body'], $value['encoding']);
// HTML-ize text if html is enabled
if ($cfg->isHtmlThreadEnabled()
&& (!isset($value['type'])
|| strcasecmp($value['type'], 'text/html')))
$value = sprintf('<pre>%s</pre>',
Format::htmlchars($value['body']));
// Text-ify html if html is disabled
elseif (!$cfg->isHtmlThreadEnabled()
&& !strcasecmp($value['type'], 'text/html'))
$value = Format::html2text(Format::safe_html(
$value['body']), 100, false);
// Noop if they content-type matches the html setting
if (!strcasecmp($value['type'], 'text/html'))
$value = new HtmlThreadBody($value['body']);
else
$value = $value['body'];
$value = new TextThreadBody($value['body']);
} else if ($key == "attachments") {
if(!isset($value['file'][':text']))
$value = $value['file'];
......@@ -390,11 +382,12 @@ class ApiJsonDataParser extends JsonDataParser {
} elseif ($key == "message") {
// Allow message specified in RFC 2397 format
$data = Format::parseRfc2397($value, 'utf-8');
if (!isset($data['type']) || $data['type'] != 'text/html')
$value = sprintf('<pre>%s</pre>',
Format::htmlchars($data['data']));
if (isset($data['type']) && $data['type'] == 'text/html')
$value = new HtmlThreadBody($data['data']);
else
$value = $data['data'];
$value = new TextThreadBody($data['data']);
} else if ($key == "attachments") {
foreach ($value as &$info) {
$data = reset($info);
......@@ -408,10 +401,8 @@ class ApiJsonDataParser extends JsonDataParser {
}
unset($info);
}
if (is_array($value)) {
$value = $this->fixup($value);
}
}
unset($value);
return $current;
}
......@@ -432,9 +423,6 @@ class ApiEmailDataParser extends EmailDataParser {
$data['source'] = 'Email';
if(!$data['message'])
$data['message'] = '--';
if(!$data['subject'])
$data['subject'] = '[No Subject]';
......
......@@ -86,7 +86,7 @@ class Attachment {
function getIdByFileHash($hash, $tid=0) {
$sql='SELECT attach_id FROM '.TICKET_ATTACHMENT_TABLE.' a '
.' INNER JOIN '.FILE_TABLE.' f ON(f.id=a.file_id) '
.' WHERE f.hash='.db_input($hash);
.' WHERE f.`key`='.db_input($hash);
if($tid)
$sql.=' AND a.ticket_id='.db_input($tid);
......@@ -128,7 +128,10 @@ class GenericAttachments {
.',object_id='.db_input($this->getId())
.',file_id='.db_input($fileId)
.',inline='.db_input($inline ? 1 : 0);
if (db_query($sql))
// File may already be associated with the draft (in the
// event it was deleted and re-added)
if (db_query($sql, function($errno) { return $errno != 1062; })
|| db_errno() == 1062)
$i[] = $fileId;
}
}
......@@ -157,7 +160,7 @@ class GenericAttachments {
function _getList($separate=false, $inlines=false) {
if(!isset($this->attachments)) {
$this->attachments = array();
$sql='SELECT f.id, f.size, f.hash, f.name, a.inline '
$sql='SELECT f.id, f.size, f.`key`, f.name, a.inline '
.' FROM '.FILE_TABLE.' f '
.' INNER JOIN '.ATTACHMENT_TABLE.' a ON(f.id=a.file_id) '
.' WHERE a.`type`='.db_input($this->getType())
......@@ -171,7 +174,6 @@ class GenericAttachments {
$attachments = array();
foreach ($this->attachments as $a) {
if ($a['inline'] != $separate || $a['inline'] == $inlines) {
$a['key'] = md5($a['id'].session_id().$a['hash']);
$a['file_id'] = $a['id'];
$attachments[] = $a;
}
......
This diff is collapsed.
<?php
/*
* Base32 encoder/decoder
*
* Jared Hancock <jared@osticket.com>
* Copyright (c) osTicket.com
*/
class Base32 {
/**
* encode a binary string
*
* @param $inString Binary string to base32 encode
* @return $outString Base32 encoded $inString
*
* Original code from
* http://www.phpkode.com/source/p/moodle/moodle/lib/base32.php. Optimized
* to double performance
*/
function encode($inString)
{
$outString = "";
$compBits = "";
static $BASE32_TABLE = array(
'00000' => 'a', '00001' => 'b', '00010' => 'c', '00011' => 'd',
'00100' => 'e', '00101' => 'f', '00110' => 'g', '00111' => 'h',
'01000' => 'i', '01001' => 'j', '01010' => 'k', '01011' => 'l',
'01100' => 'm', '01101' => 'n', '01110' => 'o', '01111' => 'p',
'10000' => 'q', '10001' => 'r', '10010' => 's', '10011' => 't',
'10100' => 'u', '10101' => 'v', '10110' => 'w', '10111' => 'x',
'11000' => 'y', '11001' => 'z', '11010' => '0', '11011' => '1',
'11100' => '2', '11101' => '3', '11110' => '4', '11111' => '5');
/* Turn the compressed string into a string that represents the bits as 0 and 1. */
for ($i = 0, $k = strlen($inString); $i < $k; $i++) {
$compBits .= str_pad(decbin(ord($inString[$i])), 8, '0', STR_PAD_LEFT);
}
/* Pad the value with enough 0's to make it a multiple of 5 */
if ((($len = strlen($compBits)) % 5) != 0) {
$compBits = str_pad($compBits, $len+(5-($len % 5)), '0', STR_PAD_RIGHT);
}
/* Create an array by chunking it every 5 chars */
$fiveBitsArray = str_split($compBits, 5);
/* Look-up each chunk and add it to $outstring */
foreach ($fiveBitsArray as $fiveBitsString) {
$outString .= $BASE32_TABLE[$fiveBitsString];
}
return $outString;
}
/**
* decode to a binary string
*
* @param $inString String to base32 decode
*
* @return $outString Base32 decoded $inString
*
* @access private
*
*/
function decode($inString) {
/* declaration */
$deCompBits = '';
$outString = '';
static $BASE32_TABLE = array(
'a' => '00000', 'b' => '00001', 'c' => '00010', 'd' => '00011',
'e' => '00100', 'f' => '00101', 'g' => '00110', 'h' => '00111',
'i' => '01000', 'j' => '01001', 'k' => '01010', 'l' => '01011',
'm' => '01100', 'n' => '01101', 'o' => '01110', 'p' => '01111',
'q' => '10000', 'r' => '10001', 's' => '10010', 't' => '10011',
'u' => '10100', 'v' => '10101', 'w' => '10110', 'x' => '10111',
'y' => '11000', 'z' => '11001', '0' => '11010', '1' => '11011',
'2' => '11100', '3' => '11101', '4' => '11110', '5' => '11111');
/* Step 1 */
$inputCheck = strlen($inString) % 8;
if(($inputCheck == 1)||($inputCheck == 3)||($inputCheck == 6)) {
trigger_error('input to Base32Decode was a bad mod length: '.$inputCheck);
return false;
}
/* $deCompBits is a string that represents the bits as 0 and 1.*/
for ($i = 0, $k = strlen($inString); $i < $k; $i++) {
$inChar = $inString[$i];
if(isset($BASE32_TABLE[$inChar])) {
$deCompBits .= $BASE32_TABLE[$inChar];
} else {
trigger_error('input to Base32Decode had a bad character: '.$inChar);
return false;
}
}
/* Break the decompressed string into octets for returning */
foreach (str_split($deCompBits, 8) as $chunk) {
if (strlen($chunk) != 8) {
// Ensure correct padding
if (substr_count($chunk, '1')>0) {
trigger_error('found non-zero padding in Base32Decode');
return false;
}
break;
}
$outString .= chr(bindec($chunk));
}
return $outString;
}
}
?>
......@@ -2,10 +2,7 @@
/*********************************************************************
class.client.php
Handles everything about client
XXX: Please note that osTicket uses email address and ticket ID to authenticate the user*!
Client is modeled on the info of the ticket used to login .
Handles everything about EndUser
Peter Rotich <peter@osticket.com>
Copyright (c) 2006-2013 osTicket
......@@ -16,112 +13,204 @@
vim: expandtab sw=4 ts=4 sts=4:
**********************************************************************/
abstract class TicketUser {
static private $token_regex = '/^(?P<type>\w{1})(?P<algo>\d+)x(?P<hash>.*)$/i';
class Client {
protected $user;
var $id;
var $fullname;
var $username;
var $email;
function __construct($user) {
$this->user = $user;
}
var $_answers;
function __call($name, $args) {
global $cfg;
var $ticket_id;
var $user_id;
var $ticketID;
$rv = null;
if($this->user && is_callable(array($this->user, $name)))
$rv = $args
? call_user_func_array(array($this->user, $name), $args)
: call_user_func(array($this->user, $name));
var $ht;
if ($rv) return $rv;
$tag = substr($name, 3);
switch (strtolower($tag)) {
case 'ticket_link':
return sprintf('%s/view.php?auth=%s',
$cfg->getBaseUrl(),
urlencode($this->getAuthToken()));
break;
}
return false;
function Client($id, $email=null) {
$this->id =0;
$this->load($id,$email);
}
function load($id=0, $email=null) {
function sendAccessLink() {
global $ost;
if(!$id && !($id=$this->getId()))
return false;
if (!($ticket = $this->getTicket())
|| !($dept = $ticket->getDept())
|| !($email = $dept->getAutoRespEmail())
|| !($tpl = $dept->getTemplate()->getMsgTemplate('user.accesslink')))
return;
$sql='SELECT ticket.ticket_id, ticketID, email.address as email, user.id as user_id '
.' FROM '.TICKET_TABLE.' ticket '
.' LEFT JOIN '.USER_TABLE.' user ON user.id = ticket.user_id'
.' LEFT JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id'
.' WHERE ticketID='.db_input($id);
$vars = array(
'url' => $ost->getConfig()->getBaseUrl(),
'ticket' => $this->getTicket(),
'recipient' => $this);
if($email)
$sql.=' AND email.address = '.db_input($email);
$msg = $ost->replaceTemplateVariables($tpl->asArray(), $vars);
$email->send($this->getEmail(), $msg['subj'], $msg['body']);
}
if(!($res=db_query($sql)) || !db_num_rows($res))
return NULL;
protected function getAuthToken($algo=1) {
$this->ht = db_fetch_array($res);
$this->id = $this->ht['ticketID']; //placeholder
$this->ticket_id = $this->ht['ticket_id'];
$this->ticketID = $this->ht['ticketID'];
//Format: // <user type><algo id used>x<pack of uid & tid><hash of the algo>
$authtoken = sprintf('%s%dx%s',
($this->isOwner() ? 'o' : 'c'),
$algo,
Base32::encode(pack('VV',$this->getId(), $this->getTicketId())));
$user = User::lookup($this->ht['user_id']);
$this->user_id = $this->ht['user_id'];
$this->fullname = $user->getFullName();
switch($algo) {
case 1:
$authtoken .= substr(base64_encode(
md5($this->getId().$this->getTicket()->getCreateDate().$this->getTicketId().SECRET_SALT, true)), 8);
break;
default:
return null;
}
$this->username = $this->ht['email'];
$this->email = $this->ht['email'];
return $authtoken;
}
static function lookupByToken($token) {
//Expecting well formatted token see getAuthToken routine for details.
$matches = array();
if (!preg_match(static::$token_regex, $token, $matches))
return null;
//Unpack the user and ticket ids
$matches +=unpack('Vuid/Vtid',
Base32::decode(strtolower(substr($matches['hash'], 0, 13))));
$user = null;
switch ($matches['type']) {
case 'c': //Collaborator c
if (($user = Collaborator::lookup($matches['uid']))
&& $user->getTicketId() != $matches['tid'])
$user = null;
break;
case 'o': //Ticket owner
if (($ticket = Ticket::lookup($matches['tid']))) {
if (($user = $ticket->getOwner())
&& $user->getId() != $matches['uid'])
$user = null;
}
break;
}
$this->stats = array();
if (!$user
|| !$user instanceof TicketUser
|| strcasecmp($user->getAuthToken($matches['algo']), $token))
return false;
return($this->id);
return $user;
}
function loadDynamicData() {
$this->_answers = array();
foreach (DynamicFormEntry::forClient($this->getId()) as $form)
foreach ($form->getAnswers() as $answer)
$this->_answers[$answer->getField()->get('name')] =
$answer->getValue();
static function lookupByEmail($email) {
if (!($user=User::lookup(array('emails__address' => $email))))
return null;
return new EndUser($user);
}
function reload() {
return $this->load();
function isOwner() {
return ($this->user
&& $this->user->getId() == $this->getTicket()->getOwnerId());
}
function isClient() {
return TRUE;
abstract function getTicketId();
abstract function getTicket();
}
class TicketOwner extends TicketUser {
protected $ticket;
function __construct($user, $ticket) {
parent::__construct($user);
$this->ticket = $ticket;
}
function getId() {
return $this->id;
function getTicket() {
return $this->ticket;
}
function getUserId() {
return $this->user_id;
function getTicketId() {
return $this->ticket->getId();
}
}
/*
* Decorator class for authenticated user
*
*/
function getEmail() {
return $this->email;
class EndUser extends AuthenticatedUser {
protected $user;
function __construct($user) {
$this->user = $user;
}
function getUserName() {
return $this->username;
/*
* Delegate calls to the user
*/
function __call($name, $args) {
if(!$this->user
|| !is_callable(array($this->user, $name)))
return false;
return $args
? call_user_func_array(array($this->user, $name), $args)
: call_user_func(array($this->user, $name));
}
function getName() {
return $this->fullname;
function getId() {
//We ONLY care about user ID at the ticket level
if ($this->user instanceof Collaborator)
return $this->user->getUserId();
return $this->user->getId();
}
function getPhone() {
return $this->_answers['phone'];
function getUserName() {
//XXX: Revisit when real usernames are introduced or when email
// requirement is removed.
return $this->user->getEmail();
}
function getTicketID() {
return $this->ticketID;
function getRole() {
return $this->isOwner() ? 'owner' : 'collaborator';
}
function getAuthBackend() {
list($authkey,) = explode(':', $this->getAuthKey());
return UserAuthenticationBackend::getBackend($authkey);
}
function getTicketStats() {
if(!$this->stats['tickets'])
$this->stats['tickets'] = Ticket::getClientStats($this->getEmail());
if (!isset($this->ht['stats']))
$this->ht['stats'] = $this->getStats();
return $this->stats['tickets'];
return $this->ht['stats'];
}
function getNumTickets() {
......@@ -136,114 +225,21 @@ class Client {
return ($stats=$this->getTicketStats())?$stats['closed']:0;
}
/* ------------- Static ---------------*/
function getLastTicketIdByEmail($email) {
$sql='SELECT ticket.ticketID FROM '.TICKET_TABLE.' ticket '
.' LEFT JOIN '.USER_TABLE.' user ON user.id = ticket.user_id'
.' LEFT JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id'
.' WHERE email.address = '.db_input($email)
.' ORDER BY ticket.created '
.' LIMIT 1';
if(($res=db_query($sql)) && db_num_rows($res))
list($tid) = db_fetch_row($res);
return $tid;
}
function lookup($id, $email=null) {
return ($id && is_numeric($id) && ($c=new Client($id,$email)) && $c->getId()==$id)?$c:null;
}
private function getStats() {
function lookupByEmail($email) {
return (($id=self::getLastTicketIdByEmail($email)))?self::lookup($id, $email):null;
}
/* static */ function login($ticketID, $email, $auth=null, &$errors=array()) {
global $ost;
$cfg = $ost->getConfig();
$auth = trim($auth);
$email = trim($email);
$ticketID = trim($ticketID);
# Only consider auth token for GET requests, and for GET requests,
# REQUIRE the auth token
$auto_login = ($_SERVER['REQUEST_METHOD'] == 'GET');
//Check time for last max failed login attempt strike.
if($_SESSION['_client']['laststrike']) {
if((time()-$_SESSION['_client']['laststrike'])<$cfg->getClientLoginTimeout()) {
$errors['login'] = 'Excessive failed login attempts';
$errors['err'] = 'You\'ve reached maximum failed login attempts allowed. Try again later or <a href="open.php">open a new ticket</a>';
$_SESSION['_client']['laststrike'] = time(); //renew the strike.
} else { //Timeout is over.
//Reset the counter for next round of attempts after the timeout.
$_SESSION['_client']['laststrike'] = null;
$_SESSION['_client']['strikes'] = 0;
}
}
if($auto_login && !$auth)
$errors['login'] = 'Invalid method';
elseif(!$ticketID || !Validator::is_email($email))
$errors['login'] = 'Valid email and ticket number required';
//Bail out on error.
if($errors) return false;
//See if we can fetch local ticket id associated with the ID given
if(($ticket=Ticket::lookupByExtId($ticketID, $email)) && $ticket->getId()) {
//At this point we know the ticket ID is valid.
//TODO: 1) Check how old the ticket is...3 months max?? 2) Must be the latest 5 tickets??
//Check the email given.
# Require auth token for automatic logins (GET METHOD).
if (!strcasecmp($ticket->getEmail(), $email) && (!$auto_login || $auth === $ticket->getAuthToken())) {
//valid match...create session goodies for the client.
$user = new ClientSession($email,$ticket->getExtId());
$_SESSION['_client'] = array(); //clear.
$_SESSION['_client']['userID'] = $ticket->getEmail(); //Email
$_SESSION['_client']['key'] = $ticket->getExtId(); //Ticket ID --acts as password when used with email. See above.
$_SESSION['_client']['token'] = $user->getSessionToken();
$_SESSION['TZ_OFFSET'] = $cfg->getTZoffset();
$_SESSION['TZ_DST'] = $cfg->observeDaylightSaving();
$user->refreshSession(true); //set the hash.
//Log login info...
$msg=sprintf('%s/%s logged in [%s]', $ticket->getEmail(), $ticket->getExtId(), $_SERVER['REMOTE_ADDR']);
$ost->logDebug('User login', $msg);
//Regenerate session ID.
$sid=session_id(); //Current session id.
session_regenerate_id(TRUE); //get new ID.
if(($session=$ost->getSession()) && is_object($session) && $sid!=session_id())
$session->destroy($sid);
return $user;
}
}
//If we get to this point we know the login failed.
$errors['login'] = 'Invalid login';
$_SESSION['_client']['strikes']+=1;
if(!$errors && $_SESSION['_client']['strikes']>$cfg->getClientMaxLogins()) {
$errors['login'] = 'Access Denied';
$errors['err'] = 'Forgot your login info? Please <a href="open.php">open a new ticket</a>.';
$_SESSION['_client']['laststrike'] = time();
$alert='Excessive login attempts by a user.'."\n".
'Email: '.$email."\n".'Ticket#: '.$ticketID."\n".
'IP: '.$_SERVER['REMOTE_ADDR']."\n".'Time:'.date('M j, Y, g:i a T')."\n\n".
'Attempts #'.$_SESSION['_client']['strikes'];
$ost->logError('Excessive login attempts (user)', $alert, ($cfg->alertONLoginError()));
} elseif($_SESSION['_client']['strikes']%2==0) { //Log every other failed login attempt as a warning.
$alert='Email: '.$email."\n".'Ticket #: '.$ticketID."\n".'IP: '.$_SERVER['REMOTE_ADDR'].
"\n".'TIME: '.date('M j, Y, g:i a T')."\n\n".'Attempts #'.$_SESSION['_client']['strikes'];
$ost->logWarning('Failed login attempt (user)', $alert);
}
return false;
$sql='SELECT count(open.ticket_id) as open, count(closed.ticket_id) as closed '
.' FROM '.TICKET_TABLE.' ticket '
.' LEFT JOIN '.TICKET_TABLE.' open
ON (open.ticket_id=ticket.ticket_id AND open.status=\'open\') '
.' LEFT JOIN '.TICKET_TABLE.' closed
ON (closed.ticket_id=ticket.ticket_id AND closed.status=\'closed\')'
.' LEFT JOIN '.TICKET_COLLABORATOR_TABLE.' collab
ON (collab.ticket_id=ticket.ticket_id
AND collab.user_id = '.db_input($this->getId()).' )'
.' WHERE ticket.user_id = '.db_input($this->getId())
.' OR collab.user_id = '.db_input($this->getId());
return db_fetch_array(db_query($sql));
}
}
?>
<?php
/*********************************************************************
class.collaborator.php
Ticket collaborator
Peter Rotich <peter@osticket.com>
Copyright (c) 2006-2013 osTicket
http://www.osticket.com
Released under the GNU General Public License WITHOUT ANY WARRANTY.
See LICENSE.TXT for details.
vim: expandtab sw=4 ts=4 sts=4:
**********************************************************************/
require_once(INCLUDE_DIR . 'class.user.php');
require_once(INCLUDE_DIR . 'class.client.php');
class Collaborator extends TicketUser {
var $ht;
var $user;
var $ticket;
function __construct($id) {
$this->load($id);
parent::__construct($this->getUser());
}
function load($id) {
if(!$id && !($id=$this->getId()))
return;
$sql='SELECT * FROM '.TICKET_COLLABORATOR_TABLE
.' WHERE id='.db_input($id);
$this->ht = db_fetch_array(db_query($sql));
$this->ticket = null;
}
function reload() {
return $this->load();
}
function __toString() {
return Format::htmlchars(sprintf('%s <%s>', $this->getName(),
$this->getEmail()));
}
function getId() {
return $this->ht['id'];
}
function isActive() {
return ($this->ht['isactive']);
}
function getCreateDate() {
return $this->ht['created'];
}
function getTicketId() {
return $this->ht['ticket_id'];
}
function getTicket() {
if(!$this->ticket && $this->getTicketId())
$this->ticket = Ticket::lookup($this->getTicketId());
return $this->ticket;
}
function getUserId() {
return $this->ht['user_id'];
}
function getUser() {
if(!$this->user && $this->getUserId())
$this->user = User::lookup($this->getUserId());
return $this->user;
}
function remove() {
$sql='DELETE FROM '.TICKET_COLLABORATOR_TABLE
.' WHERE id='.db_input($this->getId())
.' LIMIT 1';
return (db_query($sql) && db_affected_rows());
}
static function add($info, &$errors) {
if (!$info || !$info['ticketId'] || !$info['userId'])
$errors['err'] = 'Invalid or missing information';
elseif (($c=self::lookup($info)))
$errors['err'] = sprintf('%s is already a collaborator',
$c->getName());
if ($errors) return false;
$sql='INSERT INTO '.TICKET_COLLABORATOR_TABLE
.' SET updated=NOW() '
.' ,isactive='.db_input(isset($info['isactive']) ? $info['isactive'] : 0)
.' ,ticket_id='.db_input($info['ticketId'])
.' ,user_id='.db_input($info['userId']);
if(db_query($sql) && ($id=db_insert_id()))
return self::lookup($id);
$errors['err'] = 'Unable to add collaborator. Internal error';
return false;
}
static function forTicket($tid, $criteria=array()) {
$collaborators = array();
$sql='SELECT id FROM '.TICKET_COLLABORATOR_TABLE
.' WHERE ticket_id='.db_input($tid);
if(isset($criteria['isactive']))
$sql.=' AND isactive='.db_input($criteria['isactive']);
//TODO: sort by name of the user
if(($res=db_query($sql)) && db_num_rows($res))
while(list($id)=db_fetch_row($res))
$collaborators[] = self::lookup($id);
return $collaborators;
}
static function getIdByInfo($info) {
$sql='SELECT id FROM '.TICKET_COLLABORATOR_TABLE
.' WHERE ticket_id='.db_input($info['ticketId'])
.' AND user_id='.db_input($info['userId']);
return db_result(db_query($sql));
}
static function lookup($criteria) {
$id = is_numeric($criteria)
? $criteria : self::getIdByInfo($criteria);
return ($id
&& ($c = new Collaborator($id))
&& $c->getId() == $id)
? $c : null;
}
}
?>
......@@ -47,12 +47,24 @@ class Company {
}
}
function asVar() {
function getInfo() {
return $this->getForm()->getClean();
}
function getName() {
return $this->getVar('name');
}
function getInfo() {
return $this->getForm()->getClean();
function asVar() {
return $this->getName();
}
function __toString() {
try {
if ($name = $this->getForm()->getAnswer('name'))
return $name->display();
} catch (Exception $e) {}
return '';
}
/**
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment