Compare commits

...

82 Commits

Author SHA1 Message Date
Peter Rotich
b6e3376bdb Merge remote-tracking branch 'upstream/develop-next' into develop-next 2020-11-12 03:15:21 +00:00
JediKev
d5ee0df82c Merge pull request #5706 from JediKev/issue/major-version-boostrap
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2020-11-17 13:07:40 -06:00
JediKev
f525d39557 issue: Major Version Bootstrap
This updates the Major Version for 1.15
2020-11-17 18:23:44 +00:00
JediKev
462f8df8b0 Add Release Notes for v1.15 2020-11-17 18:09:42 +00:00
JediKev
faf0539118 Merge remote-tracking branch 'origin/develop' into 1.15.x 2020-11-17 18:06:30 +00:00
JediKev
7acfcccdc7 Merge pull request #5705 from aydreeihn/issue/event-yaml-upgrades
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2020-11-17 10:22:54 -06:00
aydreeihn
38232f2edb Issue: Missing Events
This commit adds to the latest database task. We need to see if the event table has the login, logout, message, and note events, and if a database does not, we need to add them to be used with the audit plugin.
2020-11-17 10:04:18 -06:00
Peter Rotich
33f2a47c1d Merge remote-tracking branch 'upstream/develop' into develop-next 2020-11-12 03:15:01 +00:00
Peter Rotich
e36975f5bb Merge remote-tracking branch 'upstream/develop' into develop-next 2020-11-11 08:54:43 +00:00
Peter Rotich
b23d2b15d8 Merge pull request #5702 from protich/issue/dept_id+email
Change dept_id and priority_id fron tinyint to int
2020-11-16 13:03:18 -06:00
Peter Rotich
97c0e727f8 Merge remote-tracking branch 'upstream/develop' into develop-next 2020-11-11 06:49:35 +00:00
Peter Rotich
e54f6f3e24 Change dept_id and priority_id fron tinyint to int
This commit changes dept_id and priority_id in email table from
tinyint(3) to int(11)
2020-11-11 06:37:01 +00:00
Peter Rotich
d6ee1a8a49 Merge remote-tracking branch 'upstream/develop' into develop-next 2020-11-10 04:31:00 +00:00
Peter Rotich
5b5c22160f Merge pull request #5690 from aydreeihn/issue/2fa-upgrade
Reviewed-By: Peter Rotich <peter@osticket.com>
2020-11-10 15:45:18 -06:00
aydreeihn
0065c3b154 Issue: 2FA Upgrade
This commit fixes an issue where the task needed in order to add the 2FA Email template to the database for upgrades was omitted from the codebase.

The new template needed to be added to fresh installs as well.
2020-11-10 15:35:14 -06:00
Peter Rotich
3dc3ed97e9 Merge remote-tracking branch 'upstream/develop' into develop-next 2020-11-10 03:11:28 +00:00
Peter Rotich
cf16ca683b Merge remote-tracking branch 'upstream/develop-next' into develop-next 2020-11-10 03:11:15 +00:00
Peter Rotich
0895654cfc Merge pull request #5680 from aydreeihn/issue/user-dept-field
Reviewed-By: <peter@osticket.com>
2020-11-10 14:25:43 -06:00
Peter Rotich
9aabb77563 Merge pull request #5679 from aydreeihn/issue/ticket-from-emails
Ticket From Email
2020-11-10 14:24:57 -06:00
Peter Rotich
143176ae2f Merge remote-tracking branch 'upstream/develop' into develop-next 2020-11-09 22:06:20 +00:00
aydreeihn
f02edd94bc Ticket From Email
This commit ensures that Agents only have the option to choose the From Email on a Ticket based on the Department Emails they have access to.
2020-11-05 12:44:33 -06:00
aydreeihn
52825f0fce Issue: User Custom Dept Field
This fixes an issue introduced with the new Visibility Permissions. It did not account for the possibility that Users may be able to view a Custom Department field. In that event, we must make sure we can get active Departments that should show up as choices in custom Dept fields.
2020-11-05 10:48:39 -06:00
Peter Rotich
b1b970b8ed Merge remote-tracking branch 'upstream/develop' into develop-next 2020-10-28 03:30:54 +00:00
Peter Rotich
cbec8b5c36 Merge pull request #5674 from protich/issue/password-policies
Staff: Password Change
2020-11-03 08:48:51 -06:00
Peter Rotich
059e3283f5 Merge pull request #5675 from protich/feature/csrftoken+rotate
csrf: Add ability to rotate tokens
2020-11-03 08:48:30 -06:00
Peter Rotich
7527ea7ef6 Staff: Password Change
Exclude current password from policies enforcement when changing
password.
2020-10-26 23:58:53 +00:00
Peter Rotich
36e614c397 csrf: Add ability to rotate token
This commit adds ability to rotate csrf token post check. This can be
useful as anti-spam measure for web tickets.
2020-10-26 22:52:51 +00:00
Peter Rotich
40b39e6be9 Merge remote-tracking branch 'upstream/develop' into develop-next 2020-10-26 21:44:08 +00:00
Peter Rotich
791a45eaf5 Merge pull request #5403 from aydreeihn/feature/agent-dept-visibility
Reviewed-By: Peter Rotich <peter@osticket.com>
2020-10-21 09:43:20 -05:00
Peter Rotich
ca9817c9e7 Merge pull request #5659 from aydreeihn/feature/search-slas
Reviewed-By: Peter Rotich <peter@osticket.com>
2020-10-21 09:42:42 -05:00
aydreeihn
4e86313ce1 Code Improvements
- Use array_keys instead of a foreach loop to filter canned responses for staff
- add $options var to AssigneeField getWidget function to match parent
2020-10-20 16:58:47 -05:00
aydreeihn
ca8117610a FAQ Topic Quick Filter
This commit makes sure that we do not show the names of any hidden Help Topics in the Quick Filter for Topics on the FAQs page in the Agent Panel.
2020-10-20 16:37:58 -05:00
aydreeihn
07b2373f2e Clean Topics Array
This commit makes sure that we have a clean array for filtered topics so that disabled/archived help topics are correctly handled in the returned selection.
2020-10-20 16:37:58 -05:00
aydreeihn
9902ac2270 Filter FAQs Based on Topic Access
This commit makes it so that FAQs that are associated with Help Topics that are associated with Departments an Agent does not have access to are hidden. It also ensures that an Agent cannot create an FAQ associated with a Help Topic that they do not have access to based on the Help Topic's Department.
2020-10-20 16:37:58 -05:00
aydreeihn
642514692c Filter Topics Based on Dept Access
This commit makes it so that Help Topics are hidden from the Agent Panel if they are assigned a Department an Agent does not have access to and the Agent does not have the 'Ability to see all Departments' permission checked.

If a Help Topic is Public, it will be shown in the Agent Panel.

If a Parent Help Topic is Private and assigned a Department an Agent does not have access to, we hide both the Parent and Children Help Topics.

If a Help Topic is not assigned a Department at all, it is shown throughout the Agent Panel.

Help Topics are filtered for:
- Inline Ticket Edit
- Ticket Edit
- Ticket Open
- Advanced Search
- Quick Filters
- Dashboard
2020-10-20 16:37:58 -05:00
aydreeihn
3722fc509b Ticket Open Canned Responses
If an Agent is opening a Ticket, they should only see Canned Responses for Departments they have access to.
2020-10-20 16:37:58 -05:00
aydreeihn
484023d099 Update Existing Staff Permissions
This commit updates existing staff members so that they will still be able to see departments and agents within the agent panel the same way they are used to seeing them. This is done during the upgrade within a task.
2020-10-20 16:37:58 -05:00
JediKev
7f0602a3ba db: Latest Indexes
This adds all indexes we've implemented/accumulated over time.
2020-10-20 16:37:58 -05:00
aydreeihn
4489b2fe3c Agent/Dept Visibility Defaults
This commit makes it so that the Agent and Department visibility permission is checked by default on fresh installs and newly created Agents unless otherwise specified.
2020-10-20 16:37:58 -05:00
aydreeihn
6fdc111812 Issue: Assignment Field Quick Filter
This commit fixes an issue where quick filters for the AssigneeField class would not work because we were returning an array of AgentsName + Team name strings which did not sanitize correctly in htmlchars.
2020-10-20 16:37:58 -05:00
aydreeihn
f306ce887e Option to Filter Criteria Choices
This commit gives us a way to determine if we should filter choices for setting field criteria. We need this to be optional because we use the same method for getting criteria choices in the Agent and Admin Panel, and for the Agent panel, we need to filter the choices based on the Agent's permission to view all Agents and Departments.

If Criteria are being set in the Admin Panel, all choices should be shown regardless of an Agents permissions.

filterVisibility needs to be applied for the following:
- AgentSelectionField
- DepartmentManagerSelectionField
- AssigneeChoiceField
- DepartmentField
- DepartmentChoiceField
2020-10-20 16:37:58 -05:00
aydreeihn
6eae7e682d Limit Agent Choices
This commit checks if the Agent has permission to see all Agents, and if they don't, only show Agents that are in Departments they have access to. This covers the following:
- Queue Assign Mass Process
- Ticket open Assign
- Ticket View assign
- Ticket Inline Edit Assign
- Ticket Refer
- Assigned Staff Quick Filter
- Assignee Quick Filter
- Department Manager Quick Filter

It also limits Agents shown in the Agent Directory based on the Agent permission checkbox as well as the Departments shown in the Department filter of the Agent Directory.
2020-10-20 16:37:58 -05:00
aydreeihn
3a8ea4b25b Canned Responses for Dept Roles
This commit fixes a bug where we did not accurately check individual Roles for Departments an Agent could access to determine if they could manage that particular Canned Response.

Now, we check to see if the Agent has a Role that can manage Canned Responses at all to determine if Canned Responses show up in the Knowledge Base.

Additionally, we filter Canned Responses shown based on the Role permissions of the Agent's Departments (Primary and Extended)
2020-10-20 16:37:58 -05:00
aydreeihn
46033d11c0 getDepartmentNames Publiconly Option
This commit allows us to determine if only public Departments should be returned or all Departments regardless of anything else. We need this for Agents that can see all Departments and search all Tickets
2020-10-20 16:37:58 -05:00
aydreeihn
49b2f1b355 Limit Departments in DepartmentField Choices
This commit checks to see if the Agent has permission to see all Departments, and if they don't, show only their Departments in forms that include a DepartmentField. This covers the following:
	- Ticket Transfers
	- Task Transfers
	- Ticket Referrals
	- Task Creation

Standard Forms:
	- Ticket Creation
2020-10-20 16:37:58 -05:00
aydreeihn
4ad7e95a2f Add Agent/Department Visibility Checkboxes
This commit adds the following checkboxes to Admin Panel | Agents | Choose an Agent | Permissions | Miscellaneous:
- Agent — Ability to see Agents in all Departments
- Department — Ability to see all Departments
2020-10-20 16:37:58 -05:00
aydreeihn
e4346d26e0 Filter Canned Response Creation
This commit makes it so that Agents can only create Canned Responses for Departments they can access or Canned Responses that will show up for all Departments.
2020-10-20 16:37:58 -05:00
aydreeihn
5fbd762483 Filter Canned Responses Page
This commit filters Canned Responses so that Agents can only see Canned Responses for Departments they can access (Primary or Extended Access) or Canned Responses assigned to all Departments.
2020-10-20 16:37:58 -05:00
Peter Rotich
a2a09d2754 Merge pull request #5660 from JediKev/oops/indexes-patch-schema
oops: Indexes Patch Schema
2020-10-20 16:36:41 -05:00
JediKev
709b55fee2 oops: Indexes Patch Schema
This addresses an issue where I forgot to recheck the hash after making
changes to installer. This updates the schema signature to the correct hash
so the upgrader can continue.
2020-10-20 16:32:04 -05:00
Peter Rotich
003ac86231 Merge pull request #5362 from JediKev/db/latest-indexes
Reviewed-By: Peter Rotich <peter@osticket.com>
2020-10-20 15:20:14 -05:00
JediKev
4c9968bd11 upgrader: Add ticket_pid Index
This updates the AddIndexMigration Task to include an index for the newly
added `ticket_pid` column in the Ticket table.
2020-10-20 15:17:03 -05:00
JediKev
ea0937384d upgrade: Add Version Column To Plugin Table
This addresses an issue where we've never added the `version` column to any
of the upgrade patches so if someone were to upgrade from pre-1.8, they will
not have the `version` column and they will not be able to install plugins.
This adds a prepared statement to check if the column already exists, if it
does not exists we will add the column. We have to use a prepared statement
as unfortunately MySQL doesn’t have the concept of `ADD COLUMN IF NOT
EXISTS` like MariaDB does.
2020-10-20 15:17:03 -05:00
JediKev
c359d12e43 forms: Task Title/Description Field Flags
This addresses an issue where the Task Title field allows any Admin to
remove the View and Required perms for Agents even though this field should
be Viewable and Required. This adds SQL so we can see if the
`FLAG_AGENT_REQUIRED`, `FLAG_AGENT_VIEW`, `FLAG_MASK_REQUIRE` and
`FLAG_MASK_VIEW` flags already exists, if not, we will add them to ensure
the field is Viewable and Required for Agents and to ensure Admins cannot
change this behavior for both perms. This also addresses an issue where the
Task Description field allows any Admin to remove the View perms for Agents
even though this field should always be Viewable. This adds SQL so we can
see if the `FLAG_AGENT_VIEW` and `FLAG_MASK_VIEW` flags already exists, if
not, we will add them to ensure the field is Viewable to Agents and to
ensure Admins cannot change this behavior.
2020-10-20 15:17:03 -05:00
JediKev
da2fd378f5 db: Latest Indexes
This adds all indexes we've implemented/accumulated over time.
2020-10-20 15:17:03 -05:00
aydreeihn
2731074729 Issue: Auto Referral
This commit fixes an issue we had with Auto Referrals. If a Ticket had been transfered/responded to by 3 or more departments and a user sent in an email response to one of the transferred departments (not including the original or current department), we created an auto referral for that department which could potentially give agents to access they should not have access to.

A new column, email_id, has been added to the thread_entry_email table so that we can see which system email each thread entry has been fetched from.

This commit also adds the event (EVENT_TABLE) relationship to the ThreadEvents (THREAD_EVENT_TABLE) class.

An index for ticket_pid on the ticket table has been added.
2020-10-20 15:14:14 -05:00
aydreeihn
0fd63b4284 SLA Plan Search Field
This commit allows Agents to do advanced searches and exports on the SLA Plan field. It also allows for adding an SLA Plan column to queues.
2020-10-20 14:56:09 -05:00
Peter Rotich
bfd5da8e20 CsvImporter: Skip Byte Order Mark (BOM) if present
This commit adds logic to skip BOM at the biggining of a file (if
        present).
2020-10-17 00:30:19 +00:00
Peter Rotich
4aef938556 Merge remote-tracking branch 'upstream/develop-next' into develop-next 2020-10-16 14:22:30 +00:00
Peter Rotich
0ce2499fbd Merge pull request #5662 from aydreeihn/issue/inheritance-compatibility
Reviewed-By: Peter Rotich <peter@osticket.com>
2020-10-21 11:39:06 -05:00
aydreeihn
cb13b82a73 Oops: Method Inheritance Compatibility
This commit fixes an issue with the getChoices method for the new SLAChoiceField class. It needed the $options variable to be added so that it would match the parent class.
2020-10-21 10:21:10 -05:00
Peter Rotich
7f0130bff6 Merge remote-tracking branch 'upstream/develop-next' into develop-next 2020-10-16 12:25:34 +00:00
Peter Rotich
c605c7d934 Merge remote-tracking branch 'upstream/develop-next' into develop-next 2020-10-16 11:24:34 +00:00
Peter Rotich
ff06cc3558 Merge remote-tracking branch 'upstream/develop-next' into develop-next 2020-10-16 11:15:24 +00:00
Peter Rotich
a62288938c Merge remote-tracking branch 'upstream/develop' into develop-next 2020-10-16 11:00:51 +00:00
Peter Rotich
c1f6b98949 Merge remote-tracking branch 'upstream/develop' into develop-next 2020-10-08 11:25:18 +00:00
Peter Rotich
eec0f4fb52 Merge pull request #5568 from protich/feature/2fa
Reviewed-By: osTicket team.
2020-10-13 10:50:48 -05:00
Peter Rotich
e9b12f3101 Merge pull request #35 from aydreeihn/feature/2fa-rewrite-mods
Reviewed-By: Peter Rotich <peter@osticket.com>
2020-10-07 15:29:08 -05:00
aydreeihn
8f4fe1892f Post Review Modifications
- Use get2FAConfig to check if 2FA backend is verified. This eliminates the is2FAConfigured function
- Check to make sure staff and staff is admin before allowing an agent to reset another agent's 2FA
- Only reset the default2fa when resetting 2FA (leave the configuration)
- Make sure there is an id before trying to call enable2fa
- Change login 'Loading' wheel to 'Verifying'
2020-08-20 16:03:59 -05:00
Peter Rotich
36ef030d34 Merge pull request #4910 from JediKev/issue/translated-form-instructions
issue: Form Instructions Translation
2020-08-18 09:11:22 -05:00
Peter Rotich
5a0a844de5 Merge pull request #5561 from protich/feature/password-policy-revisited
Password Policy Revisited
2020-07-31 13:59:00 -05:00
aydreeihn
9d46c84bcb Extra 2FA Modifications
- Make sure 2FA backend is saved AND configured
- Give Admins option to reset 2FA for an Agent if they lose ability to log in w/current 2FA config
- The getConfigsByNamespace function is useful for 2FA so that we can use the value of an Agent's default_2fa to check to see if they have their 2FA configured
- Add Help Tip for require_agent_2fa field
- Do not automatically select 2FA option if it's configured, just enable it.
- Add Login loading wheel
2020-07-27 13:20:48 -05:00
aydreeihn
3f08e626d8 Add 2FA Email Template
This commit adds an email template for sending out the 2FA token.

**Note:** We will need to add an upgrader task to import the template for existing helpdesks. I'll add it below in the comments.
2020-07-15 16:09:21 -05:00
aydreeihn
a1b78264d7 2FA Modifications
This commit adds to the functionality of two factor authentication for osTicket.

- Ensures that we do not clear out external 2fa configurations
- Enables 2fa options in the dropdown box of the profile when they are configured
- Adds help tip to the Default 2FA field
- Forces Agents to configure and save 2FA if 'Require agents to turn on 2FA' is checked upon next login
- Fixes the following error:
	- Call to a member function getTabs() on null in include/staff/templates/navigation.tmpl.php
2020-07-15 14:46:39 -05:00
Peter Rotich
4b6bc735e8 Textbox & Textarea Attributes
Make attributes processing a little dynamic.
2020-07-03 09:31:46 +00:00
Peter Rotich
ea8610348a Support 2FA on agents login form
This commit adds 2FA as part of login process for agents
2020-07-03 07:57:28 +00:00
Peter Rotich
cff12f7078 Manage 2FA
This commit adds the ability for agents to configure supported 2FA
backends and set backend to use.
2020-07-03 07:57:28 +00:00
Peter Rotich
4ef752c68e Add setting to require 2FA
This commit adds setting to Admin Panel > Settings > Agents to force
agents to setup 2FA.
2020-07-03 07:57:28 +00:00
Peter Rotich
5dd0a34f13 Add the concept of 2FA backends
This adds base functionality for 2FA backend and includes email 2fa
support out the box.
2020-07-03 07:27:36 +00:00
Peter Rotich
744676bbca Localize Password Policy checks for agents
Make sure we're using correct password policy when changing or resetting
agents passwords.
2020-06-10 08:19:25 +00:00
Peter Rotich
e1aba7c2c7 Password Policy Revisited
This commit improves upon how osTicket enforces password policies
systemwide for Agents and Users. Notable changes include;

* Ability to enforce policies on login
* Ability to specify applicable policy for Agents or Users alike
* Allow Password Policy plugins to expire passwords
2020-06-09 21:41:13 +00:00
JediKev
4f7d23c855 issue: Form Instructions Translation
This addresses issue 4909 where setting a translated Form Instruction fails
to render properly on the client side. This is due to us formatting the
instructions but not decoding before we render. This adds the decode method
to the translated Form Instructions so they are rendered properly on the
client side.
2019-06-03 09:51:31 -05:00
74 changed files with 2097 additions and 344 deletions

View File

@@ -1,3 +1,25 @@
osTicket v1.15
==============
### Enhancements
* Change dept_id and priority_id fron tinyint to int (e54f6f3)
* csrf: Add ability to rotate token (36e614c)
* Feature: Agent/Department Visibility (5fbd762, e4346d2, 4ad7e95, 49b2f1b, 46033d1, 3a8ea4b, 6eae7e6, f306ce8, 6fdc111, 4489b2f, 7f0602a, 484023d, 3722fc5, 6425146, 9902ac2, 07b2373, ca81176, 4e86313)
* db: Latest Indexes (da2fd37, 2731074, c359d12, ea09373, 4c9968b)
* SLA Plan Search Field (0fd63b4)
* 2FA Backends (5dd0a34, 4ef752c, cff12f7, ea86103, 4b6bc73, a1b7826, 3f08e62, 9d46c84, 8f4fe18)
* Password Policy Revisited (e1aba7c, 744676b)
### Improvements
* Issue: Missing Events (38232f2)
* Issue: 2FA Upgrade (0065c3b)
* Ticket From Email (f02edd9)
* Issue: User Custom Dept Field (52825f0)
* Staff: Password Change (7527ea7)
* oops: Indexes Patch Schema (709b55f)
* CsvImporter: Skip Byte Order Mark (BOM) if present (bfd5da8)
* Oops: Method Inheritance Compatibility (cb13b82)
* issue: Form Instructions Translation (4f7d23c)
osTicket v1.14.4
================
### Enhancements

View File

@@ -66,7 +66,16 @@ elseif ($_POST) {
$errors['passwd1'] = __('New password is required');
elseif (!$_POST['backend'] && $_POST['passwd2'] != $_POST['passwd1'])
$errors['passwd1'] = __('Passwords do not match');
else {
try {
UserAccount::checkPassword($_POST['passwd1']);
} catch (BadPassword $ex) {
$errors['passwd1'] = $ex->getMessage();
}
}
if ($errors)
$errors['err'] = $errors['err'] ?: __('Unable to register account. See messages below');
// XXX: The email will always be in use already if a guest is logged in
// and is registering for an account. Instead,
elseif (($addr = $user_form->getField('email')->getClean())

View File

@@ -340,7 +340,7 @@ define('CLI_DIR', INCLUDE_DIR.'cli/');
#Current version && schema signature (Changes from version to version)
define('GIT_VERSION','$git');
define('MAJOR_VERSION', '1.14');
define('MAJOR_VERSION', '1.15');
define('THIS_VERSION', MAJOR_VERSION.'-git'); //Shown on admin panel
//Path separator
if(!defined('PATH_SEPARATOR')){

View File

@@ -34,7 +34,8 @@ class DynamicFormsAjaxAPI extends AjaxController {
ob_start();
$form->getForm($_SESSION[':form-data'])->render(array(
'staff' => !$client,
'mode' => 'create'));
'mode' => 'create',
'filterVisibility' => true));
$html .= ob_get_clean();
ob_start();
print $form->getMedia();

View File

@@ -55,7 +55,7 @@ class SearchAjaxAPI extends AjaxController {
$this->_tryAgain($search);
}
function addField($name) {
function addField($name, $filterVisibility=null) {
global $thisstaff;
if (!$thisstaff)
@@ -68,7 +68,9 @@ class SearchAjaxAPI extends AjaxController {
if (!($F = $searchable[$name]))
Http::response(404, 'No such field: ', print_r($name, true));
$fields = SavedSearch::getSearchField($F, $name);
$filterVisibility = ($filterVisibility == 'true') ? true : false;
$options = array('filterVisibility' => $filterVisibility);
$fields = SavedSearch::getSearchField($F, $name, $options);
$form = new AdvancedSearchForm($fields);
// Check the box to search the field by default
if ($F = $form->getField("{$name}+search"))
@@ -151,8 +153,10 @@ class SearchAjaxAPI extends AjaxController {
}
function _tryAgain($search, $form=null, $errors=array(), $info=array()) {
if (!$form)
$form = $search->getForm();
if (!$form) {
$filterVisibility = true;
$form = $search->getForm(null, null, $filterVisibility);
}
include STAFFINC_DIR . 'templates/advanced-search.tmpl.php';
}

View File

@@ -38,7 +38,7 @@ class StaffAjaxAPI extends AjaxController {
try {
// Validate password
if (!$clean['welcome_email'])
PasswordPolicy::checkPassword($clean['passwd1'], null);
Staff::checkPassword($clean['passwd1'], null);
if ($id == 0) {
// Stash in the session later when creating the user
$_SESSION['new-agent-passwd'] = $clean;
@@ -56,8 +56,8 @@ class StaffAjaxAPI extends AjaxController {
Http::response(201, 'Successfully updated');
}
catch (BadPassword $ex) {
$passwd1 = $form->getField('passwd1');
$passwd1->addError($ex->getMessage());
if ($passwd1 = $form->getField('passwd1'))
$passwd1->addError($ex->getMessage());
}
catch (PasswordUpdateFailed $ex) {
$errors['err'] = __('Password update failed:').' '.$ex->getMessage();
@@ -108,8 +108,8 @@ class StaffAjaxAPI extends AjaxController {
}
}
catch (BadPassword $ex) {
$passwd1 = $form->getField('passwd1');
$passwd1->addError($ex->getMessage());
if ($passwd1 = $form->getField('passwd1'))
$passwd1->addError($ex->getMessage());
}
catch (PasswordUpdateFailed $ex) {
$errors['err'] = __('Password update failed:').' '.$ex->getMessage();
@@ -232,4 +232,91 @@ class StaffAjaxAPI extends AjaxController {
'code' => $code,
));
}
function configure2FA($staffId, $id=0) {
global $thisstaff;
if (!$thisstaff)
Http::response(403, 'Agent login required');
if ($staffId != $thisstaff->getId())
Http::response(403, 'Access denied');
if ($id && !($auth= Staff2FABackend::lookup($id)))
Http::response(404, 'Unknown 2FA');
$staff = $thisstaff;
$info = array();
if ($auth) {
// Simple state machine to manage settings and verification
$state = @$_POST['state'] ?: 'validate';
switch ($state) {
case 'verify':
try {
$form = $auth->getInputForm($_POST);
if ($_POST && $form
&& $form->isValid()
&& $auth->validate($form, $staff)) {
// Mark the settings as verified
if (($config = $staff->get2FAConfig($auth->getId()))) {
$config['verified'] = time();
$staff->updateConfig(array(
$auth->getId() => JsonDataEncoder::encode($config)));
}
// We're done here
$auth = null;
$info['notice'] = __('Setup completed successfully');
} else {
$info['error'] = __('Unable to verify the token - try again!');
}
} catch (ExpiredOTP $ex) {
// giving up cleanly
$info['error'] = $ex->getMessage();
$auth = null;
}
break;
case 'validate':
default:
$config = $staff->get2FAConfig($auth->getId());
$vars = $_POST ?: $config['config'] ?: array('email' => $staff->getEmail());
$form = $auth->getSetupForm($vars);
if ($_POST && $form && $form->isValid()) {
if ($config['config'] && $config['config']['external2fa'])
$external2fa = true;
// Save the setting based on setup form
$clean = $form->getClean();
if (!$external2fa) {
$config = ['config' => $clean, 'verified' => 0];
$staff->updateConfig(array(
$auth->getId() => JsonDataEncoder::encode($config)));
}
// Send verification token to the user
if ($token=$auth->send($staff)) {
// Transition to verify state
$form = $auth->getInputForm($vars);
$state = 'verify';
$info['notice'] = __('Token sent to you!');
} else {
// Generic error TODO: better wording
$info['error'] = __('Error sending Token - double check entry');
}
}
}
}
include STAFFINC_DIR . 'templates/2fas.tmpl.php';
}
function reset2fA($staffId) {
global $thisstaff;
if (!$thisstaff)
Http::response(403, 'Agent login required');
if (!$thisstaff->isAdmin())
Http::response(403, 'Access denied');
$default_2fa = ConfigItem::getConfigsByNamespace('staff.'.$staffId, 'default_2fa');
if ($default_2fa)
$default_2fa->delete();
}
}

View File

@@ -668,7 +668,7 @@ class TasksAjaxAPI extends AjaxController {
if (!$task->checkStaffPerm($thisstaff, Task::PERM_ASSIGN)
|| !($form=$task->getAssignmentForm($_POST, array(
'target' => $target))))
'target' => $target, 'filterVisibility' => true))))
Http::response(403, __('Permission denied'));
$errors = array();

View File

@@ -1086,13 +1086,7 @@ class TicketsAjaxAPI extends AjaxController {
$depts = $tickets->values_flat('dept_id');
}
$members = Staff::objects()
->distinct('staff_id')
->filter(array(
'onvacation' => 0,
'isactive' => 1,
)
);
$members = $thisstaff->getDeptAgents(array('available' => true));
if ($depts) {
$all_agent_depts = Dept::objects()->filter(

297
include/class.2fa.php Normal file
View File

@@ -0,0 +1,297 @@
<?php
/**
* TwoFactorAuthentication backend
*
* Provides the basis of abstracting 2fa backends
* The authentication backend should define a validate() method which
* receives a user and OPT.
*/
abstract class TwoFactorAuthenticationBackend {
// Global registry
static protected $registry = array();
// Grace period in minutes before OTP is expired and user logged out
// It's hardcoded to 6 minutes here but downstream backends can make it
// configurable
protected $timeout = 6;
// Maximum number of validation attempts before the user is logged out
// It's hardcoded to 3 attempts here but downstream backends can make it
// configurable
protected $maxstrikes = 3;
// Base properties
static $id;
static $name;
static $desc;
// Forms
private $_setupform;
private $_inputform;
// Send OTP to user specified and stash it
abstract function send($user);
// validate OTP provided by user
abstract function validate($form, $user);
function getId() {
return static::$id;
}
function getName() {
return __(static::$name);
}
function getDescription() {
return __(static::$desc);
}
function getTimeout() {
return $this->timeout;
}
function getMaxStrikes() {
return $this->maxstrikes;
}
protected function getSetupOptions() {
return array();
}
protected function getInputOptions() {
return array();
}
// stash OTP info in the session
protected function store($otp) {
$store = &$_SESSION['_2fa'][$this->getId()];
$store = ['otp' => $otp, 'time' => time(), 'strikes' => 0];
return $store;
}
// Validate OPT
// On strict mode check strikes and timeout
protected function _validate($otp, $strict=true) {
$store = &$_SESSION['_2fa'][$this->getId()];
// Track and check the attempts
$store['strikes'] += 1;
if ($strict && $store['strikes'] > $this->getMaxStrikes())
throw new ExpiredOTP(__('Too many attempts'));
// Check timeout - if expired throw an exception.
if ($strict
&& ($timeout=$this->getTimeout())
&& ($store['time']+($timeout*60)) < time())
throw new ExpiredOTP(__('Expired OTP'));
// Check the OTP
return (!strcmp($store['otp'], $otp));
}
// Called on a successfull validation for house keeping e.g clear 2fa
// flags
protected function onValidate($user) {
$user->clear2FA();
}
// Get a form the user uses to setup 2fa
function getSetupForm($data=null) {
if (!$this->_setupForm) {
$this->_setupForm = new SimpleForm($this->getSetupOptions(), $data);
}
return $this->_setupForm;
}
// Get a form the user uses to input OTP
function getInputForm($data=null) {
if (!$this->_inputForm) {
$this->_inputForm = new SimpleForm($this->getInputOptions(), $data);
}
return $this->_inputForm;
}
static function register($class) {
if (is_string($class) && class_exists($class))
$class = new $class();
if (!is_object($class)
|| !($class instanceof TwoFactorAuthenticationBackend))
return false;
return static::_register($class);
}
static function _register($class) {
if (isset(static::$registry[$class::$id]))
return false;
static::$registry[$class::$id] = $class;
}
static function allRegistered() {
return static::$registry;
}
static function getBackend($id) {
if ($id
&& ($backends = static::allRegistered())
&& isset($backends[$id]))
return $backends[$id];
}
static function lookup($id) {
return static::getBackend($id);
}
}
class ExpiredOTP extends Exception {}
/*
* user type container classes to aid in registry segmentation
*
*/
abstract class Staff2FABackend extends TwoFactorAuthenticationBackend {
static private $_registry = array();
static function _register($class) {
if (isset(static::$_registry[$class::$id]))
return false;
static::$_registry[$class::$id] = $class;
}
static function allRegistered() {
return array_merge(self::$_registry, parent::allRegistered());
}
abstract function send($user);
abstract function validate($form, $user);
}
abstract class User2FABackend extends TwoFactorAuthenticationBackend {
static private $_registry = array();
static function _register($class) {
if (isset(static::$_registry[$class::$id]))
return false;
static::$_registry[$class::$id] = $class;
}
static function allRegistered() {
return array_merge(self::$_registry, parent::allRegistered());
}
abstract function send($user);
abstract function validate($form, $user);
}
/*
* Email2FABackend
*
* Email based two factor authentication.
*
* This is the default 2FA that works out of the box once users configure
* it.
*
*/
class Email2FABackend extends TwoFactorAuthenticationBackend {
static $id = "2fa-email";
static $name = /* @trans */ 'Email';
static $desc = /* @trans */ 'Verification codes are sent by email';
protected function getSetupOptions() {
return array(
'email' => new TextboxField(array(
'id'=>2, 'label'=>__('Email Address'), 'required'=>true, 'default'=>'',
'validator'=>'email', 'hint'=>__('Valid email address'),
'configuration'=>array('size'=>40, 'length'=>40),
)),
);
}
protected function getInputOptions() {
return array(
'token' => new TextboxField(array(
'id'=>1, 'label'=>__('Verification Code'), 'required'=>true, 'default'=>'',
'validator'=>'number',
'hint'=>__('Please enter the code you were sent'),
'configuration'=>array(
'size'=>40, 'length'=>40,
'autocomplete' => 'one-time-code',
'inputmode' => 'numeric',
'pattern' => '[0-9]*',
'validator-error' => __('Invalid Code format'),
),
)),
);
}
function validate($form, $user) {
// Make sure form is valid and token exists
if (!($form->isValid()
&& ($clean=$form->getClean())
&& $clean['token']))
return false;
// upstream validation might throw an exception due to expired token
// or too many attempts (timeout). It's the responsibility of the
// caller to catch and handle such exceptions.
if (!$this->_validate($clean['token']))
return false;
// Validator doesn't do house cleaning - it's our responsibility
$this->onValidate($user);
return true;
}
function send($user) {
global $ost, $cfg;
// Get backend configuration for this user
if (!$cfg || !($info = $user->get2FAConfig($this->getId())))
return false;
// Email to send the OTP via
if (!($email = $cfg->getAlertEmail() ?: $cfg->getDefaultEmail()))
return false;
// Generate OTP
$otp = Misc::randNumber(6);
// Stash it in the session
$this->store($otp);
$template = 'email2fa-staff';
$content = Page::lookupByType($template);
if (!$content)
return new BaseError(/* @trans */ 'Unable to retrieve two factor authentication email template');
$vars = array(
'url' => $ost->getConfig()->getBaseUrl(),
'otp' => $otp,
'staff' => $user,
'recipient' => $user,
);
$lang = $user->lang ?: $user->getExtraAttr('browser_lang');
$msg = $ost->replaceTemplateVariables(array(
'subj' => $content->getLocalName($lang),
'body' => $content->getLocalBody($lang),
), $vars);
$email->send($user->getEmail(), Format::striptags($msg['subj']),
$msg['body']);
// MD5 here is not meant to be secure here - just done to avoid plain leaks
return md5($otp);
}
}
// Register email2fa for both agents and users (parent class)
TwoFactorAuthenticationBackend::register('Email2FABackend');
?>

View File

@@ -1,16 +1,23 @@
<?php
require_once(INCLUDE_DIR.'class.2fa.php');
interface AuthenticatedUser {
/* PHP 5.3 < 5.3.8 will crash with some abstract inheritance issue
* ------------------------------------------------------------
// Get basic information
function getId();
function getUsername();
function getUserType();
// Get password reset timestamp
function getPasswdResetTimestamp();
//Backend used to authenticate the user
function getAuthBackend();
// Get 2FA Backend
function get2FABackend();
//Authentication key
function setAuthKey($key);
@@ -22,7 +29,6 @@ interface AuthenticatedUser {
// Signal method to allow performing extra things when a user is logged
// into the sysem
function onLogin($bk);
*/
}
abstract class BaseAuthenticatedUser
@@ -35,9 +41,17 @@ implements AuthenticatedUser {
abstract function getUsername();
abstract function getUserType();
// Get password reset timestamp
function getPasswdResetTimestamp() {
return null;
}
//Backend used to authenticate the user
abstract function getAuthBackend();
// Get 2FA Backend
abstract function get2FABackend();
//Authentication key
function setAuthKey($key) {
$this->authkey = $key;
@@ -350,6 +364,15 @@ abstract class AuthenticationBackend {
return false;
}
/**
* Get supported password policies for the backend.
*
*/
function getPasswordPolicies($user=null) {
return PasswordPolicy::allActivePolicies();
}
/**
* Request the backend to update the password for a user. This method is
* the main entry for password updates so that password policies can be
@@ -361,7 +384,9 @@ abstract class AuthenticationBackend {
* PasswordUpdateFailed — if backend failed to update the password
*/
function setPassword($user, $password, $current=false) {
PasswordPolicy::checkPassword($password, $current);
foreach ($this->getPasswordPolicies($user) as $P)
$P->onSet($password, $current);
$rv = $this->syncPassword($user, $password);
if ($rv) {
$info = array('password' => $password, 'current' => $current);
@@ -370,6 +395,17 @@ abstract class AuthenticationBackend {
return $rv;
}
/*
* Request the backend to check the policies for a just logged
* in user.
* Throws: BadPassword & ExpiredPassword - for password related failures
*/
function checkPolicies($user, $password) {
// Password policies
foreach ($this->getPasswordPolicies($user) as $P)
$P->onLogin($user, $password);
}
/**
* Request the backend to update the user's password with the password
* given. This method should only be used if the backend advertises
@@ -454,6 +490,18 @@ abstract class StaffAuthenticationBackend extends AuthenticationBackend {
return in_array($bk::$id, array_map('strtolower', $backends));
}
function getPasswordPolicies($user=null) {
global $cfg;
$policies = PasswordPolicy::allActivePolicies();
if ($cfg && ($policy = $cfg->getStaffPasswordPolicy())) {
foreach ($policies as $P)
if ($policy == $P::$id)
return array($P);
}
return $policies;
}
function getAllowedBackends($userid) {
$backends =array();
@@ -491,24 +539,32 @@ abstract class StaffAuthenticationBackend extends AuthenticationBackend {
$agent = Staff::lookup($staff->getId());
$type = array('type' => 'login');
Signal::send('person.login', $agent, $type);
// Check if the agent has 2fa enabled
$auth2fa = null;
if (($_2fa = $staff->get2FABackend())
&& ($token=$_2fa->send($staff))) {
$auth2fa = sprintf('%s:%s:%s',
$_2fa->getId(), md5($token.$staff->getId()), time());
}
// Tag the authkey.
$authkey = $bk::$id.':'.$authkey;
// Now set session crap and lets roll baby!
$authsession = &$_SESSION['_auth']['staff'];
$authsession = array(); //clear.
$authsession['id'] = $staff->getId();
$authsession['key'] = $authkey;
$authsession['2fa'] = $auth2fa;
$staff->setAuthKey($authkey);
$staff->refreshSession(true); //set the hash.
Signal::send('auth.login.succeeded', $staff);
if ($bk->supportsInteractiveAuthentication())
$staff->cancelResetTokens();
// Update last-used language, login time, etc
$staff->onLogin($bk);
@@ -642,6 +698,19 @@ abstract class UserAuthenticationBackend extends AuthenticationBackend {
return array_merge(self::$_registry, parent::allRegistered());
}
function getPasswordPolicies($user=null) {
global $cfg;
$policies = PasswordPolicy::allActivePolicies();
if ($cfg && ($policy = $cfg->getClientPasswordPolicy())) {
foreach ($policies as $P)
if ($policy == $P::$id)
return array($P);
}
return $policies;
}
function getAllowedBackends($userid) {
$backends = array();
$sql = 'SELECT A1.backend FROM '.USER_ACCOUNT_TABLE
@@ -1008,21 +1077,18 @@ class UserAuthStrikeBackend extends AuthStrikeBackend {
UserAuthenticationBackend::register('UserAuthStrikeBackend');
class osTicketAuthentication extends StaffAuthenticationBackend {
class osTicketStaffAuthentication extends StaffAuthenticationBackend {
static $name = "Local Authentication";
static $id = "local";
function authenticate($username, $password) {
if (($user = StaffSession::lookup($username)) && $user->getId() &&
$user->check_passwd($password)) {
//update last login && password reset stuff.
$sql='UPDATE '.STAFF_TABLE.' SET lastlogin=NOW() ';
if($user->isPasswdResetDue() && !$user->isAdmin())
$sql.=',change_passwd=1';
$sql.=' WHERE staff_id='.db_input($user->getId());
db_query($sql);
try {
$this->checkPolicies($user, $password);
} catch (BadPassword | ExpiredPassword $ex) {
$user->change_passwd = 1;
}
return $user;
}
}
@@ -1035,8 +1101,12 @@ class osTicketAuthentication extends StaffAuthenticationBackend {
$staff->passwd = Passwd::hash($password);
}
static function checkPassword($new, $current) {
PasswordPolicy::checkPassword($new, $current, new self());
}
}
StaffAuthenticationBackend::register('osTicketAuthentication');
StaffAuthenticationBackend::register('osTicketStaffAuthentication');
class PasswordResetTokenBackend extends StaffAuthenticationBackend {
static $id = "pwreset.staff";
@@ -1253,11 +1323,15 @@ class osTicketClientAuthentication extends UserAuthenticationBackend {
if (($client = new ClientSession(new EndUser($acct->getUser())))
&& !$client->getId())
return false;
elseif (!$acct->checkPassword($password))
elseif (!$acct->check_passwd($password))
return false;
else
return $client;
}
static function checkPassword($new, $current) {
PasswordPolicy::checkPassword($new, $current, new self());
}
}
UserAuthenticationBackend::register('osTicketClientAuthentication');
@@ -1329,26 +1403,49 @@ UserAuthenticationBackend::register('ClientAcctConfirmationTokenBackend');
// ----- Password Policy --------------------------------------
class BadPassword extends Exception {}
class ExpiredPassword extends Exception {}
class PasswordUpdateFailed extends Exception {}
abstract class PasswordPolicy {
static protected $registry = array();
static $id;
static $name;
/**
* Check a password and throw BadPassword with a meaningful message if
* the password cannot be accepted.
*/
abstract function processPassword($new, $current);
abstract function onset($new, $current);
static function checkPassword($new, $current) {
foreach (static::allActivePolicies() as $P) {
$P->processPassword($new, $current);
}
/*
* Called on login to enforce policies & check for expired passwords
*/
abstract function onLogin($user, $password);
/*
* get friendly name of the policy
*/
function getName() {
return static::$name;
}
/*
* Check a password aganist all available policies
*/
static function checkPassword($new, $current, $bk=null) {
if ($bk && is_a($bk, 'AuthenticationBackend'))
$policies = $bk->getPasswordPolicies();
else
$policies = self::allActivePolicies();
foreach ($policies as $P)
$P->onSet($new, $current);
}
static function allActivePolicies() {
$policies = array();
foreach (static::$registry as $P) {
foreach (array_reverse(static::$registry) as $P) {
if (is_string($P) && class_exists($P))
$P = new $P();
if ($P instanceof PasswordPolicy)
@@ -1390,9 +1487,29 @@ abstract class PasswordPolicy {
}
Signal::connect('auth.clean', array('PasswordPolicy', 'cleanSessions'));
/*
* Basic default password policy that ships with osTicket.
*
*/
class osTicketPasswordPolicy
extends PasswordPolicy {
function processPassword($passwd, $current) {
static $id = "basic";
static $name = /* @trans */ "Default Basic Policy";
function onLogin($user, $password) {
global $cfg;
// Check for possible password expiration
// Check is only here for legacy reasons - password management
// policies are now done via plugins.
if ($cfg && $user
&& ($period=$cfg->getPasswdResetPeriod())
&& ($time=$user->getPasswdResetTimestamp())
&& $time < time()-($period*2629800))
throw new ExpiredPassword(__('Expired password'));
}
function onSet($passwd, $current) {
if (strlen($passwd) < 6) {
throw new BadPassword(
__('Password must be at least 6 characters'));

View File

@@ -229,11 +229,22 @@ extends VerySimpleModel {
}
static function getCannedResponses($deptId=0, $explicit=false) {
global $thisstaff;
$canned = static::objects()
->filter(array('isenabled' => true))
->order_by('title')
->values_flat('canned_id', 'title');
if ($thisstaff) {
$staffDepts = array();
$staffDepts = $thisstaff->getDepts();
$staffDepts[] = 0;
$canned->filter(array('dept_id__in' => $staffDepts));
}
if ($deptId) {
$depts = array($deptId);
if (!$explicit)

View File

@@ -247,6 +247,11 @@ class EndUser extends BaseAuthenticatedUser {
return UserAuthenticationBackend::getBackend($authkey);
}
function get2FABackend() {
//TODO: support 2FA on client portal
return null;
}
function getTicketStats() {
if (!isset($this->_stats))
$this->_stats = $this->getStats();
@@ -381,7 +386,7 @@ class EndUser extends BaseAuthenticatedUser {
class ClientAccount extends UserAccount {
function checkPassword($password, $autoupdate=true) {
function check_passwd($password, $autoupdate=true) {
/*bcrypt based password match*/
if(Passwd::cmp($password, $this->get('passwd')))
@@ -402,7 +407,7 @@ class ClientAccount extends UserAccount {
}
function hasCurrentPassword($password) {
return $this->checkPassword($password, false);
return $this->check_passwd($password, false);
}
function cancelResetTokens() {
@@ -431,32 +436,37 @@ class ClientAccount extends UserAccount {
return parent::update($vars, $errors);
$rtoken = $_SESSION['_client']['reset-token'];
if ($vars['passwd1'] || $vars['passwd2'] || $vars['cpasswd'] || $rtoken) {
if ($rtoken) {
$_config = new Config('pwreset');
if ($_config->get($rtoken) != 'c'.$this->getUserId())
$errors['err'] =
__('Invalid reset token. Logout and try again');
elseif (!($ts = $_config->lastModified($rtoken))
&& ($cfg->getPwResetWindow() < (time() - strtotime($ts))))
$errors['err'] =
__('Invalid reset token. Logout and try again');
} elseif ($vars['passwd1'] || $vars['passwd2'] || $vars['cpasswd']) {
if (!$vars['passwd1'])
$errors['passwd1']=__('New password is required');
elseif ($vars['passwd1'] && strlen($vars['passwd1'])<6)
$errors['passwd1']=__('Password must be at least 6 characters');
elseif ($vars['passwd1'] && strcmp($vars['passwd1'], $vars['passwd2']))
$errors['passwd2']=__('Passwords do not match');
if ($rtoken) {
$_config = new Config('pwreset');
if ($_config->get($rtoken) != 'c'.$this->getUserId())
$errors['err'] =
__('Invalid reset token. Logout and try again');
elseif (!($ts = $_config->lastModified($rtoken))
&& ($cfg->getPwResetWindow() < (time() - strtotime($ts))))
$errors['err'] =
__('Invalid reset token. Logout and try again');
}
elseif ($this->get('passwd')) {
if (!$vars['cpasswd'])
$errors['cpasswd']=__('Current password is required');
elseif (!$this->hasCurrentPassword($vars['cpasswd']))
$errors['cpasswd']=__('Invalid current password!');
elseif (!strcasecmp($vars['passwd1'], $vars['cpasswd']))
$errors['passwd1']=__('New password MUST be different from the current password!');
}
// Check password policies
if (!$errors) {
try {
UserAccount::checkPassword($vars['passwd1'], @$vars['cpasswd']);
} catch (BadPassword $ex) {
$errors['passwd1'] = $ex->getMessage();
}
}
}

View File

@@ -186,6 +186,23 @@ extends VerySimpleModel {
'updated__lt' => SqlFunction::NOW()->minus(SqlInterval::SECOND($period)),
))->delete();
}
function getConfigsByNamespace($namespace=false, $key, $value=false) {
$filter = array();
$filter['key'] = $key;
if ($namespace)
$filter['namespace'] = $namespace;
if ($value)
$filter['value'] = $value;
$token = ConfigItem::objects()
->filter($filter);
return $namespace ? $token[0] : $token;
}
}
class OsticketConfig extends Config {
@@ -451,10 +468,25 @@ class OsticketConfig extends Config {
return $this->get('overdue_grace_period');
}
// This is here for legacy reasons - default osTicket Password Policy
// uses it, if previously set.
function getPasswdResetPeriod() {
return $this->get('passwd_reset_period');
}
function getStaffPasswordPolicy() {
return $this->get('agent_passwd_policy');
}
function getClientPasswordPolicy() {
return $this->get('client_passwd_policy');
}
function require2FAForAgents() {
return $this->get('require_agent_2fa');
}
function isRichTextEnabled() {
return $this->get('enable_richtext');
}
@@ -1321,13 +1353,14 @@ class OsticketConfig extends Config {
return false;
return $this->updateAll(array(
'passwd_reset_period'=>$vars['passwd_reset_period'],
'agent_passwd_policy'=>$vars['agent_passwd_policy'],
'staff_max_logins'=>$vars['staff_max_logins'],
'staff_login_timeout'=>$vars['staff_login_timeout'],
'staff_session_timeout'=>$vars['staff_session_timeout'],
'staff_ip_binding'=>isset($vars['staff_ip_binding'])?1:0,
'allow_pw_reset'=>isset($vars['allow_pw_reset'])?1:0,
'pw_reset_window'=>$vars['pw_reset_window'],
'require_agent_2fa'=>$vars['require_agent_2fa'],
'agent_name_format'=>$vars['agent_name_format'],
'hide_staff_name'=>isset($vars['hide_staff_name']) ? 1 : 0,
'agent_avatar'=>$vars['agent_avatar'],
@@ -1348,6 +1381,7 @@ class OsticketConfig extends Config {
return false;
return $this->updateAll(array(
'client_passwd_policy'=>$vars['client_passwd_policy'],
'client_max_logins'=>$vars['client_max_logins'],
'client_login_timeout'=>$vars['client_login_timeout'],
'client_session_timeout'=>$vars['client_session_timeout'],

View File

@@ -14,6 +14,7 @@
vim: expandtab sw=4 ts=4 sts=4:
**********************************************************************/
require_once INCLUDE_DIR . 'class.search.php';
require_once INCLUDE_DIR.'class.role.php';
class Dept extends VerySimpleModel
implements TemplateVariable, Searchable {
@@ -76,6 +77,16 @@ implements TemplateVariable, Searchable {
const FLAG_ASSIGN_PRIMARY_ONLY = 0x0010;
const FLAG_DISABLE_REOPEN_AUTO_ASSIGN = 0x0020;
const PERM_DEPT = 'visibility.departments';
static protected $perms = array(
self::PERM_DEPT => array(
'title' => /* @trans */ 'Department',
'desc' => /* @trans */ 'Ability to see all Departments',
'primary' => true,
),
);
function asVar() {
return $this->getName();
}
@@ -632,6 +643,15 @@ implements TemplateVariable, Searchable {
return $row ? $row[0] : 0;
}
static function getEmailIdById($id) {
$row = static::objects()
->filter(array('id' => $id))
->values_flat('email_id')
->first();
return $row ? $row[0] : 0;
}
function getNameById($id) {
$names = Dept::getDepartments();
return $names[$id];
@@ -654,7 +674,7 @@ implements TemplateVariable, Searchable {
// XXX: This will upset the static $depts array
$depts = array();
$query = self::objects();
if (isset($criteria['publiconly']))
if (isset($criteria['publiconly']) && $criteria['publiconly'])
$query->filter(array(
'flags__hasbit' => Dept::FLAG_ACTIVE));
@@ -973,7 +993,12 @@ implements TemplateVariable, Searchable {
return true;
}
static function getPermissions() {
return self::$perms;
}
}
RolePermission::register(/* @trans */ 'Miscellaneous', Dept::getPermissions());
class DepartmentQuickAddForm
extends Form {

View File

@@ -1487,7 +1487,7 @@ class SelectionField extends FormField {
return $this->_list;
}
function getWidget($widgetClass=false) {
function getWidget($widgetClass=false, $options=array()) {
$config = $this->getConfiguration();
if ($config['widget'] == 'typeahead' && $config['multiselect'] == false)
$widgetClass = 'TypeaheadSelectionWidget';
@@ -1744,7 +1744,7 @@ class SelectionField extends FormField {
return $config;
}
function getChoices($verbose=false) {
function getChoices($verbose=false, $options=array()) {
if (!$this->_choices || $verbose) {
$choices = array();
foreach ($this->getList()->getItems() as $i)
@@ -1925,7 +1925,7 @@ class TypeaheadSelectionWidget extends ChoicesWidget {
return array($this->getValue() => $this->getEnteredValue());
}
function getValue() {
function getValue($options=array()) {
$data = $this->field->getSource();
$name = $this->field->get('name');
if (isset($data["{$this->name}_id"]) && is_numeric($data["{$this->name}_id"])) {

View File

@@ -489,6 +489,9 @@ class Email extends VerySimpleModel {
if ($options['smtp'])
$objects = $objects->filter(array('smtp_active'=>true));
if ($options['depts'])
$objects = $objects->filter(array('dept_id__in'=>$options['depts']));
if (!$flat)
return $objects;

View File

@@ -112,7 +112,7 @@ class Form {
}
function getTitle() { return $this->title; }
function getInstructions() { return $this->instructions; }
function getInstructions() { return Format::htmldecode($this->instructions); }
function getSource() { return $this->_source; }
function setSource($source) { $this->_source = $source; }
@@ -222,11 +222,11 @@ class Form {
echo $this->getMedia();
}
function getMedia() {
function getMedia($options=array()) {
static $dedup = array();
foreach ($this->getFields() as $f) {
if (($M = $f->getMedia()) && is_array($M)) {
if (($M = $f->getMedia($options)) && is_array($M)) {
foreach ($M as $type=>$files) {
foreach ($files as $url) {
$key = strtolower($type.$url);
@@ -1215,8 +1215,8 @@ class FormField {
return;
}
function getMedia() {
$widget = $this->getWidget();
function getMedia($options=array()) {
$widget = $this->getWidget(false,$options);
return $widget::$media;
}
@@ -1330,13 +1330,13 @@ class FormField {
$this->_config[$prop] = $value;
}
function getWidget($widgetClass=false) {
function getWidget($widgetClass=false, $options=array()) {
if (!static::$widget)
throw new Exception(__('Widget not defined for this field'));
if (!isset($this->_widget)) {
$wc = $widgetClass ?: $this->get('widget') ?: static::$widget;
$this->_widget = new $wc($this);
$this->_widget->parseValue();
$this->_widget->parseValue($options);
}
return $this->_widget;
}
@@ -1535,7 +1535,8 @@ class PasswordField extends TextboxField {
function __construct($options=array()) {
parent::__construct($options);
$this->set('validator', 'password');
if (!isset($options['validator']))
$this->set('validator', 'password');
}
function parse($value) {
@@ -1960,7 +1961,7 @@ class ChoiceField extends FormField {
return $selection;
}
function getChoices($verbose=false) {
function getChoices($verbose=false, $options=array()) {
if ($this->_choices === null || $verbose) {
// Allow choices to be set in this->ht (for configurationOptions)
$this->_choices = $this->get('choices');
@@ -2005,16 +2006,16 @@ class ChoiceField extends FormField {
);
}
function getSearchMethodWidgets() {
function getSearchMethodWidgets($options=array()) {
return array(
'set' => null,
'nset' => null,
'includes' => array('ChoiceField', array(
'choices' => $this->getChoices(),
'choices' => $this->getChoices(false, $options),
'configuration' => array('multiselect' => true),
)),
'!includes' => array('ChoiceField', array(
'choices' => $this->getChoices(),
'choices' => $this->getChoices(false, $options),
'configuration' => array('multiselect' => true),
)),
);
@@ -2051,7 +2052,7 @@ class ChoiceField extends FormField {
}
function getQuickFilterChoices() {
return $this->getChoices();
return $this->getChoices(false, array('filterVisibility' => true));
}
function applyQuickFilter($query, $qf_value, $name=false) {
@@ -2713,7 +2714,7 @@ class ThreadEntryField extends FormField {
function isPresentationOnly() {
return true;
}
function getMedia() {
function getMedia($options=array()) {
$config = $this->getConfiguration();
$media = parent::getMedia() ?: array();
if ($config['attachments'])
@@ -2762,7 +2763,7 @@ class ThreadEntryField extends FormField {
return $config['attachments'];
}
function getWidget($widgetClass=false) {
function getWidget($widgetClass=false, $options=array()) {
if ($hint = $this->getLocal('hint'))
$this->set('placeholder', Format::striptags($hint));
$this->set('hint', null);
@@ -2776,8 +2777,10 @@ class TopicField extends ChoiceField {
var $_choices;
function getTopics() {
global $thisstaff;
if (!isset($this->topics))
$this->topics = Topic::getHelpTopics(false, false, true);
$this->topics = $thisstaff->getTopicNames();
return $this->topics;
}
@@ -2788,7 +2791,7 @@ class TopicField extends ChoiceField {
return Topic::lookup($id);
}
function getWidget($widgetClass=false) {
function getWidget($widgetClass=false, $options=array()) {
$default = $this->get('default');
$widget = parent::getWidget($widgetClass);
if ($widget->value instanceof Topic)
@@ -2802,7 +2805,7 @@ class TopicField extends ChoiceField {
return true;
}
function getChoices($verbose=false) {
function getChoices($verbose=false, $options=array()) {
if (!isset($this->_choices)) {
$this->_choices = $this->getTopics();
}
@@ -2894,7 +2897,7 @@ class SLAField extends ChoiceField {
return SLA::lookup($id);
}
function getWidget($widgetClass=false) {
function getWidget($widgetClass=false, $options=array()) {
$default = $this->get('default');
$widget = parent::getWidget($widgetClass);
if ($widget->value instanceof SLA)
@@ -2908,7 +2911,7 @@ class SLAField extends ChoiceField {
return true;
}
function getChoices($verbose=false) {
function getChoices($verbose=false, $options=array()) {
if (!isset($this->_choices)) {
$choices = array();
foreach ($this->getSLAs() as $s)
@@ -3005,7 +3008,7 @@ class PriorityField extends ChoiceField {
return Priority::lookup($id);
}
function getWidget($widgetClass=false) {
function getWidget($widgetClass=false, $options=array()) {
$widget = parent::getWidget($widgetClass);
if ($widget->value instanceof Priority)
$widget->value = $widget->value->getId();
@@ -3016,7 +3019,7 @@ class PriorityField extends ChoiceField {
return true;
}
function getChoices($verbose=false) {
function getChoices($verbose=false, $options=array()) {
if (!isset($this->_choices)) {
$choices = array();
foreach ($this->getPriorities() as $p)
@@ -3141,7 +3144,7 @@ class TimezoneField extends ChoiceField {
return false;
}
function getChoices($verbose=false) {
function getChoices($verbose=false, $options=array()) {
global $cfg;
$choices = array();
@@ -3177,7 +3180,7 @@ class TimezoneField extends ChoiceField {
class DepartmentField extends ChoiceField {
function getWidget($widgetClass=false) {
function getWidget($widgetClass=false, $options=array()) {
$widget = $this->_widget ?: parent::getWidget($widgetClass);
if (is_object($widget->value))
$widget->value = $widget->value->getId();
@@ -3188,8 +3191,8 @@ class DepartmentField extends ChoiceField {
return true;
}
function getChoices($verbose=false) {
global $cfg;
function getChoices($verbose=false, $options=array()) {
global $cfg, $thisstaff;
$selected = self::getWidget();
if($selected && $selected->value) {
@@ -3205,30 +3208,42 @@ class DepartmentField extends ChoiceField {
}
}
$active_depts = Dept::objects()
->filter(array('flags__hasbit' => Dept::FLAG_ACTIVE))
->values('id', 'name')
->order_by('name');
if ($thisstaff) {
$active = $thisstaff->getDepartmentNames(true);
$choices = array();
if ($depts = Dept::getDepartments(null, true, Dept::DISPLAY_DISABLED)) {
//create array w/queryset
$active = array();
foreach ($active_depts as $dept)
$active[$dept['id']] = $dept['name'];
$choices = array();
if ($options['filterVisibility'])
$depts = $thisstaff->getDepartmentNames();
else {
$depts = Dept::getDepartments(null, true, Dept::DISPLAY_DISABLED);
return $depts;
}
} else {
$active_depts = Dept::objects()
->filter(array('flags__hasbit' => Dept::FLAG_ACTIVE))
->values('id', 'name')
->order_by('name');
//add selected dept to list
if($current_id)
$choices = array();
if ($depts = Dept::getDepartments(null, true, Dept::DISPLAY_DISABLED)) {
//create array w/queryset
$active = array();
foreach ($active_depts as $dept)
$active[$dept['id']] = $dept['name'];
}
}
//add selected dept to list
if($current_id)
$active[$current_id] = $current_name;
else
else
return $active;
foreach ($depts as $id => $name) {
foreach ($depts as $id => $name) {
$choices[$id] = $name;
if(!array_key_exists($id, $active) && $current_id)
unset($choices[$id]);
}
}
}
return $choices;
}
@@ -3313,7 +3328,7 @@ class AssigneeField extends ChoiceField {
var $_choices = null;
var $_criteria = null;
function getWidget($widgetClass=false) {
function getWidget($widgetClass=false, $options=array()) {
$widget = parent::getWidget($widgetClass);
$value = $widget->value;
if (is_object($value)) {
@@ -3328,7 +3343,7 @@ class AssigneeField extends ChoiceField {
function getCriteria() {
if (!isset($this->_criteria)) {
$this->_criteria = array('available' => true);
$this->_criteria = array('available' => true, 'namesOnly' => true);
if (($c=parent::getCriteria()))
$this->_criteria = array_merge($this->_criteria, $c);
}
@@ -3353,8 +3368,8 @@ class AssigneeField extends ChoiceField {
return $value;
}
function getChoices($verbose=false) {
global $cfg;
function getChoices($verbose=false, $options=array()) {
global $cfg, $thisstaff;
if (!isset($this->_choices)) {
$config = $this->getConfiguration();
@@ -3364,14 +3379,18 @@ class AssigneeField extends ChoiceField {
$A = current($choices);
$criteria = $this->getCriteria();
$agents = array();
if (($dept=$config['dept']) && $dept->assignMembersOnly()) {
if (($members = $dept->getAvailableMembers()))
foreach ($members as $member)
$agents[$member->getId()] = $member;
} else {
$agents = Staff::getStaffMembers($criteria);
if ($options['filterVisibility'])
$agents = $thisstaff->getDeptAgents($criteria);
else {
if (($dept=$config['dept']) && $dept->assignMembersOnly()) {
if (($members = $dept->getAvailableMembers()))
foreach ($members as $member)
$agents[$member->getId()] = $member;
} else
$agents = Staff::getStaffMembers($criteria);
}
foreach ($agents as $id => $name)
$A['s'.$id] = $name;
@@ -3387,6 +3406,7 @@ class AssigneeField extends ChoiceField {
return $this->_choices;
}
function getChoice($value) {
$choices = $this->getChoices();
$selection = array();
@@ -3406,6 +3426,17 @@ class AssigneeField extends ChoiceField {
return parent::getChoice($value);
}
function getQuickFilterChoices() {
$choices = $this->getChoices(false, array('filterVisibility' => true));
$names = array();
foreach ($choices as $value) {
foreach ($value as $key => $value)
$names[$key] = is_object($value) ? $value->name : $value;
}
return $names;
}
function getValue() {
if (($value = parent::getValue()) && ($id=$this->getClean())) {
$name = (is_object($value[key($value)]) && get_class($value[key($value)]) == 'AgentsName') ?
@@ -3557,7 +3588,7 @@ class TicketStateField extends ChoiceField {
return false;
}
function getChoices($verbose=false) {
function getChoices($verbose=false, $options=array()) {
static $_choices;
$states = static::$_states;
@@ -3645,7 +3676,7 @@ class TicketFlagField extends ChoiceField {
return true;
}
function getChoices($verbose=false) {
function getChoices($verbose=false, $options=array()) {
$this->ht['default'] = '';
if (!$this->_choices) {
@@ -4207,8 +4238,8 @@ class Widget {
$this->id = '_' . $this->name;
}
function parseValue() {
$this->value = $this->getValue();
function parseValue($options=array()) {
$this->value = $this->getValue($options);
if (!isset($this->value) && is_object($this->field->getAnswer()))
$this->value = $this->field->getAnswer()->getValue();
if (!isset($this->value) && isset($this->field->value))
@@ -4265,20 +4296,46 @@ class TextboxWidget extends Widget {
if (!isset($config[$k]) || !$config[$k])
$config[$k] = $v;
}
if (isset($config['size']))
$size = "size=\"{$config['size']}\"";
if (isset($config['length']) && $config['length'])
$maxlength = "maxlength=\"{$config['length']}\"";
if (isset($config['classes']))
$classes = 'class="'.$config['classes'].'"';
if (isset($config['autocomplete']))
$autocomplete = 'autocomplete="'.$config['autocomplete'].'"';
// Input attributes
$attrs = array();
foreach ($config as $k => $v) {
switch ($k) {
case 'autocomplete':
if (is_numeric($v))
$v = $v ? 'on' : 'off';
$attrs[$k] = '"'.$v.'"';
break;
case 'disabled';
$attrs[$k] = '"disabled"';
break;
case 'translatable':
if ($v)
$attrs['data-translate-tag'] = '"'.$v.'"';
break;
case 'size':
case 'maxlength':
if ($v && is_numeric($v))
$attrs[$k] = '"'.$v.'"';
break;
case 'class':
case 'classes':
$attrs['class'] = '"'.$v.'"';
break;
case 'inputmode':
case 'pattern':
$attrs[$k] = '"'.$v.'"';
break;
}
}
// autofocus
$autofocus = '';
if (isset($config['autofocus']))
$autofocus = 'autofocus';
if (isset($config['disabled']))
$disabled = 'disabled="disabled"';
if (isset($config['translatable']) && $config['translatable'])
$translatable = 'data-translate-tag="'.$config['translatable'].'"';
// placeholder
$attrs['placeholder'] = $this->field->getLocal('placeholder',
$config['placeholder']);
$type = static::$input_type;
$types = array(
'email' => 'email',
@@ -4286,14 +4343,11 @@ class TextboxWidget extends Widget {
);
if ($type == 'text' && isset($types[$config['validator']]))
$type = $types[$config['validator']];
$placeholder = sprintf('placeholder="%s"', $this->field->getLocal('placeholder',
$config['placeholder']));
?>
<input type="<?php echo $type; ?>"
id="<?php echo $this->id; ?>"
<?php echo implode(' ', array_filter(array(
$size, $maxlength, $classes, $autocomplete, $disabled,
$translatable, $placeholder, $autofocus))); ?>
<?php echo $autofocus .' '.Format::array_implode('=', ' ',
array_filter($attrs)); ?>
name="<?php echo $this->name; ?>"
value="<?php echo Format::htmlchars($this->value); ?>"/>
<?php
@@ -4332,7 +4386,7 @@ class PasswordWidget extends TextboxWidget {
return parent::render($mode, $extra);
}
function parseValue() {
function parseValue($options=array()) {
parent::parseValue();
// Show empty box unless failed POST
if ($_SERVER['REQUEST_METHOD'] != 'POST'
@@ -4344,27 +4398,41 @@ class PasswordWidget extends TextboxWidget {
class TextareaWidget extends Widget {
function render($options=array()) {
$config = $this->field->getConfiguration();
$class = $cols = $rows = $maxlength = "";
// process textarea attributes
$attrs = array();
if (isset($config['rows']))
$rows = "rows=\"{$config['rows']}\"";
if (isset($config['cols']))
$cols = "cols=\"{$config['cols']}\"";
if (isset($config['length']) && $config['length'])
$maxlength = "maxlength=\"{$config['length']}\"";
if (isset($config['html']) && $config['html']) {
$class = array('richtext', 'no-bar');
$class[] = @$config['size'] ?: 'small';
$class = sprintf('class="%s"', implode(' ', $class));
$this->value = Format::viewableImages($this->value);
foreach ($config as $k => $v) {
switch ($k) {
case 'rows':
case 'cols':
case 'length':
case 'maxlength':
if ($v && is_numeric($v))
$attrs[$k] = '"'.$v.'"';;
break;
case 'context':
$attrs['data-root-context'] = '"'.$v.'"';
break;
case 'class':
// This might conflict with html attr below
$attrs[$k] = '"'.$v.'"';
break;
case 'html':
if ($v) {
$class = array('richtext', 'no-bar');
$class[] = @$config['size'] ?: 'small';
$attrs['class'] = '"'.implode(' ', $class).'"';
$this->value = Format::viewableImages($this->value);
}
break;
}
}
if (isset($config['context']))
$attrs['data-root-context'] = '"'.$config['context'].'"';
// placeholder
$attrs['placeholder'] = $this->field->getLocal('placeholder',
$config['placeholder']);
?>
<span style="display:inline-block;width:100%">
<textarea <?php echo $rows." ".$cols." ".$maxlength." ".$class
.' '.Format::array_implode('=', ' ', $attrs)
.' placeholder="'.$this->field->getLocal('placeholder', $config['placeholder']).'"'; ?>
<textarea <?php echo Format::array_implode('=', ' ',
array_filter($attrs)); ?>
id="<?php echo $this->id; ?>"
name="<?php echo $this->name; ?>"><?php
echo Format::htmlchars($this->value);
@@ -4373,7 +4441,7 @@ class TextareaWidget extends Widget {
<?php
}
function parseValue() {
function parseValue($options=array()) {
parent::parseValue();
if (isset($this->value)) {
$value = $this->value;
@@ -4440,7 +4508,7 @@ class ChoicesWidget extends Widget {
// Determine the value for the default (the one listed if nothing is
// selected)
$choices = $this->field->getChoices(true);
$choices = $this->field->getChoices(true, $options);
$prompt = ($config['prompt'])
? $this->field->getLocal('prompt', $config['prompt'])
: __('Select'
@@ -4535,7 +4603,7 @@ class ChoicesWidget extends Widget {
}
}
function getValue() {
function getValue($options=array()) {
if (!($value = parent::getValue()))
return null;
@@ -4545,7 +4613,7 @@ class ChoicesWidget extends Widget {
// Assume multiselect
$values = array();
$choices = $this->field->getChoices();
$choices = $this->field->getChoices(false, $options);
if ($choices && is_array($value)) {
// Complex choices
@@ -5020,7 +5088,7 @@ class ThreadEntryWidget extends Widget {
return $field;
}
function parseValue() {
function parseValue($options=array()) {
parent::parseValue();
if (isset($this->value)) {
$value = $this->value;
@@ -5533,7 +5601,6 @@ class AssignmentForm extends Form {
if (isset($this->_assignees))
$fields['assignee']->setChoices($this->_assignees);
$this->setFields($fields);
return $this->fields;
@@ -5760,7 +5827,7 @@ class ReferralForm extends Form {
),
)
),
'agent' => new ChoiceField(array(
'agent' => new AgentSelectionField(array(
'id'=>2,
'label' => '',
'flags' => hexdec(0X450F3),

View File

@@ -126,7 +126,7 @@ class Internationalization {
'registration-staff', 'pwreset-staff', 'banner-staff',
'registration-client', 'pwreset-client', 'banner-client',
'registration-confirm', 'registration-thanks',
'access-link') as $type) {
'access-link', 'email2fa-staff') as $type) {
$tpl = $this->getTemplate("templates/page/{$type}.yaml");
if (!($page = $tpl->getData()))
continue;

View File

@@ -112,16 +112,16 @@ class osTicket {
return ($token && $this->getCSRF()->validateToken($token));
}
function checkCSRFToken($name=false) {
function checkCSRFToken($name=false, $rotate=false) {
$name = $name ?: $this->getCSRF()->getTokenName();
if(isset($_POST[$name]) && $this->validateCSRFToken($_POST[$name]))
return true;
if(isset($_SERVER['HTTP_X_CSRFTOKEN']) && $this->validateCSRFToken($_SERVER['HTTP_X_CSRFTOKEN']))
$token = $_POST[$name] ?: $_SERVER['HTTP_X_CSRFTOKEN'];
if ($token && $this->validateCSRFToken($token)) {
if ($rotate) $this->getCSRF()->rotate();
return true;
}
$msg=sprintf(__('Invalid CSRF token [%1$s] on %2$s'),
(Format::htmlchars($_POST[$name]).''.$_SERVER['HTTP_X_CSRFTOKEN']), THISPAGE);
Format::htmlchars($token), THISPAGE);
$this->logWarning(__('Invalid CSRF Token').' '.$name, $msg, false);
return false;

View File

@@ -172,7 +172,7 @@ class CustomQueue extends VerySimpleModel {
* search beyond the current configuration of the search criteria
* $searchables - search fields - default to current if not provided
*/
function getForm($source=null, $searchable=null) {
function getForm($source=null, $searchable=null, $filterVisibility=null) {
$fields = array();
if (!isset($searchable)) {
$fields = array(
@@ -196,7 +196,7 @@ class CustomQueue extends VerySimpleModel {
}
foreach ($searchable ?: array() as $path => $field)
$fields = array_merge($fields, static::getSearchField($field, $path));
$fields = array_merge($fields, static::getSearchField($field, $path, array('filterVisibility' => $filterVisibility)));
$form = new AdvancedSearchForm($fields, $source);
@@ -435,7 +435,7 @@ class CustomQueue extends VerySimpleModel {
* representing the configurable search
* $name - <string> ORM path for the search
*/
static function getSearchField($F, $name) {
static function getSearchField($F, $name, $options=array()) {
list($label, $field) = $F;
$pieces = array();
@@ -460,7 +460,7 @@ class CustomQueue extends VerySimpleModel {
)), VisibilityConstraint::HIDDEN),
));
$offs = 0;
foreach ($field->getSearchMethodWidgets() as $m=>$w) {
foreach ($field->getSearchMethodWidgets($options) as $m=>$w) {
if (!$w)
continue;
list($class, $args) = $w;
@@ -598,6 +598,7 @@ class CustomQueue extends VerySimpleModel {
'status__id' =>__('Current Status'),
'lastupdate' => __('Last Updated'),
'est_duedate' => __('SLA Due Date'),
'sla_id' => __('SLA Plan'),
'duedate' => __('Due Date'),
'closed' => __('Closed Date'),
'isoverdue' => __('Overdue'),
@@ -1943,7 +1944,7 @@ extends QueueColumnAnnotation {
class DataSourceField
extends ChoiceField {
function getChoices($verbose=false) {
function getChoices($verbose=false, $options=array()) {
$config = $this->getConfiguration();
$root = $config['root'];
$fields = array();
@@ -2154,7 +2155,7 @@ extends ChoiceField {
return new $choices(array('name' => $prop));
}
function getChoices($verbose=false) {
function getChoices($verbose=false, $options=array()) {
if (isset($this->property))
return static::$properties[$this->property];

View File

@@ -226,7 +226,7 @@ class OverviewReport {
$headers = array(__('Help Topic'));
$header = function($row) { return Topic::getLocalNameById($row['topic_id'], $row['topic__topic']); };
$pk = 'topic_id';
$topics = Topic::getHelpTopics(false, Topic::DISPLAY_DISABLED);
$topics = $thisstaff->getTopicNames(false, Topic::DISPLAY_DISABLED);
if (empty($topics))
return array("columns" => array_merge($headers, $dash_headers),
"data" => array());

View File

@@ -745,14 +745,14 @@ class SavedQueue extends CustomQueue {
* configuring this search in the user interface.
*
*/
function getForm($source=null, $searchable=array()) {
function getForm($source=null, $searchable=array(), $filterVisibility=NULL) {
$searchable = null;
if ($this->isAQueue())
// Only allow supplemental matches.
$searchable = array_intersect_key($this->getCurrentSearchFields($source),
$this->getSupplementalMatches());
return parent::getForm($source, $searchable);
return parent::getForm($source, $searchable, $filterVisibility);
}
/**
@@ -1181,23 +1181,48 @@ class HelpTopicChoiceField extends AdvancedSearchSelectionField {
return true;
}
function getChoices($verbose=false) {
if (!isset($this->_topics))
$this->_topics = Topic::getHelpTopics(false, Topic::DISPLAY_DISABLED);
function getChoices($verbose=false, $options=array()) {
global $thisstaff;
if (!isset($this->_topics)) {
if ($options['filterVisibility'])
$this->_topics = $thisstaff->getTopicNames(false, Topic::DISPLAY_DISABLED);
else
$this->_topics = Topic::getHelpTopics(false, Topic::DISPLAY_DISABLED);
}
return $this->_topics;
}
}
class SLAChoiceField extends AdvancedSearchSelectionField {
static $_slas;
function hasIdValue() {
return true;
}
function getChoices($verbose=false, $options=array()) {
if (!isset($this->_slas))
$this->_slas = SLA::getSLAs(array('nameOnly' => true));
return $this->_slas;
}
}
require_once INCLUDE_DIR . 'class.dept.php';
class DepartmentChoiceField extends AdvancedSearchSelectionField {
static $_depts;
var $_choices;
function getChoices($verbose=false) {
if (!isset($this->_depts))
$this->_depts = Dept::getDepartments();
function getChoices($verbose=false, $options=array()) {
global $thisstaff;
if (!isset($this->_depts)) {
if ($options['filterVisibility'])
$this->_depts = $thisstaff->getDepartmentNames();
else
$this->_depts = Dept::getDepartments();
}
return $this->_depts;
}
@@ -1239,7 +1264,7 @@ class AssigneeChoiceField extends ChoiceField {
protected $_items;
function getChoices($verbose=false) {
function getChoices($verbose=false, $options=array()) {
global $thisstaff;
if (!isset($this->_items)) {
@@ -1247,7 +1272,12 @@ class AssigneeChoiceField extends ChoiceField {
'M' => __('Me'),
'T' => __('One of my teams'),
);
foreach (Staff::getStaffMembers() as $id=>$name) {
if ($options['filterVisibility'])
$assignees = $thisstaff->getDeptAgents(array('available' => true, 'namesOnly' => true));
else
$assignees = Staff::getStaffMembers();
foreach ($assignees as $id=>$name) {
// Don't include $thisstaff (since that's 'Me')
if ($thisstaff && $thisstaff->getId() == $id)
continue;
@@ -1277,16 +1307,16 @@ class AssigneeChoiceField extends ChoiceField {
);
}
function getSearchMethodWidgets() {
function getSearchMethodWidgets($options=array()) {
return array(
'assigned' => null,
'!assigned' => null,
'includes' => array('ChoiceField', array(
'choices' => $this->getChoices(),
'choices' => $this->getChoices(false, $options),
'configuration' => array('multiselect' => true),
)),
'!includes' => array('ChoiceField', array(
'choices' => $this->getChoices(),
'choices' => $this->getChoices(false, $options),
'configuration' => array('multiselect' => true),
)),
);
@@ -1384,7 +1414,7 @@ class AssigneeChoiceField extends ChoiceField {
class AssignedField extends AssigneeChoiceField {
function getChoices($verbose=false) {
function getChoices($verbose=false, $options=array()) {
return array(
'assigned' => __('Assigned'),
'!assigned' => __('Unassigned'),
@@ -1527,10 +1557,25 @@ class AgentSelectionField extends AdvancedSearchSelectionField {
static $_agents;
function getChoices($verbose=false) {
function getChoices($verbose=false, $options=array()) {
global $thisstaff;
if (!isset($this->_agents)) {
$this->_agents = array('M' => __('Me')) +
Staff::getStaffMembers();
//determine if we should filter results based on panel being visited
if ($options['filterVisibility']) {
$this->_agents = array('M' => __('Me'));
$agents = $thisstaff->getDeptAgents(array('available' => true, 'namesOnly' => true));
foreach (Staff::getStaffMembers() as $id => $name) {
if (!$agents || array_key_exists($id, $agents)) {
// Don't include $thisstaff (since that's 'Me')
if ($thisstaff && $thisstaff->getId() == $id)
continue;
$this->_agents[$id] = $name;
}
}
} else
$this->_agents = array('M' => __('Me')) + Staff::getStaffMembers();
}
return $this->_agents;
}
@@ -1574,12 +1619,17 @@ class AgentSelectionField extends AdvancedSearchSelectionField {
class DepartmentManagerSelectionField extends AgentSelectionField {
static $_members;
function getChoices($verbose=false) {
function getChoices($verbose=false, $options=array()) {
global $thisstaff;
if (!isset($this->_members)) {
$managers = array();
$staff = Staff::objects()->filter(array('dept__manager_id__gt' => 0));
foreach ($staff as $s) {
$managers['s'.$s->getId()] = $s->getName()->name;
$mgr = Dept::objects()->filter(array('manager_id__gt' => 0))->values_flat('manager_id');
$staff = $thisstaff->getDeptAgents(array('available' => true, 'namesOnly' => true));
foreach ($mgr as $mid) {
$mid = $mid[0];
if (array_key_exists($mid, $staff))
$managers['s'.$mid] = $staff[$mid]->getName()->name;
}
$this->_members = $managers;
}
@@ -1595,7 +1645,7 @@ class DepartmentManagerSelectionField extends AgentSelectionField {
class TeamSelectionField extends AdvancedSearchSelectionField {
static $_teams;
function getChoices($verbose=false) {
function getChoices($verbose=false, $options=array()) {
if (!isset($this->_teams) && $teams = Team::getTeams())
$this->_teams = array('T' => __('One of my teams')) +
$teams;
@@ -1641,7 +1691,7 @@ class TeamSelectionField extends AdvancedSearchSelectionField {
}
class TicketStateChoiceField extends AdvancedSearchSelectionField {
function getChoices($verbose=false) {
function getChoices($verbose=false, $options=array()) {
return array(
'open' => __('Open'),
'closed' => __('Closed'),
@@ -1663,7 +1713,7 @@ class TicketStateChoiceField extends AdvancedSearchSelectionField {
}
class TicketFlagChoiceField extends ChoiceField {
function getChoices($verbose=false) {
function getChoices($verbose=false, $options=array()) {
return array(
'isanswered' => __('Answered'),
'isoverdue' => __('Overdue'),
@@ -1691,7 +1741,7 @@ class TicketFlagChoiceField extends ChoiceField {
}
class TicketSourceChoiceField extends ChoiceField {
function getChoices($verbose=false) {
function getChoices($verbose=false, $options=array()) {
return Ticket::getSources();
}

View File

@@ -249,12 +249,16 @@ implements TemplateVariable {
$entries = array();
foreach ($slas as $row) {
$row[2] = $row[2] & self::FLAG_ACTIVE;
$entries[$row[0]] = sprintf(__('%s (%d hours - %s)'
/* Tokens are <name> (<#> hours - <Active|Disabled>) */),
self::getLocalById($row[0], 'name', $row[1]),
$row[3],
$row[2] ? __('Active') : __('Disabled'));
if ($criteria['nameOnly'])
$entries[$row[0]] = __(self::getLocalById($row[0], 'name', $row[1]));
else {
$row[2] = $row[2] & self::FLAG_ACTIVE;
$entries[$row[0]] = sprintf(__('%s (%d hours - %s)'
/* Tokens are <name> (<#> hours - <Active|Disabled>) */),
self::getLocalById($row[0], 'name', $row[1]),
$row[3],
$row[2] ? __('Active') : __('Disabled'));
}
}
return $entries;

View File

@@ -54,17 +54,17 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable {
var $_config = null;
var $_perm;
const PERM_STAFF = 'visibility.agents';
static protected $perms = array(
self::PERM_STAFF => array(
'title' => /* @trans */ 'Agent',
'desc' => /* @trans */ 'Ability to see Agents in all Departments',
'primary' => true,
),
);
function __onload() {
// WE have to patch info here to support upgrading from old versions.
$time = null;
if (isset($this->passwdreset) && $this->passwdreset)
$time=strtotime($this->passwdreset);
elseif (isset($this->added) && $this->added)
$time=strtotime($this->added);
if ($time)
$this->passwd_change = time()-$time; //XXX: check timezone issues.
}
function get($field, $default=false) {
@@ -82,6 +82,10 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable {
return $this->_config[$field];
}
function getConfigObj() {
return new Config('staff.'.$this->getId());
}
function getConfig() {
if (!isset($this->_config) && $this->getId()) {
@@ -102,6 +106,12 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable {
return $this->_config;
}
function updateConfig($vars) {
$config = $this->getConfigObj();
$config->updateAll($vars);
$this->_config = null;
}
function __toString() {
return (string) $this->getName();
}
@@ -178,6 +188,21 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable {
return StaffAuthenticationBackend::getBackend($bk);
}
function get2FABackendId() {
return $this->default_2fa;
}
function get2FABackend() {
return Staff2FABackend::getBackend($this->get2FABackendId());
}
// gets configured backends
function get2FAConfig($id) {
$config = $this->getConfig();
return isset($config[$id]) ?
JsonDataParser::decode($config[$id]) : array();
}
function setAuthKey($key) {
$this->authkey = $key;
}
@@ -228,11 +253,22 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable {
return $this->save();
}
/* check if passwd reset is due. */
function isPasswdResetDue() {
global $cfg;
return ($cfg && $cfg->getPasswdResetPeriod()
&& $this->passwd_change>($cfg->getPasswdResetPeriod()*30*24*60*60));
function getPasswdResetTimestamp() {
if (!isset($this->passwd_change)) {
// WE have to patch info here to support upgrading from old versions.
if (isset($this->passwdreset) && $this->passwdreset)
$this->passwd_change = strtotime($this->passwdreset);
elseif (isset($this->added) && $this->added)
$this->passwd_change = strtotime($this->added);
elseif (isset($this->created) && $this->created)
$this->passwd_change = strtotime($this->created);
}
return $this->passwd_change;
}
static function checkPassword($new, $current=null) {
osTicketStaffAuthentication::checkPassword($new, $current);
}
function setPassword($new, $current=false) {
@@ -246,7 +282,7 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable {
|| !$bk instanceof AuthBackend
) {
// Fallback to osTicket authentication token udpates
$bk = new osTicketAuthentication();
$bk = new osTicketStaffAuthentication();
}
// And now for the magic
@@ -275,10 +311,6 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable {
return true;
}
function isPasswdChangeDue() {
return $this->isPasswdResetDue();
}
function getRefreshRate() {
return $this->auto_refresh_rate;
}
@@ -367,6 +399,18 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable {
return $this->change_passwd;
}
function force2faConfig() {
global $cfg;
$id = $this->get2FABackendId();
$config = $this->get2FAConfig($id);
//2fa is required and
//1. agent doesn't have default_2fa or
//2. agent has default_2fa, but that default_2fa is not configured
return ($cfg->require2FAForAgents() && !$id || ($id && empty($config)));
}
function getDepartments() {
// TODO: Cache this in the agent's session as it is unlikely to
// change while logged in
@@ -417,6 +461,53 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable {
return $this->getDepartments();
}
function getDepartmentNames($publiconly=false) {
$depts = Dept::getDepartments(array('publiconly' => $publiconly));
//filter out departments the agent does not have access to
if (!$this->hasPerm(Dept::PERM_DEPT) && $staffDepts = $this->getDepts()) {
foreach ($depts as $id => $name) {
if (!in_array($id, $staffDepts))
unset($depts[$id]);
}
}
return $depts;
}
function getTopicNames($publicOnly=false, $disabled=false) {
$allInfo = !$this->hasPerm(Dept::PERM_DEPT) ? true : false;
$topics = Topic::getHelpTopics($publicOnly, $disabled, true, array(), $allInfo);
$topicsClean = array();
if (!$this->hasPerm(Dept::PERM_DEPT) && $staffDepts = $this->getDepts()) {
foreach ($topics as $id => $info) {
if ($info['pid']) {
$childDeptId = $info['dept_id'];
//show child if public, has access to topic dept_id, or no dept at all
if ($info['public'] || !$childDeptId || ($childDeptId && in_array($childDeptId, $staffDepts)))
$topicsClean[$id] = $info['topic'];
$parent = Topic::lookup($info['pid']);
if (!$parent->isPublic() && $parent->getDeptId() && !in_array($parent->getDeptId(), $staffDepts)) {
//hide child if parent topic is private and no access to parent topic dept_id
unset($topicsClean[$id]);
}
} elseif ($info['public']) {
//show all public topics
$topicsClean[$id] = $info['topic'];
} else {
//show private topics if access to topic dept_id or no dept at all
if ($info['dept_id'] && in_array($info['dept_id'], $staffDepts) || !$info['dept_id'])
$topicsClean[$id] = $info['topic'];
}
}
return $topicsClean;
}
return $topics;
}
function getManagedDepartments() {
return ($depts=Dept::getDepartments(
@@ -779,6 +870,7 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable {
$_config->updateAll(array(
'datetime_format' => $vars['datetime_format'],
'default_from_name' => $vars['default_from_name'],
'default_2fa' => $vars['default_2fa'],
'thread_view_order' => $vars['thread_view_order'],
'default_ticket_queue_id' => $vars['default_ticket_queue_id'],
'reply_redirect' => ($vars['reply_redirect'] == 'Queue') ? 'Queue' : 'Ticket',
@@ -897,6 +989,40 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable {
return self::getStaffMembers(array('available'=>true));
}
//returns agents in departments this agent has access to
function getDeptAgents($criteria=array()) {
$agents = static::objects()
->distinct('staff_id')
->select_related('dept')
->select_related('dept_access');
if (!$this->hasPerm(Staff::PERM_STAFF)) {
$agents = Staff::objects()
->distinct('staff_id')
->filter(Q::any(array(
'dept_access__dept_id__in' => $this->getDepts(),
'dept_id__in' => $this->getDepts(),
)));
}
if (isset($criteria['available'])) {
$agents = $agents->filter(array(
'onvacation' => 0,
'isactive' => 1,
));
}
if (isset($criteria['namesOnly'])) {
$clean = array();
foreach ($agents as $a) {
$clean[$a->getId()] = $a->getName();
}
return $clean;
}
return $agents;
}
static function getsortby($path='', $format=null) {
global $cfg;
@@ -1289,7 +1415,12 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable {
Export::agents($agents, $filename);
}
static function getPermissions() {
return self::$perms;
}
}
RolePermission::register(/* @trans */ 'Miscellaneous', Staff::getPermissions());
interface RestrictedAccess {
function checkStaffPerm($staff);
@@ -1355,6 +1486,14 @@ extends AbstractForm {
new Q(array('welcome_email' => false)),
VisibilityConstraint::HIDDEN
),
'validator' => '',
'validators' => function($self, $v) {
try {
Staff::checkPassword($v, null);
} catch (BadPassword $ex) {
$self->addError($ex->getMessage());
}
},
)),
'passwd2' => new PasswordField(array(
'placeholder' => __('Confirm Password'),
@@ -1366,6 +1505,14 @@ extends AbstractForm {
new Q(array('welcome_email' => false)),
VisibilityConstraint::HIDDEN
),
'validator' => '',
'validators' => function($self, $v) {
try {
Staff::checkPassword($v, null);
} catch (BadPassword $ex) {
$self->addError($ex->getMessage());
}
},
)),
'change_passwd' => new BooleanField(array(
'default' => true,
@@ -1397,15 +1544,32 @@ extends AbstractForm {
'configuration' => array(
'autofocus' => true,
),
'validator' => 'noop',
)),
'passwd1' => new PasswordField(array(
'label' => __('Enter a new password'),
'placeholder' => __('New Password'),
'required' => true,
'validator' => '',
'validators' => function($self, $v) {
try {
Staff::checkPassword($v, null);
} catch (BadPassword $ex) {
$self->addError($ex->getMessage());
}
},
)),
'passwd2' => new PasswordField(array(
'placeholder' => __('Confirm Password'),
'required' => true,
'validator' => '',
'validators' => function($self, $v) {
try {
Staff::checkPassword($v, null);
} catch (BadPassword $ex) {
$self->addError($ex->getMessage());
}
},
)),
);
@@ -1594,6 +1758,14 @@ extends AbstractForm {
'placeholder' => __("Temporary Password"),
'autocomplete' => 'new-password',
),
'validator' => '',
'validators' => function($self, $v) {
try {
Staff::checkPassword($v, null);
} catch (BadPassword $ex) {
$self->addError($ex->getMessage());
}
},
'visibility' => new VisibilityConstraint(
new Q(array('welcome_email' => false))
),
@@ -1635,6 +1807,8 @@ extends AbstractForm {
Organization::PERM_EDIT,
Organization::PERM_DELETE,
FAQ::PERM_MANAGE,
Dept::PERM_DEPT,
Staff::PERM_STAFF,
);
return $clean;
}

View File

@@ -483,14 +483,21 @@ class Task extends TaskModel implements RestrictedAccess, Threadable {
}
function getAssignmentForm($source=null, $options=array()) {
global $thisstaff;
$prompt = $assignee = '';
// Possible assignees
$assignees = array();
switch (strtolower($options['target'])) {
case 'agents':
$dept = $this->getDept();
foreach ($dept->getAssignees() as $member)
$assignees['s'.$member->getId()] = $member;
if ($options['filterVisibility']) {
foreach ($thisstaff->getDeptAgents(array('available' => true)) as $member)
$assignees['s'.$member->getId()] = $member;
} else {
foreach ($dept->getAssignees() as $member)
$assignees['s'.$member->getId()] = $member;
}
if (!$source && $this->isOpen() && $this->staff)
$assignee = sprintf('s%d', $this->staff->getId());

View File

@@ -1319,12 +1319,14 @@ implements TemplateVariable {
/* static */
function logEmailHeaders($id, $mid, $header=false) {
$headerInfo = Mail_Parse::splitHeaders($header);
if (!$id || !$mid)
return false;
$this->email_info = new ThreadEntryEmailInfo(array(
'thread_entry_id' => $id,
'email_id' => Email::getIdByEmail($headerInfo['Delivered-To']),
'mid' => $mid,
));
@@ -1999,6 +2001,12 @@ class ThreadEvent extends VerySimpleModel {
),
'null' => true,
),
'event' => array(
'constraint' => array(
'event_id' => 'Event.id',
),
'null' => true,
),
),
);

View File

@@ -985,6 +985,8 @@ implements RestrictedAccess, Threadable, Searchable {
}
function getAssignmentForm($source=null, $options=array()) {
global $thisstaff;
$prompt = $assignee = '';
// Possible assignees
$assignees = null;
@@ -992,7 +994,7 @@ implements RestrictedAccess, Threadable, Searchable {
switch (strtolower($options['target'])) {
case 'agents':
$assignees = array();
foreach ($dept->getAssignees() as $member)
foreach ($thisstaff->getDeptAgents(array('available' => true)) as $member)
$assignees['s'.$member->getId()] = $member;
if (!$source && $this->isOpen() && $this->staff)
@@ -2317,6 +2319,9 @@ implements RestrictedAccess, Threadable, Searchable {
'dept_id' => new DepartmentChoiceField(array(
'label' => __('Department'),
)),
'sla_id' => new SLAChoiceField(array(
'label' => __('SLA Plan'),
)),
'topic_id' => new HelpTopicChoiceField(array(
'label' => __('Help Topic'),
)),
@@ -2985,12 +2990,27 @@ implements RestrictedAccess, Threadable, Searchable {
}
function systemReferral($emails) {
global $cfg;
if (!$this->getThread())
if (!$thread = $this->getThread())
return;
$eventEmails = array();
$events = ThreadEvent::objects()
->filter(array('thread_id' => $thread->getId(),
'event__name' => 'transferred'));
if ($events) {
foreach ($events as $e) {
$emailId = Dept::getEmailIdById($e->dept_id) ?: $cfg->getDefaultEmailId();
if (!in_array($emailId, $eventEmails))
$eventEmails[] = $emailId;
}
}
foreach ($emails as $id) {
$refer = $eventEmails ? !in_array($id, $eventEmails) : true;
if ($id != $this->email_id
&& $refer
&& ($email=Email::lookup($id))
&& $this->getDeptId() != $email->getDeptId()
&& ($dept=Dept::lookup($email->getDeptId()))

View File

@@ -319,25 +319,25 @@ implements TemplateVariable, Searchable {
$this->flags &= ~$flag;
}
static function getHelpTopics($publicOnly=false, $disabled=false, $localize=true, $whitelist=array()) {
static function getHelpTopics($publicOnly=false, $disabled=false, $localize=true, $whitelist=array(), $allData=false) {
global $cfg;
static $topics, $names = array();
// If localization is specifically requested, then rebuild the list.
if (!$names || $localize) {
$objects = self::objects()->values_flat(
'topic_id', 'topic_pid', 'ispublic', 'flags', 'topic'
'topic_id', 'topic_pid', 'ispublic', 'flags', 'topic', 'dept_id'
)
->order_by('sort');
// Fetch information for all topics, in declared sort order
$topics = array();
foreach ($objects as $T) {
list($id, $pid, $pub, $flags, $topic) = $T;
list($id, $pid, $pub, $flags, $topic, $deptId) = $T;
$display = ($flags & self::FLAG_ACTIVE);
$topics[$id] = array('pid'=>$pid, 'public'=>$pub,
'disabled'=>!$display, 'topic'=>$topic);
'disabled'=>!$display, 'topic'=>$topic, 'dept_id'=>$deptId);
}
$localize_this = function($id, $default) use ($localize) {
@@ -371,6 +371,7 @@ implements TemplateVariable, Searchable {
// Apply requested filters
$requested_names = array();
$topicsClean = array();
foreach ($names as $id=>$n) {
$info = $topics[$id];
if ($publicOnly && !$info['public'])
@@ -381,8 +382,12 @@ implements TemplateVariable, Searchable {
if ($disabled === self::DISPLAY_DISABLED && $info['disabled'])
$n .= " - ".__("(disabled)");
$requested_names[$id] = $n;
$topicsClean[$id] = $info;
}
if ($allData)
return $topicsClean;
// If localization requested and the current locale is not the
// primary, the list may need to be sorted. Caching is ok here,
// because the locale is not going to be changed within a single

View File

@@ -1273,10 +1273,13 @@ class UserAccount extends VerySimpleModel {
if ($vars['passwd1'] || $vars['passwd2']) {
if (!$vars['passwd1'])
$errors['passwd1'] = __('New password is required');
elseif ($vars['passwd1'] && strlen($vars['passwd1'])<6)
$errors['passwd1'] = __('Must be at least 6 characters');
elseif ($vars['passwd1'] && strcmp($vars['passwd1'], $vars['passwd2']))
$errors['passwd2'] = __('Passwords do not match');
else {
try {
self::checkPassword($vars['passwd1']);
} catch (BadPassword $ex) {
$errors['passwd1'] = $ex->getMessage();
}
}
}
// Make sure the username is not an email.
@@ -1359,10 +1362,15 @@ class UserAccount extends VerySimpleModel {
&& !isset($vars['sendemail'])) {
if (!$vars['passwd1'])
$errors['passwd1'] = 'Temporary password required';
elseif ($vars['passwd1'] && strlen($vars['passwd1'])<6)
$errors['passwd1'] = 'Must be at least 6 characters';
elseif ($vars['passwd1'] && strcmp($vars['passwd1'], $vars['passwd2']))
$errors['passwd2'] = 'Passwords do not match';
else {
try {
self::checkPassword($vars['passwd1']);
} catch (BadPassword $ex) {
$errors['passwd1'] = $ex->getMessage();
}
}
}
if ($errors) return false;
@@ -1397,6 +1405,10 @@ class UserAccount extends VerySimpleModel {
return $account;
}
static function checkPassword($new, $current=null) {
osTicketClientAuthentication::checkPassword($new, $current);
}
}
class UserAccountStatus {

View File

@@ -177,12 +177,28 @@ class StaffSession extends Staff {
return $staff;
}
function clear2FA() {
$_SESSION['_auth']['staff']['2fa'] = null;
return true;
}
// If 2fa is set then it means it's pending
function is2FAPending() {
if (!isset($_SESSION['_auth']['staff']['2fa']))
return false;
return true;
}
function isValid(){
global $_SESSION, $cfg;
global $cfg;
if(!$this->getId() || $this->session->getSessionId()!=session_id())
return false;
if ($this->is2FAPending())
return false;
return $this->session->isvalidSession($this->token,$cfg->getStaffTimeout(),$cfg->enableStaffIPBinding())?true:false;
}

View File

@@ -27,6 +27,14 @@ username:
- title: Change a username as an Administrator
href: /scp/staff.php
config2fa:
title: Two Factor Authentication
content: >
Two Factor Authentication adds an extra layer of security
when logging into the helpdesk. Once you correctly submit
your username and password, you will need to enter a token
to finish logging into the helpdesk.
time_zone:
title: Time Zone
content: >
@@ -83,11 +91,11 @@ confirm_new_password:
signature:
title: Signature
content: >
Create an optional <span class="doc-desc-title">Signature</span>
that perhaps appears at the end of your Ticket Responses. Whether this
<span class="doc-desc-title">Signature</span> appears, or not, depends
on the <span class="doc-desc-title">Email Template</span> that will be
used in a Ticket Response.
Create an optional <span class="doc-desc-title">Signature</span>
that perhaps appears at the end of your Ticket Responses. Whether this
<span class="doc-desc-title">Signature</span> appears, or not, depends
on the <span class="doc-desc-title">Email Template</span> that will be
used in a Ticket Response.
links:
- title: Create Emails Templates in the Admin Panel
href: /scp/templates.php

View File

@@ -39,19 +39,12 @@ disable_agent_collabs:
This setting is global for all User created Tickets via API, Piping, and Fetching.
# Authentication settings
password_reset:
title: Password Expiration Policy
agent_password_policy:
title: Password Management Policy
content: >
Sets how often (in months) Agents will be required to change
their password. If disabled (set to "No expiration"), passwords will
not expire.
password_expiration_policy:
title: Password Expiration Policy
content: >
Choose how often Agents will be required to change their password. If
disabled (i.e., <span class="doc-desc-opt">No Expiration</span>), passwords
will not expire.
Chose a <span class="doc-desc-title">Password Policy</span> for <span class="doc-desc-title">Agents</span>.
<br><br>
Additional policies can be added by installing <span class="doc-desc-title">Password Policy</span> plugins.
allow_password_resets:
title: Allow Password Resets
@@ -85,3 +78,11 @@ bind_staff_session_to_ip:
upon Log In.
<br><br>
This setting is not recommened for users assigned IP addresses dynamically.
require_agent_2fa:
title: Require Two Factor Authentication
content: >
Enable this feature if you would like to have an extra layer of authentication
set up for Agents when they log into the helpdesk. Once they correctly submit
their username and password, they will be required to submit a token to finish
logging into the helpdesk.

View File

@@ -21,6 +21,13 @@ client_name_format:
will use it for names if no other format is specified.
# Authentication settings
client_password_policy:
title: Password Management Policy
content: >
Chose a <span class="doc-desc-title">Password Policy</span> for <span class="doc-desc-title">Users</span>.
<br><br>
Additional policies can be added by installing <span class="doc-desc-title">Password Policy</span> plugins.
client_session_timeout:
title: User Session Timeout
content: >

View File

@@ -20,6 +20,14 @@ username:
that is unique to your <span class="doc-desc-title">Help
Desk</span>.
reset2fa:
title: Reset Two Factor Authentication
content: >
In the event that an Agent loses the ability to log into the helpdesk
using their current 2FA configuration, an Admin can reset the Agent's
2FA configuration so that they can reconfigure it upon their next
successful login.
email_address:
title: Email Address
content: >

View File

@@ -0,0 +1,24 @@
#
# email2fa-staff.yaml
#
# Template of the email sent to staff members when using the Email
# Two Factor Authentication
---
notes: >
This template defines the email sent to Staff who use Email for
Two Factor Authentication
name: "osTicket Two Factor Authentication"
body: >
<h3><strong>Hi %{staff.name.first},</strong></h3>
<div>
You have just logged into for the helpdesk at %{url}.<br />
<br />
Use the verification code below to finish logging into the helpdesk.<br />
<br />
%{otp}<br />
<br />
<em style="font-size: small">Your friendly Customer Support System</em>
<br />
<img src="cid:b56944cb4722cc5cda9d1e23a3ea7fbc" alt="Powered by
osTicket" width="126" height="19" style="width: 126px" />
</div>

View File

@@ -57,7 +57,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
<select name="dept_id">
<option value="0">&mdash; <?php echo __('All Departments');?> &mdash;</option>
<?php
if (($depts=Dept::getDepartments(array('publiconly' => true)))) {
if (($depts=$thisstaff->getDepartmentNames())) {
foreach($depts as $id => $name) {
$selected=($info['dept_id'] && $id==$info['dept_id'])?'selected="selected"':'';
echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name);

View File

@@ -1,13 +1,24 @@
<?php
if(!defined('OSTSCPINC') || !$thisstaff) die('Access Denied');
//get canned responses for depts with a role that can manage canned responses
$departments = array();
if ($thisstaff->getRole()->hasPerm(Canned::PERM_MANAGE, false))
$departments[] = $thisstaff->getDeptId();
foreach($thisstaff->dept_access as $da) {
if ($da->role->hasPerm(Canned::PERM_MANAGE, false))
$departments[] = $da->dept->getId();
}
$departments = implode(',', $departments);
$qs = array();
$sql='SELECT canned.*, count(attach.file_id) as files, dept.name as department '.
' FROM '.CANNED_TABLE.' canned '.
' LEFT JOIN '.DEPT_TABLE.' dept ON (dept.id=canned.dept_id) '.
' LEFT JOIN '.ATTACHMENT_TABLE.' attach
ON (attach.object_id=canned.canned_id AND attach.`type`=\'C\' AND NOT attach.inline)';
$sql.=' WHERE 1';
$sql.= sprintf(' WHERE canned.dept_id = 0 OR dept.id IN (%s)', $departments);
$sortOptions=array('title'=>'canned.title','status'=>'canned.isenabled','dept'=>'department','updated'=>'canned.updated');
$orderWays=array('DESC'=>'DESC','ASC'=>'ASC');

View File

@@ -1,9 +1,7 @@
<?php
if(!defined('OSTSTAFFINC') || !$thisstaff || !$thisstaff->isStaff()) die('Access Denied');
$qs = array();
$agents = Staff::objects()
->select_related('dept');
$agents = $thisstaff->getDeptAgents();
// Sanitize 'order' param To Escape XSS
if ($_REQUEST['order'])
@@ -86,7 +84,7 @@ $qstr.='&amp;order='.($order=='DESC' ? 'ASC' : 'DESC');
<select name="did" id="did">
<option value="0">&mdash; <?php echo __('All Departments');?> &mdash;</option>
<?php
foreach (Dept::getDepartments(array('nonempty'=>1)) as $id=>$name) {
foreach ($thisstaff->getDepartmentNames() as $id=>$name) {
$sel=($_REQUEST['did'] && $_REQUEST['did']==$id)?'selected="selected"':'';
echo sprintf('<option value="%d" %s>%s</option>',$id,$sel,$name);
}

View File

@@ -77,16 +77,21 @@ usort($topics, function($a, $b) {
return strcmp($a->getFullName(), $b->getFullName());
});
array_unshift($topics, new Topic(array('id' => 0, 'topic' => __('All Topics'), 'faq_count' => $total)));
if (!$thisstaff->hasPerm(Dept::PERM_DEPT))
$staffTopics = $thisstaff->getTopicNames(false);
foreach ($topics as $T) {
$active = $_REQUEST['topicId'] == $T->getId(); ?>
<li <?php if ($active) echo 'class="active"'; ?>>
<a href="#" data-topic-id="<?php echo $T->getId(); ?>">
<i class="icon-fixed-width <?php
if ($active) echo 'icon-hand-right'; ?>"></i>
<?php echo sprintf('%s (%d)',
Format::htmlchars($T->getFullName()),
$T->faq_count); ?></a>
</li> <?php
$active = $_REQUEST['topicId'] == $T->getId();
if (!$staffTopics || is_null($T->getId()) || ($staffTopics && array_key_exists($T->getId(), $staffTopics))) { ?>
<li <?php if ($active) echo 'class="active"'; ?>>
<a href="#" data-topic-id="<?php echo $T->getId(); ?>">
<i class="icon-fixed-width <?php
if ($active) echo 'icon-hand-right'; ?>"></i>
<?php echo sprintf('%s (%d)',
Format::htmlchars($T->getFullName()),
$T->faq_count); ?></a>
</li> <?php
}
} ?>
</ul>
</div>

View File

@@ -58,7 +58,10 @@ echo sprintf('<div class="pull-right flush-right">
?>
</div>
<?php
if (!$thisstaff->hasPerm(Dept::PERM_DEPT)) {
$staffTopics = $thisstaff->getTopicNames(false);
$filter = true;
}
$faqs = $category->faqs
->constrain(array('attachments__inline' => 0))
@@ -67,11 +70,23 @@ if ($faqs->exists(true)) {
echo '<div id="faq">
<ol>';
foreach ($faqs as $faq) {
echo sprintf('
<li><strong><a href="faq.php?id=%d" class="previewfaq">%s <span>- %s</span></a> %s</strong></li>',
$faq->getId(),$faq->getQuestion(),$faq->isPublished() ? __('Published'):__('Internal'),
$faq->attachments ? '<i class="icon-paperclip"></i>' : ''
);
if ($filter) {
if ($faqTopics = $faq->getHelpTopicsIds()) {
foreach ($faqTopics as $key => $value) {
if (array_key_exists($value, $staffTopics))
$show = true;
}
} else
$show = true;
} else
$show = true;
if ($show)
echo sprintf('
<li><strong><a href="faq.php?id=%d" class="previewfaq">%s <span>- %s</span></a> %s</strong></li>',
$faq->getId(),$faq->getQuestion(),$faq->isPublished() ? __('Published'):__('Internal'),
$faq->attachments ? '<i class="icon-paperclip"></i>' : ''
);
}
echo ' </ol>
</div>';

View File

@@ -71,7 +71,7 @@ $qstr = Http::build_query($qs);
<div class="error"><?php echo $errors['category_id']; ?></div>
<?php
if ($topics = Topic::getAllHelpTopics()) {
if ($topics = $thisstaff->getTopicNames()) {
if (!is_array(@$info['topics']))
$info['topics'] = array();
?>

View File

@@ -1,6 +1,11 @@
<?php
include_once(INCLUDE_DIR.'staff/login.header.php');
$info = ($_POST && $errors)?Format::htmlchars($_POST):array();
if ($thisstaff && $thisstaff->is2FAPending())
$msg = "2FA Pending";
?>
<div id="brickwall"></div>
<div id="loginBox">
@@ -13,22 +18,44 @@ $info = ($_POST && $errors)?Format::htmlchars($_POST):array();
</a></h1>
<h3 id="login-message"><?php echo Format::htmlchars($msg); ?></h3>
<div class="banner"><small><?php echo ($content) ? Format::display($content->getLocalBody()) : ''; ?></small></div>
<div id="loading" style="display:none;" class="dialog">
<h1><i class="icon-spinner icon-spin icon-large"></i>
<?php echo __('Verifying');?></h1>
</div>
<form action="login.php" method="post" id="login" onsubmit="attemptLoginAjax(event)">
<?php csrf_token(); ?>
<input type="hidden" name="do" value="scplogin">
<fieldset>
<input type="text" name="userid" id="name" value="<?php
echo $info['userid']; ?>" placeholder="<?php echo __('Email or Username'); ?>"
autofocus autocorrect="off" autocapitalize="off">
<input type="password" name="passwd" id="pass" placeholder="<?php echo __('Password'); ?>" autocorrect="off" autocapitalize="off">
<h3 style="display:inline"><a id="reset-link" class="<?php
if (!$show_reset || !$cfg->allowPasswordReset()) echo 'hidden';
?>" href="pwreset.php"><?php echo __('Forgot My Password'); ?></a></h3>
<button class="submit button pull-right" type="submit"
<?php csrf_token();
if ($thisstaff
&& $thisstaff->is2FAPending()
&& ($bk=$thisstaff->get2FABackend())
&& ($form=$bk->getInputForm($_POST))) {
// Render 2FA input form
include STAFFINC_DIR . 'templates/dynamic-form-simple.tmpl.php';
?>
<fieldset style="padding-top:10px;">
<input type="hidden" name="do" value="2fa">
<button class="submit button pull-center" type="submit"
name="submit"><i class="icon-signin"></i>
<?php echo __('Log In'); ?>
<?php echo __('Verify'); ?>
</button>
</fieldset>
</fieldset>
<?php
} else { ?>
<input type="hidden" name="do" value="scplogin">
<fieldset>
<input type="text" name="userid" id="name" value="<?php
echo $info['userid']; ?>" placeholder="<?php echo __('Email or Username'); ?>"
autofocus autocorrect="off" autocapitalize="off">
<input type="password" name="passwd" id="pass" placeholder="<?php echo __('Password'); ?>" autocorrect="off" autocapitalize="off">
<h3 style="display:inline"><a id="reset-link" class="<?php
if (!$show_reset || !$cfg->allowPasswordReset()) echo 'hidden';
?>" href="pwreset.php"><?php echo __('Forgot My Password'); ?></a></h3>
<button class="submit button pull-right" type="submit"
name="submit"><i class="icon-signin"></i>
<?php echo __('Log In'); ?>
</button>
</fieldset>
<?php
} ?>
</form>
<?php
$ext_bks = array();
@@ -64,6 +91,7 @@ if (count($ext_bks)) { ?>
});
function attemptLoginAjax(e) {
$('#loading').show();
var objectifyForm = function(formArray) { //serialize data function
var returnArray = {};
for (var i = 0; i < formArray.length; i++) {
@@ -78,6 +106,7 @@ if (count($ext_bks)) { ?>
var oldEffect = $.fn.effect;
$.fn.effect = function (effectName) {
if (effectName === "shake") {
$('#loading').hide();
var old = $.effects.createWrapper;
$.effects.createWrapper = function (element) {
var result;
@@ -101,12 +130,14 @@ if (count($ext_bks)) { ?>
var form = $(e.target),
data = objectifyForm(form.serializeArray())
data.ajax = 1;
$('button[type=submit]', form).attr('disabled', 'disabled');
$.ajax({
url: form.attr('action'),
method: 'POST',
data: data,
cache: false,
success: function(json) {
$('button[type=submit]', form).removeAttr('disabled');
if (!typeof(json) === 'object' || !json.status)
return;
switch (json.status) {

View File

@@ -121,6 +121,50 @@ if ($avatar->isChangeable()) { ?>
<div class="error"><?php echo $errors['username']; ?></div>
</td>
</tr>
<?php
if (($bks=Staff2FABackend::allRegistered())) {
$current = $staff->get2FABackendId();
$required2fa = $cfg->require2FAForAgents();
$_config = $staff->getConfig();
?>
<tr>
<td <?php if ($required2fa) echo 'class="required"'; ?>><?php echo __('Default 2FA'); ?>:</td>
<td>
<select name="default_2fa" id="default2fa-selection"
style="width:300px">
<?php
if (!$required2fa) { ?>
<option value="">&mdash; <?php echo __('Disable'); ?> &mdash;</option>
<?php
}
foreach ($bks as $bk) {
$configuration = $staff->get2FAConfig($bk->getId());
$configured = $configuration['verified'];
?>
<option id="<?php echo $bk->getId(); ?>"
value="<?php echo $bk->getId(); ?>" <?php
if ($current == $bk->getId() && $configured)
echo ' selected="selected" '; ?>
<?php
if (!$configured)
echo ' disabled="disabled" '; ?>
><?php
echo $bk->getName(); ?></option>
<?php } ?>
</select>
&nbsp;
<button type="button" id="config2fa-button" class="action-button" onclick="javascript:
$.dialog('ajax.php/staff/'+<?php echo $staff->getId();
?>+'/2fa/configure', 201);">
<i class="icon-gear"></i> <?php echo __('Configure Options'); ?>
</button>
<i class="offset help-tip icon-question-sign" href="#config2fa"></i>
<div class="error"><?php echo $errors['default_2fa']; ?></div>
</td>
</tr>
<?php
} ?>
</tbody>
<!-- ================================================ -->
<tbody>

View File

@@ -94,7 +94,8 @@ else {
<div class="error"><?php echo $errors['criteria']; ?></div>
<div class="advanced-search">
<?php
$form = $queue->getForm();
$filterVisibility = false;
$form = $queue->getForm(null, null, $filterVisibility);
$search = $queue;
$matches = $queue->getSupportedMatches();
include STAFFINC_DIR . 'templates/advanced-search-criteria.tmpl.php';

View File

@@ -83,20 +83,23 @@ if (!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$config
</th>
</tr>
<tr>
<td><?php echo __('Password Expiration Policy'); ?>:</td>
<td><?php echo __('Password Policy'); ?>:</td>
<td>
<select name="passwd_reset_period">
<option value="0"> &mdash; <?php echo __('No expiration'); ?> &mdash;</option>
<select name="agent_passwd_policy">
<option value=" "> &mdash; <?php echo __('All Active Policies'); ?> &mdash;</option>
<?php
for ($i = 1; $i <= 12; $i++) {
echo sprintf('<option value="%d" %s>%s</option>',
$i,(($config['passwd_reset_period']==$i)?'selected="selected"':''),
sprintf(_N('Monthly', 'Every %d months', $i), $i));
foreach (PasswordPolicy::allActivePolicies()
as $P) {
echo sprintf('<option value="%s" %s>%s</option>',
$P::$id,
(($config['agent_passwd_policy'] == $P::$id) ? 'selected="selected"' : ''),
$P->getName());
}
?>
</select>
<font class="error"><?php echo $errors['passwd_reset_period']; ?></font>
<i class="help-tip icon-question-sign" href="#password_expiration_policy"></i>
<font class="error"><?php echo
$errors['agent_passwd_policy']; ?></font>
<i class="help-tip icon-question-sign" href="#agent_password_policy"></i>
</td>
</tr>
<tr>
@@ -116,6 +119,19 @@ if (!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$config
&nbsp;<font class="error"><?php echo $errors['pw_reset_window']; ?></font>
</td>
</tr>
<tr>
<td><?php echo __('Multifactor Authentication'); ?>:</td>
<td>
<input type="checkbox" name="require_agent_2fa" <?php
echo $config['require_agent_2fa'] ? 'checked="checked"' : ''; ?>>
&nbsp;
<?php
echo __('Require agents to turn on 2FA');
?>
&nbsp;<i class="help-tip icon-question-sign"
href="#require_agent_2fa"></i>
</td>
</tr>
<tr>
<td><?php echo __('Agent Excessive Logins'); ?>:</td>
<td>
@@ -207,6 +223,7 @@ if (!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$config
<?php $manage_content(__('Agent Welcome Email'), 'registration-staff'); ?>
<?php $manage_content(__('Sign-in Login Banner'), 'banner-staff'); ?>
<?php $manage_content(__('Password Reset Email'), 'pwreset-staff'); ?>
<?php $manage_content(__('Two Factor Authentication Email'), 'email2fa-staff'); ?>
</tbody>
</table>
</div>

View File

@@ -89,6 +89,26 @@ if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$config)
<i class="help-tip icon-question-sign" href="#registration_method"></i>
</td>
</tr>
<tr>
<td><?php echo __('Password Policy'); ?>:</td>
<td>
<select name="client_passwd_policy">
<option value=" "> &mdash; <?php echo __('All Active Policies'); ?> &mdash;</option>
<?php
foreach (PasswordPolicy::allActivePolicies()
as $P) {
echo sprintf('<option value="%s" %s>%s</option>',
$P::$id,
(($config['client_passwd_policy'] == $P::$id) ? 'selected="selected"' : ''),
$P->getName());
}
?>
</select>
<font class="error"><?php echo
$errors['client_passwd_policy']; ?></font>
<i class="help-tip icon-question-sign" href="#client_password_policy"></i>
</td>
</tr>
<tr><td><?php echo __('User Excessive Logins'); ?>:</td>
<td>
<select name="client_max_logins">

View File

@@ -19,6 +19,8 @@ if ($_REQUEST['a']=='add'){
Organization::PERM_EDIT,
Organization::PERM_DELETE,
FAQ::PERM_MANAGE,
Dept::PERM_DEPT,
Staff::PERM_STAFF,
));
}
$title=__('Add New Agent');
@@ -162,6 +164,35 @@ if (count($bks) > 1) {
</td>
</tr>
<?php
} ?>
<?php
if ($bks=Staff2FABackend::allRegistered() && $current = $staff->get2FABackend()) {
$_config = $staff->getConfig();
?>
<tr>
<td><?php echo __('Default 2FA'); ?>:</td>
<td>
<input type="text" size="40" style="width:300px"
name="default_2fa" disabled value="<?php echo $current->getName(); ?>" />
&nbsp;
<button type="button" id="reset-2fa" class="action-button" onclick="javascript:
if (confirm('<?php echo __('You sure?'); ?>')) {
$.ajax({
url: 'ajax.php/staff/'+<?php echo $staff->getId(); ?>+'/reset-2fa',
type: 'POST',
data: {'staffId':<?php echo $staff->getId(); ?>},
success: function(data) {
location.reload();
}
});
}
return false;">
<i class="icon-gear"></i> <?php echo __('Reset 2FA'); ?>
</button>
<i class="offset help-tip icon-question-sign" href="#reset2fa"></i>
</td>
</tr>
<?php
} ?>
</tbody>
<!-- ================================================ -->

View File

@@ -0,0 +1,113 @@
<?php
$title = __('Manage 2FA Options');
if ($auth)
$title = sprintf('%s %s %s', $auth->getName(), '2FA', __('Setup'));
?>
<h3 class="drag-handle"><?php echo $title; ?></h3>
<a class="close" href=""><i class="icon-remove-circle"></i></a>
<hr/>
<?php
if ($info['error']) {
echo sprintf('<p id="msg_error">%s</p>', $info['error']);
} elseif ($info['warning']) {
echo sprintf('<p id="msg_warning">%s</p>', $info['warning']);
} elseif ($info['notice']) {
echo sprintf('<p id="msg_notice">%s</p>', $info['notice']);
} ?>
<div id="backends" <?php if ($auth) echo 'class="hidden"'; ?>>
<table class="table">
<tbody>
<?php
foreach (Staff2FABackend::allRegistered() ?: array() as $bk) {
$configuration = $staff->get2FAConfig($bk->getId());
$isVerified = $configuration['verified'];
$vclass = $isVerified ? 'verified' : 'unverified'; ?>
<tr id="<?php echo $bk->getId(); ?>" class="2fa-type <?php echo $vclass; ?>">
<td nowrap width="10px">
<i class="faded-more <?php echo sprintf('icon-check-%s',
$isVerified ? 'sign' : 'empty'); ?>"></i>
<span data-name="label"></span>
</td>
<td width="300px">
<a class="config2fa"
href="<?php echo sprintf('#staff/%d/2fa/configure/%s',
$staff->getId(), urlencode($bk->getId())); ?>"> <?php echo $bk->getName(); ?>
</a>
<div align="left" class="faded"><?php
echo Format::htmlchars($bk->getDescription()); ?></div>
</td>
</tr>
<?php
} ?>
</tbody>
</table>
<hr>
</div>
<div id="backend" <?php if (!$auth) echo 'class="hidden"'; ?>>
<?php
if ($auth && $form) {
if ($state == 'verify')
$instruction = __('Enter the token sent to you and click Verify');
else
$instruction = __('Complete the form below and then click Next to verify the setup');
?>
<div><?php echo Format::htmlchars($instruction); ?></div>
<br>
<form class="bk" method="post" action="<?php echo sprintf('#staff/%d/2fa/configure/%s',
$staff->getId(), $auth->getId()); ?>">
<input type="hidden" name="state" value="<?php
echo $state ?: 'validate'; ?>" />
<?php
echo csrf_token();
include STAFFINC_DIR . 'templates/dynamic-form-simple.tmpl.php';
?>
<br>
<hr>
<p class="full-width">
<span class="buttons pull-left">
<input type="reset" value="<?php echo __('Reset'); ?>">
<input type="button" name="close" value="<?php echo __('Cancel'); ?>" class="close">
</span>
<span class="buttons pull-right">
<input type="submit" value="<?php echo ($state == 'verify') ?
__('Verify') : __('Next'); ?>">
</span>
</p>
</form>
<?php
} ?>
</div>
<div class="clear"></div>
<script type="text/javascript">
$(function() {
var ids = $('.verified').map(function() {
id2fa = $(this).attr('id');
document.getElementById(id2fa).disabled=false;
});
$('a.config2fa').click( function(e) {
e.preventDefault();
if ($(this).attr('href').length > 1) {
var url = 'ajax.php/'+$(this).attr('href').substr(1);
$.dialog(url, [201, 204], function (xhr) {
window.location.href = window.location.href;
}, {
onshow: function() { $('#user-search').focus(); }
});
} else {
$('div#backends').hide();
$('div#backend').fadeIn();
}
return false;
});
$(document).on('click', 'input.close', function (e) {
e.preventDefault();
alert('Alert');
$('div#backend').hide();
$('div#backends').fadeIn();
return false;
});
});
</script>

View File

@@ -111,7 +111,7 @@ $(function() {
$('#search-add-new-field').on('change', function() {
var that=this;
$.ajax({
url: 'ajax.php/tickets/search/field/'+$(this).val(),
url: 'ajax.php/tickets/search/field/'+$(this).val()+'/<?php echo $filterVisibility ? 'true' : 'false' ?>',
type: 'get',
dataType: 'json',
success: function(json) {

View File

@@ -44,7 +44,7 @@ $action = $info[':action'] ?: ('#');
<tbody>
<tr><td colspan=2>
<?php
$options = array('template' => 'simple', 'form_id' => 'assign');
$options = array('template' => 'simple', 'form_id' => 'assign', 'filterVisibility' => true);
$form->render($options);
?>
</td> </tr>

View File

@@ -1,6 +1,6 @@
<div class="form-simple">
<?php
echo $form->getMedia();
echo $form->getMedia($options);
foreach ($form->getFields() as $name=>$f) { ?>
<div class="flush-left custom-field" id="field<?php echo $f->getWidget()->id;
?>" <?php if (!$f->isVisible()) echo 'style="display:none;"'; ?>>

View File

@@ -1,7 +1,7 @@
<?php
global $cfg;
$options = array('template' => 'simple', 'form_id' => $form->getId());
$options = array('template' => 'simple', 'form_id' => $form->getId(), 'filterVisibility' => true);
?>
<h3 class="drag-handle"><?php echo $info[':title']; ?></h3>

View File

@@ -1,5 +1,5 @@
<?php
if(($tabs=$nav->getTabs()) && is_array($tabs)){
if($nav && ($tabs=$nav->getTabs()) && is_array($tabs)){
foreach($tabs as $name =>$tab) {
if ($tab['href'][0] != '/')
$tab['href'] = ROOT_PATH . 'scp/' . $tab['href'];

View File

@@ -53,7 +53,7 @@ $manage = (!$target);
<tbody>
<tr><td colspan=2>
<?php
$options = array('template' => 'simple', 'form_id' => 'refer');
$options = array('template' => 'simple', 'form_id' => 'refer', 'filterVisibility' => true);
$form->render($options);
?>
</td> </tr>

View File

@@ -34,7 +34,7 @@ if ($info['error']) {
echo $form->getForm(false, array('mode' => 'edit'))->asTable(
__('Task Information'),
array(
'draft-namespace' => $namespace,
'draft-namespace' => $namespace, 'filterVisibility' => true
)
);
}

View File

@@ -26,11 +26,11 @@ if ($info['error']) {
<?php
$form = $form ?: TaskForm::getInstance();
echo $form->getForm($vars)->asTable(' ',
array('draft-namespace' => $namespace)
array('draft-namespace' => $namespace, 'filterVisibility' => true)
);
$iform = $iform ?: TaskForm::getInternalForm();
echo $iform->asTable(__("Task Visibility & Assignment"));
echo $iform->asTable(__("Task Visibility & Assignment"), array('filterVisibility' => true));
?>
<hr>
<p class="full-width">

View File

@@ -39,7 +39,7 @@ $action = $info[':action'] ?: ('#');
<tbody>
<tr><td colspan=2>
<?php
$options = array('template' => 'simple', 'form_id' => 'transfer');
$options = array('template' => 'simple', 'form_id' => 'transfer', 'filterVisibility' => true);
$form->render($options);
?>
</td> </tr>

View File

@@ -93,7 +93,7 @@ if ($_POST)
<select name="topicId">
<option value="" selected >&mdash; <?php echo __('Select Help Topic');?> &mdash;</option>
<?php
if($topics=Topic::getHelpTopics()) {
if ($topics=$thisstaff->getTopicNames()) {
if($ticket->topic_id && !array_key_exists($ticket->topic_id, $topics)) {
$topics[$ticket->topic_id] = $ticket->topic;
$errors['topicId'] = sprintf(__('%s selected must be active'), __('Help Topic'));
@@ -154,7 +154,7 @@ if ($_POST)
<table class="form_table dynamic-forms" width="940" border="0" cellspacing="0" cellpadding="2">
<?php if ($forms)
foreach ($forms as $form) {
$form->render(array('staff'=>true,'mode'=>'edit','width'=>160,'entry'=>$form));
$form->render(array('staff'=>true,'mode'=>'edit','width'=>160,'entry'=>$form, 'filterVisibility' => true));
} ?>
</table>
<table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">

View File

@@ -228,7 +228,7 @@ if ($_POST)
}
});">
<?php
if ($topics=Topic::getHelpTopics(false, false, true)) {
if ($topics=$thisstaff->getTopicNames(false, false)) {
if (count($topics) == 1)
$selected = 'selected="selected"';
else { ?>
@@ -257,7 +257,7 @@ if ($_POST)
<select name="deptId">
<option value="" selected >&mdash; <?php echo __('Select Department'); ?>&mdash;</option>
<?php
if($depts=Dept::getPublicDepartments()) {
if($depts=$thisstaff->getDepartmentNames(true)) {
foreach($depts as $id =>$name) {
if (!($role = $thisstaff->getRole($id))
|| !$role->hasPerm(Ticket::PERM_CREATE)
@@ -318,7 +318,7 @@ if ($_POST)
<select id="assignId" name="assignId">
<option value="0" selected="selected">&mdash; <?php echo __('Select an Agent OR a Team');?> &mdash;</option>
<?php
if(($users=Staff::getAvailableStaffMembers())) {
if(($users=$assignees = $thisstaff->getDeptAgents(array('available' => true, 'namesOnly' => true)))) {
echo '<OPTGROUP label="'.sprintf(__('Agents (%d)'), count($users)).'">';
foreach($users as $id => $name) {
$k="s$id";

View File

@@ -836,9 +836,10 @@ if ($errors['err'] && isset($_POST['a'])) {
$e->getId(),
Format::htmlchars($e->getAddress()));
}
$staffDepts = $thisstaff->getDepts();
// Optional SMTP addreses user can send email via
if (($emails = Email::getAddresses(array('smtp' =>
true), false)) && count($emails)) {
if (($emails = Email::getAddresses(array('smtp' => true,
'depts' => $staffDepts), false)) && count($emails)) {
echo '<option value=""
disabled="disabled">&nbsp;</option>';
$emailId = $_POST['from_email_id'] ?: 0;

View File

@@ -1 +1 @@
4bd47d94b10bd8a6bab35c119dadf41f
add628927ee030469f5d3272ebda1e16

View File

@@ -0,0 +1,8 @@
-- Rename old thread_event table
RENAME TABLE `%TABLE_PREFIX%thread_entry_email` TO `%TABLE_PREFIX%thread_entry_email_old`;
-- Change tmp_table to thread_event
RENAME TABLE `%TABLE_PREFIX%thread_entry_email_new` TO `%TABLE_PREFIX%thread_entry_email`;
-- Drop old thread_event table
DROP TABLE `%TABLE_PREFIX%thread_entry_email_old`;

View File

@@ -0,0 +1,24 @@
/**
* @signature e7038ce9762843b6cfbe9fa2d98feaf3
* @version v1.15
* @title Add email_id to thread_entry_email
*
* This patch adds an email_id column to the thread_entry_email table
*/
-- Create a blank temporary table with the new email_id column
CREATE TABLE `%TABLE_PREFIX%thread_entry_email_new` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`thread_entry_id` int(11) unsigned NOT NULL,
`email_id` int(11) unsigned DEFAULT NULL,
`mid` varchar(255) NOT NULL,
`headers` text,
PRIMARY KEY (`id`),
KEY `thread_entry_id` (`thread_entry_id`),
KEY `mid` (`mid`),
KEY `email_id` (`email_Id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- Finished with patch
UPDATE `%TABLE_PREFIX%config`
SET `value` = 'e7038ce9762843b6cfbe9fa2d98feaf3'
WHERE `key` = 'schema_signature' AND `namespace` = 'core';

View File

@@ -0,0 +1,154 @@
<?php
/*
* Populate the temporary thread_entry_email table in batches
* to avoid an alter table statement to add the email_id column.
*/
require_once INCLUDE_DIR.'class.migrater.php';
define('THREAD_ENTRY_EMAIL_NEW_TABLE', TABLE_PREFIX.'thread_entry_email_new');
class ThreadEntryEmailNew extends VerySimpleModel {
static $meta = array(
'table' => THREAD_ENTRY_EMAIL_NEW_TABLE,
'pk' => array('id'),
);
}
class ThreadEntryEmailMigration extends MigrationTask {
var $description = "Add an email_id column to the thread_entry_email table";
var $queue;
var $skipList;
var $errorList = array();
var $limit = 20000;
function sleep() {
return array('queue'=>$this->queue, 'skipList'=>$this->skipList);
}
function wakeup($stuff) {
$this->queue = $stuff['queue'];
$this->skipList = $stuff['skipList'];
while (!$this->isFinished())
$this->do_batch(30, $this->limit);
}
function run($max_time) {
$this->do_batch($max_time * 0.9, $this->limit);
}
function isFinished() {
return $this->getQueueLength() == 0;
}
function do_batch($time=30, $max=0) {
if(!$this->queueEvents($max) || !$this->getQueueLength())
return 0;
$this->setStatus("{$this->getQueueLength()} records remaining");
$count = 0;
$start = Misc::micro_time();
while ($this->getQueueLength() && (Misc::micro_time()-$start) < $time) {
if($this->next() && $max && ++$count>=$max) {
break;
}
}
return $this->queueEvents($max);
}
function queueEvents($limit=0){
global $cfg, $ost;
# Since the queue is persistent - we want to make sure we get to empty
# before we find more events.
if(($qc=$this->getQueueLength()))
return $qc;
// Count how many rows are left
$sql = "SELECT COUNT(old.id)
FROM ". THREAD_ENTRY_EMAIL_TABLE." old
WHERE old.id NOT IN (SELECT new.id FROM ". THREAD_ENTRY_EMAIL_NEW_TABLE ." new)";
//XXX: Do a hard fail or error querying the database?
if(!($res=db_query($sql)))
return $this->error('Unable to query DB for Thread Entry Email migration!');
$count = db_result($res);
// Force the log message to the database
$ost->logDebug("Thread Entry Email Migration", 'Found '.$count
.' records to migrate', true);
if($count == 0)
return 0; //Nothing else to do!!
// Get id of the next record to be inserted into the new table
$start = db_result(db_query("SELECT id
FROM ". THREAD_ENTRY_EMAIL_TABLE ."
WHERE id > (SELECT MAX(id) FROM ". THREAD_ENTRY_EMAIL_NEW_TABLE .")
LIMIT 1"));
$start = intval($start);
$this->queue = array();
$info=array(
'count' => $count,
'start' => $start,
'end' => $start + $limit
);
$this->enqueue($info);
return $this->getQueueLength();
}
function skip($id, $error) {
$this->skipList[] = $id;
return $this->error($error." (ID #$id)");
}
function error($what) {
global $ost;
$this->errors++;
$this->errorList[] = $what;
// Log the error but don't send the alert email
$ost->logError('Upgrader: Thread Entry Email Migrater', $what, false);
# Assist in returning FALSE for inline returns with this method
return false;
}
function getErrors() {
return $this->errorList;
}
function getSkipList() {
return $this->skipList;
}
function enqueue($info) {
$this->queue[] = $info;
}
function getQueueLength() {
return $this->queue[0]['count'];
}
function next() {
# Fetch next item -- use the last item so the array indices don't
# need to be recalculated for every shift() operation.
$info = array_pop($this->queue);
// Insert rows into the new table starting at the start and stopping at the end id
$sql = "INSERT INTO ". THREAD_ENTRY_EMAIL_NEW_TABLE ."
SELECT B.id, B.thread_entry_id, NULL, B.mid, B.headers
FROM ". THREAD_ENTRY_EMAIL_TABLE . " B
WHERE B.id >= ".$info['start']." AND B.id < ". $info['end'];
db_query($sql);
return true;
}
}
return 'ThreadEntryEmailMigration';
?>

View File

@@ -0,0 +1,17 @@
/**
* @signature add628927ee030469f5d3272ebda1e16
* @version v1.15
* @title Alter dept_id and priority_id in email table
*
* This patch changes dept_id and priority_id fron tinyint to int
*/
-- change dept_id
ALTER TABLE `%TABLE_PREFIX%email` CHANGE `dept_id` `dept_id` INT( 11 ) UNSIGNED NOT NULL DEFAULT '0';
-- change priority_id
ALTER TABLE `%TABLE_PREFIX%email` CHANGE `priority_id` `priority_id` INT( 11 ) UNSIGNED NOT NULL DEFAULT '2';
-- Finished with patch
UPDATE `%TABLE_PREFIX%config`
SET `value` = 'add628927ee030469f5d3272ebda1e16'
WHERE `key` = 'schema_signature' AND `namespace` = 'core';

View File

@@ -0,0 +1,47 @@
/**
* @signature ddbe2e76ec38a2e58bdbff9109c07930
* @version v1.15
* @title Add indexes accumulated over time
*
* This patch adds all indexes we've implemented over time and adds the
* `version` column to the plugin table (if not exists)
*/
-- Add FLAG_AGENT_REQUIRED For Task Title
UPDATE `%TABLE_PREFIX%form_field`
SET `flags` = IF((`flags` & 16384) != 16384, `flags` | 16384, `flags`)
WHERE `form_id` = (SELECT `id` FROM `%TABLE_PREFIX%form` WHERE `type` = 'A') AND `name` = 'title';
-- Add FLAG_AGENT_VIEW for Task Title/Description
UPDATE `%TABLE_PREFIX%form_field`
SET `flags` = IF((`flags` & 4096) != 4096, `flags` | 4096, `flags`)
WHERE `form_id` = (SELECT `id` FROM `%TABLE_PREFIX%form` WHERE `type` = 'A') AND `name` IN ('title', 'description');
-- Add FLAG_MASK_REQUIRE for Task Title
UPDATE `%TABLE_PREFIX%form_field`
SET `flags` = IF((`flags` & 65536) != 65536, `flags` | 65536, `flags`)
WHERE `form_id` = (SELECT `id` FROM `%TABLE_PREFIX%form` WHERE `type` = 'A') AND `name` = 'title';
-- Add FLAG_MASK_VIEW for Task Title/Description
UPDATE `%TABLE_PREFIX%form_field`
SET `flags` = IF((`flags` & 131072) != 131072, `flags` | 131072, `flags`)
WHERE `form_id` = (SELECT `id` FROM `%TABLE_PREFIX%form` WHERE `type` = 'A') AND `name` IN ('title', 'description');
-- Add `version` column to the plugin table (if not exists)
SET @s = (SELECT IF(
(SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = '%TABLE_PREFIX%plugin'
AND table_schema = DATABASE()
AND column_name = 'version'
) > 0,
"SELECT 1",
"ALTER TABLE `%TABLE_PREFIX%plugin` ADD `version` VARCHAR(64) DEFAULT NULL AFTER `isactive`"
));
PREPARE stmt FROM @s;
EXECUTE stmt;
-- Finished with patch
UPDATE `%TABLE_PREFIX%config`
SET `value` = 'ddbe2e76ec38a2e58bdbff9109c07930'
WHERE `key` = 'schema_signature' AND `namespace` = 'core';

View File

@@ -0,0 +1,120 @@
<?php
/*
* Add all indexes we've implemented over time. Check if exists
* to avoid errors.
*/
class AddIndexMigration extends MigrationTask {
var $description = "Add indexes accumulated over time";
var $indexes = array(
'department:flags',
'draft:staff_id',
'draft:namespace',
'file:type',
'file:created',
'file:size',
'form:type',
'form_field:form_id',
'form_field:sort',
'queue:staff_id',
'queue:parent_id',
'staff:isactive',
'staff:onvacation',
'task:flags',
'ticket:ticket_pid',
'team_member:staff_id',
'user:default_email_id',
'user:name'
);
function run($max_time) {
global $ost;
foreach ($this->indexes as $index) {
list($t, $i) = explode(':', $index);
// Check if INDEX already exists via SHOW INDEX
$sql = sprintf("SHOW INDEX FROM `%s` WHERE `Column_name` = '%s' AND `Key_name` != 'PRIMARY'",
TABLE_PREFIX.$t, $i);
// Hardfail if we cannot check if exists
if(!($res=db_query($sql)))
return $this->error('Unable to query DB for Add Index migration!');
$count = db_num_rows($res);
if (!$count || ($count && ($count == 0))) {
// CREATE INDEX if not exists
$create = sprintf('CREATE INDEX `%s` ON `%s`.`%s` (`%s`)',
$i, DBNAME, TABLE_PREFIX.$t, $i);
if(!($res=db_query($create))) {
$message = "Unable to create index `$i` on `".TABLE_PREFIX.$t."`.";
// Log the error but don't send the alert email
$ost->logError('Upgrader: Add Index Migrater', $message, false);
}
}
}
//add permissions to staff
foreach (Staff::objects() as $staff) {
$perms = array();
foreach ($staff->getPermissionInfo() as $value => $setting)
$perms[] = $value;
array_push($perms, 'visibility.departments', 'visibility.agents');
$staff->updatePerms($perms);
$staff->save();
}
//add 2fa template
foreach (array('email2fa-staff') as $type) {
$i18n = new Internationalization();
$tpl = $i18n->getTemplate("templates/page/{$type}.yaml");
if (!($page = $tpl->getData()))
// No such template on disk
continue;
if ($id = db_result(db_query('select id from '.PAGE_TABLE
.' where `type`='.db_input($type))))
// Already have a template for the content type
continue;
$sql = 'INSERT INTO '.PAGE_TABLE.' SET type='.db_input($type)
.', name='.db_input($page['name'])
.', body='.db_input($page['body'])
.', notes='.db_input($page['notes'])
.', created=NOW(), updated=NOW(), isactive=1';
db_query($sql);
}
// See if there are missing events that should be added to the database
$event_type = array('login', 'logout', 'message', 'note');
foreach($event_type as $eType) {
$sql = sprintf("SELECT * FROM `%s` WHERE name = '%s'",
TABLE_PREFIX.'event', $eType);
$res=db_query($sql);
$count = db_num_rows($res);
if($count > 0) {
$message = "Event '$eType' already exists.";
$ost->logError('Upgrader: Add Events', $message, false);
} else {
// Add event
$sql = sprintf("INSERT INTO `%s` (`id`, `name`, `description`)
VALUES
('','%s',NULL)",
TABLE_PREFIX.'event', $eType);
if(!($res=db_query($sql))) {
$message = "Unable to add $eType event to `".TABLE_PREFIX.'event'."`.";
$ost->logError('Upgrader: Add Events', $message, false);
}
}
}
}
}
return 'AddIndexMigration';
?>

View File

@@ -203,7 +203,7 @@ $dispatcher = patterns('',
url_post('^/(?P<id>\d+)/save$', 'saveSearch'),
url_post('^/save$', 'saveSearch'),
url_delete('^/(?P<id>\d+)$', 'deleteSearch'),
url_get('^/field/(?P<id>[\w_!:]+)$', 'addField'),
url_get('^/field/(?P<id>[\w_!:]+)/(?P<type>\w+)$', 'addField'),
url('^/column/edit/(?P<id>\d+)$', 'editColumn'),
url('^/sort/edit/(?P<id>\d+)$', 'editSort'),
url_post('^(?P<id>\d+)/delete$', 'deleteQueues'),
@@ -295,7 +295,9 @@ $dispatcher = patterns('',
url_get('^/(?P<id>\d+)/perms', 'getAgentPerms'),
url('^/reset-permissions', 'resetPermissions'),
url('^/change-department', 'changeDepartment'),
url('^/(?P<id>\d+)/avatar/change', 'setAvatar')
url('^/(?P<id>\d+)/avatar/change', 'setAvatar'),
url('^/(?P<id>\d+)/2fa/configure(?:/(?P<mfid>.+))?$', 'configure2FA'),
url('^/(?P<id>\d+)/reset-2fa', 'reset2fA')
)),
url('^/queue/', patterns('ajax.search.php:SearchAjaxAPI',
url('^(?P<id>\d+/)?preview$', 'previewQueue'),

View File

@@ -16,9 +16,17 @@
require('staff.inc.php');
include_once(INCLUDE_DIR.'class.canned.php');
if ($thisstaff && $roles = $thisstaff->getRoles()) {
$cannedManage = array();
foreach ($roles as $r) {
if ($r->hasPerm(Canned::PERM_MANAGE, false))
$cannedManage[] = 1;
}
}
/* check permission */
if(!$thisstaff
|| !$thisstaff->getRole()->hasPerm(Canned::PERM_MANAGE, false)
|| !in_array(1, $cannedManage)
|| !$cfg->isCannedResponseEnabled()) {
header('Location: kb.php');
exit;

View File

@@ -24,7 +24,7 @@ require_once(INCLUDE_DIR.'class.staff.php');
require_once(INCLUDE_DIR.'class.csrf.php');
$content = Page::lookupByType('banner-staff');
$thisstaff = StaffAuthenticationBackend::getUser();
$dest = $_SESSION['_staff']['auth']['dest'];
$msg = $_SESSION['_staff']['auth']['msg'];
$msg = $msg ?: ($content ? $content->getLocalName() : __('Authentication Required'));
@@ -70,7 +70,7 @@ if ($_POST && isset($_POST['userid'])) {
$username = trim($_POST['userid']);
if ($user = StaffAuthenticationBackend::process($username,
$_POST['passwd'], $errors)) {
$redirect($dest);
$redirect($user->isValid() ? $dest : 'login.php');
}
$msg = $errors['err'] ?: __('Invalid login');
@@ -84,6 +84,31 @@ if ($_POST && isset($_POST['userid'])) {
$ost->getCSRF()->rotate();
}
}
elseif ($_POST
&& !strcmp($_POST['do'], '2fa')
&& $thisstaff
&& $thisstaff->is2FAPending()
&& ($auth=$thisstaff->get2FABackend())) {
try {
$form = $auth->getInputForm($_POST);
if ($form->isValid() && $auth->validate($form, $thisstaff))
$redirect($dest);
} catch (ExpiredOTP $ex) {
// Expired or too many attempts
$thisstaff->logOut();
$redirect('login.php');
}
$msg = __('Invalid Code');
if ($json) {
$respond(401, ['message' => $msg]);
}
else {
// Rotate the CSRF token (original cannot be reused)
$ost->getCSRF()->rotate();
}
}
elseif ($_GET['do']) {
switch ($_GET['do']) {
case 'ext':

View File

@@ -44,6 +44,15 @@ if($thisstaff->forcePasswdChange() && !$errors['err'])
$thisstaff->getFirstName()
)
);
elseif($thisstaff->force2faConfig() && !$errors['err'])
$errors['err'] = str_replace(
'<a>',
sprintf('<a data-dialog="ajax.php/staff/%d/2fa/configure" href="#">', $thisstaff->getId()),
sprintf(
__('<b>Hi %s</b> - You must <a>configure and save Two Factor Authentication </a>!'),
$thisstaff->getFirstName()
)
);
elseif($thisstaff->onVacation() && !$warn)
$warn=sprintf(__("<b>Welcome back %s</b>! You are listed as 'on vacation' Please let your manager know that you are back."),$thisstaff->getFirstName());

View File

@@ -136,6 +136,10 @@ if($thisstaff->forcePasswdChange() && !$exempt) {
$sysnotice = __('Password change required to continue');
require('profile.php'); //profile.php must request this file as require_once to avoid problems.
exit;
} elseif ($thisstaff->force2faConfig() && !$exempt) {
$sysnotice = __('Two Factor Authentication configuration required to continue');
require('profile.php');
exit;
}
$ost->setWarning($sysnotice);
$ost->setPageTitle(__('osTicket :: Staff Control Panel'));

View File

@@ -206,6 +206,8 @@ class Installer extends SetupWizard {
Organization::PERM_DELETE,
FAQ::PERM_MANAGE,
Email::PERM_BANLIST,
Dept::PERM_DEPT,
Staff::PERM_STAFF,
));
$staff->setPassword($vars['passwd']);
if (!$staff->save()) {

View File

@@ -124,7 +124,8 @@ CREATE TABLE `%TABLE_PREFIX%form` (
`notes` text,
`created` datetime NOT NULL,
`updated` datetime NOT NULL,
PRIMARY KEY (`id`)
PRIMARY KEY (`id`),
KEY `type` (`type`)
) DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `%TABLE_PREFIX%form_field`;
@@ -140,7 +141,9 @@ CREATE TABLE `%TABLE_PREFIX%form_field` (
`hint` varchar(512),
`created` datetime NOT NULL,
`updated` datetime NOT NULL,
PRIMARY KEY (`id`)
PRIMARY KEY (`id`),
KEY `form_id` (`form_id`),
KEY `sort` (`sort`)
) DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `%TABLE_PREFIX%form_entry`;
@@ -222,7 +225,8 @@ CREATE TABLE `%TABLE_PREFIX%department` (
UNIQUE KEY `name` (`name`, `pid`),
KEY `manager_id` (`manager_id`),
KEY `autoresp_email_id` (`autoresp_email_id`),
KEY `tpl_id` (`tpl_id`)
KEY `tpl_id` (`tpl_id`),
KEY `flags` (`flags`)
) DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `%TABLE_PREFIX%draft`;
@@ -234,15 +238,17 @@ CREATE TABLE `%TABLE_PREFIX%draft` (
`extra` text,
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
PRIMARY KEY (`id`),
KEY `staff_id` (`staff_id`),
KEY `namespace` (`namespace`)
) DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `%TABLE_PREFIX%email`;
CREATE TABLE `%TABLE_PREFIX%email` (
`email_id` int(11) unsigned NOT NULL auto_increment,
`noautoresp` tinyint(1) unsigned NOT NULL default '0',
`priority_id` tinyint(3) unsigned NOT NULL default '2',
`dept_id` tinyint(3) unsigned NOT NULL default '0',
`priority_id` int(11) unsigned NOT NULL default '2',
`dept_id` int(11) unsigned NOT NULL default '0',
`topic_id` int(11) unsigned NOT NULL default '0',
`email` varchar(255) NOT NULL default '',
`name` varchar(255) NOT NULL default '',
@@ -388,7 +394,10 @@ CREATE TABLE `%TABLE_PREFIX%file` (
PRIMARY KEY (`id`),
KEY `ft` (`ft`),
KEY `key` (`key`),
KEY `signature` (`signature`)
KEY `signature` (`signature`),
KEY `type` (`type`),
KEY `created` (`created`),
KEY `size` (`size`)
) DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `%TABLE_PREFIX%file_chunk`;
@@ -600,7 +609,9 @@ CREATE TABLE `%TABLE_PREFIX%staff` (
PRIMARY KEY (`staff_id`),
UNIQUE KEY `username` (`username`),
KEY `dept_id` (`dept_id`),
KEY `issuperuser` (`isadmin`)
KEY `issuperuser` (`isadmin`),
KEY `isactive` (`isactive`),
KEY `onvacation` (`onvacation`)
) DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `%TABLE_PREFIX%staff_dept_access`;
@@ -646,7 +657,8 @@ CREATE TABLE `%TABLE_PREFIX%team_member` (
`team_id` int(10) unsigned NOT NULL default '0',
`staff_id` int(10) unsigned NOT NULL,
`flags` int(10) unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`team_id`,`staff_id`)
PRIMARY KEY (`team_id`,`staff_id`),
KEY `staff_id` (`staff_id`)
) DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `%TABLE_PREFIX%thread`;
@@ -695,11 +707,13 @@ DROP TABLE IF EXISTS `%TABLE_PREFIX%thread_entry_email`;
CREATE TABLE `%TABLE_PREFIX%thread_entry_email` (
`id` int(11) unsigned NOT NULL auto_increment,
`thread_entry_id` int(11) unsigned NOT NULL,
`email_id` int(11) unsigned DEFAULT NULL,
`mid` varchar(255) NOT NULL,
`headers` text,
PRIMARY KEY (`id`),
KEY `thread_entry_id` (`thread_entry_id`),
KEY `mid` (`mid`)
KEY `mid` (`mid`),
KEY `email_id` (`email_id`)
) DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `%TABLE_PREFIX%thread_entry_merge`;
@@ -750,7 +764,8 @@ CREATE TABLE `%TABLE_PREFIX%ticket` (
KEY `closed` (`closed`),
KEY `duedate` (`duedate`),
KEY `topic_id` (`topic_id`),
KEY `sla_id` (`sla_id`)
KEY `sla_id` (`sla_id`),
KEY `ticket_pid` (`ticket_pid`)
) DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `%TABLE_PREFIX%lock`;
@@ -872,7 +887,8 @@ CREATE TABLE `%TABLE_PREFIX%task` (
KEY `staff_id` (`staff_id`),
KEY `team_id` (`team_id`),
KEY `created` (`created`),
KEY `object` (`object_id`,`object_type`)
KEY `object` (`object_id`,`object_type`),
KEY `flags` (`flags`)
) DEFAULT CHARSET=utf8;
-- pages
@@ -918,7 +934,9 @@ CREATE TABLE `%TABLE_PREFIX%queue` (
`path` varchar(80) NOT NULL DEFAULT '/',
`created` datetime not null,
`updated` datetime not null,
primary key (`id`)
primary key (`id`),
KEY `staff_id` (`staff_id`),
KEY `parent_id` (`parent_id`)
) DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `%TABLE_PREFIX%queue_column`;
@@ -1014,7 +1032,9 @@ CREATE TABLE `%TABLE_PREFIX%user` (
`created` datetime NOT NULL,
`updated` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `org_id` (`org_id`)
KEY `org_id` (`org_id`),
KEY `default_email_id` (`default_email_id`),
KEY `name` (`name`)
) DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `%TABLE_PREFIX%user_email`;