Merge branch '457-category-descriptions' into '3-2-0'

Add Description to Categories

See merge request !68
master
Mike Koch 7 years ago
commit 0dcac4f324

@ -5,8 +5,6 @@ stages:
- deploy
before_script:
- apt-get update
- apt-get install zip unzip
- cd api
- php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
- php composer-setup.php
@ -24,9 +22,11 @@ deploy:
when: manual
stage: deploy
script:
- apt-get update
- apt-get install zip unzip
- composer install --no-dev
- cd ../ci
- bash build_zip.sh
artifacts:
paths:
- release.zip
- release.zip

@ -33,7 +33,6 @@ Mods for HESK is a set of modifications for [HESK](https://www.hesk.com) v2.7.x,
<li>Custom service message icons</li>
<li>Permission templates</li>
<li>Request users location in tickets</li>
<li>Category managers</li>
<li>Show number of merged tickets in ticket search view</li>
<li>Enable / disable staff members</li>
<li>More-restricted settings page access</li>

@ -39,9 +39,7 @@ hesk_dbConnect();
hesk_isLoggedIn();
/* Check permissions for this feature */
if (!isset($_REQUEST['isManager']) || !$_REQUEST['isManager']) {
hesk_checkPermission('can_reply_tickets');
}
hesk_checkPermission('can_reply_tickets');
/* A security check */
# hesk_token_check('POST');
@ -281,7 +279,7 @@ if ($time_worked == '00:00:00') {
$sql .= ",`time_worked` = ADDTIME(`time_worked`,'" . hesk_dbEscape($time_worked) . "') ";
}
if (!empty($_POST['assign_self']) && (hesk_checkPermission('can_assign_self', 0) || (isset($_REQUEST['isManager']) && $_REQUEST['isManager']))) {
if (!empty($_POST['assign_self']) && (hesk_checkPermission('can_assign_self', 0))) {
$revision = sprintf($hesklang['thist2'], hesk_date(), $_SESSION['name'] . ' (' . $_SESSION['user'] . ')', $_SESSION['name'] . ' (' . $_SESSION['user'] . ')');
$sql .= " , `owner`=" . intval($_SESSION['id']) . ", `history`=CONCAT(`history`,'" . hesk_dbEscape($revision) . "') ";
}

@ -107,33 +107,14 @@ if ($ticket['lastreplier']) {
}
/* Get category name and ID */
$result = hesk_dbQuery("SELECT `id`, `name`, `manager` FROM `" . hesk_dbEscape($hesk_settings['db_pfix']) . "categories` WHERE `id`='" . intval($ticket['category']) . "' LIMIT 1");
$result = hesk_dbQuery("SELECT `id`, `name` FROM `" . hesk_dbEscape($hesk_settings['db_pfix']) . "categories` WHERE `id`='" . intval($ticket['category']) . "' LIMIT 1");
/* If this category has been deleted use the default category with ID 1 */
if (hesk_dbNumRows($result) != 1) {
$result = hesk_dbQuery("SELECT `id`, `name`, `manager` FROM `" . hesk_dbEscape($hesk_settings['db_pfix']) . "categories` WHERE `id`='1' LIMIT 1");
$result = hesk_dbQuery("SELECT `id`, `name` FROM `" . hesk_dbEscape($hesk_settings['db_pfix']) . "categories` WHERE `id`='1' LIMIT 1");
}
$category = hesk_dbFetchAssoc($result);
$managerRS = hesk_dbQuery('SELECT * FROM `' . hesk_dbEscape($hesk_settings['db_pfix']) . 'users` WHERE `id` = ' . intval($_SESSION['id']));
$managerRow = hesk_dbFetchAssoc($managerRS);
$isManager = $managerRow['id'] == $category['manager'];
if ($isManager) {
$can_del_notes =
$can_reply =
$can_delete =
$can_edit =
$can_archive =
$can_assign_self =
$can_view_unassigned =
$can_change_own_cat =
$can_change_cat =
$can_ban_emails =
$can_unban_emails =
$can_ban_ips =
$can_unban_ips =
$can_resolve = true;
}
/* Is this user allowed to view tickets inside this category? */
hesk_okCategory($category['id']);
@ -973,9 +954,6 @@ require_once(HESK_PATH . 'inc/show_admin_nav.inc.php');
<input type="submit" style="display: none" value="' . $hesklang['go'] . '" /><input type="hidden" name="track" value="' . $trackingID . '" />
<input type="hidden" name="token" value="' . hesk_token_echo(0) . '" />';
if ($isManager) {
echo '<input type="hidden" name="isManager" value="1">';
}
echo '</span>
</form>
@ -999,15 +977,12 @@ require_once(HESK_PATH . 'inc/show_admin_nav.inc.php');
<input type="submit" style="display:none;" value="' . $hesklang['go'] . '" class="btn btn-default" /><input type="hidden" name="track" value="' . $trackingID . '" />
<input type="hidden" name="token" value="' . hesk_token_echo(0) . '" />';
if ($isManager) {
echo '<input type="hidden" name="isManager" value="1">';
}
echo '</span>
</form>
</div>';
echo '<div class="col-md-3 col-sm-12 ticket-cell-admin"><p class="ticket-property-title">' . $hesklang['owner'] . '</p>';
if (hesk_checkPermission('can_assign_others', 0) || $isManager) {
if (hesk_checkPermission('can_assign_others', 0)) {
echo '
<form style="margin-bottom:0;" id="changeOwnerForm" action="assign_owner.php" method="post">
<span style="white-space:nowrap;">
@ -1250,15 +1225,14 @@ require_once(HESK_PATH . 'inc/footer.inc.php');
function hesk_getAdminButtons($category_id)
{
global $hesk_settings, $hesklang, $modsForHesk_settings, $ticket, $reply, $trackingID, $can_edit, $can_archive, $can_delete, $can_resolve, $isManager;
global $hesk_settings, $hesklang, $modsForHesk_settings, $ticket, $reply, $trackingID, $can_edit, $can_archive, $can_delete, $can_resolve;
$options = '';
/* Edit post */
if ($can_edit) {
$tmp = $reply ? '&amp;reply=' . $reply['id'] : '';
$mgr = $isManager ? '&amp;isManager=true' : '';
$options .= '<a class="btn btn-default" href="edit_post.php?track=' . $trackingID . $tmp . $mgr . '"><i class="fa fa-pencil orange"></i> ' . $hesklang['edit'] . '</a> ';
$options .= '<a class="btn btn-default" href="edit_post.php?track=' . $trackingID . $tmp . '"><i class="fa fa-pencil orange"></i> ' . $hesklang['edit'] . '</a> ';
}
@ -1304,13 +1278,12 @@ function hesk_getAdminButtons($category_id)
$isTicketClosed = $isTicketClosedRow['IsClosed'];
$isClosable = $isTicketClosedRow['Closable'] == 'yes' || $isTicketClosedRow['Closable'] == 'sonly';
$mgr = $isManager ? '&amp;isManager=1' : '';
if ($isTicketClosed == 0 && $isClosable && $can_resolve) // Ticket is still open
{
$options .= '<a class="btn btn-default" href="change_status.php?track=' . $trackingID . $mgr . '&amp;s=' . $staffClosedOptionStatus['ID'] . '&amp;Refresh=' . $random . '&amp;token=' . hesk_token_echo(0) . '">
$options .= '<a class="btn btn-default" href="change_status.php?track=' . $trackingID . '&amp;s=' . $staffClosedOptionStatus['ID'] . '&amp;Refresh=' . $random . '&amp;token=' . hesk_token_echo(0) . '">
<i class="fa fa-check-circle green"></i> ' . $hesklang['close_action'] . '</a> ';
} elseif ($isTicketClosed == 1) {
$options .= '<a class="btn btn-default" href="change_status.php?track=' . $trackingID . $mgr . '&amp;s=' . $staffReopenedStatus['ID'] . '&amp;Refresh=' . $random . '&amp;token=' . hesk_token_echo(0) . '">
$options .= '<a class="btn btn-default" href="change_status.php?track=' . $trackingID . '&amp;s=' . $staffReopenedStatus['ID'] . '&amp;Refresh=' . $random . '&amp;token=' . hesk_token_echo(0) . '">
<i class="fa fa-folder-open-o green"></i> ' . $hesklang['open_action'] . '</a> ';
}
@ -1496,7 +1469,7 @@ function hesk_getAdminButtons($category_id)
function hesk_getAdminButtonsInTicket($reply = 0, $white = 1)
{
global $hesk_settings, $hesklang, $ticket, $reply, $trackingID, $can_edit, $can_archive, $can_delete, $isManager;
global $hesk_settings, $hesklang, $ticket, $reply, $trackingID, $can_edit, $can_archive, $can_delete;
$options = $reply ? '' : '<div class="pull-right">';
@ -1519,8 +1492,7 @@ function hesk_getAdminButtonsInTicket($reply = 0, $white = 1)
/* Edit post */
if ($can_edit) {
$tmp = $reply ? '&amp;reply=' . $reply['id'] : '';
$mgr = $isManager ? '&amp;isManager=true' : '';
$options .= '<a class="btn btn-default" href="edit_post.php?track=' . $trackingID . $tmp . $mgr . '"><i class="fa fa-pencil orange"></i> ' . $hesklang['edtt'] . '</a> ';
$options .= '<a class="btn btn-default" href="edit_post.php?track=' . $trackingID . $tmp . '"><i class="fa fa-pencil orange"></i> ' . $hesklang['edtt'] . '</a> ';
}
@ -1834,7 +1806,7 @@ function hesk_printTicketReplies()
function hesk_printReplyForm()
{
global $hesklang, $hesk_settings, $ticket, $admins, $can_options, $can_resolve, $options, $can_assign_self, $isManager, $modsForHesk_settings;
global $hesklang, $hesk_settings, $ticket, $admins, $can_options, $can_resolve, $options, $can_assign_self, $modsForHesk_settings;
// Force assigning a ticket before allowing to reply?
if ($hesk_settings['require_owner'] && ! $ticket['owner'])
@ -2056,9 +2028,6 @@ function hesk_printReplyForm()
</ul>
</div>
<input class="btn btn-default" type="submit" name="save_reply" value="<?php echo $hesklang['sacl']; ?>">
<?php if ($isManager): ?>
<input type="hidden" name="isManager" value="1">
<?php endif; ?>
</div>
</div>
</form>

@ -26,10 +26,8 @@ hesk_isLoggedIn();
$modsForHesk_settings = mfh_getSettings();
/* Check permissions for this feature */
if (!isset($_REQUEST['isManager']) || !$_REQUEST['isManager']) {
hesk_checkPermission('can_view_tickets');
hesk_checkPermission('can_reply_tickets');
}
hesk_checkPermission('can_view_tickets');
hesk_checkPermission('can_reply_tickets');
/* A security check */
hesk_token_check();

@ -32,10 +32,8 @@ hesk_dbConnect();
hesk_isLoggedIn();
/* Check permissions for this feature */
if (!isset($_REQUEST['isManager']) || !$_REQUEST['isManager']) {
hesk_checkPermission('can_view_tickets');
hesk_checkPermission('can_edit_tickets');
}
hesk_checkPermission('can_view_tickets');
hesk_checkPermission('can_edit_tickets');
$modsForHesk_settings = mfh_getSettings();
/* Ticket ID */
@ -61,9 +59,7 @@ if (defined('HESK_DEMO')) {
}
/* Is this user allowed to view tickets inside this category? */
if (!isset($_REQUEST['isManager']) || !$_REQUEST['isManager']) {
hesk_okCategory($ticket['category']);
}
hesk_okCategory($ticket['category']);
if (hesk_isREQUEST('reply')) {
$tmpvar['id'] = intval(hesk_REQUEST('reply')) or die($hesklang['id_not_valid']);
@ -630,9 +626,6 @@ require_once(HESK_PATH . 'inc/show_admin_nav.inc.php');
?>
<input type="hidden" name="html" value="<?php echo $html; ?>">
<input type="submit" value="<?php echo $hesklang['save_changes']; ?>" class="btn btn-default">
<?php if (isset($_REQUEST['isManager']) && $_REQUEST['isManager']): ?>
<input type="hidden" name="isManager" value="1">
<?php endif; ?>
<a class="btn btn-default" href="javascript:history.go(-1)"><?php echo $hesklang['back']; ?></a>
</div>
</form>

File diff suppressed because it is too large Load Diff

@ -621,31 +621,6 @@ function update_user()
hesk_dbQuery("UPDATE `" . hesk_dbEscape($hesk_settings['db_pfix']) . "tickets` SET `owner`=0 WHERE `owner`='" . intval($myuser['id']) . "' AND `category` NOT IN (" . $myuser['categories'] . ")");
}
// Find the list of categories they are manager of. If they no longer have access to the category, revoke their manager permission.
if ($myuser['isadmin']) {
// Admins can't be managers
hesk_dbQuery('UPDATE `' . hesk_dbEscape($hesk_settings['db_pfix']) . 'categories` SET `manager` = 0 WHERE `manager` = ' . intval($myuser['id']));
} else {
$currentCatRs = hesk_dbQuery("SELECT `categories` FROM `" . hesk_dbEscape($hesk_settings['db_pfix']) . "users` WHERE `id` = '" . intval($myuser['id']) . "' LIMIT 1");
$rowOfCategories = hesk_dbFetchAssoc($currentCatRs);
$cats = $rowOfCategories['categories'];
$currentCategories = explode(',', $cats);
$newCategories = explode(',', $myuser['categories']);
// If any any elements are in current but not in new, add them to the revoke array
$revokeCats = array();
foreach ($currentCategories as $currentCategory) {
if (!in_array($currentCategory, $newCategories) && $currentCategory != '') {
array_push($revokeCats, $currentCategory);
}
}
if (count($revokeCats) > 0) {
hesk_dbQuery("UPDATE `" . hesk_dbEscape($hesk_settings['db_pfix']) . "categories` SET `manager` = 0 WHERE `id` IN (" . implode(',', $revokeCats) . ")");
}
}
hesk_dbQuery(
"UPDATE `" . hesk_dbEscape($hesk_settings['db_pfix']) . "users` SET
`user`='" . hesk_dbEscape($myuser['user']) . "',
@ -676,11 +651,6 @@ function update_user()
`default_calendar_view`=" . intval($myuser['default_calendar_view']) . "
WHERE `id`='" . intval($myuser['id']) . "'");
// If they are now inactive, remove any manager rights
if (!$myuser['active']) {
hesk_dbQuery("UPDATE `" . hesk_dbEscape($hesk_settings['db_pfix']) . "categories` SET `manager` = 0 WHERE `manager` = " . intval($myuser['id']));
}
unset($_SESSION['save_userdata']);
unset($_SESSION['userdata']);
@ -844,9 +814,6 @@ function remove()
hesk_process_messages($hesklang['cant_del_own'], './manage_users.php');
}
// Revoke manager rights
hesk_dbQuery("UPDATE `" . hesk_dbEscape($hesk_settings['db_pfix']) . "categories` SET `manager` = 0 WHERE `manager` = " . intval($myuser));
/* Un-assign all tickets for this user */
$res = hesk_dbQuery("UPDATE `" . hesk_dbEscape($hesk_settings['db_pfix']) . "tickets` SET `owner`=0 WHERE `owner`='" . intval($myuser) . "'");
@ -910,9 +877,6 @@ function toggle_active()
$active = 0;
$tmp = $hesklang['user_deactivated'];
// Revoke any manager rights
hesk_dbQuery("UPDATE `" . hesk_dbEscape($hesk_settings['db_pfix']) . "categories` SET `manager` = 0 WHERE `manager` = " . intval($myuser));
$notificationSql = ", `autoassign` = '0', `notify_new_unassigned` = '0', `notify_new_my` = '0', `notify_reply_unassigned` = '0',
`notify_reply_my` = '0', `notify_assigned` = '0', `notify_pm` = '0', `notify_note` = '0', `notify_note_unassigned` = '0', `notify_overdue_unassigned` = '0'";
}

@ -113,13 +113,13 @@ require_once(HESK_PATH . 'inc/show_admin_nav.inc.php');
$hesk_settings['categories'] = array();
if (hesk_checkPermission('can_submit_any_cat', 0)) {
$res = hesk_dbQuery("SELECT `id`, `name` FROM `".hesk_dbEscape($hesk_settings['db_pfix'])."categories` ORDER BY `cat_order` ASC");
$res = hesk_dbQuery("SELECT `id`, `name`, `mfh_description` FROM `".hesk_dbEscape($hesk_settings['db_pfix'])."categories` ORDER BY `cat_order` ASC");
} else {
$res = hesk_dbQuery("SELECT `id`, `name` FROM `".hesk_dbEscape($hesk_settings['db_pfix'])."categories` WHERE ".hesk_myCategories('id')." ORDER BY `cat_order` ASC");
$res = hesk_dbQuery("SELECT `id`, `name`, `mfh_description` FROM `".hesk_dbEscape($hesk_settings['db_pfix'])."categories` WHERE ".hesk_myCategories('id')." ORDER BY `cat_order` ASC");
}
while ($row = hesk_dbFetchAssoc($res)) {
$hesk_settings['categories'][$row['id']] = $row['name'];
$hesk_settings['categories'][$row['id']] = $row;
}
$number_of_categories = count($hesk_settings['categories']);
@ -147,7 +147,7 @@ $show_quick_help = $show['show'];
<li><a href="admin_main.php"><?php echo $hesk_settings['hesk_title']; ?></a></li>
<?php if ($number_of_categories > 1): ?>
<li><a href="new_ticket.php"><?php echo $hesklang['nti2']; ?></a></li>
<li class="active"><?php echo $hesk_settings['categories'][$category]; ?></li>
<li class="active"><?php echo $hesk_settings['categories'][$category]['name']; ?></li>
<?php else: ?>
<li class="active"><?php echo $hesklang['nti2']; ?></li>
<?php endif; ?>
@ -1038,9 +1038,10 @@ function print_select_category($number_of_categories) {
// Print a select box if number of categories is large
if ($number_of_categories > $hesk_settings['cat_show_select'])
{
$firstDescription = null;
?>
<form action="new_ticket.php" method="get">
<select name="category" id="select_category" class="form-control">
<select name="category" id="select_category" class="form-control" onchange="showDescription()">
<?php
if ($hesk_settings['select_cat'])
{
@ -1048,23 +1049,45 @@ function print_select_category($number_of_categories) {
}
foreach ($hesk_settings['categories'] as $k=>$v)
{
echo '<option value="'.$k.'">'.$v.'</option>';
if ($firstDescription === null) {
$firstDescription = $v['mfh_description'];
}
echo '<option value="'.$k.'" data-description="'.$v['mfh_description'].'">'.$v['name'].'</option>';
}
?>
</select>
<?php
$display = ' style="display: none"';
&nbsp;<br />
if (!$hesk_settings['select_cat'] && $firstDescription !== null && trim($firstDescription) !== '') {
$display = '';
}
?>
<span id="category-description"<?php echo $display; ?>>
<b><?php echo $hesklang['description_colon']; ?></b>
<span><?php echo $firstDescription; ?></span>
</span>
<br>
<div style="text-align:center">
<input type="submit" value="<?php echo $hesklang['c2c']; ?>" class="btn btn-default">
</div>
</form>
<script>
function showDescription() {
var $value = $('#select_category').find(':selected');
if ($value.data('description') !== '') {
$('#category-description').show().find('span').text($value.data('description'));
} else {
$('#category-description').hide();
}
}
</script>
<?php
}
// Otherwise print quick links
else
{
// echo '<li><a href="new_ticket.php?a=add&amp;category='.$k.'">&raquo; '.$v.'</a></li>';
$new_row = 1;
foreach ($hesk_settings['categories'] as $k=>$v):
@ -1079,7 +1102,14 @@ function print_select_category($number_of_categories) {
<div class="panel-body">
<div class="row">
<div class="col-xs-12">
<?php echo $v; ?>
<?php
echo $v['name'];
if ($v['mfh_description'] !== null && trim($v['mfh_description']) !== '') {
echo '&nbsp;<i class="fa fa-info-circle" data-toggle="popover"
title="'. $hesklang['description'] .'" data-content="' . $v['mfh_description'] . '"></i>';
}
?>
</div>
</div>
</div>

@ -1,142 +0,0 @@
<?php
// Responsible for loading in all necessary classes. AKA a poor man's DI solution.
use BusinessLogic\Attachments\AttachmentHandler;
use BusinessLogic\Attachments\AttachmentRetriever;
use BusinessLogic\Categories\CategoryRetriever;
use BusinessLogic\Emails\BasicEmailSender;
use BusinessLogic\Emails\EmailSenderHelper;
use BusinessLogic\Emails\EmailTemplateParser;
use BusinessLogic\Emails\EmailTemplateRetriever;
use BusinessLogic\Emails\MailgunEmailSender;
use BusinessLogic\Navigation\CustomNavElementHandler;
use BusinessLogic\Security\BanRetriever;
use BusinessLogic\Security\UserContextBuilder;
use BusinessLogic\Security\UserToTicketChecker;
use BusinessLogic\Settings\ApiChecker;
use BusinessLogic\Settings\SettingsRetriever;
use BusinessLogic\Statuses\StatusRetriever;
use BusinessLogic\Tickets\Autoassigner;
use BusinessLogic\Tickets\TicketDeleter;
use BusinessLogic\Tickets\TicketEditor;
use BusinessLogic\Tickets\TicketRetriever;
use BusinessLogic\Tickets\TicketCreator;
use BusinessLogic\Tickets\NewTicketValidator;
use BusinessLogic\Tickets\TicketValidators;
use BusinessLogic\Tickets\TrackingIdGenerator;
use BusinessLogic\Tickets\VerifiedEmailChecker;
use DataAccess\Attachments\AttachmentGateway;
use DataAccess\Categories\CategoryGateway;
use DataAccess\Files\FileDeleter;
use DataAccess\Files\FileReader;
use DataAccess\Files\FileWriter;
use DataAccess\Logging\LoggingGateway;
use DataAccess\Navigation\CustomNavElementGateway;
use DataAccess\Security\BanGateway;
use DataAccess\Security\UserGateway;
use DataAccess\Settings\ModsForHeskSettingsGateway;
use DataAccess\Statuses\StatusGateway;
use DataAccess\Tickets\TicketGateway;
use DataAccess\Tickets\VerifiedEmailGateway;
class ApplicationContext {
public $get;
/**
* ApplicationContext constructor.
*/
function __construct() {
$this->get = array();
// Settings
$this->get[ModsForHeskSettingsGateway::class] = new ModsForHeskSettingsGateway();
// API Checker
$this->get[ApiChecker::class] = new ApiChecker($this->get[ModsForHeskSettingsGateway::class]);
// Custom Navigation
$this->get[CustomNavElementGateway::class] = new CustomNavElementGateway();
$this->get[CustomNavElementHandler::class] = new CustomNavElementHandler($this->get[CustomNavElementGateway::class]);
// Logging
$this->get[LoggingGateway::class] = new LoggingGateway();
// Verified Email Checker
$this->get[VerifiedEmailGateway::class] = new VerifiedEmailGateway();
$this->get[VerifiedEmailChecker::class] = new VerifiedEmailChecker($this->get[VerifiedEmailGateway::class]);
// Users
$this->get[UserGateway::class] = new UserGateway();
$this->get[UserContextBuilder::class] = new UserContextBuilder($this->get[UserGateway::class]);
// Categories
$this->get[CategoryGateway::class] = new CategoryGateway();
$this->get[CategoryRetriever::class] = new CategoryRetriever($this->get[CategoryGateway::class]);
// Bans
$this->get[BanGateway::class] = new BanGateway();
$this->get[BanRetriever::class] = new BanRetriever($this->get[BanGateway::class]);
// Statuses
$this->get[StatusGateway::class] = new StatusGateway();
// Email Sender
$this->get[EmailTemplateRetriever::class] = new EmailTemplateRetriever();
$this->get[EmailTemplateParser::class] = new EmailTemplateParser($this->get[StatusGateway::class],
$this->get[CategoryGateway::class],
$this->get[UserGateway::class],
$this->get[EmailTemplateRetriever::class]);
$this->get[BasicEmailSender::class] = new BasicEmailSender();
$this->get[MailgunEmailSender::class] = new MailgunEmailSender();
$this->get[EmailSenderHelper::class] = new EmailSenderHelper($this->get[EmailTemplateParser::class],
$this->get[BasicEmailSender::class],
$this->get[MailgunEmailSender::class]);
// Tickets
$this->get[UserToTicketChecker::class] = new UserToTicketChecker($this->get[UserGateway::class]);
$this->get[TicketGateway::class] = new TicketGateway();
$this->get[TicketRetriever::class] = new TicketRetriever($this->get[TicketGateway::class],
$this->get[UserToTicketChecker::class]);
$this->get[TicketValidators::class] = new TicketValidators($this->get[TicketGateway::class]);
$this->get[TrackingIdGenerator::class] = new TrackingIdGenerator($this->get[TicketGateway::class]);
$this->get[Autoassigner::class] = new Autoassigner($this->get[CategoryGateway::class], $this->get[UserGateway::class]);
$this->get[NewTicketValidator::class] = new NewTicketValidator($this->get[CategoryRetriever::class],
$this->get[BanRetriever::class],
$this->get[TicketValidators::class]);
$this->get[TicketCreator::class] = new TicketCreator($this->get[NewTicketValidator::class],
$this->get[TrackingIdGenerator::class],
$this->get[Autoassigner::class],
$this->get[StatusGateway::class],
$this->get[TicketGateway::class],
$this->get[VerifiedEmailChecker::class],
$this->get[EmailSenderHelper::class],
$this->get[UserGateway::class],
$this->get[ModsForHeskSettingsGateway::class]);
$this->get[FileWriter::class] = new FileWriter();
$this->get[FileReader::class] = new FileReader();
$this->get[FileDeleter::class] = new FileDeleter();
$this->get[AttachmentGateway::class] = new AttachmentGateway();
$this->get[AttachmentHandler::class] = new AttachmentHandler($this->get[TicketGateway::class],
$this->get[AttachmentGateway::class],
$this->get[FileWriter::class],
$this->get[UserToTicketChecker::class],
$this->get[FileDeleter::class]);
$this->get[AttachmentRetriever::class] = new AttachmentRetriever($this->get[AttachmentGateway::class],
$this->get[FileReader::class],
$this->get[TicketGateway::class],
$this->get[UserToTicketChecker::class]);
$this->get[TicketDeleter::class] =
new TicketDeleter($this->get[TicketGateway::class],
$this->get[UserToTicketChecker::class],
$this->get[AttachmentHandler::class]);
$this->get[TicketEditor::class] =
new TicketEditor($this->get[TicketGateway::class], $this->get[UserToTicketChecker::class]);
// Statuses
$this->get[StatusRetriever::class] = new StatusRetriever($this->get[StatusGateway::class]);
// Settings
$this->get[SettingsRetriever::class] = new SettingsRetriever($this->get[ModsForHeskSettingsGateway::class]);
}
}

@ -33,7 +33,11 @@ class AttachmentHandler {
/* @var $userToTicketChecker UserToTicketChecker */
private $userToTicketChecker;
function __construct($ticketGateway, $attachmentGateway, $fileWriter, $userToTicketChecker, $fileDeleter) {
function __construct(TicketGateway $ticketGateway,
AttachmentGateway $attachmentGateway,
FileWriter $fileWriter,
UserToTicketChecker $userToTicketChecker,
FileDeleter $fileDeleter) {
$this->ticketGateway = $ticketGateway;
$this->attachmentGateway = $attachmentGateway;
$this->fileWriter = $fileWriter;

@ -52,12 +52,17 @@ class Category {
public $priority;
/**
* @var int|null The manager for the Categories, if applicable
* @var bool Indication if the user has access to the Categories
*/
public $manager;
public $accessible;
/**
* @var bool Indication if the user has access to the Categories
* @var string
*/
public $accessible;
public $description;
/**
* @var int
*/
public $numberOfTickets;
}

@ -0,0 +1,190 @@
<?php
namespace BusinessLogic\Categories;
use BusinessLogic\Exceptions\AccessViolationException;
use BusinessLogic\Exceptions\ValidationException;
use BusinessLogic\Navigation\Direction;
use BusinessLogic\Security\PermissionChecker;
use BusinessLogic\Security\UserPrivilege;
use BusinessLogic\ValidationModel;
use DataAccess\Categories\CategoryGateway;
use DataAccess\Settings\ModsForHeskSettingsGateway;
use DataAccess\Tickets\TicketGateway;
class CategoryHandler {
/* @var $categoryGateway CategoryGateway */
private $categoryGateway;
/* @var $ticketGateway TicketGateway */
private $ticketGateway;
/* @var $permissionChecker PermissionChecker */
private $permissionChecker;
/* @var $modsForHeskSettingsGateway ModsForHeskSettingsGateway */
private $modsForHeskSettingsGateway;
function __construct(CategoryGateway $categoryGateway,
TicketGateway $ticketGateway,
PermissionChecker $permissionChecker,
ModsForHeskSettingsGateway $modsForHeskSettingsGateway) {
$this->categoryGateway = $categoryGateway;
$this->ticketGateway = $ticketGateway;
$this->permissionChecker = $permissionChecker;
$this->modsForHeskSettingsGateway = $modsForHeskSettingsGateway;
}
/**
* @param $category Category
* @param $userContext
* @param $heskSettings array
* @return Category The newly created category with ID
* @throws ValidationException When validation fails
* @throws \Exception When the newly created category was not retrieved
*/
//TODO Test
function createCategory($category, $userContext, $heskSettings) {
$modsForHeskSettings = $this->modsForHeskSettingsGateway->getAllSettings($heskSettings);
$validationModel = $this->validate($category, $userContext);
if (count($validationModel->errorKeys) > 0) {
throw new ValidationException($validationModel);
}
$id = $this->categoryGateway->createCategory($category, $heskSettings);
$allCategories = $this->categoryGateway->getAllCategories($heskSettings, $modsForHeskSettings);
foreach ($allCategories as $innerCategory) {
if ($innerCategory->id === $id) {
return $innerCategory;
}
}
throw new \Exception("Newly created category {$id} lost! :O");
}
/**
* @param $category Category
* @param $userContext
* @param $creating bool
* @return ValidationModel
* @throws AccessViolationException
*/
//TODO Test
private function validate($category, $userContext, $creating = true) {
$validationModel = new ValidationModel();
if (!$this->permissionChecker->doesUserHavePermission($userContext, UserPrivilege::CAN_MANAGE_CATEGORIES)) {
throw new AccessViolationException('User cannot manage categories!');
}
if (!$creating && $category->id < 1) {
$validationModel->errorKeys[] = 'ID_MISSING';
}
if ($category->backgroundColor === null || trim($category->backgroundColor) === '') {
$validationModel->errorKeys[] = 'BACKGROUND_COLOR_MISSING';
}
if ($category->foregroundColor === null || trim($category->foregroundColor) === '') {
$validationModel->errorKeys[] = 'FOREGROUND_COLOR_MISSING';
}
if ($category->name === null || trim($category->name) === '') {
$validationModel->errorKeys[] = 'NAME_MISSING';
}
if ($category->priority === null || intval($category->priority) < 0 || intval($category->priority) > 3) {
$validationModel->errorKeys[] = 'INVALID_PRIORITY';
}
if ($category->autoAssign === null || !is_bool($category->autoAssign)) {
$validationModel->errorKeys[] = 'INVALID_AUTOASSIGN';
}
if ($category->displayBorder === null || !is_bool($category->displayBorder)) {
$validationModel->errorKeys[] = 'INVALID_DISPLAY_BORDER';
}
if ($category->type === null || (intval($category->type) !== 0 && intval($category->type) !== 1)) {
$validationModel->errorKeys[] = 'INVALID_TYPE';
}
return $validationModel;
}
/**
* @param $category Category
* @param $userContext
* @param $heskSettings array
* @return Category
* @throws ValidationException
* @throws \Exception When the category is missing
*/
function editCategory($category, $userContext, $heskSettings) {
$modsForHeskSettings = $this->modsForHeskSettingsGateway->getAllSettings($heskSettings);
$validationModel = $this->validate($category, $userContext, false);
if (count($validationModel->errorKeys) > 0) {
throw new ValidationException($validationModel);
}
$this->categoryGateway->updateCategory($category, $heskSettings);
$this->categoryGateway->resortAllCategories($heskSettings);
$allCategories = $this->categoryGateway->getAllCategories($heskSettings, $modsForHeskSettings);
foreach ($allCategories as $innerCategory) {
if ($innerCategory->id === $category->id) {
return $innerCategory;
}
}
throw new \Exception("Category {$category->id} vanished! :O");
}
function deleteCategory($id, $userContext, $heskSettings) {
if (!$this->permissionChecker->doesUserHavePermission($userContext, UserPrivilege::CAN_MANAGE_CATEGORIES)) {
throw new AccessViolationException('User cannot manage categories!');
}
if ($id === 1) {
throw new \Exception("Category 1 cannot be deleted!");
}
$this->ticketGateway->moveTicketsToDefaultCategory($id, $heskSettings);
$this->categoryGateway->deleteCategory($id, $heskSettings);
$this->categoryGateway->resortAllCategories($heskSettings);
}
function sortCategory($id, $direction, $heskSettings) {
$modsForHeskSettings = $this->modsForHeskSettingsGateway->getAllSettings($heskSettings);
$categories = $this->categoryGateway->getAllCategories($heskSettings, $modsForHeskSettings);
$category = null;
foreach ($categories as $innerCategory) {
if ($innerCategory->id === intval($id)) {
$category = $innerCategory;
break;
}
}
if ($category === null) {
throw new \Exception("Could not find category with ID {$id}!");
}
if ($direction === Direction::UP) {
$category->catOrder -= 15;
} else {
$category->catOrder += 15;
}
$this->categoryGateway->updateCategory($category, $heskSettings);
$this->categoryGateway->resortAllCategories($heskSettings);
}
}

@ -4,6 +4,7 @@ namespace BusinessLogic\Categories;
use BusinessLogic\Security\UserContext;
use DataAccess\Categories\CategoryGateway;
use DataAccess\Settings\ModsForHeskSettingsGateway;
class CategoryRetriever {
/**
@ -11,8 +12,15 @@ class CategoryRetriever {
*/
private $categoryGateway;
function __construct($categoryGateway) {
/**
* @var ModsForHeskSettingsGateway
*/
private $modsForHeskSettingsGateway;
function __construct(CategoryGateway $categoryGateway,
ModsForHeskSettingsGateway $modsForHeskSettingsGateway) {
$this->categoryGateway = $categoryGateway;
$this->modsForHeskSettingsGateway = $modsForHeskSettingsGateway;
}
/**
@ -21,7 +29,9 @@ class CategoryRetriever {
* @return array
*/
function getAllCategories($heskSettings, $userContext) {
$categories = $this->categoryGateway->getAllCategories($heskSettings);
$modsForHeskSettings = $this->modsForHeskSettingsGateway->getAllSettings($heskSettings);
$categories = $this->categoryGateway->getAllCategories($heskSettings, $modsForHeskSettings);
foreach ($categories as $category) {
$category->accessible = $userContext->admin ||

@ -21,7 +21,9 @@ class EmailSenderHelper {
*/
private $mailgunEmailSender;
function __construct($emailTemplateParser, $basicEmailSender, $mailgunEmailSender) {
function __construct(EmailTemplateParser $emailTemplateParser,
BasicEmailSender $basicEmailSender,
MailgunEmailSender $mailgunEmailSender) {
$this->emailTemplateParser = $emailTemplateParser;
$this->basicEmailSender = $basicEmailSender;
$this->mailgunEmailSender = $mailgunEmailSender;

@ -35,7 +35,10 @@ class EmailTemplateParser {
*/
private $emailTemplateRetriever;
function __construct($statusGateway, $categoryGateway, $userGateway, $emailTemplateRetriever) {
function __construct(StatusGateway $statusGateway,
CategoryGateway $categoryGateway,
UserGateway $userGateway,
EmailTemplateRetriever $emailTemplateRetriever) {
$this->statusGateway = $statusGateway;
$this->categoryGateway = $categoryGateway;
$this->userGateway = $userGateway;
@ -50,6 +53,7 @@ class EmailTemplateParser {
* @param $modsForHeskSettings array
* @return ParsedEmailProperties
* @throws InvalidEmailTemplateException
* @throws \Exception
*/
function getFormattedEmailForLanguage($templateId, $languageCode, $ticket, $heskSettings, $modsForHeskSettings) {
global $hesklang;

@ -1,10 +1,4 @@
<?php
/**
* Created by PhpStorm.
* User: mkoch
* Date: 2/28/2017
* Time: 9:36 PM
*/
namespace BusinessLogic\Emails;

@ -10,7 +10,7 @@ class CustomNavElementHandler {
/* @var $customNavElementGateway CustomNavElementGateway */
private $customNavElementGateway;
function __construct($customNavElementGateway) {
function __construct(CustomNavElementGateway $customNavElementGateway) {
$this->customNavElementGateway = $customNavElementGateway;
}

@ -11,7 +11,7 @@ class BanRetriever {
*/
private $banGateway;
function __construct($banGateway) {
function __construct(BanGateway $banGateway) {
$this->banGateway = $banGateway;
}

@ -0,0 +1,23 @@
<?php
namespace BusinessLogic\Security;
class PermissionChecker {
/**
* @param $userContext UserContext
* @param $permission string
* @return bool
*/
function doesUserHavePermission($userContext, $permission) {
if ($userContext->admin) {
return true;
}
if (in_array($permission, $userContext->permissions)) {
return true;
}
return false;
}
}

@ -14,7 +14,7 @@ class UserContextBuilder {
*/
private $userGateway;
function __construct($userGateway) {
function __construct(UserGateway $userGateway) {
$this->userGateway = $userGateway;
}

@ -14,4 +14,5 @@ class UserPrivilege {
const CAN_REPLY_TO_TICKETS = 'can_reply_tickets';
const CAN_EDIT_TICKETS = 'can_edit_tickets';
const CAN_DELETE_TICKETS = 'can_del_tickets';
const CAN_MANAGE_CATEGORIES = 'can_man_cat';
}

@ -10,7 +10,7 @@ class UserToTicketChecker {
/* @var $userGateway UserGateway */
private $userGateway;
function __construct($userGateway) {
function __construct(UserGateway $userGateway) {
$this->userGateway = $userGateway;
}
@ -31,12 +31,6 @@ class UserToTicketChecker {
return false;
}
$categoryManagerId = $this->userGateway->getManagerForCategory($ticket->categoryId, $heskSettings);
if ($user->id === $categoryManagerId) {
return true;
}
$extraPermissions[] = UserPrivilege::CAN_VIEW_TICKETS;
foreach ($extraPermissions as $permission) {

@ -9,7 +9,7 @@ class ApiChecker {
/* @var $modsForHeskSettingsGateway ModsForHeskSettingsGateway */
private $modsForHeskSettingsGateway;
function __construct($modsForHeskSettingsGateway) {
function __construct(ModsForHeskSettingsGateway $modsForHeskSettingsGateway) {
$this->modsForHeskSettingsGateway = $modsForHeskSettingsGateway;
}

@ -9,7 +9,7 @@ class SettingsRetriever {
/* @var $modsForHeskSettingsGateway ModsForHeskSettingsGateway */
private $modsForHeskSettingsGateway;
function __construct($modsForHeskSettingsGateway) {
function __construct(ModsForHeskSettingsGateway $modsForHeskSettingsGateway) {
$this->modsForHeskSettingsGateway = $modsForHeskSettingsGateway;
}

@ -10,7 +10,7 @@ class StatusRetriever {
/* @var $statusGateway StatusGateway */
private $statusGateway;
function __construct($statusGateway) {
function __construct(StatusGateway $statusGateway) {
$this->statusGateway = $statusGateway;
}

@ -15,7 +15,8 @@ class Autoassigner {
/* @var $userGateway UserGateway */
private $userGateway;
function __construct($categoryGateway, $userGateway) {
function __construct(CategoryGateway $categoryGateway,
UserGateway $userGateway) {
$this->categoryGateway = $categoryGateway;
$this->userGateway = $userGateway;
}

@ -24,7 +24,9 @@ class NewTicketValidator {
*/
private $ticketValidators;
function __construct($categoryRetriever, $banRetriever, $ticketValidators) {
function __construct(CategoryRetriever $categoryRetriever,
BanRetriever $banRetriever,
TicketValidators $ticketValidators) {
$this->categoryRetriever = $categoryRetriever;
$this->banRetriever = $banRetriever;
$this->ticketValidators = $ticketValidators;

@ -56,8 +56,15 @@ class TicketCreator {
/* @var $modsForHeskSettingsGateway ModsForHeskSettingsGateway */
private $modsForHeskSettingsGateway;
function __construct($newTicketValidator, $trackingIdGenerator, $autoassigner, $statusGateway, $ticketGateway,
$verifiedEmailChecker, $emailSenderHelper, $userGateway, $modsForHeskSettingsGateway) {
function __construct(NewTicketValidator $newTicketValidator,
TrackingIdGenerator $trackingIdGenerator,
Autoassigner $autoassigner,
StatusGateway $statusGateway,
TicketGateway $ticketGateway,
VerifiedEmailChecker $verifiedEmailChecker,
EmailSenderHelper $emailSenderHelper,
UserGateway $userGateway,
ModsForHeskSettingsGateway $modsForHeskSettingsGateway) {
$this->newTicketValidator = $newTicketValidator;
$this->trackingIdGenerator = $trackingIdGenerator;
$this->autoassigner = $autoassigner;

@ -20,7 +20,9 @@ class TicketDeleter {
/* @var $attachmentHandler AttachmentHandler */
private $attachmentHandler;
function __construct($ticketGateway, $userToTicketChecker, $attachmentHandler) {
function __construct(TicketGateway $ticketGateway,
UserToTicketChecker $userToTicketChecker,
AttachmentHandler $attachmentHandler) {
$this->ticketGateway = $ticketGateway;
$this->userToTicketChecker = $userToTicketChecker;
$this->attachmentHandler = $attachmentHandler;

@ -22,7 +22,8 @@ class TicketEditor {
/* @var $userToTicketChecker UserToTicketChecker */
private $userToTicketChecker;
function __construct($ticketGateway, $userToTicketChecker) {
function __construct(TicketGateway $ticketGateway,
UserToTicketChecker $userToTicketChecker) {
$this->ticketGateway = $ticketGateway;
$this->userToTicketChecker = $userToTicketChecker;
}

@ -19,7 +19,8 @@ class TicketRetriever {
/* @var $userToTicketChecker UserToTicketChecker */
private $userToTicketChecker;
function __construct($ticketGateway, $userToTicketChecker) {
function __construct(TicketGateway $ticketGateway,
UserToTicketChecker $userToTicketChecker) {
$this->ticketGateway = $ticketGateway;
$this->userToTicketChecker = $userToTicketChecker;
}

@ -10,7 +10,7 @@ class TicketValidators {
*/
private $ticketGateway;
function __construct($ticketGateway) {
function __construct(TicketGateway $ticketGateway) {
$this->ticketGateway = $ticketGateway;
}

@ -12,7 +12,7 @@ class TrackingIdGenerator {
*/
private $ticketGateway;
function __construct($ticketGateway) {
function __construct(TicketGateway $ticketGateway) {
$this->ticketGateway = $ticketGateway;
}

@ -17,7 +17,7 @@ class VerifiedEmailChecker {
*/
private $verifiedEmailGateway;
function __construct($verifiedEmailGateway) {
function __construct(VerifiedEmailGateway $verifiedEmailGateway) {
$this->verifiedEmailGateway = $verifiedEmailGateway;
}

@ -14,7 +14,7 @@ class PublicAttachmentController {
self::verifyAttachmentsAreEnabled($hesk_settings);
/* @var $attachmentRetriever AttachmentRetriever */
$attachmentRetriever = $applicationContext->get[AttachmentRetriever::class];
$attachmentRetriever = $applicationContext->get(AttachmentRetriever::class);
$attachment = $attachmentRetriever->getAttachmentContentsForTrackingId($trackingId, $attachmentId, $userContext, $hesk_settings);

@ -18,7 +18,7 @@ class StaffTicketAttachmentsController {
$this->verifyAttachmentsAreEnabled($hesk_settings);
/* @var $attachmentRetriever AttachmentRetriever */
$attachmentRetriever = $applicationContext->get[AttachmentRetriever::class];
$attachmentRetriever = $applicationContext->get(AttachmentRetriever::class);
$contents = $attachmentRetriever->getAttachmentContentsForTicket($ticketId, $attachmentId, $userContext, $hesk_settings);
@ -37,7 +37,7 @@ class StaffTicketAttachmentsController {
$this->verifyAttachmentsAreEnabled($hesk_settings);
/* @var $attachmentHandler AttachmentHandler */
$attachmentHandler = $applicationContext->get[AttachmentHandler::class];
$attachmentHandler = $applicationContext->get(AttachmentHandler::class);
$createAttachmentForTicketModel = $this->createModel(JsonRetriever::getJsonData(), $ticketId);
@ -61,7 +61,7 @@ class StaffTicketAttachmentsController {
global $applicationContext, $hesk_settings, $userContext;
/* @var $attachmentHandler AttachmentHandler */
$attachmentHandler = $applicationContext->get[AttachmentHandler::class];
$attachmentHandler = $applicationContext->get(AttachmentHandler::class);
$attachmentHandler->deleteAttachmentFromTicket($ticketId, $attachmentId, $userContext, $hesk_settings);

@ -2,18 +2,24 @@
namespace Controllers\Categories;
use BusinessLogic\Categories\Category;
use BusinessLogic\Categories\CategoryHandler;
use BusinessLogic\Categories\CategoryRetriever;
use BusinessLogic\Exceptions\ApiFriendlyException;
use BusinessLogic\Helpers;
use Controllers\JsonRetriever;
class CategoryController {
function get($id) {
$categories = self::getAllCategories();
if (!isset($categories[$id])) {
throw new ApiFriendlyException("Category {$id} not found!", "Category Not Found", 404);
foreach ($categories as $category) {
if ($category->id === $id) {
return output($category);
}
}
output($categories[$id]);
throw new ApiFriendlyException("Category {$id} not found!", "Category Not Found", 404);
}
static function printAllCategories() {
@ -24,8 +30,80 @@ class CategoryController {
global $hesk_settings, $applicationContext, $userContext;
/* @var $categoryRetriever CategoryRetriever */
$categoryRetriever = $applicationContext->get[CategoryRetriever::class];
$categoryRetriever = $applicationContext->get(CategoryRetriever::class);
return $categoryRetriever->getAllCategories($hesk_settings, $userContext);
}
function post() {
global $hesk_settings, $userContext, $applicationContext;
$data = JsonRetriever::getJsonData();
$category = $this->buildCategoryFromJson($data);
/* @var $categoryHandler CategoryHandler */
$categoryHandler = $applicationContext->get(CategoryHandler::class);
$category = $categoryHandler->createCategory($category, $userContext, $hesk_settings);
return output($category, 201);
}
/**
* @param $json
* @return Category
*/
private function buildCategoryFromJson($json) {
$category = new Category();
$category->autoAssign = Helpers::safeArrayGet($json, 'autoassign');
$category->backgroundColor = Helpers::safeArrayGet($json, 'backgroundColor');
$category->catOrder = Helpers::safeArrayGet($json, 'catOrder');
$category->description = Helpers::safeArrayGet($json, 'description');
$category->displayBorder = Helpers::safeArrayGet($json, 'displayBorder');
$category->foregroundColor = Helpers::safeArrayGet($json, 'foregroundColor');
$category->name = Helpers::safeArrayGet($json, 'name');
$category->priority = Helpers::safeArrayGet($json, 'priority');
$category->type = Helpers::safeArrayGet($json, 'type');
$category->usage = Helpers::safeArrayGet($json, 'usage');
return $category;
}
function put($id) {
global $hesk_settings, $userContext, $applicationContext;
$data = JsonRetriever::getJsonData();
$category = $this->buildCategoryFromJson($data);
$category->id = intval($id);
/* @var $categoryHandler CategoryHandler */
$categoryHandler = $applicationContext->get(CategoryHandler::class);
$category = $categoryHandler->editCategory($category, $userContext, $hesk_settings);
return output($category);
}
function delete($id) {
global $hesk_settings, $userContext, $applicationContext;
/* @var $categoryHandler CategoryHandler */
$categoryHandler = $applicationContext->get(CategoryHandler::class);
$categoryHandler->deleteCategory($id, $userContext, $hesk_settings);
return http_response_code(204);
}
static function sort($id, $direction) {
global $applicationContext, $hesk_settings;
/* @var $handler CategoryHandler */
$handler = $applicationContext->get(CategoryHandler::class);
$handler->sortCategory(intval($id), $direction, $hesk_settings);
}
}

@ -3,7 +3,6 @@
namespace Controllers\Navigation;
use BusinessLogic\Exceptions\ApiFriendlyException;
use BusinessLogic\Helpers;
use BusinessLogic\Navigation\CustomNavElement;
use BusinessLogic\Navigation\CustomNavElementHandler;
@ -17,7 +16,7 @@ class CustomNavElementController extends InternalApiController {
self::staticCheckForInternalUseOnly();
/* @var $handler CustomNavElementHandler */
$handler = $applicationContext->get[CustomNavElementHandler::class];
$handler = $applicationContext->get(CustomNavElementHandler::class);
output($handler->getAllCustomNavElements($hesk_settings));
}
@ -28,7 +27,7 @@ class CustomNavElementController extends InternalApiController {
self::staticCheckForInternalUseOnly();
/* @var $handler CustomNavElementHandler */
$handler = $applicationContext->get[CustomNavElementHandler::class];
$handler = $applicationContext->get(CustomNavElementHandler::class);
$handler->sortCustomNavElement(intval($id), $direction, $hesk_settings);
}
@ -39,7 +38,7 @@ class CustomNavElementController extends InternalApiController {
$this->checkForInternalUseOnly();
/* @var $handler CustomNavElementHandler */
$handler = $applicationContext->get[CustomNavElementHandler::class];
$handler = $applicationContext->get(CustomNavElementHandler::class);
output($handler->getCustomNavElement($id, $hesk_settings));
}
@ -50,7 +49,7 @@ class CustomNavElementController extends InternalApiController {
$this->checkForInternalUseOnly();
/* @var $handler CustomNavElementHandler */
$handler = $applicationContext->get[CustomNavElementHandler::class];
$handler = $applicationContext->get(CustomNavElementHandler::class);
$data = JsonRetriever::getJsonData();
$element = $handler->createCustomNavElement($this->buildElementModel($data), $hesk_settings);
@ -64,7 +63,7 @@ class CustomNavElementController extends InternalApiController {
$this->checkForInternalUseOnly();
/* @var $handler CustomNavElementHandler */
$handler = $applicationContext->get[CustomNavElementHandler::class];
$handler = $applicationContext->get(CustomNavElementHandler::class);
$data = JsonRetriever::getJsonData();
$handler->saveCustomNavElement($this->buildElementModel($data, $id), $hesk_settings);
@ -78,7 +77,7 @@ class CustomNavElementController extends InternalApiController {
$this->checkForInternalUseOnly();
/* @var $handler CustomNavElementHandler */
$handler = $applicationContext->get[CustomNavElementHandler::class];
$handler = $applicationContext->get(CustomNavElementHandler::class);
$handler->deleteCustomNavElement($id, $hesk_settings);

@ -7,11 +7,11 @@ use BusinessLogic\Settings\SettingsRetriever;
class SettingsController {
function get() {
global $applicationContext, $hesk_settings, $modsForHesk_settings;
global $applicationContext, $hesk_settings;
/* @var $settingsRetriever SettingsRetriever */
$settingsRetriever = $applicationContext->get[SettingsRetriever::class];
$settingsRetriever = $applicationContext->get(SettingsRetriever::class);
output($settingsRetriever->getAllSettings($hesk_settings, $modsForHesk_settings));
output($settingsRetriever->getAllSettings($hesk_settings));
}
}

@ -10,7 +10,7 @@ class StatusController {
global $applicationContext, $hesk_settings;
/* @var $statusRetriever StatusRetriever */
$statusRetriever = $applicationContext->get[StatusRetriever::class];
$statusRetriever = $applicationContext->get(StatusRetriever::class);
output($statusRetriever->getAllStatuses($hesk_settings));
}

@ -18,7 +18,7 @@ class CustomerTicketController {
$emailAddress = isset($_GET['email']) ? $_GET['email'] : null;
/* @var $ticketRetriever TicketRetriever */
$ticketRetriever = $applicationContext->get[TicketRetriever::class];
$ticketRetriever = $applicationContext->get(TicketRetriever::class);
output($ticketRetriever->getTicketByTrackingIdAndEmail($trackingId, $emailAddress, $hesk_settings));
}
@ -27,7 +27,7 @@ class CustomerTicketController {
global $applicationContext, $hesk_settings, $userContext;
/* @var $ticketCreator TicketCreator */
$ticketCreator = $applicationContext->get[TicketCreator::class];
$ticketCreator = $applicationContext->get(TicketCreator::class);
$jsonRequest = JsonRetriever::getJsonData();

@ -19,15 +19,15 @@ class ResendTicketEmailToCustomerController extends InternalApiController {
$this->checkForInternalUseOnly();
/* @var $ticketRetriever TicketRetriever */
$ticketRetriever = $applicationContext->get[TicketRetriever::class];
$ticketRetriever = $applicationContext->get(TicketRetriever::class);
$ticket = $ticketRetriever->getTicketById($ticketId, $hesk_settings, $userContext);
/* @var $modsForHeskSettingsGateway ModsForHeskSettingsGateway */
$modsForHeskSettingsGateway = $applicationContext->get[ModsForHeskSettingsGateway::class];
$modsForHeskSettingsGateway = $applicationContext->get(ModsForHeskSettingsGateway::class);
$modsForHeskSettings = $modsForHeskSettingsGateway->getAllSettings($hesk_settings);
/* @var $emailSender EmailSenderHelper */
$emailSender = $applicationContext->get[EmailSenderHelper::class];
$emailSender = $applicationContext->get(EmailSenderHelper::class);
$language = $ticket->language;

@ -15,7 +15,7 @@ class StaffTicketController {
global $applicationContext, $userContext, $hesk_settings;
/* @var $ticketRetriever TicketRetriever */
$ticketRetriever = $applicationContext->get[TicketRetriever::class];
$ticketRetriever = $applicationContext->get(TicketRetriever::class);
output($ticketRetriever->getTicketById($id, $hesk_settings, $userContext));
}
@ -24,7 +24,7 @@ class StaffTicketController {
global $applicationContext, $userContext, $hesk_settings;
/* @var $ticketDeleter TicketDeleter */
$ticketDeleter = $applicationContext->get[TicketDeleter::class];
$ticketDeleter = $applicationContext->get(TicketDeleter::class);
$ticketDeleter->deleteTicket($id, $userContext, $hesk_settings);
@ -35,7 +35,7 @@ class StaffTicketController {
global $applicationContext, $userContext, $hesk_settings;
/* @var $ticketEditor TicketEditor */
$ticketEditor = $applicationContext->get[TicketEditor::class];
$ticketEditor = $applicationContext->get(TicketEditor::class);
$jsonRequest = JsonRetriever::getJsonData();

@ -4,6 +4,7 @@ namespace DataAccess\Categories;
use BusinessLogic\Categories\Category;
use DataAccess\CommonDao;
use DataAccess\Logging\LoggingGateway;
use Exception;
class CategoryGateway extends CommonDao {
@ -11,10 +12,18 @@ class CategoryGateway extends CommonDao {
* @param $hesk_settings
* @return Category[]
*/
function getAllCategories($hesk_settings) {
function getAllCategories($hesk_settings, $modsForHesk_settings) {
$this->init();
$sql = 'SELECT * FROM `' . hesk_dbEscape($hesk_settings['db_pfix']) . 'categories`';
$sortColumn = $modsForHesk_settings['category_order_column'];
$sql = 'SELECT `cat`.*, COUNT(`tickets`.`id`) AS `number_of_tickets`
FROM `' . hesk_dbEscape($hesk_settings['db_pfix']) . 'categories` `cat`
LEFT JOIN `' . hesk_dbEscape($hesk_settings['db_pfix']) . 'tickets` `tickets`
ON `cat`.`id` = `tickets`.`category`
GROUP BY `cat`.`id`
ORDER BY `cat`.`' . $sortColumn . '` ASC';
$response = hesk_dbQuery($sql);
@ -32,12 +41,94 @@ class CategoryGateway extends CommonDao {
$category->foregroundColor = $row['foreground_color'];
$category->displayBorder = $row['display_border_outline'] === '1';
$category->priority = intval($row['priority']);
$category->manager = intval($row['manager']) == 0 ? NULL : intval($row['manager']);
$results[$category->id] = $category;
$category->description = $row['mfh_description'];
$category->numberOfTickets = intval($row['number_of_tickets']);
$results[] = $category;
}
$this->close();
return $results;
}
/**
* @param $category Category
* @param $heskSettings array
* @return int The ID of the newly created category
*/
function createCategory($category, $heskSettings) {
$this->init();
$newOrderRs = hesk_dbQuery("SELECT `cat_order` FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "categories` ORDER BY `cat_order` DESC LIMIT 1");
$newOrder = hesk_dbFetchAssoc($newOrderRs);
$sql = "INSERT INTO `" . hesk_dbEscape($heskSettings['db_pfix']) . "categories`
(`name`, `cat_order`, `autoassign`, `type`, `priority`, `background_color`, `usage`,
`foreground_color`, `display_border_outline`, `mfh_description`)
VALUES ('" . hesk_dbEscape($category->name) . "', " . intval($newOrder['cat_order']) . ",
'" . ($category->autoAssign ? 1 : 0) . "', '" . intval($category->type) . "',
'" . intval($category->priority) . "',
'" . hesk_dbEscape($category->backgroundColor) . "', " . intval($category->usage) . ",
'" . hesk_dbEscape($category->foregroundColor) . "', '" . ($category->displayBorder ? 1 : 0) . "',
'" . hesk_dbEscape($category->description) . "')";
hesk_dbQuery($sql);
$id = hesk_dbInsertID();
$this->close();
return $id;
}
/**
* @param $category Category
* @param $heskSettings array
*/
function updateCategory($category, $heskSettings) {
$this->init();
$sql = "UPDATE `" . hesk_dbEscape($heskSettings['db_pfix']) . "categories` SET
`name` = '" . hesk_dbEscape($category->name) . "',
`cat_order` = " . intval($category->catOrder) . ",
`autoassign` = '" . ($category->autoAssign ? 1 : 0) . "',
`type` = '" . intval($category->type) . "',
`priority` = '" . intval($category->priority) . "',
`background_color` = '" . hesk_dbEscape($category->backgroundColor) . "',
`usage` = " . intval($category->usage) . ",
`foreground_color` = '" . hesk_dbEscape($category->foregroundColor) . "',
`display_border_outline` = '" . ($category->displayBorder ? 1 : 0) . "',
`mfh_description` = '" . hesk_dbEscape($category->description) . "'
WHERE `id` = " . intval($category->id);
hesk_dbQuery($sql);
$this->close();
}
function resortAllCategories($heskSettings) {
$this->init();
$rs = hesk_dbQuery("SELECT `id` FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "categories`
ORDER BY `cat_order` ASC");
$sortValue = 10;
while ($row = hesk_dbFetchAssoc($rs)) {
hesk_dbQuery("UPDATE `" . hesk_dbEscape($heskSettings['db_pfix']) . "categories`
SET `cat_order` = " . intval($sortValue) . "
WHERE `id` = " . intval($row['id']));
$sortValue += 10;
}
$this->close();
}
function deleteCategory($id, $heskSettings) {
$this->init();
hesk_dbQuery("DELETE FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "categories` WHERE `id` = " . intval($id));
$this->close();
}
}

@ -99,25 +99,4 @@ class UserGateway extends CommonDao {
return $users;
}
function getManagerForCategory($categoryId, $heskSettings) {
$this->init();
$rs = hesk_dbQuery("SELECT * FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "users`
WHERE `id` = (
SELECT `manager`
FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "categories`
WHERE `id` = " . intval($categoryId) . ")");
if (hesk_dbNumRows($rs) === 0) {
$this->close();
return null;
}
$user = UserContext::fromDataRow(hesk_dbFetchAssoc($rs));
$this->close();
return $user;
}
}

@ -358,4 +358,14 @@ class TicketGateway extends CommonDao {
$this->close();
}
function moveTicketsToDefaultCategory($oldCategoryId, $heskSettings) {
$this->init();
hesk_dbQuery("UPDATE `" . hesk_dbEscape($heskSettings['db_pfix']) . "tickets`
SET `category` = 1
WHERE `category` = " . intval($oldCategoryId));
$this->close();
}
}

@ -1,5 +1,7 @@
<?php
require_once 'RequestMethod.php';
/**
* @class Main class of the Link router that helps you create and deploy routes
*/
@ -40,9 +42,11 @@ class Link
self::$routes = $routes;
$method = self::getRequestMethod();
$acceptedMethods = RequestMethod::ALL;
$path = '/';
$handler = null;
$matched = array();
$middleware = null;
if ( !empty ( $_SERVER['PATH_INFO'] ) ) {
$path = $_SERVER['PATH_INFO'];
} else if ( !empty ( $_SERVER['REQUEST_URI'] ) ) {
@ -52,6 +56,8 @@ class Link
if ( isset($routes[$path] ) ) {
if( is_array( $routes[$path] ) ) {
$handler = $routes[$path][0];
$middleware = $routes[$path][2];
$acceptedMethods = $routes[$path][3];
} else {
$handler = $routes[$path];
}
@ -76,6 +82,7 @@ class Link
$handler = $routeDesc[0];
if( isset( $routeDesc[2] )) {
$middleware = $routeDesc[2];
$acceptedMethods = $routeDesc[3];
}
}
else
@ -88,15 +95,15 @@ class Link
unset( $matched[0] );
if( isset($middleware) ){
$newMatched = self::callFunction( $middleware, $matched, $method );
$newMatched = self::callFunction( $middleware, $matched, $method, $acceptedMethods );
/* If new wildcard param are there pass them to main handler */
if( $newMatched ) {
self::callFunction( $handler, $newMatched, $method );
self::callFunction( $handler, $newMatched, $method, $acceptedMethods );
} else {
self::callFunction( $handler, $matched, $method );
self::callFunction( $handler, $matched, $method, $acceptedMethods );
}
} else {
self::callFunction( $handler, $matched, $method );
self::callFunction( $handler, $matched, $method, $acceptedMethods );
}
/* Call all the function that are to be executed after routing */
@ -160,17 +167,25 @@ class Link
}
}
/**
* Static function to handle both middlewares' call and main handler's call.
*
* @param array|string $handler Handler that will handle the routes call or middleware
* @param array $matched The parameters that we get from the route wildcard
* @return array $newParams The parameters return in the case of middleware if you intend to
* the wildcards that were originally passed, this newParams will
* be next passed to main handler
*/
public static function callFunction( $handler , $matched, $method )
/**
* Static function to handle both middlewares' call and main handler's call.
*
* @param array|string $handler Handler that will handle the routes call or middleware
* @param array $matched The parameters that we get from the route wildcard
* @param $method string request method
* @param $acceptedMethods array Accepted request methods for the request
* @return array $newParams The parameters return in the case of middleware if you intend to
* the wildcards that were originally passed, this newParams will
* be next passed to main handler
*/
public static function callFunction( $handler , $matched, $method, $acceptedMethods )
{
if (!in_array($method, $acceptedMethods)) {
print_r('Request method ' . $method . ' not allowed');
header($_SERVER['SERVER_PROTOCOL'] . ' 405 Method Not Allowed', true, 405);
die();
}
if ( $handler ) {
if ( is_callable( $handler ) ) {
$newParams = call_user_func_array( $handler, $matched ) ;

@ -0,0 +1,10 @@
<?php
class RequestMethod {
const GET = 'GET';
const POST = 'POST';
const PUT = 'PUT';
const DELETE = 'DELETE';
const PATCH = 'PATCH';
const ALL = [self::GET, self::POST, self::PUT, self::DELETE, self::PATCH];
}

@ -0,0 +1,74 @@
<?php
namespace BusinessLogic\Categories;
use BusinessLogic\Security\PermissionChecker;
use BusinessLogic\Security\UserContext;
use Core\Constants\Priority;
use DataAccess\Categories\CategoryGateway;
use DataAccess\Settings\ModsForHeskSettingsGateway;
use DataAccess\Tickets\TicketGateway;
use PHPUnit\Framework\TestCase;
class CategoryHandlerTest extends TestCase {
/* @var $categoryGateway CategoryGateway|\PHPUnit_Framework_MockObject_MockObject */
private $categoryGateway;
/* @var $categoryHandler CategoryHandler */
private $categoryHandler;
/* @var $ticketGateway TicketGateway|\PHPUnit_Framework_MockObject_MockObject */
private $ticketGateway;
/* @var $permissionChecker PermissionChecker|\PHPUnit_Framework_MockObject_MockObject */
private $permissionChecker;
/* @var $modsForHeskSettingsGateway ModsForHeskSettingsGateway|\PHPUnit_Framework_MockObject_MockObject */
private $modsForHeskSettingsGateway;
/* @var $heskSettings array */
private $heskSettings;
protected function setUp() {
$this->categoryGateway = $this->createMock(CategoryGateway::class);
$this->ticketGateway = $this->createMock(TicketGateway::class);
$this->permissionChecker = $this->createMock(PermissionChecker::class);
$this->modsForHeskSettingsGateway = $this->createMock(ModsForHeskSettingsGateway::class);
$this->categoryHandler = new CategoryHandler($this->categoryGateway,
$this->ticketGateway,
$this->permissionChecker,
$this->modsForHeskSettingsGateway);
$this->heskSettings = array();
//TODO write proper tests!
$this->permissionChecker->method('doesUserHavePermission')->willReturn(true);
}
function testCreateCallsTheGatewayWithTheCategory() {
//-- Arrange
$category = new Category();
$category->autoAssign = true;
$category->backgroundColor = 'a';
$category->foregroundColor = 'a';
$category->catOrder = 1000;
$category->description = 'd';
$category->displayBorder = false;
$category->name = 'n';
$category->priority = Priority::LOW;
$category->usage = 0;
$category->type = 0;
$category->id = 1;
$this->categoryGateway->method('getAllCategories')->willReturn([$category]);
//-- Assert
$this->categoryGateway->expects($this->once())->method('createCategory')
->willReturn(1)
->with($category, $this->heskSettings);
//-- Act
$this->categoryHandler->createCategory($category, new UserContext(), $this->heskSettings);
}
}

@ -92,23 +92,4 @@ class UserToTicketCheckerTest extends TestCase {
//-- Assert
self::assertThat($result, self::isFalse());
}
function testItReturnsTrueWhenTheUserDoesNotHaveEditPermissionsButIsTheCategoryManager() {
//-- Arrange
$user = new UserContext();
$user->admin = false;
$user->categories = array(1);
$user->permissions = array(UserPrivilege::CAN_VIEW_TICKETS, 'something else');
$user->id = 1;
$this->userGateway->method('getManagerForCategory')->willReturn(1);
$ticket = new Ticket();
$ticket->categoryId = 1;
//-- Act
$result = $this->userToTicketChecker->isTicketAccessibleToUser($user, $ticket, $this->heskSettings, array(UserPrivilege::CAN_EDIT_TICKETS));
//-- Assert
self::assertThat($result, self::isTrue());
}
}

@ -21,4 +21,7 @@ global $hesk_settings;
require_once(__DIR__ . '/../inc/custom_fields.inc.php');
// Load the ApplicationContext
$applicationContext = new \ApplicationContext();
$builder = new \DI\ContainerBuilder();
$builder->setDefinitionCache(new \Doctrine\Common\Cache\ArrayCache());
$applicationContext = $builder->build();

@ -10,7 +10,7 @@
}
],
"require-dev": {
"phpunit/phpunit": "5.7.9"
"phpunit/phpunit": "6.3.0"
},
"require": {
"phpmailer/phpmailer": "^5.2",
@ -18,6 +18,7 @@
"php-http/guzzle6-adapter": "^1.1",
"php-http/message": "^1.5",
"php-http/curl-client": "^1.7",
"guzzlehttp/psr7": "^1.3"
"guzzlehttp/psr7": "^1.3",
"mike-koch/php-di": "4.4.11"
}
}

2343
api/composer.lock generated

File diff suppressed because it is too large Load Diff

@ -50,7 +50,7 @@ function assertApiIsEnabled() {
global $applicationContext, $hesk_settings;
/* @var $apiChecker \BusinessLogic\Settings\ApiChecker */
$apiChecker = $applicationContext->get[\BusinessLogic\Settings\ApiChecker::class];
$apiChecker = $applicationContext->get(\BusinessLogic\Settings\ApiChecker::class);
if (!$apiChecker->isApiEnabled($hesk_settings)) {
print output(array('message' => 'API Disabled'), 404);
@ -77,7 +77,7 @@ function buildUserContext($xAuthToken) {
global $applicationContext, $userContext, $hesk_settings;
/* @var $userContextBuilder \BusinessLogic\Security\UserContextBuilder */
$userContextBuilder = $applicationContext->get[\BusinessLogic\Security\UserContextBuilder::class];
$userContextBuilder = $applicationContext->get(\BusinessLogic\Security\UserContextBuilder::class);
$userContext = $userContextBuilder->buildUserContext($xAuthToken, $hesk_settings);
}
@ -90,17 +90,13 @@ function errorHandler($errorNumber, $errorMessage, $errorFile, $errorLine) {
* @param $exception Exception
*/
function exceptionHandler($exception) {
global $applicationContext, $userContext, $hesk_settings;
global $userContext, $hesk_settings;
if (strpos($exception->getTraceAsString(), 'LoggingGateway') !== false) {
//-- Suppress these for now, as it would cause issues to output two JSONs at one time.
return;
}
/* @var $loggingGateway \DataAccess\Logging\LoggingGateway */
$loggingGateway = $applicationContext->get[\DataAccess\Logging\LoggingGateway::class];
// We don't cast API Friendly Exceptions as they're user-generated errors
if (exceptionIsOfType($exception, \BusinessLogic\Exceptions\ApiFriendlyException::class)) {
/* @var $castedException \BusinessLogic\Exceptions\ApiFriendlyException */
@ -142,7 +138,7 @@ function tryToLog($location, $message, $stackTrace, $userContext, $heskSettings)
global $applicationContext;
/* @var $loggingGateway \DataAccess\Logging\LoggingGateway */
$loggingGateway = $applicationContext->get[\DataAccess\Logging\LoggingGateway::class];
$loggingGateway = $applicationContext->get(\DataAccess\Logging\LoggingGateway::class);
try {
return $loggingGateway->logError($location, $message, $stackTrace, $userContext, $heskSettings);
@ -187,8 +183,10 @@ Link::before('globalBefore');
Link::all(array(
// Categories
'/v1/categories' => action(\Controllers\Categories\CategoryController::class . '::printAllCategories'),
'/v1/categories/{i}' => action(\Controllers\Categories\CategoryController::class),
'/v1/categories/all' => action(\Controllers\Categories\CategoryController::class . '::printAllCategories', [RequestMethod::GET], SecurityHandler::INTERNAL_OR_AUTH_TOKEN),
'/v1/categories' => action(\Controllers\Categories\CategoryController::class, [RequestMethod::POST], SecurityHandler::INTERNAL_OR_AUTH_TOKEN),
'/v1/categories/{i}' => action(\Controllers\Categories\CategoryController::class, [RequestMethod::GET, RequestMethod::PUT, RequestMethod::DELETE], SecurityHandler::INTERNAL_OR_AUTH_TOKEN),
'/v1-internal/categories/{i}/sort/{s}' => action(\Controllers\Categories\CategoryController::class . '::sort', [RequestMethod::POST], SecurityHandler::INTERNAL),
// Tickets
'/v1/tickets' => action(\Controllers\Tickets\CustomerTicketController::class),
// Tickets - Staff
@ -205,28 +203,34 @@ Link::all(array(
/* Internal use only routes */
// Resend email response
'/v1-internal/staff/tickets/{i}/resend-email' =>
action(\Controllers\Tickets\ResendTicketEmailToCustomerController::class, SecurityHandler::INTERNAL),
action(\Controllers\Tickets\ResendTicketEmailToCustomerController::class, RequestMethod::ALL, SecurityHandler::INTERNAL),
// Custom Navigation
'/v1-internal/custom-navigation/all' =>
action(\Controllers\Navigation\CustomNavElementController::class . '::getAll', SecurityHandler::INTERNAL),
action(\Controllers\Navigation\CustomNavElementController::class . '::getAll', RequestMethod::ALL, SecurityHandler::INTERNAL),
'/v1-internal/custom-navigation' =>
action(\Controllers\Navigation\CustomNavElementController::class, SecurityHandler::INTERNAL),
action(\Controllers\Navigation\CustomNavElementController::class, RequestMethod::ALL, SecurityHandler::INTERNAL),
'/v1-internal/custom-navigation/{i}' =>
action(\Controllers\Navigation\CustomNavElementController::class, SecurityHandler::INTERNAL),
action(\Controllers\Navigation\CustomNavElementController::class, RequestMethod::ALL, SecurityHandler::INTERNAL),
'/v1-internal/custom-navigation/{i}/sort/{s}' =>
action(\Controllers\Navigation\CustomNavElementController::class . '::sort', SecurityHandler::INTERNAL),
action(\Controllers\Navigation\CustomNavElementController::class . '::sort', RequestMethod::ALL, SecurityHandler::INTERNAL),
'/v1-public/hesk-version' =>
action(\Controllers\System\HeskVersionController::class . '::getHeskVersion', SecurityHandler::OPEN),
action(\Controllers\System\HeskVersionController::class . '::getHeskVersion', RequestMethod::ALL, SecurityHandler::OPEN),
'/v1-public/mods-for-hesk-version' =>
action(\Controllers\System\HeskVersionController::class . '::getModsForHeskVersion', SecurityHandler::OPEN),
action(\Controllers\System\HeskVersionController::class . '::getModsForHeskVersion', RequestMethod::ALL, SecurityHandler::OPEN),
// Any URL that doesn't match goes to the 404 handler
'404' => 'handle404'
));
function action($class, $securityHandler = SecurityHandler::AUTH_TOKEN) {
return [$class, $class, $securityHandler];
/**
* @param $class object|string The class name (and optional static method)
* @param $requestMethods array The accepted request methods for this endpoint
* @param $securityHandler string The proper security handler
* @return array The configured path
*/
function action($class, $requestMethods = RequestMethod::ALL, $securityHandler = SecurityHandler::AUTH_TOKEN) {
return [$class, $class, $securityHandler, $requestMethods];
}
class SecurityHandler {

@ -324,4 +324,13 @@ div.ticket-info {
#toast-container > div {
opacity: 1;
}
.input-group-addon {
padding: 0;
border: 0;
}
.input-group-addon.button > button {
border-radius: 0;
}

@ -2026,7 +2026,6 @@ function hesk_getFeatureArray()
'can_service_msg', /* User can manage service messages shown in customer interface */
'can_email_tpl', /* User can manage email templates */
'can_man_ticket_statuses', /* User can manage ticket statuses */
'can_set_manager', /* User can set category managers */
'can_man_permission_tpl', /* User can manage permission templates */
'can_man_settings', /* User can manage helpdesk settings */
'can_change_notification_settings', /* User can change notification settings */

@ -75,6 +75,7 @@ header('X-UA-Compatible: IE=edge');
<script type="text/javascript" src="<?php echo HESK_PATH; ?>js/jquery.magnific-popup.min.js?v=<?php echo MODS_FOR_HESK_BUILD; ?>"></script>
<script type="text/javascript" src="<?php echo HESK_PATH; ?>internal-api/js/alerts.js?v=<?php echo MODS_FOR_HESK_BUILD; ?>"></script>
<script type="text/javascript" src="<?php echo HESK_PATH; ?>internal-api/js/lang.js?v=<?php echo MODS_FOR_HESK_BUILD; ?>"></script>
<script type="text/javascript" src="<?php echo HESK_PATH; ?>js/clipboard.min.js?v=<?php echo MODS_FOR_HESK_BUILD; ?>"></script>
<?php
if (defined('EXTRA_JS')) {
echo EXTRA_JS;
@ -306,6 +307,7 @@ if (defined('MFH_PAGE_LAYOUT') && MFH_PAGE_LAYOUT == 'TOP_ONLY') {
unset($onload); ?>" class="<?php echo $layout_tag ?> fixed js">
<?php // GLOBAL JAVASCRIPT IDs ?>
<p style="display: none" id="hesk-url"><?php echo $hesk_settings['hesk_url']; ?></p>
<p style="display: none" id="hesk-path"><?php echo HESK_PATH; ?></p>
<p style="display: none" id="admin-dir"><?php echo $hesk_settings['admin_dir']; ?></p>
<p style="display: none" id="lang_alert_success"><?php echo $hesklang['alert_success']; ?></p>

@ -87,9 +87,10 @@ function print_select_category($number_of_categories)
// Print a select box if number of categories is large
if ($number_of_categories > $hesk_settings['cat_show_select'])
{
$firstDescription = null;
?>
<form action="index.php" method="get">
<select name="category" id="select_category" class="form-control">
<select name="category" id="select_category" class="form-control" onchange="showDescription()">
<?php
if ($hesk_settings['select_cat'])
{
@ -97,17 +98,40 @@ function print_select_category($number_of_categories)
}
foreach ($hesk_settings['categories'] as $k=>$v)
{
echo '<option value="'.$k.'">'.$v.'</option>';
if ($firstDescription === null) {
$firstDescription = $v['mfh_description'];
}
echo '<option value="'.$k.'" data-description="'.$v['mfh_description'].'">'.$v['name'].'</option>';
}
?>
</select>
<?php
$display = ' style="display: none"';
&nbsp;<br />
if (!$hesk_settings['select_cat'] && $firstDescription !== null && trim($firstDescription) !== '') {
$display = '';
}
?>
<span id="category-description"<?php echo $display; ?>>
<b><?php echo $hesklang['description_colon']; ?></b>
<span><?php echo $firstDescription; ?></span>
</span>
<br>
<div style="text-align:center">
<input type="submit" value="<?php echo $hesklang['c2c']; ?>" class="btn btn-default">
<input type="hidden" name="a" value="add" />
</div>
<script>
function showDescription() {
var $value = $('#select_category').find(':selected');
if ($value.data('description') !== '') {
$('#category-description').show().find('span').text($value.data('description'));
} else {
$('#category-description').hide();
}
}
</script>
</form>
<?php
}
@ -128,7 +152,14 @@ function print_select_category($number_of_categories)
<div class="panel-body">
<div class="row">
<div class="col-xs-12">
<?php echo $v; ?>
<?php
echo $v['name'];
if ($v['mfh_description'] !== null && trim($v['mfh_description']) !== '') {
echo '&nbsp;<i class="fa fa-info-circle" data-toggle="popover"
title="'. $hesklang['description'] .'" data-content="' . $v['mfh_description'] . '"></i>';
}
?>
</div>
</div>
</div>
@ -220,9 +251,9 @@ function print_add_ticket()
// Get categories
$hesk_settings['categories'] = array();
$res = hesk_dbQuery("SELECT `id`, `name` FROM `".hesk_dbEscape($hesk_settings['db_pfix'])."categories` WHERE `type`='0' ORDER BY `cat_order` ASC");
$res = hesk_dbQuery("SELECT `id`, `name`, `mfh_description` FROM `".hesk_dbEscape($hesk_settings['db_pfix'])."categories` WHERE `type`='0' ORDER BY `cat_order` ASC");
while ($row=hesk_dbFetchAssoc($res)) {
$hesk_settings['categories'][$row['id']] = $row['name'];
$hesk_settings['categories'][$row['id']] = $row;
}
$number_of_categories = count($hesk_settings['categories']);
@ -254,7 +285,7 @@ function print_add_ticket()
<?php echo $hesklang['sub_support']; ?>
</a>
</li>
<li class="active"><?php echo $hesk_settings['categories'][$category]; ?></li>
<li class="active"><?php echo $hesk_settings['categories'][$category]['name']; ?></li>
<?php } else { ?>
<li class="active"><?php echo $hesklang['sub_support']; ?></li>
<?php } ?>

@ -93,7 +93,6 @@ hesk_dbConnect();
$all_good = $all_good & run_column_check('tickets', 'longitude');
$all_good = $all_good & run_column_check('stage_tickets', 'latitude');
$all_good = $all_good & run_column_check('stage_tickets', 'longitude');
$all_good = $all_good & run_column_check('categories', 'manager');
$all_good = $all_good & run_column_check('users', 'permission_template');
$all_good = $all_good & run_table_check('permission_templates');
$all_good = $all_good & run_column_check('permission_templates', 'id');

@ -1130,4 +1130,14 @@ function execute311Scripts() {
hesk_dbConnect();
updateVersion('3.1.1');
}
function execute320Scripts() {
global $hesk_settings;
hesk_dbConnect();
executeQuery("ALTER TABLE `" . hesk_dbEscape($hesk_settings['db_pfix']) . "categories`
ADD COLUMN `mfh_description` VARCHAR(255)");
updateVersion('3.2.0');
}

@ -1,5 +1,8 @@
var mfhLang = {
text: function(key) {
return $('#lang_' + key).text();
},
html: function(key) {
return $('#lang_' + key).html()
}
};

@ -0,0 +1,383 @@
var categories = [];
$(document).ready(function() {
loadTable();
bindEditModal();
bindModalCancelCallback();
bindFormSubmit();
bindDeleteButton();
bindCreateModal();
bindGenerateLinkModal();
bindSortButtons();
});
function loadTable() {
$('#overlay').show();
var heskUrl = $('p#hesk-path').text();
var $tableBody = $('#table-body');
$.ajax({
method: 'GET',
url: heskUrl + 'api/index.php/v1/categories/all',
headers: { 'X-Internal-Call': true },
success: function(data) {
$tableBody.html('');
if (data.length === 0) {
mfhAlert.error("No categories were found. This shouldn't happen.", "No categories found");
$('#overlay').hide();
return;
}
var totalNumberOfTickets = 0;
$.each(data, function() {
totalNumberOfTickets += this.numberOfTickets;
});
var first = true;
var lastElement = null;
$.each(data, function() {
var $template = $($('#category-row-template').html());
$template.find('span[data-property="id"]').text(this.id).attr('data-value', this.id);
var $nameField = $template.find('span[data-property="category-name"]');
if (this.foregroundColor === 'AUTO') {
$nameField.addClass('background-volatile');
} else {
$nameField.css('color', this.foregroundColor);
}
$nameField.css('background', this.backgroundColor);
if (this.displayBorder && this.foregroundColor !== 'AUTO') {
$nameField.css('border', 'solid 1px ' + this.foregroundColor);
}
$nameField.html(this.name);
if (this.description === '' || this.description === null) {
$template.find('.fa-info-circle').hide();
} else {
$template.find('.fa-info-circle').attr('data-content', this.description);
}
var $priority = $template.find('span[data-property="priority"]');
if (this.priority === 0) {
// Critical
$priority.text(mfhLang.text('critical')).addClass('critical');
} else if (this.priority === 1) {
// High
$priority.text(mfhLang.text('high')).addClass('important');
} else if (this.priority === 2) {
// Medium
$priority.text(mfhLang.text('medium')).addClass('medium');
} else {
// Low
$priority.text(mfhLang.text('low')).addClass('normal');
}
var linkPattern = $('input[name="show-tickets-path"]').val();
$template.find('a[data-property="number-of-tickets"]')
.text(this.numberOfTickets)
.attr('href', linkPattern.replace('{0}', this.id));
var percentText = mfhLang.text('perat');
var percentage = Math.round(this.numberOfTickets / totalNumberOfTickets * 100);
$template.find('div.progress').attr('title', percentText.replace('%s', percentage + '%'));
$template.find('div.progress-bar').attr('aria-value-now', percentage).css('width', percentage + '%');
$template.find('[data-property="generate-link"]')
.attr('data-category-id', this.id)
.find('i').attr('title', mfhLang.text('geco'));
if (this.usage === 1) {
// Tickets only
$template.find('.fa-calendar').removeClass('fa-calendar');
} else if (this.usage === 2) {
// Events only
$template.find('.fa-ticket').removeClass('fa-ticket');
}
if (this.autoAssign) {
$template.find('.fa-bolt').addClass('orange');
$template.find('[data-property="autoassign"]').text(mfhLang.text('enabled_title_case'));
} else {
$template.find('.fa-bolt').addClass('gray');
$template.find('[data-property="autoassign"]').text(mfhLang.text('disabled_title_case'));
}
if (this.type === 1) {
// Private
$template.find('[data-property="type"]').text(mfhLang.text('cat_private'));
$template.find('.fa-lock').show();
$template.find('[data-property="generate-link"]').find('i')
.addClass('fa-ban')
.addClass('red')
.attr('title', mfhLang.text('cpric'));
} else {
// Public
$template.find('[data-property="type"]').text(mfhLang.text('cat_public'));
$template.find('.fa-unlock-alt').show();
$template.find('[data-property="generate-link"]').find('i')
.addClass('fa-code')
.addClass('green')
.attr('title', mfhLang.text('geco'));
}
if (this.id === 1) {
$template.find('[data-action="delete"]').hide();
}
$tableBody.append($template);
categories[this.id] = this;
lastElement = this;
if (first) {
$template.find('[data-direction="up"]').css('visibility', 'hidden');
first = false;
}
});
if (lastElement) {
//-- Hide the down arrow on the last element
$('[data-value="' + lastElement.id + '"]').parent().parent()
.find('[data-direction="down"]').css('visibility', 'hidden');
}
},
error: function(data) {
mfhAlert.errorWithLog(mfhLang.text('error_retrieving_categories'), data.responseJSON);
console.error(data);
},
complete: function() {
refreshBackgroundVolatileItems();
$('[data-toggle="popover"]').popover({
trigger: 'hover',
container: 'body'
});
$('#overlay').hide();
}
});
}
function bindEditModal() {
$(document).on('click', '[data-action="edit"]', function() {
var element = categories[$(this).parent().parent().find('[data-property="id"]').text()];
var $modal = $('#category-modal');
$modal.find('#edit-label').show();
$modal.find('#create-label').hide();
$modal.find('input[name="name"]').val(element.name).end()
.find('select[name="priority"]').val(element.priority).end()
.find('input[name="id"]').val(element.id).end()
.find('select[name="usage"]').val(element.usage).end()
.find('input[name="display-border"][value="' + (element.displayBorder ? 1 : 0) + '"]')
.prop('checked', 'checked').end();
var backgroundColor = element.backgroundColor;
var foregroundColor = element.foregroundColor;
var colorpickerOptions = {
format: 'hex',
color: backgroundColor
};
$modal.find('input[name="background-color"]')
.colorpicker(colorpickerOptions).end().modal('show');
colorpickerOptions = {
format: 'hex'
};
if (foregroundColor != '' && foregroundColor !== 'AUTO') {
colorpickerOptions.color = foregroundColor;
}
$modal.find('input[name="foreground-color"]')
.colorpicker(colorpickerOptions).end().modal('show');
if (foregroundColor === '' || foregroundColor === 'AUTO') {
$modal.find('input[name="foreground-color"]').colorpicker('setValue', '#fff');
$modal.find('input[name="foreground-color"]').val('');
}
$modal.find('input[name="cat-order"]').val(element.catOrder);
$modal.find('input[name="autoassign"][value="' + (element.autoAssign ? 1 : 0) + '"]')
.prop('checked', 'checked');
$modal.find('input[name="type"][value="' + (element.type ? 1 : 0) + '"]')
.prop('checked', 'checked');
$modal.find('textarea[name="description"]').val(element.description === null ? '' : element.description);
$modal.modal('show');
});
}
function bindCreateModal() {
$('#create-button').click(function() {
var $modal = $('#category-modal');
$modal.find('#edit-label').hide();
$modal.find('#create-label').show();
$modal.find('input[name="name"]').val('');
$modal.find('select[name="priority"]').val(3); // Low priority
$modal.find('select[name="usage"]').val(0); // Tickets and events
$modal.find('input[name="id"]').val(-1);
$modal.find('textarea[name="description"]').val('');
$modal.find('input[name="cat-order"]').val('');
$modal.find('input[name="type"][value="0"]').prop('checked', 'checked');
$modal.find('input[name="autoassign"][value="0"]').prop('checked', 'checked');
$modal.find('input[name="display-border"][value="0"]')
.prop('checked', 'checked');
var colorpickerOptions = {
format: 'hex',
color: '#fff'
};
$modal.find('input[name="background-color"]')
.colorpicker(colorpickerOptions).end().modal('show');
$modal.find('input[name="background-color"]').val('');
$modal.find('input[name="foreground-color"]')
.colorpicker(colorpickerOptions).end().modal('show');
$modal.find('input[name="foreground-color"]').val('');
$modal.modal('show');
});
}
function bindModalCancelCallback() {
$('.cancel-callback').click(function() {
var $editCategoryModal = $('#category-modal');
$editCategoryModal.find('input[name="background-color"]').val('').colorpicker('destroy').end();
$editCategoryModal.find('input[name="foreground-color"]').val('').colorpicker('destroy').end();
$editCategoryModal.find('input[name="display-border"][value="1"]').prop('checked');
$editCategoryModal.find('input[name="display-border"][value="0"]').prop('checked');
$editCategoryModal.find('input[name="autoassign"][value="1"]').prop('checked');
$editCategoryModal.find('input[name="autoassign"][value="0"]').prop('checked');
});
}
function bindFormSubmit() {
$('form#manage-category').submit(function(e) {
e.preventDefault();
var heskUrl = $('p#hesk-path').text();
var $modal = $('#category-modal');
var data = {
autoassign: $modal.find('input[name="autoassign"]').val() === 'true',
backgroundColor: $modal.find('input[name="background-color"]').val(),
description: $modal.find('textarea[name="description"]').val(),
displayBorder: $modal.find('input[name="display-border"]:checked').val() === '1',
foregroundColor: $modal.find('input[name="foreground-color"]').val() === '' ? 'AUTO' : $modal.find('input[name="foreground-color"]').val(),
name: $modal.find('input[name="name"]').val(),
priority: parseInt($modal.find('select[name="priority"]').val()),
type: parseInt($modal.find('input[name="type"]:checked').val()),
usage: parseInt($modal.find('select[name="usage"]').val()),
catOrder: parseInt($modal.find('input[name="cat-order"]').val())
};
var url = heskUrl + 'api/index.php/v1/categories/';
var method = 'POST';
var categoryId = parseInt($modal.find('input[name="id"]').val());
if (categoryId !== -1) {
url += categoryId;
method = 'PUT';
}
$modal.find('#action-buttons').find('.cancel-button').attr('disabled', 'disabled');
$modal.find('#action-buttons').find('.save-button').attr('disabled', 'disabled');
$.ajax({
method: 'POST',
url: url,
headers: {
'X-Internal-Call': true,
'X-HTTP-Method-Override': method
},
data: JSON.stringify(data),
success: function(data) {
var format = undefined;
if (categoryId === -1) {
format = mfhLang.html('cat_name_added');
mfhAlert.success(format.replace('%s', data.name));
} else {
format = mfhLang.html('category_updated');
mfhAlert.success(format.replace('%s', data.name));
}
$modal.modal('hide');
loadTable();
},
error: function(data) {
mfhAlert.errorWithLog(mfhLang.text('error_saving_updating_category'), data.responseJSON);
console.error(data);
},
complete: function(data) {
$modal.find('#action-buttons').find('.cancel-button').removeAttr('disabled');
$modal.find('#action-buttons').find('.save-button').removeAttr('disabled');
}
});
});
}
function bindDeleteButton() {
$(document).on('click', '[data-action="delete"]', function() {
$('#overlay').show();
var heskUrl = $('p#hesk-path').text();
var element = categories[$(this).parent().parent().find('[data-property="id"]').text()];
$.ajax({
method: 'POST',
url: heskUrl + 'api/index.php/v1/categories/' + element.id,
headers: {
'X-Internal-Call': true,
'X-HTTP-Method-Override': 'DELETE'
},
success: function() {
mfhAlert.success(mfhLang.text('cat_removed'));
loadTable();
},
error: function(data) {
$('#overlay').hide();
mfhAlert.errorWithLog(mfhLang.text('error_deleting_category'), data.responseJSON);
console.error(data);
}
});
});
}
function bindGenerateLinkModal() {
var $modal = $('#generate-link-modal');
$modal.find('.input-group-addon').click(function() {
clipboard.copy($modal.find('input[type="text"]').val());
mfhAlert.success(mfhLang.text('copied_to_clipboard'));
});
$(document).on('click', '[data-property="generate-link"] i.fa-code', function () {
var heskUrl = $('p#hesk-url').text();
var url = heskUrl + '/index.php?a=add&catid=' + $(this).parent().data('category-id');
$modal.find('input[type="text"]').val(url).end().modal('show');
});
}
function bindSortButtons() {
$(document).on('click', '[data-action="sort"]', function() {
$('#overlay').show();
var heskUrl = $('p#hesk-path').text();
var direction = $(this).data('direction');
var element = categories[$(this).parent().parent().parent().find('[data-property="id"]').text()];
$.ajax({
method: 'POST',
url: heskUrl + 'api/index.php/v1-internal/categories/' + element.id + '/sort/' + direction,
headers: { 'X-Internal-Call': true },
success: function() {
loadTable();
},
error: function(data) {
mfhAlert.errorWithLog(mfhLang.text('error_sorting_categories'), data.responseJSON);
console.error(data);
$('#overlay').hide();
}
})
});
}

@ -0,0 +1,9 @@
(function(e,c){"undefined"!==typeof module?module.exports=c():"function"===typeof define&&"object"===typeof define.amd?define(c):this[e]=c()})("clipboard",function(){if("undefined"===typeof document||!document.addEventListener)return null;var e={};e.copy=function(){function c(){d=!1;b=null;g&&window.getSelection().removeAllRanges();g=!1}var d=!1,b=null,g=!1;document.addEventListener("copy",function(c){if(d){for(var g in b)c.clipboardData.setData(g,b[g]);c.preventDefault()}});return function(f){return new Promise(function(m,
e){function k(b){try{if(document.execCommand("copy"))c(),m();else{if(b)throw c(),Error("Unable to copy. Perhaps it's not available in your browser?");var d=document.getSelection();if(!document.queryCommandEnabled("copy")&&d.isCollapsed){var f=document.createRange();f.selectNodeContents(document.body);d.removeAllRanges();d.addRange(f);g=!0}k(!0)}}catch(a){c(),e(a)}}d=!0;"string"===typeof f?b={"text/plain":f}:f instanceof Node?b={"text/html":(new XMLSerializer).serializeToString(f)}:f instanceof Object?
b=f:e("Invalid data type. Must be string, DOM node, or an object mapping MIME types to strings.");k(!1)})}}();e.paste=function(){var c=!1,d,b;document.addEventListener("paste",function(g){if(c){c=!1;g.preventDefault();var f=d;d=null;f(g.clipboardData.getData(b))}});return function(g){return new Promise(function(f,e){c=!0;d=f;b=g||"text/plain";try{document.execCommand("paste")||(c=!1,e(Error("Unable to paste. Pasting only works in Internet Explorer at the moment.")))}catch(h){c=!1,e(Error(h))}})}}();
"undefined"===typeof ClipboardEvent&&"undefined"!==typeof window.clipboardData&&"undefined"!==typeof window.clipboardData.setData&&(function(c){function d(a,b){return function(){a.apply(b,arguments)}}function b(a){if("object"!=typeof this)throw new TypeError("Promises must be constructed via new");if("function"!=typeof a)throw new TypeError("not a function");this._value=this._state=null;this._deferreds=[];l(a,d(f,this),d(e,this))}function g(a){var b=this;return null===this._state?void this._deferreds.push(a):
void n(function(){var c=b._state?a.onFulfilled:a.onRejected;if(null===c)return void(b._state?a.resolve:a.reject)(b._value);var d;try{d=c(b._value)}catch(e){return void a.reject(e)}a.resolve(d)})}function f(a){try{if(a===this)throw new TypeError("A promise cannot be resolved with itself.");if(a&&("object"==typeof a||"function"==typeof a)){var b=a.then;if("function"==typeof b)return void l(d(b,a),d(f,this),d(e,this))}this._state=!0;this._value=a;h.call(this)}catch(c){e.call(this,c)}}function e(a){this._state=
!1;this._value=a;h.call(this)}function h(){for(var a=0,b=this._deferreds.length;b>a;a++)g.call(this,this._deferreds[a]);this._deferreds=null}function k(a,b,c,d){this.onFulfilled="function"==typeof a?a:null;this.onRejected="function"==typeof b?b:null;this.resolve=c;this.reject=d}function l(a,b,c){var d=!1;try{a(function(a){d||(d=!0,b(a))},function(a){d||(d=!0,c(a))})}catch(e){d||(d=!0,c(e))}}var n=b.immediateFn||"function"==typeof setImmediate&&setImmediate||function(a){setTimeout(a,1)},p=Array.isArray||
function(a){return"[object Array]"===Object.prototype.toString.call(a)};b.prototype["catch"]=function(a){return this.then(null,a)};b.prototype.then=function(a,c){var d=this;return new b(function(b,e){g.call(d,new k(a,c,b,e))})};b.all=function(){var a=Array.prototype.slice.call(1===arguments.length&&p(arguments[0])?arguments[0]:arguments);return new b(function(b,c){function d(f,g){try{if(g&&("object"==typeof g||"function"==typeof g)){var h=g.then;if("function"==typeof h)return void h.call(g,function(a){d(f,
a)},c)}a[f]=g;0===--e&&b(a)}catch(k){c(k)}}if(0===a.length)return b([]);for(var e=a.length,f=0;f<a.length;f++)d(f,a[f])})};b.resolve=function(a){return a&&"object"==typeof a&&a.constructor===b?a:new b(function(b){b(a)})};b.reject=function(a){return new b(function(b,c){c(a)})};b.race=function(a){return new b(function(b,c){for(var d=0,e=a.length;e>d;d++)a[d].then(b,c)})};"undefined"!=typeof module&&module.exports?module.exports=b:c.Promise||(c.Promise=b)}(this),e.copy=function(c){return new Promise(function(d,
b){if("string"!==typeof c&&!("text/plain"in c))throw Error("You must provide a text/plain type.");window.clipboardData.setData("Text","string"===typeof c?c:c["text/plain"])?d():b(Error("Copying was rejected."))})},e.paste=function(){return new Promise(function(c,d){var b=window.clipboardData.getData("Text");b?c(b):d(Error("Pasting was rejected."))})});return e});

@ -1831,10 +1831,6 @@ $hesklang['your_current_location'] = 'Your location';
$hesklang['requesting_location_ellipsis'] = 'Requesting location...';
$hesklang['unable_to_determine_location'] = 'Unable to determine your location, or you declined to share it.';
$hesklang['save_to_see_updated_address'] = 'Save the new location to see the updated address';
$hesklang['manager'] = 'Manager';
$hesklang['manager_updated'] = 'Category manager has been updated.';
$hesklang['can_set_manager'] = 'Can set category managers';
$hesklang['no_manager'] = 'No manager';
$hesklang['manage_permission_templates'] = 'Manage Permission Templates';
$hesklang['manage_permission_templates_help'] = 'Here you can create and edit permission templates. These templates will appear when creating/editing a user.
Please note that if you change the permission template\'s settings, it will NOT change the permissions of any users that are set to this permission template.';
@ -2173,6 +2169,15 @@ $hesklang['url_rewrite_help'] = 'Remove the need to include /index.php in API UR
$hesklang['url_rewrite_saved'] = 'URL rewrite setting saved!';
$hesklang['api_settings_saved'] = 'API settings saved!';
$hesklang['an_error_occurred'] = 'An error occurred. Check the logs for more information';
$hesklang['error_deleting_category'] = 'An error occurred when trying to delete the category.';
$hesklang['cat_private'] = 'Private';
$hesklang['cat_public'] = 'Public';
$hesklang['cat_name_description'] = 'Name / Description';
$hesklang['error_sorting_categories'] = 'An error occurred sorting categories!';
$hesklang['error_retrieving_categories'] = 'An error occurred retrieving categories!';
$hesklang['error_saving_updating_category'] = 'An error occurred creating / saving the category!';
$hesklang['description_colon'] = 'Description:'; // Same as 'description', but with a colon (:) afterwards
$hesklang['copied_to_clipboard'] = 'Copied to clipboard';
// DO NOT CHANGE BELOW
if (!defined('IN_SCRIPT')) die('PHP syntax OK!');

Loading…
Cancel
Save