More work on exception handling and user context building

remotes/upstream/api-rewrite
Mike Koch 7 years ago
parent e68ecf50c6
commit 8968be1ffd

@ -5,8 +5,10 @@ namespace Core;
// Responsible for loading in all necessary classes. AKA a poor man's DI solution.
use BusinessLogic\Category\CategoryRetriever;
use BusinessLogic\Security\BanRetriever;
use BusinessLogic\Security\UserContextBuilder;
use DataAccess\CategoryGateway;
use DataAccess\Security\BanGateway;
use DataAccess\Security\UserGateway;
class ApplicationContext {
public $get;
@ -14,10 +16,16 @@ class ApplicationContext {
function __construct() {
$this->get = array();
// Categories
$this->get['CategoryGateway'] = new CategoryGateway();
$this->get['CategoryRetriever'] = new CategoryRetriever($this->get['CategoryGateway']);
// Bans
$this->get['BanGateway'] = new BanGateway();
$this->get['BanRetriever'] = new BanRetriever($this->get['BanGateway']);
// User Context
$this->get['UserGateway'] = new UserGateway();
$this->get['UserContextBuilder'] = new UserContextBuilder($this->get['UserGateway']);
}
}

@ -12,6 +12,15 @@ require_once(__DIR__ . '/../hesk_settings.inc.php');
// Mods for HESK API Files
require_once(__DIR__ . '/http_response_code.php');
require_once(__DIR__ . '/dao/CommonDao.php');
require_once(__DIR__ . '/businesslogic/Helpers.php');
// User Context
require_once(__DIR__ . '/dao/security/UserGateway.php');
require_once(__DIR__ . '/businesslogic/security/UserContextBuilder.php');
require_once(__DIR__ . '/businesslogic/security/UserContextNotifications.php');
require_once(__DIR__ . '/businesslogic/security/UserContextPreferences.php');
require_once(__DIR__ . '/businesslogic/security/UserContext.php');
// Categories
require_once(__DIR__ . '/dao/category/CategoryGateway.php');
@ -25,6 +34,11 @@ require_once(__DIR__ . '/businesslogic/security/BanRetriever.php');
require_once(__DIR__ . '/businesslogic/security/BannedEmail.php');
require_once(__DIR__ . '/businesslogic/security/BannedIp.php');
// Exceptions
require_once(__DIR__ . '/businesslogic/exception/InvalidAuthenticationTokenException.php');
require_once(__DIR__ . '/businesslogic/exception/MissingAuthenticationTokenException.php');
require_once(__DIR__ . '/businesslogic/exception/ValidationException.php');
hesk_load_api_database_functions();
// HESK files that require database access

@ -0,0 +1,29 @@
<?php
/**
* Created by PhpStorm.
* User: mkoch
* Date: 1/28/2017
* Time: 8:54 PM
*/
namespace BusinessLogic\Helpers;
class Helpers {
static function getHeader($key) {
$headers = getallheaders();
$uppercaseHeaders = array();
foreach ($headers as $header => $value) {
$uppercaseHeaders[strtoupper($header)] = $value;
}
return isset($uppercaseHeaders[$key])
? $uppercaseHeaders[$key]
: NULL;
}
static function hashToken($token) {
return hash('sha512', $token);
}
}

@ -39,7 +39,12 @@ class Category {
public $priority;
/**
* @var int? The manager for the category, if applicable
* @var int|null The manager for the category, if applicable
*/
public $manager;
/**
* @var bool Indication if the user has access to the category
*/
public $accessible;
}

@ -2,6 +2,7 @@
namespace BusinessLogic\Category;
use BusinessLogic\Security\UserContext;
use DataAccess\CategoryGateway;
class CategoryRetriever {
@ -14,7 +15,19 @@ class CategoryRetriever {
$this->categoryGateway = $categoryGateway;
}
function getAllCategories($hesk_settings) {
return $this->categoryGateway->getAllCategories($hesk_settings);
/**
* @param $heskSettings array
* @param $userContext UserContext
* @return array
*/
function getAllCategories($heskSettings, $userContext) {
$categories = $this->categoryGateway->getAllCategories($heskSettings);
foreach ($categories as $category) {
$category->accessible = $userContext->admin ||
in_array($category->id, $userContext->categories);
}
return $categories;
}
}

@ -1,5 +0,0 @@
<?php
namespace BusinessLogic\Category;
use DataAccess\CategoryGateway;

@ -0,0 +1,11 @@
<?php
namespace BusinessLogic\Exceptions;
use Exception;
class InvalidAuthenticationTokenException extends Exception {
public function __construct() {
parent::__construct('The X-Auth-Token is invalid. The token must be for an active helpdesk user.');
}
}

@ -0,0 +1,17 @@
<?php
/**
* Created by PhpStorm.
* User: mkoch
* Date: 1/28/2017
* Time: 9:55 PM
*/
namespace BusinessLogic\Exceptions;
use Exception;
class MissingAuthenticationTokenException extends Exception {
function __construct() {
parent::__construct("An 'X-Auth-Token' is required for all requests");
}
}

@ -1,7 +1,8 @@
<?php
namespace BusinessLogic\Validation;
namespace BusinessLogic\Exceptions;
use BusinessLogic\Validation\ValidationModel;
use Exception;
class ValidationException extends Exception {

@ -3,14 +3,37 @@
namespace BusinessLogic\Security;
use BusinessLogic\Exceptions\InvalidAuthenticationTokenException;
use BusinessLogic\Exceptions\MissingAuthenticationTokenException;
use BusinessLogic\Helpers\Helpers;
use DataAccess\Security\UserGateway;
class UserContextBuilder {
static function buildUserContext($authToken, $hesk_settings) {
require_once(__DIR__ . '/../../dao/security/UserGateway.php');
/**
* @var UserGateway
*/
private $userGateway;
function __construct($userGateway) {
$this->userGateway = $userGateway;
}
function buildUserContext($authToken, $heskSettings) {
$NULL_OR_EMPTY_STRING = 'cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e';
$hashedToken = hash('sha512', $authToken);
return UserGateway::getUserForAuthToken($hashedToken, $hesk_settings);
$hashedToken = Helpers::hashToken($authToken);
if ($hashedToken === $NULL_OR_EMPTY_STRING) {
throw new MissingAuthenticationTokenException();
}
$userRow = $this->userGateway->getUserForAuthToken($hashedToken, $heskSettings);
if ($userRow === null) {
throw new InvalidAuthenticationTokenException();
}
return $this->fromDataRow($userRow);
}
/**
@ -18,11 +41,7 @@ class UserContextBuilder {
* @param $dataRow array the $_SESSION superglobal or the hesk_users result set
* @return UserContext the built user context
*/
static function fromDataRow($dataRow) {
require_once(__DIR__ . '/UserContext.php');
require_once(__DIR__ . '/UserContextPreferences.php');
require_once(__DIR__ . '/UserContextNotifications.php');
function fromDataRow($dataRow) {
$userContext = new UserContext();
$userContext->id = $dataRow['id'];
$userContext->username = $dataRow['user'];

@ -1,9 +0,0 @@
<?php
require_once(API_PATH . 'dao/security_dao.php');
function get_user_for_token($token, $hesk_settings) {
$hash = hash('sha512', $token);
return get_user_for_token_hash($hash, $hesk_settings);
}

@ -3,9 +3,9 @@
namespace BusinessLogic\Tickets;
use BusinessLogic\Exceptions\ValidationException;
use BusinessLogic\Validation\ValidationModel;
use BusinessObjects\CreateTicketByCustomerModel;
use BusinessLogic\Validation\ValidationException;
class TicketCreator {
/**
@ -18,8 +18,6 @@ class TicketCreator {
$validationModel = validate($ticketRequest, false, $heskSettings, $modsForHeskSettings);
if (count($validationModel->errorKeys) > 0) {
require_once(__DIR__ . '/../ValidationException.php');
// Validation failed
throw new ValidationException($validationModel);
}
@ -35,11 +33,6 @@ class TicketCreator {
* @return ValidationModel If errorKeys is empty, validation successful. Otherwise invalid ticket
*/
function validate($ticketRequest, $staff, $heskSettings, $modsForHeskSettings) {
require_once(__DIR__ . '/../email_validators.php');
require_once(__DIR__ . '/../category/CategoryRetriever.php');
//require_once('../category/retriever.php');
//require_once('../bans/retriever.php');
$TICKET_PRIORITY_CRITICAL = 0;
$validationModel = new ValidationModel();

@ -1,107 +0,0 @@
<?php
require_once(API_PATH . 'dao/ticket_dao.php');
function get_ticket_for_staff($hesk_settings, $user, $id = NULL) {
$tickets = get_ticket_for_id($hesk_settings, $user, $id);
if ($tickets == NULL) {
return NULL;
}
if ($id === NULL) {
$original_tickets = $tickets;
$tickets = array();
foreach ($original_tickets as $ticket) {
$ticket = remove_common_properties($ticket);
$ticket = convert_to_camel_case($ticket);
$ticket = handle_dates($ticket);
$tickets[] = $ticket;
}
} else {
$tickets = remove_common_properties($tickets);
$tickets = handle_dates($tickets);
$tickets = convert_to_camel_case($tickets);
}
return $tickets;
}
function remove_common_properties($ticket) {
unset($ticket['lastchange']);
unset($ticket['firstreply']);
unset($ticket['closedat']);
unset($ticket['openedby']);
unset($ticket['firstreplyby']);
unset($ticket['closedby']);
unset($ticket['replies']);
unset($ticket['staffreplies']);
unset($ticket['lastreplier']);
unset($ticket['replierid']);
return $ticket;
}
function handle_dates($ticket) {
$ticket['dt'] = hesk_date($ticket['dt'], true);
return $ticket;
}
function convert_to_camel_case($ticket) {
if (isset($ticket['articles'])) {
$ticket['suggestedArticles'] = $ticket['articles'];
unset($ticket['articles']);
}
$ticket['legacyAuditTrail'] = $ticket['history'];
unset($ticket['history']);
$ticket['linkedTo'] = $ticket['parent'];
unset($ticket['parent']);
$ticket['timeWorked'] = $ticket['time_worked'];
unset($ticket['time_worked']);
$ticket['userAgent'] = $ticket['user_agent'];
unset($ticket['user_agent']);
$ticket['screenResolutionWidth'] = $ticket['screen_resolution_width'];
unset($ticket['screen_resolution_width']);
$ticket['screenResolutionHeight'] = $ticket['screen_resolution_height'];
unset($ticket['screen_resolution_height']);
$ticket['trackingId'] = $ticket['trackid'];
unset($ticket['trackid']);
$ticket['dateCreated'] = $ticket['dt'];
unset($ticket['dt']);
$ticket['dueDate'] = $ticket['due_date'];
unset($ticket['due_date']);
$ticket['overdueEmailSent'] = $ticket['overdue_email_sent'];
return $ticket;
}
function get_ticket($hesk_settings, $id) {
$ticket = get_ticket_for_id($hesk_settings, $id);
$ticket = remove_common_properties($ticket);
$ticket = remove_staff_specific_properties($ticket);
$ticket = convert_to_camel_case($ticket);
return $ticket;
}
function remove_staff_specific_properties($ticket) {
unset($ticket['articles']);
unset($ticket['ip']);
unset($ticket['language']);
unset($ticket['owner']);
unset($ticket['time_worked']);
unset($ticket['history']);
unset($ticket['latitude']);
unset($ticket['longitude']);
unset($ticket['user_agent']);
unset($ticket['screen_resolution_width']);
unset($ticket['screen_resolution_height']);
unset($ticket['parent']);
unset($ticket['due_date']);
unset($ticket['overdue_email_sent']);
return $ticket;
}

@ -1,77 +0,0 @@
<?php
require_once(API_PATH . 'dao/user_dao.php');
function retrieve_user($hesk_settings, $id = NULL) {
$users = get_user($hesk_settings, $id);
if ($id === NULL) {
$original_users = $users;
$users = array();
foreach ($original_users as $user) {
$user = remove_unneeded_properties($user);
$user = convert_to_camel_case($user);
$users[] = $user;
}
} else {
$users = remove_unneeded_properties($users);
$users = convert_to_camel_case($users);
}
return $users;
}
function remove_unneeded_properties($user) {
unset($user['pass']);
unset($user['permission_template']);
unset($user['language']);
unset($user['replies']);
return $user;
}
function convert_to_camel_case($user) {
$user['username'] = $user['user'];
unset($user['user']);
$user['admin'] = $user['isadmin'];
unset($user['isadmin']);
$user['afterReply'] = $user['afterreply'];
unset($user['afterreply']);
$user['autoStart'] = $user['autostart'];
unset($user['autostart']);
$user['notifyCustomerNew'] = $user['notify_customer_new'];
unset($user['notify_customer_new']);
$user['notifyCustomerReply'] = $user['notify_customer_reply'];
unset($user['notify_customer_reply']);
$user['showSuggested'] = $user['show_suggested'];
unset($user['show_suggested']);
$user['notifyNewUnassigned'] = $user['notify_new_unassigned'];
unset($user['notify_new_unassigned']);
$user['notifyNewMy'] = $user['notify_new_my'];
unset($user['notify_new_my']);
$user['notifyReplyUnassigned'] = $user['notify_reply_unassigned'];
unset($user['notify_reply_unassigned']);
$user['notifyReplyMy'] = $user['notify_reply_my'];
unset($user['notify_reply_my']);
$user['notifyAssigned'] = $user['notify_assigned'];
unset($user['notify_assigned']);
$user['notifyPm'] = $user['notify_pm'];
unset($user['notify_pm']);
$user['notifyNote'] = $user['notify_note'];
unset($user['notify_note']);
$user['notifyNoteUnassigned'] = $user['notify_note_unassigned'];
unset($user['notify_note_unassigned']);
$user['defaultList'] = $user['default_list'];
unset($user['default_list']);
$user['ratingNeg'] = $user['ratingneg'];
unset($user['ratingneg']);
$user['ratingPos'] = $user['ratingpos'];
unset($user['ratingpos']);
$user['heskPrivileges'] = $user['heskprivileges'];
unset($user['heskprivileges']);
$user['defaultCalendarView'] = $user['default_calendar_view'];
unset($user['default_calendar_view']);
$user['notifyOverdueUnassigned'] = $user['notify_overdue_unassigned'];
unset($user['notify_overdue_unassigned']);
return $user;
}

@ -15,11 +15,11 @@ class CategoryController {
}
private static function getAllCategories() {
global $hesk_settings, $applicationContext;
global $hesk_settings, $applicationContext, $userContext;
/* @var $categoryRetriever CategoryRetriever */
$categoryRetriever = $applicationContext->get['CategoryRetriever'];
return $categoryRetriever->getAllCategories($hesk_settings);
return $categoryRetriever->getAllCategories($hesk_settings, $userContext);
}
}

@ -120,9 +120,8 @@ function hesk_dbConnect()
{
$message = $hesklang['contact_webmaster'] . $hesk_settings['webmaster_email'];
}
header('Content-Type: application/json');
print_error($hesklang['cant_connect_db'], $message);
return http_response_code(500);
//TODO Throw exception
//print_error($hesklang['cant_connect_db'], $message);
}
if ( ! @mysql_select_db($hesk_settings['db_name'], $hesk_db_link))
@ -135,9 +134,8 @@ function hesk_dbConnect()
{
$message = $hesklang['contact_webmaster'] . $hesk_settings['webmaster_email'];
}
header('Content-Type: application/json');
print_error($hesklang['cant_connect_db'], $message);
die();
//TODO Throw exception
//print_error($hesklang['cant_connect_db'], $message);
}
// Check MySQL/PHP version and set encoding to utf8
@ -182,10 +180,9 @@ function hesk_dbQuery($query)
{
$message = $hesklang['contact_webmaster'] . $hesk_settings['webmaster_email'];
}
header('Content-Type: application/json');
print_error($hesklang['cant_sql'], $message);
die();
//TODO Throw exception
//print_error($hesklang['cant_sql'], $message);
return null;
} // END hesk_dbQuery()

@ -128,9 +128,9 @@ function hesk_dbConnect()
{
$message = $hesklang['contact_webmaster'] . $hesk_settings['webmaster_email'];
}
header('Content-Type: application/json');
print_error($hesklang['cant_connect_db'], $message);
http_response_code(500);
//TODO Throw exception instead
//print_error($hesklang['cant_connect_db'], $message);
}
// Check MySQL/PHP version and set encoding to utf8
@ -177,9 +177,10 @@ function hesk_dbQuery($query)
{
$message = $hesklang['contact_webmaster'] . $hesk_settings['webmaster_email'];
}
header('Content-Type: application/json');
print_error($hesklang['cant_sql'], $message);
die(http_response_code(500));
//TODO Throw exception instead
//print_error($hesklang['cant_sql'], $message);
return null;
} // END hesk_dbQuery()

@ -1,9 +0,0 @@
<?php
function get_header($key) {
$headers = getallheaders();
return isset($headers[$key])
? $headers[$key]
: NULL;
}

@ -6,6 +6,10 @@ use BusinessObjects\Category;
use Exception;
class CategoryGateway extends CommonDao {
/**
* @param $hesk_settings
* @return Category[]
*/
function getAllCategories($hesk_settings) {
$this->init();

@ -1,28 +0,0 @@
<?php
function get_category($hesk_settings, $id = NULL) {
$sql = "SELECT * FROM `" . hesk_dbEscape($hesk_settings['db_pfix']) . "categories` ";
if ($id != NULL) {
$sql .= "WHERE `id` = ".intval($id);
}
$response = hesk_dbQuery($sql);
if (hesk_dbNumRows($response) == 0) {
return NULL;
}
$results = array();
while ($row = hesk_dbFetchAssoc($response)) {
$row['id'] = intval($row['id']);
$row['displayOrder'] = intval($row['cat_order']);
unset($row['cat_order']);
$row['autoassign'] = $row['autoassign'] == 1;
$row['type'] = intval($row['type']);
$row['priority'] = intval($row['priority']);
$row['manager'] = intval($row['manager']) == 0 ? NULL : intval($row['manager']);
$results[] = $row;
}
return $id == NULL ? $results : $results[0];
}

@ -10,25 +10,32 @@ namespace DataAccess\Security;
use BusinessLogic\Security\UserContextBuilder;
use DataAccess\CommonDao;
use Exception;
class UserGateway {
static function getUserForAuthToken($hashedToken, $hesk_settings) {
require_once(__DIR__ . '/../../businesslogic/security/UserContextBuilder.php');
class UserGateway extends CommonDao {
/**
* @param $hashedToken string The pre-hashed token from Helpers::hashToken
* @param $heskSettings
* @return array|null User ResultSet if an active user for the token is found, null otherwise
*/
function getUserForAuthToken($hashedToken, $heskSettings) {
$this->init();
if (!function_exists('hesk_dbConnect')) {
throw new Exception('Database not loaded!');
}
hesk_dbConnect();
$rs = hesk_dbQuery('SELECT * FROM `' . hesk_dbEscape($hesk_settings['db_pfix']) . 'users` WHERE `id` = (
$rs = hesk_dbQuery("SELECT * FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "users` WHERE `id` = (
SELECT ``
FROM `' . hesk_dbEscape($hesk_settings['db_pfix']) . 'user_api_tokens`
WHERE `token` = ' . hesk_dbEscape($hashedToken) . '
)');
FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "user_api_tokens`
WHERE `tokens`.`token` = " . hesk_dbEscape($hashedToken) . "
) AND `active` = '1'");
if (hesk_dbNumRows($rs) === 0) {
return null;
}
$row = hesk_dbFetchAssoc($rs);
return UserContextBuilder::fromDataRow($row);
$this->close();
return $row;
}
}

@ -1,11 +0,0 @@
<?php
class AccessException extends Exception {
public function __construct($code)
{
$message = '';
if ($code == 401) {
$message = 'The X-Auth-Token is invalid';
}
parent::__construct($message, $code);
}
}

@ -3,22 +3,64 @@
require_once(__DIR__ . '/autoload.php');
error_reporting(0);
set_error_handler('errorHandler');
set_exception_handler('exceptionHandler');
register_shutdown_function('fatalErrorShutdownHandler');
$userContext = null;
function handle404() {
http_response_code(404);
print json_encode('404 found');
}
function before() {
assertApiIsEnabled();
$token = \BusinessLogic\Helpers\Helpers::getHeader('X-AUTH-TOKEN');
buildUserContext($token);
}
function assertApiIsEnabled() {
return true;
}
function buildUserContext($xAuthToken) {
global $applicationContext, $userContext, $hesk_settings;
/* @var $userContextBuilder \BusinessLogic\Security\UserContextBuilder */
$userContextBuilder = $applicationContext->get['UserContextBuilder'];
$userContext = $userContextBuilder->buildUserContext($xAuthToken, $hesk_settings);
}
function errorHandler($errorNumber, $errorMessage, $errorFile, $errorLine) {
print_error(sprintf("Uncaught error in %s", $errorFile), $errorMessage);
throw new Exception(sprintf("%s:%d\n\n%s", $errorFile, $errorLine, $errorMessage));
}
/**
* @param $exception Exception
*/
function exceptionHandler($exception) {
if (exceptionIsOfType($exception, 'MissingAuthenticationTokenException')) {
print_error("Security Exception", $exception->getMessage(), 400);
} elseif (exceptionIsOfType($exception, 'InvalidAuthenticationTokenException')) {
print_error("Security Exception", $exception->getMessage(), 401);
} else {
print_error("Fought an uncaught exception", sprintf("%s\n\n%s", $exception->getMessage(), $exception->getTraceAsString()));
}
// Log more stuff to logging table if possible; we'll catch any exceptions from this
die();
}
/**
* @param $exception Exception thrown exception
* @param $class string The name of the expected exception type
* @return bool
*/
function exceptionIsOfType($exception, $class) {
return strpos(get_class($exception), $class) !== false;
}
function fatalErrorShutdownHandler() {
$last_error = error_get_last();
if ($last_error['type'] === E_ERROR) {
@ -27,8 +69,7 @@ function fatalErrorShutdownHandler() {
}
}
// Must use fully-qualified namespace to controllers
Link::before('assertApiIsEnabled');
Link::before('before');
Link::all(array(
// Categories

Loading…
Cancel
Save