diff --git a/apps/.htaccess b/apps/.htaccess new file mode 100644 index 0000000000000000000000000000000000000000..184048348cd306d635249908e82c174a4c0e6f60 --- /dev/null +++ b/apps/.htaccess @@ -0,0 +1,11 @@ +<IfModule mod_rewrite.c> + +RewriteEngine On + +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteCond %{REQUEST_URI} (.*/apps) + +RewriteRule ^(.*)$ %1/dispatcher.php/$1 [L] + +</IfModule> diff --git a/apps/dispatcher.php b/apps/dispatcher.php new file mode 100644 index 0000000000000000000000000000000000000000..0a3ed9d342ffea060923a11213ecd6317a62939e --- /dev/null +++ b/apps/dispatcher.php @@ -0,0 +1,32 @@ +<?php +/********************************************************************* + dispatcher.php + + Dispatcher for client applications + + Jared Hancock <jared@osticket.com> + 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: +**********************************************************************/ + +function clientLoginPage($msg='Unauthorized') { + Http::response(403,'Must login: '.Format::htmlchars($msg)); + exit; +} + +require('client.inc.php'); + +if(!defined('INCLUDE_DIR')) Http::response(500, 'Server configuration error'); +require_once INCLUDE_DIR.'/class.dispatcher.php'; + +$dispatcher = patterns('', +); + +Signal::send('ajax.client', $dispatcher); +print $dispatcher->resolve($ost->get_path_info()); diff --git a/include/ajax.forms.php b/include/ajax.forms.php index 1ec760b077e1c934ee13cffa568147eb22897344..b6e5d2903b3ea7a7f41afbb3c666a3656a550e05 100644 --- a/include/ajax.forms.php +++ b/include/ajax.forms.php @@ -50,5 +50,22 @@ class DynamicFormsAjaxAPI extends AjaxController { $ent->delete(); } + + function getListItemProperties($item_id) { + if (!($item = DynamicListItem::lookup($item_id))) + Http::response(404, 'No such list item'); + + include(STAFFINC_DIR . 'templates/list-item-properties.tmpl.php'); + } + + function saveListItemProperties($item_id) { + if (!($item = DynamicListItem::lookup($item_id))) + Http::response(404, 'No such list item'); + + if (!$item->setConfiguration()) + include(STAFFINC_DIR . 'templates/list-item-properties.tmpl.php'); + else + $item->save(); + } } ?> diff --git a/include/class.app.php b/include/class.app.php new file mode 100644 index 0000000000000000000000000000000000000000..8153c32dfcc25a7ec14fe17e3dd6ea2f9c8ad9a8 --- /dev/null +++ b/include/class.app.php @@ -0,0 +1,52 @@ +<?php +/********************************************************************* + class.app.php + + Application registration system + Apps, usually to be distributed as plugins, can register themselves + using this utility class, and navigation links will be added to the + staff and client interfaces. + + Jared Hancock <jared@osticket.com> + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2014 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: +**********************************************************************/ + +class Application { + private static $client_apps; + private static $staff_apps; + private static $admin_apps; + + function registerStaffApp($desc, $href, $info=array()) { + self::$staff_apps[] = array_merge($info, + array('desc'=>$desc, 'href'=>$href)); + } + + function getStaffApps() { + return self::$staff_apps; + } + + function registerClientApp($desc, $href, $info=array()) { + self::$client_apps[] = array_merge($info, + array('desc'=>$desc, 'href'=>$href)); + } + + function getClientApps() { + return self::$client_apps; + } + + function registerAdminApp($desc, $href, $info=array()) { + self::$admin_apps[] = array_merge($info, + array('desc'=>$desc, 'href'=>$href)); + } + + function getAdminApps() { + return self::$admin_apps; + } +} diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php index c1b790ccb46410923fcd11364539bd0ed057ad4a..af36795fafecf615ba9ad47f98cfc4284fb15278 100644 --- a/include/class.dynamic_forms.php +++ b/include/class.dynamic_forms.php @@ -765,7 +765,13 @@ class DynamicFormEntryAnswer extends VerySimpleModel { } function asVar() { - return $this->toString(); + return (is_object($this->getValue())) + ? $this->getValue() : $this->toString(); + } + + function getVar($tag) { + if (is_object($this->getValue()) && method_exists($this->getValue(), 'getVar')) + return $this->getValue()->getVar($tag); } function __toString() { @@ -789,6 +795,7 @@ class DynamicList extends VerySimpleModel { ); var $_items; + var $_form; function getSortModes() { return array( @@ -813,10 +820,17 @@ class DynamicList extends VerySimpleModel { return $this->get('name') . 's'; } + function getAllItems() { + return DynamicListItem::objects()->filter( + array('list_id'=>$this->get('id'))) + ->order_by($this->getListOrderBy()); + } + function getItems($limit=false, $offset=false) { if (!$this->_items) { $this->_items = DynamicListItem::objects()->filter( - array('list_id'=>$this->get('id'))) + array('list_id'=>$this->get('id'), + 'status__hasbit'=>DynamicListItem::ENABLED)) ->order_by($this->getListOrderBy()); if ($limit) $this->_items->limit($limit); @@ -831,6 +845,14 @@ class DynamicList extends VerySimpleModel { ->count(); } + function getConfigurationForm() { + if (!$this->_form) { + $this->_form = DynamicForm::lookup( + array('type'=>'L'.$this->get('id'))); + } + return $this->_form; + } + function save($refetch=false) { if (count($this->dirty)) $this->set('updated', new SqlFunction('NOW')); @@ -892,10 +914,83 @@ class DynamicListItem extends VerySimpleModel { ), ); + var $_config; + var $_form; + + const ENABLED = 0x0001; + + protected function hasStatus($flag) { + return 0 !== ($this->get('status') & $flag); + } + + protected function clearStatus($flag) { + return $this->set('status', $this->get('status') & ~$flag); + } + + protected function setStatus($flag) { + return $this->set('status', $this->get('status') | $flag); + } + + function isEnabled() { + return $this->hasStatus(self::ENABLED); + } + + function enable() { + $this->setStatus(self::ENABLED); + } + function disable() { + $this->clearStatus(self::ENABLED); + } + + function getConfiguration() { + if (!$this->_config) { + $this->_config = $this->get('properties'); + if (is_string($this->_config)) + $this->_config = JsonDataParser::parse($this->_config); + elseif (!$this->_config) + $this->_config = array(); + } + return $this->_config; + } + + function setConfiguration(&$errors=array()) { + $config = array(); + foreach ($this->getConfigurationForm()->getFields() as $field) { + $val = $field->to_database($field->getClean()); + $config[$field->get('id')] = is_array($val) ? $val[1] : $val; + $errors = array_merge($errors, $field->errors()); + } + if (count($errors) === 0) + $this->set('properties', JsonDataEncoder::encode($config)); + + return count($errors) === 0; + } + + function getConfigurationForm() { + if (!$this->_form) { + $this->_form = DynamicForm::lookup( + array('type'=>'L'.$this->get('list_id'))); + } + return $this->_form; + } + + function getVar($name) { + $config = $this->getConfiguration(); + $name = mb_strtolower($name); + foreach ($this->getConfigurationForm()->getFields() as $field) { + if (mb_strtolower($field->get('name')) == $name) + return $config[$field->get('id')]; + } + } + function toString() { return $this->get('value'); } + function __toString() { + return $this->toString(); + } + function delete() { # Don't really delete, just unset the list_id to un-associate it with # the list @@ -986,6 +1081,10 @@ class SelectionField extends FormField { $this->_choices = array(); foreach ($this->getList()->getItems() as $i) $this->_choices[$i->get('id')] = $i->get('value'); + if ($this->value && !isset($this->_choices[$this->value])) { + $v = DynamicListItem::lookup($this->value); + $this->_choices[$v->get('id')] = $v->get('value').' (Disabled)'; + } } return $this->_choices; } diff --git a/include/class.nav.php b/include/class.nav.php index 992daf107f8f8c212d4fc5528a9912e0f5e21e1b..20246ad4ba0df9e0b7895c6d41cef932c04ecc65 100644 --- a/include/class.nav.php +++ b/include/class.nav.php @@ -13,6 +13,7 @@ vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ +require_once(INCLUDE_DIR.'class.app.php'); class StaffNav { var $tabs=array(); @@ -43,6 +44,10 @@ class StaffNav { return (!$this->isAdminPanel()); } + function getRegisteredApps() { + return Application::getStaffApps(); + } + function setTabActive($tab, $menu=''){ if($this->tabs[$tab]){ @@ -100,6 +105,8 @@ class StaffNav { $this->tabs['users'] = array('desc' => 'Users', 'href' => 'users.php', 'title' => 'User Directory'); $this->tabs['tickets'] = array('desc'=>'Tickets','href'=>'tickets.php','title'=>'Ticket Queue'); $this->tabs['kbase'] = array('desc'=>'Knowledgebase','href'=>'kb.php','title'=>'Knowledgebase'); + if (count($this->getRegisteredApps())) + $this->tabs['apps']=array('desc'=>'Applications','href'=>'apps.php','title'=>'Applications'); } return $this->tabs; @@ -148,6 +155,10 @@ class StaffNav { $subnav[]=array('desc'=>'Canned Responses','href'=>'canned.php','iconclass'=>'canned'); } break; + case 'apps': + foreach ($this->getRegisteredApps() as $app) + $subnav[] = $app; + break; } if($subnav) $submenus[$this->getPanel().'.'.strtolower($k)]=$subnav; @@ -173,6 +184,10 @@ class AdminNav extends StaffNav{ parent::StaffNav($staff, 'admin'); } + function getRegisteredApps() { + return Application::getAdminApps(); + } + function getTabs(){ @@ -184,6 +199,8 @@ class AdminNav extends StaffNav{ $tabs['manage']=array('desc'=>'Manage','href'=>'helptopics.php','title'=>'Manage Options'); $tabs['emails']=array('desc'=>'Emails','href'=>'emails.php','title'=>'Email Settings'); $tabs['staff']=array('desc'=>'Staff','href'=>'staff.php','title'=>'Manage Staff'); + if (count($this->getRegisteredApps())) + $tabs['apps']=array('desc'=>'Applications','href'=>'apps.php','title'=>'Applications'); $this->tabs=$tabs; } @@ -234,6 +251,10 @@ class AdminNav extends StaffNav{ $subnav[]=array('desc'=>'Groups','href'=>'groups.php','iconclass'=>'groups'); $subnav[]=array('desc'=>'Departments','href'=>'departments.php','iconclass'=>'departments'); break; + case 'apps': + foreach ($this->getRegisteredApps() as $app) + $subnav[] = $app; + break; } if($subnav) $submenus[$this->getPanel().'.'.strtolower($k)]=$subnav; @@ -258,6 +279,10 @@ class UserNav { $this->setActiveNav($active); } + function getRegisteredApps() { + return Application::getClientApps(); + } + function setActiveNav($nav){ if($nav && $this->navs[$nav]){ diff --git a/include/class.orm.php b/include/class.orm.php index 16bdad71130867cf7a31a4c61e2921f38a59e2bd..b76c9c292a66811657e689e46b3b430710738fe4 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -322,6 +322,11 @@ class QuerySet implements IteratorAggregate, ArrayAccess { return $this->getIterator()->asArray(); } + function one() { + $this->limit(1); + return $this[0]; + } + function count() { $class = $this->compiler; $compiler = new $class(); @@ -774,6 +779,7 @@ class MySqlCompiler extends SqlCompiler { 'lte' => '%1$s <= %2$s', 'isnull' => '%1$s IS NULL', 'like' => '%1$s LIKE %2$s', + 'hasbit' => '%1$s & %2$s != 0', 'in' => array('self', '__in'), ); diff --git a/include/class.ticket.php b/include/class.ticket.php index 6cde950f52d494aa64ca9ec1324df15fd3fb8be5..4b7bf4c412c0b14bd8b951008d128e3aab125ea0 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -1278,7 +1278,7 @@ class Ticket { // The answer object is retrieved here which will // automatically invoke the toString() method when the // answer is coerced into text - return (string)$this->_answers[$tag]; + return $this->_answers[$tag]; } return false; diff --git a/include/class.variable.php b/include/class.variable.php index 233c9fc18696886460991b35bf3dba18701762cc..5d92709d979666f7435de2733ead2e8e1184d559 100644 --- a/include/class.variable.php +++ b/include/class.variable.php @@ -82,16 +82,14 @@ class VariableReplacer { if (!$var || !method_exists($obj, 'getVar')) return ""; - $parts = explode('.', $var); - if(($rv = call_user_func(array($obj, 'getVar'), $parts[0]))===false) + list($tag, $remainder) = explode('.', $var, 2); + if(($rv = call_user_func(array($obj, 'getVar'), $tag))===false) return ""; if(!is_object($rv)) return $rv; - list(, $part) = explode('.', $var, 2); - - return $this->getVar($rv, $part); + return $this->getVar($rv, $remainder); } function replaceVars($input) { diff --git a/include/staff/dynamic-list.inc.php b/include/staff/dynamic-list.inc.php index 4beb705768cf298e1d62255a4bdc16334066642b..ffeb871e62f0321b619458e12b91b39593cd590e 100644 --- a/include/staff/dynamic-list.inc.php +++ b/include/staff/dynamic-list.inc.php @@ -22,6 +22,17 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); <input type="hidden" name="a" value="<?php echo $_REQUEST['a']; ?>"> <input type="hidden" name="id" value="<?php echo $info['id']; ?>"> <h2>Custom List</h2> + +<ul class="tabs"> + <li><a href="#definition" class="active"> + <i class="icon-plus"></i> Definition</a></li> + <li><a href="#items"> + <i class="icon-list"></i> Items</a></li> + <li><a href="#properties"> + <i class="icon-asterisk"></i> Properties</a></li> +</ul> + +<div id="definition" class="tab_content"> <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> <thead> <tr> @@ -52,7 +63,128 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); </select></td> </tr> </tbody> + <tbody> + <tr> + <th colspan="7"> + <em><strong>Internal Notes:</strong> be liberal, they're internal</em> + </th> + </tr> + <tr> + <td colspan="7"><textarea name="notes" class="richtext no-bar" + rows="6" cols="80"><?php + echo $info['notes']; ?></textarea> + </td> + </tr> + </tbody> </table> +</div> +<div id="properties" class="tab_content" style="display:none"> + <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="7"> + <em><strong>Item Properties</strong> properties definable for each item</em> + </th> + </tr> + <tr> + <th nowrap>Sort + <i class="help-tip icon-question-sign" href="#field_sort"></i></th> + <th nowrap>Label + <i class="help-tip icon-question-sign" href="#field_label"></i></th> + <th nowrap>Type + <i class="help-tip icon-question-sign" href="#field_type"></i></th> + <th nowrap>Variable + <i class="help-tip icon-question-sign" href="#field_variable"></i></th> + <th nowrap>Delete + <i class="help-tip icon-question-sign" href="#field_delete"></i></th> + </tr> + </thead> + <tbody class="sortable-rows" data-sort="prop-sort-"> + <?php if ($form) foreach ($form->getDynamicFields() as $f) { + $id = $f->get('id'); + $deletable = !$f->isDeletable() ? 'disabled="disabled"' : ''; + $force_name = $f->isNameForced() ? 'disabled="disabled"' : ''; + $fi = $f->getImpl(); + $ferrors = $f->errors(); ?> + <tr> + <td><i class="icon-sort"></i></td> + <td><input type="text" size="32" name="prop-label-<?php echo $id; ?>" + value="<?php echo Format::htmlchars($f->get('label')); ?>"/> + <font class="error"><?php + if ($ferrors['label']) echo '<br/>'; echo $ferrors['label']; ?> + </td> + <td nowrap><select name="type-<?php echo $id; ?>" <?php + if (!$fi->isChangeable()) echo 'disabled="disabled"'; ?>> + <?php foreach (FormField::allTypes() as $group=>$types) { + ?><optgroup label="<?php echo Format::htmlchars($group); ?>"><?php + foreach ($types as $type=>$nfo) { + if ($f->get('type') != $type + && isset($nfo[2]) && !$nfo[2]) continue; ?> + <option value="<?php echo $type; ?>" <?php + if ($f->get('type') == $type) echo 'selected="selected"'; ?>> + <?php echo $nfo[0]; ?></option> + <?php } ?> + </optgroup> + <?php } ?> + </select> + <?php if ($f->isConfigurable()) { ?> + <a class="action-button" style="float:none;overflow:inherit" + href="ajax.php/form/field-config/<?php + echo $f->get('id'); ?>" + onclick="javascript: + $('#overlay').show(); + $('#field-config .body').load(this.href); + $('#field-config').show(); + return false; + "><i class="icon-edit"></i> Config</a> + <?php } ?></td> + <td> + <input type="text" size="20" name="name-<?php echo $id; ?>" + value="<?php echo Format::htmlchars($f->get('name')); + ?>" <?php echo $force_name ?>/> + <font class="error"><?php + if ($ferrors['name']) echo '<br/>'; echo $ferrors['name']; + ?></font> + </td> + <td><input type="checkbox" name="delete-<?php echo $id; ?>" + <?php echo $deletable; ?>/> + <input type="hidden" name="prop-sort-<?php echo $id; ?>" + value="<?php echo $f->get('sort'); ?>"/> + </td> + </tr> + <?php + } + for ($i=0; $i<$newcount; $i++) { ?> + <td><em>+</em> + <input type="hidden" name="prop-sort-new-<?php echo $i; ?>" + value="<?php echo $info["prop-sort-new-$i"]; ?>"/></td> + <td><input type="text" size="32" name="prop-label-new-<?php echo $i; ?>" + value="<?php echo $info["prop-label-new-$i"]; ?>"/></td> + <td><select name="type-new-<?php echo $i; ?>"> + <?php foreach (FormField::allTypes() as $group=>$types) { + ?><optgroup label="<?php echo Format::htmlchars($group); ?>"><?php + foreach ($types as $type=>$nfo) { + if (isset($nfo[2]) && !$nfo[2]) continue; ?> + <option value="<?php echo $type; ?>" + <?php if ($info["type-new-$i"] == $type) echo 'selected="selected"'; ?>> + <?php echo $nfo[0]; ?> + </option> + <?php } ?> + </optgroup> + <?php } ?> + </select></td> + <td><input type="text" size="20" name="name-new-<?php echo $i; ?>" + value="<?php echo $info["name-new-$i"]; ?>"/> + <font class="error"><?php + if ($errors["new-$i"]['name']) echo '<br/>'; echo $errors["new-$i"]['name']; + ?></font> + <td></td> + </tr> + <?php } ?> + </tbody> +</table> +</div> +<div id="items" class="tab_content" style="display:none"> <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> <thead> <?php if ($list) { @@ -66,7 +198,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); else $showing = 'Add a few initial items to the list'; ?> <tr> - <th colspan="4"> + <th colspan="5"> <em><?php echo $showing; ?></em> </th> </tr> @@ -74,25 +206,41 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); <th></th> <th>Value</th> <th>Extra <em style="display:inline">— abbreviations and such</em></th> + <th>Disabled</th> <th>Delete</th> </tr> </thead> + <tbody <?php if ($info['sort_mode'] == 'SortCol') { ?> class="sortable-rows" data-sort="sort-"<?php } ?>> <?php if ($list) $icon = ($info['sort_mode'] == 'SortCol') ? '<i class="icon-sort"></i> ' : ''; if ($list) { - foreach ($list->getItems() as $i) { + foreach ($list->getAllItems() as $i) { $id = $i->get('id'); ?> - <tr> + <tr class="<?php if (!$i->isEnabled()) echo 'disabled'; ?>"> <td><?php echo $icon; ?> <input type="hidden" name="sort-<?php echo $id; ?>" value="<?php echo $i->get('sort'); ?>"/></td> <td><input type="text" size="40" name="value-<?php echo $id; ?>" - value="<?php echo $i->get('value'); ?>"/></td> + value="<?php echo $i->get('value'); ?>"/> + <?php if ($form->getFields()) { ?> + <a class="action-button" style="float:none;overflow:inherit" + href="ajax.php/list/item/<?php + echo $i->get('id'); ?>/properties" + onclick="javascript: + $('#overlay').show(); + $('#field-config .body').load(this.href); + $('#field-config').show(); + return false; + "><i class="icon-edit"></i> Properties</a> + <?php } ?></td> <td><input type="text" size="30" name="extra-<?php echo $id; ?>" value="<?php echo $i->get('extra'); ?>"/></td> + <td> + <input type="checkbox" name="disable-<?php echo $id; ?>" <?php + if (!$i->isEnabled()) echo 'checked="checked"'; ?>/></td> <td> <input type="checkbox" name="delete-<?php echo $id; ?>"/></td> </tr> @@ -105,27 +253,19 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); <td><input type="text" size="40" name="value-new-<?php echo $i; ?>"/></td> <td><input type="text" size="30" name="extra-new-<?php echo $i; ?>"/></td> <td></td> + <td></td> </tr> <?php } ?> </tbody> - <tbody> - <tr> - <th colspan="7"> - <em><strong>Internal Notes:</strong> be liberal, they're internal</em> - </th> - </tr> - <tr> - <td colspan="7"><textarea name="notes" class="richtext no-bar" - rows="6" cols="80"><?php - echo $info['notes']; ?></textarea> - </td> - </tr> - </tbody> - </table> </table> +</div> <p class="centered"> <input type="submit" name="submit" value="<?php echo $submit_text; ?>"> <input type="reset" name="reset" value="Reset"> <input type="button" name="cancel" value="Cancel" onclick='window.location.href="?"'> </p> </form> + +<div style="display:none;" class="dialog draggable" id="field-config"> + <div class="body"></div> +</div> diff --git a/include/staff/templates/list-item-properties.tmpl.php b/include/staff/templates/list-item-properties.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..0a2ba9635df68b0b1a2b61795b1a4bb89e3e6e8e --- /dev/null +++ b/include/staff/templates/list-item-properties.tmpl.php @@ -0,0 +1,68 @@ + <h3>Item Properties — <?php echo $item->get('value') ?></h3> + <a class="close" href=""><i class="icon-remove-circle"></i></a> + <hr/> + <form method="post" action="ajax.php/list/item/<?php + echo $item->get('id'); ?>/properties" onsubmit="javascript: + var form = $(this); + $.post(this.action, form.serialize(), function(data, status, xhr) { + if (!data.length) { + form.closest('.dialog').hide(); + $('#overlay').hide(); + } else { + form.closest('.dialog').empty().append(data); + } + }); + return false;"> + <table width="100%"> + <?php + echo csrf_token(); + $config = $item->getConfiguration(); + foreach ($item->getConfigurationForm()->getFields() as $f) { + $name = $f->get('id'); + if (isset($config[$name])) + $f->value = $config[$name]; + else if ($f->get('default')) + $f->value = $f->get('default'); + ?> + <tr><td class="multi-line"> + <label for="<?php echo $f->getWidget()->name; ?>" + style="vertical-align:top;padding-top:0.2em"> + <?php echo Format::htmlchars($f->get('label')); ?>:</label> + </td><td> + <span style="display:inline-block"> + <?php + $f->render(); + if ($f->get('required')) { ?> + <font class="error">*</font> + <?php + } + if ($f->get('hint')) { ?> + <br /><em style="color:gray;display:inline-block"><?php + echo Format::htmlchars($f->get('hint')); ?></em> + <?php + } + ?> + </span> + <?php + foreach ($f->errors() as $e) { ?> + <br /> + <font class="error"><?php echo $e; ?></font> + <?php } ?> + </td></tr> + <?php + } + ?> + </table> + <hr> + <p class="full-width"> + <span class="buttons" style="float:left"> + <input type="reset" value="Reset"> + <input type="button" value="Cancel" class="close"> + </span> + <span class="buttons" style="float:right"> + <input type="submit" value="Save"> + </span> + </p> + </form> + <div class="clear"></div> + diff --git a/include/upgrader/streams/core.sig b/include/upgrader/streams/core.sig index 19fc2a074f6659a91cc9a83bc9ce83334660c472..267cb8e334b83f11d55b2e6341254bc297b0f766 100644 --- a/include/upgrader/streams/core.sig +++ b/include/upgrader/streams/core.sig @@ -1 +1 @@ -4323a6a81c35efbf7722b7fc4e475440 +9ef33a062ca3a20190dfad594d594a69 diff --git a/include/upgrader/streams/core/4323a6a8-9ef33a06.patch.sql b/include/upgrader/streams/core/4323a6a8-9ef33a06.patch.sql new file mode 100644 index 0000000000000000000000000000000000000000..9867ef23cb3e7a72b141aa6d56276b3a8d2d8c2d --- /dev/null +++ b/include/upgrader/streams/core/4323a6a8-9ef33a06.patch.sql @@ -0,0 +1,41 @@ +/** + * @version v1.8.2 + * @signature 9ef33a062ca3a20190dfad594d594a69 + * @title Add organization features + * + */ + +ALTER TABLE `%TABLE_PREFIX%form` + CHANGE `type` `type` varchar(8) NOT NULL DEFAULT 'G'; + +ALTER TABLE `%TABLE_PREFIX%list_items` + ADD `status` int(11) unsigned NOT NULL DEFAULT 1 AFTER `list_id`, + ADD `properties` text AFTER `sort`; + +ALTER TABLE `%TABLE_PREFIX%organization` + ADD `status` int(11) NOT NULL DEFAULT 0 AFTER `staff_id`, + ADD `domain` varchar(128) NOT NULL DEFAULT '' AFTER `status`, + ADD `extra` text AFTER `domain`; + +ALTER TABLE `%TABLE_PREFIX%filter` + ADD `status` int(11) unsigned NOT NULL DEFAULT '0' AFTER `isactive`, + ADD `ext_id` varchar(11) AFTER `topic_id`; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%note`; +CREATE TABLE `%TABLE_PREFIX%note` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `pid` int(11) unsigned, + `staff_id` int(11) unsigned NOT NULL DEFAULT 0, + `ext_id` varchar(10), + `body` text, + `status` int(11) unsigned NOT NULL DEFAULT 0, + `sort` int(11) unsigned NOT NULL DEFAULT 0, + `created` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', + `updated` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) DEFAULT CHARSET=utf8; + +-- Finished with patch +UPDATE `%TABLE_PREFIX%config` + SET `value` = '9ef33a062ca3a20190dfad594d594a69' + WHERE `key` = 'schema_signature' AND `namespace` = 'core'; diff --git a/scp/ajax.php b/scp/ajax.php index 5d12a8b43ebc215fce392f197b203bf842cfd1ec..65ce1c01143df2976f82c3ad2b21f97285af15f4 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -56,6 +56,10 @@ $dispatcher = patterns('', url_post('^field-config/(?P<id>\d+)$', 'saveFieldConfiguration'), url_delete('^answer/(?P<entry>\d+)/(?P<field>\d+)$', 'deleteAnswer') )), + url('^/list/', patterns('ajax.forms.php:DynamicFormsAjaxAPI', + url_get('^item/(?P<id>\d+)/properties$', 'getListItemProperties'), + url_post('^item/(?P<id>\d+)/properties$', 'saveListItemProperties') + )), url('^/report/overview/', patterns('ajax.reports.php:OverviewReportAjaxAPI', # Send url_get('^graph$', 'getPlotData'), diff --git a/scp/apps/.htaccess b/scp/apps/.htaccess new file mode 100644 index 0000000000000000000000000000000000000000..184048348cd306d635249908e82c174a4c0e6f60 --- /dev/null +++ b/scp/apps/.htaccess @@ -0,0 +1,11 @@ +<IfModule mod_rewrite.c> + +RewriteEngine On + +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteCond %{REQUEST_URI} (.*/apps) + +RewriteRule ^(.*)$ %1/dispatcher.php/$1 [L] + +</IfModule> diff --git a/scp/apps/dispatcher.php b/scp/apps/dispatcher.php new file mode 100644 index 0000000000000000000000000000000000000000..d1cf05fd81fc6a1b8fcf11e71346338e5eb8b161 --- /dev/null +++ b/scp/apps/dispatcher.php @@ -0,0 +1,41 @@ +<?php +/********************************************************************* + dispatcher.php + + Dispatcher for staff applications + + Jared Hancock <jared@osticket.com> + 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: +**********************************************************************/ +# Override staffLoginPage() defined in staff.inc.php to return an +# HTTP/Forbidden status rather than the actual login page. +# XXX: This should be moved to the AjaxController class +function staffLoginPage($msg='Unauthorized') { + Http::response(403,'Must login: '.Format::htmlchars($msg)); + exit; +} + +require('staff.inc.php'); + +//Clean house...don't let the world see your crap. +ini_set('display_errors','0'); //Disable error display +ini_set('display_startup_errors','0'); + +//TODO: disable direct access via the browser? i,e All request must have REFER? +if(!defined('INCLUDE_DIR')) Http::response(500, 'Server configuration error'); + +require_once INCLUDE_DIR.'/class.dispatcher.php'; +$dispatcher = patterns('', +); + +Signal::send('apps.scp', $dispatcher); + +# Call the respective function +print $dispatcher->resolve($ost->get_path_info()); diff --git a/scp/css/scp.css b/scp/css/scp.css index 55b706178234fb2d8a20134cbb55f35553176d2c..c73e107cf7fa7b7d8c54417541f7490909acc36a 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -1629,3 +1629,9 @@ div.selected-signature .inner { background: #fc9f41; /* Old browsers */ color: rgba(255,255,255,0.8) !important; } + +tr.disabled td, +tr.disabled th { + opacity: 0.6; + background: #f5f5f5; +} diff --git a/scp/lists.php b/scp/lists.php index a68267703661f332df7f8b5f4ad4d973b59f60cd..17f36d18bb2fbe7ca6df5615e50071967e1e03f4 100644 --- a/scp/lists.php +++ b/scp/lists.php @@ -6,6 +6,10 @@ $list=null; if($_REQUEST['id'] && !($list=DynamicList::lookup($_REQUEST['id']))) $errors['err']='Unknown or invalid dynamic list ID.'; +if ($list) { + $form = DynamicForm::lookup(array('type'=>'L'.$_REQUEST['id'])); +} + if($_POST) { $fields = array('name', 'name_plural', 'sort_mode', 'notes'); $required = array('name'); @@ -24,7 +28,7 @@ if($_POST) { else $errors['err'] = 'Unable to update custom list. Unknown internal error'; - foreach ($list->getItems() as $item) { + foreach ($list->getAllItems() as $item) { $id = $item->get('id'); if ($_POST["delete-$id"] == 'on') { $item->delete(); @@ -33,8 +37,54 @@ if($_POST) { foreach (array('sort','value','extra') as $i) if (isset($_POST["$i-$id"])) $item->set($i, $_POST["$i-$id"]); + + if ($_POST["disable-$id"] == 'on') + $item->disable(); + else + $item->enable(); + $item->save(); } + + $names = array(); + if (!$form) { + $form = DynamicForm::create(array( + 'type'=>'L'.$_REQUEST['id'], + 'title'=>$_POST['name'] . ' Properties' + )); + $form->save(true); + } + foreach ($form->getDynamicFields() as $field) { + $id = $field->get('id'); + if ($_POST["delete-$id"] == 'on' && $field->isDeletable()) { + $field->delete(); + // Don't bother updating the field + continue; + } + if (isset($_POST["type-$id"]) && $field->isChangeable()) + $field->set('type', $_POST["type-$id"]); + if (isset($_POST["name-$id"]) && !$field->isNameForced()) + $field->set('name', $_POST["name-$id"]); + # TODO: make sure all help topics still have all required fields + foreach (array('sort','label') as $f) { + if (isset($_POST["prop-$f-$id"])) { + $field->set($f, $_POST["prop-$f-$id"]); + } + } + if (in_array($field->get('name'), $names)) + $field->addError('Field variable name is not unique', 'name'); + if (preg_match('/[.{}\'"`; ]/u', $field->get('name'))) + $field->addError('Invalid character in variable name. Please use letters and numbers only.', 'name'); + if ($field->get('name')) + $names[] = $field->get('name'); + if ($field->isValid()) + $field->save(); + else + # notrans (not shown) + $errors["field-$id"] = 'Field has validation errors'; + // Keep track of the last sort number + $max_sort = max($max_sort, $field->get('sort')); + } break; case 'add': foreach ($fields as $f) @@ -47,13 +97,20 @@ if($_POST) { 'sort_mode'=>$_POST['sort_mode'], 'notes'=>$_POST['notes'])); + $form = DynamicForm::create(array( + 'title'=>$_POST['name'] . ' Properties' + )); + if ($errors) $errors['err'] = 'Unable to create custom list. Correct any error(s) below and try again.'; - elseif ($list->save(true)) - $msg = 'Custom list added successfully'; - else - $errors['err'] = 'Unable to create custom list. Unknown internal error'; + elseif (!$list->save(true)) + $errors['err'] = 'Unable to create custom list: Unknown internal error'; + $form->set('type', 'L'.$list->get('id')); + if (!$errors && !$form->save(true)) + $errors['err'] = 'Unable to create properties for custom list: Unknown internal error'; + else + $msg = 'Custom list added successfully'; break; case 'mass_process': @@ -82,7 +139,7 @@ if($_POST) { } if ($list) { - for ($i=0; isset($_POST["sort-new-$i"]); $i++) { + for ($i=0; isset($_POST["prop-sort-new-$i"]); $i++) { if (!$_POST["value-new-$i"]) continue; $item = DynamicListItem::create(array( @@ -96,6 +153,29 @@ if($_POST) { # Invalidate items cache $list->_items = false; } + + if ($form) { + for ($i=0; isset($_POST["prop-sort-new-$i"]); $i++) { + if (!$_POST["prop-label-new-$i"]) + continue; + $field = DynamicFormField::create(array( + 'form_id'=>$form->get('id'), + 'sort'=>$_POST["prop-sort-new-$i"] + ? $_POST["prop-sort-new-$i"] : ++$max_sort, + 'label'=>$_POST["prop-label-new-$i"], + 'type'=>$_POST["type-new-$i"], + 'name'=>$_POST["name-new-$i"], + )); + $field->setForm($form); + if ($field->isValid()) + $field->save(); + else + $errors["new-$i"] = $field->errors(); + } + // XXX: Move to an instrumented list that can handle this better + if (!$errors) + $form->_dfields = $form->_fields = null; + } } $page='dynamic-lists.inc.php'; diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql index c15a946a6aa97c5e965a1ebebe6667f36b46b681..acd48f9bb4cc395860fb93293a6b6e470d2ed2e8 100644 --- a/setup/inc/streams/core/install-mysql.sql +++ b/setup/inc/streams/core/install-mysql.sql @@ -96,7 +96,7 @@ INSERT INTO `%TABLE_PREFIX%config` (`namespace`, `key`, `value`) VALUES DROP TABLE IF EXISTS `%TABLE_PREFIX%form`; CREATE TABLE `%TABLE_PREFIX%form` ( `id` int(11) unsigned NOT NULL auto_increment, - `type` char(1) NOT NULL DEFAULT 'G', + `type` varchar(8) NOT NULL DEFAULT 'G', `deletable` tinyint(1) NOT NULL DEFAULT 1, `title` varchar(255) NOT NULL, `instructions` varchar(512), @@ -163,10 +163,12 @@ DROP TABLE IF EXISTS `%TABLE_PREFIX%list_items`; CREATE TABLE `%TABLE_PREFIX%list_items` ( `id` int(11) unsigned NOT NULL auto_increment, `list_id` int(11), + `status` int(11) unsigned NOT NULL DEFAULT 1, `value` varchar(255) NOT NULL, -- extra value such as abbreviation `extra` varchar(255), `sort` int(11) NOT NULL DEFAULT 1, + `properties` text, PRIMARY KEY (`id`), KEY `list_item_lookup` (`list_id`) ) DEFAULT CHARSET=utf8; @@ -268,6 +270,7 @@ CREATE TABLE `%TABLE_PREFIX%filter` ( `id` int(11) unsigned NOT NULL auto_increment, `execorder` int(10) unsigned NOT NULL default '99', `isactive` tinyint(1) unsigned NOT NULL default '1', + `status` int(11) unsigned NOT NULL DEFAULT '0', `match_all_rules` tinyint(1) unsigned NOT NULL default '0', `stop_onmatch` tinyint(1) unsigned NOT NULL default '0', `reject_ticket` tinyint(1) unsigned NOT NULL default '0', @@ -282,6 +285,7 @@ CREATE TABLE `%TABLE_PREFIX%filter` ( `sla_id` int(10) unsigned NOT NULL default '0', `form_id` int(11) unsigned NOT NULL default '0', `topic_id` int(11) unsigned NOT NULL default '0', + `ext_id` varchar(11), `target` ENUM( 'Any', 'Web', 'Email', 'API' ) NOT NULL DEFAULT 'Any', `name` varchar(32) NOT NULL default '', `notes` text, @@ -426,6 +430,9 @@ CREATE TABLE `%TABLE_PREFIX%organization` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(128) NOT NULL DEFAULT '', `staff_id` int(10) unsigned NOT NULL DEFAULT '0', + `status` int(11) unsigned NOT NULL DEFAULT '0', + `domain` varchar(128) NOT NULL DEFAULT '', + `extra` text, `created` timestamp NULL DEFAULT NULL, `updated` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) @@ -448,6 +455,20 @@ CREATE TABLE `%TABLE_PREFIX%canned_response` ( KEY `active` (`isenabled`) ) DEFAULT CHARSET=utf8; +DROP TABLE IF EXISTS `%TABLE_PREFIX%note`; +CREATE TABLE `%TABLE_PREFIX%note` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `pid` int(11) unsigned, + `staff_id` int(11) unsigned NOT NULL DEFAULT 0, + `ext_id` varchar(10), + `body` text, + `status` int(11) unsigned NOT NULL DEFAULT 0, + `sort` int(11) unsigned NOT NULL DEFAULT 0, + `created` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', + `updated` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) DEFAULT CHARSET=utf8; + DROP TABLE IF EXISTS `%TABLE_PREFIX%session`; CREATE TABLE `%TABLE_PREFIX%session` ( `session_id` varchar(255) collate ascii_general_ci NOT NULL default '', diff --git a/web.config b/web.config index fd61b6a95f6776289e5b1fd85660f05ceec5174f..ee754443fd21e93c111e2cd962fd019ded871c30 100644 --- a/web.config +++ b/web.config @@ -34,6 +34,16 @@ </conditions> <action type="Rewrite" url="{R:1}pages/index.php/{R:2}"/> </rule> + <rule name="Staff applications" stopProcessing="true"> + <match url="^(.*/)?scp/apps/(.*)$" ignoreCase="true"/> + <conditions> + <add input="{REQUEST_FILENAME}" matchType="IsFile" + ignoreCase="false" negate="true" /> + <add input="{REQUEST_FILENAME}" matchType="IsDirectory" + ignoreCase="false" negate="true" /> + </conditions> + <action type="Rewrite" url="{R:1}scp/apps/dispatcher.php/{R:2}"/> + </rule> </rules> </rewrite> <defaultDocument>