TimeTrex/classes/modules/api/core/APIFactory.class.php

756 lines
26 KiB
PHP

<?php
/*********************************************************************************
*
* TimeTrex is a Workforce Management program developed by
* TimeTrex Software Inc. Copyright (C) 2003 - 2021 TimeTrex Software Inc.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by
* the Free Software Foundation with the addition of the following permission
* added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
* WORK IN WHICH THE COPYRIGHT IS OWNED BY TIMETREX, TIMETREX DISCLAIMS THE
* WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
*
* You should have received a copy of the GNU Affero General Public License along
* with this program; if not, see http://www.gnu.org/licenses or write to the Free
* Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA.
*
*
* You can contact TimeTrex headquarters at Unit 22 - 2475 Dobbin Rd. Suite
* #292 West Kelowna, BC V4T 2E9, Canada or at email address info@timetrex.com.
*
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
*
* In accordance with Section 7(b) of the GNU Affero General Public License
* version 3, these Appropriate Legal Notices must retain the display of the
* "Powered by TimeTrex" logo. If the display of the logo is not reasonably
* feasible for technical reasons, the Appropriate Legal Notices must display
* the words "Powered by TimeTrex".
*
********************************************************************************/
/**
* @package API\Core
*/
abstract class APIFactory {
public $data = [];
protected $main_class_obj = null;
protected $api_message_id = null;
protected $pager_obj = null;
protected $current_company = null;
protected $current_user = null;
protected $current_user_prefs = null;
protected $permission = null;
protected $progress_bar_obj = null;
/**
* APIFactory constructor.
*/
function __construct() {
global $current_company, $current_user, $current_user_prefs;
$this->current_company = $current_company;
$this->current_user = $current_user;
$this->current_user_prefs = $current_user_prefs;
$this->permission = new Permission();
return true;
}
/**
* @return int
*/
function getProtocolVersion() {
if ( isset( $_GET['v'] ) && $_GET['v'] != '' ) {
return (int)$_GET['v']; //1=Initial, 2=Always return detailed
} else {
$retval = 2; //Default to v2.
$mobile_app_client_version = Misc::getMobileAppClientVersion();
if ( $mobile_app_client_version !== false ) { //If mobile app version is specified at all, default to API protocol v1, otherwise it should be specified on its own.
//NOTE: Mobile app currently requires API v1, but older versions of the app don't send the protocol version. So we can't default to v2 without breaking the app.
// Also app versions >= v5.0.0 to < v5.1.0 did not specify a protocol version.
$retval = 1;
Debug::Text( 'NOTICE: Falling back to API protocol v1.0...', __FILE__, __LINE__, __METHOD__, 10 );
}
}
return $retval;
}
/**
* Returns the API messageID for each individual call.
* @return bool|null
*/
function getAPIMessageID() {
if ( $this->api_message_id != null ) {
return $this->api_message_id;
}
return false;
}
/**
* @param string $id UUID
* @return bool
*/
function setAPIMessageID( $id ) {
if ( $id != '' ) {
global $api_message_id; //Make this global so Debug() class can reference it on Shutdown()
$this->api_message_id = $api_message_id = $id;
return true;
}
return false;
}
/**
* @return bool|CompanyFactory
*/
function getCurrentCompanyObject() {
if ( is_object( $this->current_company ) ) {
return $this->current_company;
}
return false;
}
/**
* @return bool|UserFactory
*/
function getCurrentUserObject() {
if ( is_object( $this->current_user ) ) {
return $this->current_user;
}
return false;
}
/**
* @return bool|UserPreferenceFactory
*/
function getCurrentUserPreferenceObject() {
if ( is_object( $this->current_user_prefs ) ) {
return $this->current_user_prefs;
}
return false;
}
/**
* @return bool|null|Permission
*/
function getPermissionObject() {
if ( is_object( $this->permission ) ) {
return $this->permission;
}
return false;
}
/**
* @return bool
*/
function isProgressBarStarted() {
if ( is_object( $this->progress_bar_obj ) ) {
return true;
}
return false;
}
/**
* @param $progress_bar_obj
* @return bool
*/
function setProgressBarObject( $progress_bar_obj ) {
$this->progress_bar_obj = $progress_bar_obj;
return true;
}
/**
* @return null|ProgressBar
*/
function getProgressBarObject() {
if ( !is_object( $this->progress_bar_obj ) ) {
$this->progress_bar_obj = new ProgressBar();
}
return $this->progress_bar_obj;
}
/**
* @param object $lf
* @return bool
*/
function setPagerObject( $lf ) {
if ( is_object( $lf ) ) {
$this->pager_obj = new Pager( $lf );
}
return true;
}
/**
* @return Pager
*/
function getPagerObject() {
return $this->pager_obj;
}
/**
* @return array|bool
*/
function getPagerData() {
if ( is_object( $this->pager_obj ) ) {
return $this->pager_obj->getPageVariables();
}
return false;
}
/**
* Allow storing the main class object persistently in memory, so we can build up other variables to help out things like getOptions()
* Mainly used for the APIReport class.
* @param object $obj
* @return bool
*/
function setMainClassObject( $obj ) {
if ( is_object( $obj ) ) {
$this->main_class_obj = $obj;
return true;
}
return false;
}
/**
* @return string
*/
function getMainClassObject() {
if ( !is_object( $this->main_class_obj ) ) {
$this->main_class_obj = new $this->main_class;
return $this->main_class_obj;
} else {
return $this->main_class_obj;
}
}
/**
* @param array $data
* @param bool $disable_paging
* @return array|bool
*/
function initializeFilterAndPager( $data, $disable_paging = false ) {
//If $data is not an array, it will trigger PHP errors, so force it that way and report an error so we can troubleshoot if needed.
//This will avoid the PHP fatal errors that look like the below, but it doesn't actually fix the root cause, which is currently unknown.
// DEBUG [L0228] [00014ms] Array: [Function](): Arguments: (Size: 114)
// array(4) {
// ["POST_/api/json/api_php?Class"]=> string(18) "APIUserGenericData"
// ["Method"]=> string(18) "getUserGenericData"
// ["v"]=> string(1) "2"
// ["MessageID"]=> string(26) "5dd90933-f97c-9001-9efe-e2"
// }
// DEBUG [L0139] [00030ms] Array: Debug::ErrorHandler(): Raw POST Request:
// string(114) "POST /api/json/api.php?Class=APIUserGenericData&Method=getUserGenericData&v=2&MessageID=5dd90933-f97c-9001-9efe-e2"
if ( is_array( $data ) == false ) {
Debug::Arr( $data, 'ERROR: Input data is not an array: ', __FILE__, __LINE__, __METHOD__, 10 );
$data = [];
}
//Preset values for LF search function.
$data = Misc::preSetArrayValues( $data, [ 'filter_data', 'filter_columns', 'filter_items_per_page', 'filter_page', 'filter_sort' ], null );
if ( $disable_paging == false && (int)$data['filter_items_per_page'] <= 0 ) { //Used to check $data['filter_items_per_page'] === NULL
$data['filter_items_per_page'] = $this->getCurrentUserPreferenceObject()->getItemsPerPage();
}
if ( $disable_paging == true ) {
$data['filter_items_per_page'] = $data['filter_page'] = false;
}
//Debug::Arr($data, 'Getting Data: ', __FILE__, __LINE__, __METHOD__, 10);
return $data;
}
/**
* In cases where data can be displayed in just a list_view (dropdown boxes), ie: branch, department, job, task in In/Out punch view
* restrict the dropdown box to just a subset of columns, so not all data is shown.
* @param array $filter_columns
* @param array $allowed_columns
* @return array|null
*/
function handlePermissionFilterColumns( $filter_columns, $allowed_columns ) {
//Always allow these columns to be returned.
$allowed_columns['id'] = true;
$allowed_columns['is_owner'] = true;
$allowed_columns['is_child'] = true;
if ( is_array( $filter_columns ) ) {
$retarr = Misc::arrayIntersectByKey( $allowed_columns, $filter_columns );
} else {
$retarr = $allowed_columns;
}
//If no valid columns are being returned, revert back to allowed columns.
//Never return *NULL* or a blank array from here, as that will allow all columns to be displayed.
if ( !is_array( $retarr ) ) {
//Return all allowed columns
$retarr = $allowed_columns;
}
return $retarr;
}
/**
* @param array $data
* @return mixed
*/
function convertToSingleRecord( $data ) {
if ( isset( $data[0] ) && !isset( $data[1] ) ) {
return $data[0];
} else {
return $data;
}
}
/**
* @param array $data
* @return array
*/
function convertToMultipleRecords( $data ) {
//if ( isset($data[0]) AND is_array($data[0]) ) {
//Better way to detect if $data has numeric or string keys, which works across sparse arrays that could come from importing. ie: 3 => array(), 6 => array(), ...
// Array indexes can only be integer or string, so (string)"8" can never happen as it would always be (int)8
if ( count( array_filter( array_keys( $data ), 'is_string' ) ) == 0 ) {
$retarr = [
//'data' => $data,
//'total_records' => count($data)
//Switch to an array that is compatible with list() rather than extract() as it allows IDEs to better inspect code.
$data,
count( $data ),
];
} else {
$retarr = [
//'data' => array( 0 => $data ),
//'total_records' => 1
//Switch to an array that is compatible with list() rather than extract() as it allows IDEs to better inspect code.
[ 0 => $data ],
1,
];
}
//Debug::Arr($retarr, 'Array: ', __FILE__, __LINE__, __METHOD__, 10);
return $retarr;
}
/**
* @return string
*/
function getNextInsertID() {
return $this->getMainClassObject()->getNextInsertId();
}
/**
* @return array
*/
function getPermissionChildren() {
return $this->getPermissionObject()->getPermissionHierarchyChildren( $this->getCurrentCompanyObject()->getId(), $this->getCurrentUserObject()->getId() );
}
/**
* Controls returning information to client in a standard format.
* FIXME: Need to return the original request (with any modified values due to restrictions/validation issues)
* Also need to return paging data variables here too, as JSON can't make multiple calls.
* In order to do this we need to always return a special data structure that includes this information.
* static function returnHandler( $retval = TRUE, $args = array( 'code' => FALSE, 'description' => FALSE, 'details' = FALSE, 'validator_stats' => FALSE, 'user_generic_status_batch_id' => FALSE ) ) {
* The above will require too many changes, just add two more variables at the end, as it will only really be used by API->get*() functions.
* FIXME: Use a requestHandler() to handle all input requests, so we can parse out things like validate_only, ignore_warning (for user acknowledgable warnings) and handling all parameter parsing in a central place.
* static function returnHandler( $retval = TRUE, $code = FALSE, $description = FALSE, $details = FALSE, $validator_stats = FALSE, $user_generic_status_batch_id = FALSE, $request = FALSE, $pager = FALSE ) {
* @param bool|mixed $retval
* @param string|bool $code
* @param string|bool $description
* @param array|bool $details
* @param array|bool $validator_stats
* @param string|int|bool $user_generic_status_batch_id
* @param array|bool $request_data
* @param string|bool $system_job_queue
* @return array|bool
*/
function returnHandler( $retval = true, $code = false, $description = false, $details = false, $validator_stats = false, $user_generic_status_batch_id = false, $request_data = false, $system_job_queue = false ) {
global $config_vars;
if ( isset( $config_vars['other']['enable_job_queue'] ) && $config_vars['other']['enable_job_queue'] != true ) { //If the job queue is disabled, force system_job_queue flag to always be false so we don't trigger a spinner in the UI.
$system_job_queue = false;
}
if ( $this->getProtocolVersion() == 1 ) {
if ( $retval === false || ( $retval === true && $code !== false ) || ( $user_generic_status_batch_id !== false ) ) {
if ( $retval === false ) {
if ( $code == '' ) {
$code = 'GENERAL';
}
if ( $description == '' ) {
$description = 'Insufficient data to carry out action';
}
} else if ( $retval === true ) {
if ( $code == '' ) {
$code = 'SUCCESS';
}
}
$validator_stats = Misc::preSetArrayValues( $validator_stats, [ 'total_records', 'valid_records', 'invalids_records' ], 0 );
$retarr = [
'api_retval' => $retval,
'api_details' => [
'code' => $code,
'description' => $description,
'record_details' => [
'total' => $validator_stats['total_records'],
'valid' => $validator_stats['valid_records'],
'invalid' => ( $validator_stats['total_records'] - $validator_stats['valid_records'] ),
],
'user_generic_status_batch_id' => $user_generic_status_batch_id,
'system_job_queue' => $system_job_queue,
'details' => $details,
],
];
if ( $retval === false ) {
Debug::Arr( $retarr, 'returnHandler v1 ERROR: ' . (int)$retval, __FILE__, __LINE__, __METHOD__, 10 );
}
//Handle progress bar here, make sure they are stopped and if an error occurs display the error.
if ( $retval === false ) {
//Try to show detailed validation error messages if at all possible.
// Check for $details[0] because returnHandlers that lead into this seem to force an array with '0' key as per:
// $this->returnHandler( FALSE, 'VALIDATION', TTi18n::getText('INVALID DATA'), array( 0 => $validation_obj->getErrorsArray() ), array('total_records' => 1, 'valid_records' => 0 ) );
if ( isset( $details ) && is_array( $details ) && isset( $details[0] ) ) {
$validator = new Validator();
$description .= "<br>\n<br>\n" . $validator->getTextErrors( true, $details[0] );
unset( $validator );
}
if ( $this->isProgressBarStarted() == true ) {
$this->getProgressBarObject()->error( $this->getAPIMessageID(), $description );
}
} else {
if ( $this->isProgressBarStarted() == true ) {
$this->getProgressBarObject()->stop( $this->getAPIMessageID() );
}
}
return $retarr;
}
//No errors, or additional information, return unmodified data.
return $retval;
} else {
if ( $retval === false ) {
if ( $code == '' ) {
$code = 'GENERAL';
}
if ( $description == '' ) {
$description = 'Insufficient data to carry out action';
}
} else if ( $retval === true ) {
if ( $code == '' ) {
$code = 'SUCCESS';
}
}
$validator_stats = Misc::preSetArrayValues( $validator_stats, [ 'total_records', 'valid_records', 'invalids_records' ], 0 );
$retarr = [
'api_retval' => $retval,
'api_details' => [
'code' => $code,
'description' => $description,
'record_details' => [
'total' => $validator_stats['total_records'],
'valid' => $validator_stats['valid_records'],
'invalid' => ( $validator_stats['total_records'] - $validator_stats['valid_records'] ),
],
'user_generic_status_batch_id' => $user_generic_status_batch_id,
'system_job_queue' => $system_job_queue,
//Allows the API to modify the original request data to send back to the UI for notifying the user.
//We would like to implement validation on non-set*() calls as well perhaps?
'request' => $request_data,
'pager' => $this->getPagerData(),
'details' => $details,
],
];
if ( $retval === false ) {
Debug::Arr( $retarr, 'returnHandler v2 ERROR: ' . (int)$retval, __FILE__, __LINE__, __METHOD__, 10 );
}
//Handle progress bar here, make sure they are stopped and if an error occurs display the error.
if ( $retval === false ) {
//Try to show detailed validation error messages if at all possible.
// Check for $details[0] because returnHandlers that lead into this seem to force an array with '0' key as per:
// $this->returnHandler( FALSE, 'VALIDATION', TTi18n::getText('INVALID DATA'), array( 0 => $validation_obj->getErrorsArray() ), array('total_records' => 1, 'valid_records' => 0 ) );
if ( isset( $details ) && is_array( $details ) && isset( $details[0] ) ) {
$validator = new Validator();
$description .= "<br>\n<br>\n" . $validator->getTextErrors( true, $details[0] );
unset( $validator );
}
if ( $this->isProgressBarStarted() ) {
$this->getProgressBarObject()->error( $this->getAPIMessageID(), $description );
}
} else {
if ( $this->isProgressBarStarted() ) {
$this->getProgressBarObject()->stop( $this->getAPIMessageID() );
}
}
//Debug::Arr($retarr, 'returnHandler: '. (int)$retval, __FILE__, __LINE__, __METHOD__, 10);
return $retarr;
}
}
/**
* @param mixed $retarr
* @return mixed
*/
function stripReturnHandler( $retarr ) {
if ( isset( $retarr['api_retval'] ) ) {
return $retarr['api_retval'];
}
return $retarr;
}
/**
* Bridge to main class getVariableToFunctionMap factory.
* @param string $name
* @param string|int $parent
* @return array
*/
function getVariableToFunctionMap( $name, $parent = null ) {
return $this->getMainClassObject()->getVariableToFunctionMap( $name, $parent );
}
/**
* Take a API ReturnHandler array and pulls out the Validation errors/warnings to be merged back into another Validator
* This is useful for calling one API function from another one when their are sub-classes.
* @param $api_retarr
* @param bool $validator_obj
* @return bool|Validator
*/
function convertAPIReturnHandlerToValidatorObject( $api_retarr, $validator_obj = false ) {
if ( is_object( $validator_obj ) ) {
$validator = $validator_obj;
} else {
$validator = new Validator;
}
if ( isset( $api_retarr['api_retval'] ) && $api_retarr['api_retval'] === false && isset( $api_retarr['api_details']['details'] ) ) {
foreach ( $api_retarr['api_details']['details'] as $tmp_validation_error_label => $validation_row ) {
if ( isset( $validation_row['error'] ) ) {
foreach ( $validation_row['error'] as $validation_error_label => $validation_error_msg ) {
$validator->Error( $validation_error_label, $validation_error_msg[0] );
}
}
if ( isset( $validation_row['warning'] ) ) {
foreach ( $validation_row['warning'] as $validation_warning_label => $validation_warning_msg ) {
$validator->Warning( $validation_warning_label, $validation_warning_msg[0] );
}
}
//Before warnings were added, validation errors were just directly in the details array, so try to handle those here.
// This is used by TimeTrexPaymentServices API, since it doesn't use warnings.
if ( !isset( $validation_row['error'] ) && !isset( $validation_row['warning'] ) ) {
foreach ( $validation_row as $tmp_validation_error_label_b => $validation_error_msg ) {
if ( is_array( $validation_error_msg ) && isset( $validation_error_msg[0] ) ) {
$validator->Error( $tmp_validation_error_label_b, $validation_error_msg[0] );
} else {
$validator->Error( $tmp_validation_error_label_b, $validation_error_msg );
}
}
}
}
}
return $validator;
}
/**
* @param Validator[] $validator_obj_arr Array of Validator objects.
* @param string $record_label Prefix for record label if performing a mass function to differentiate one record from another.
* @return array|bool
*/
function setValidationArray( $validator_obj_arr, $record_label = null ) {
//Handle validator array objects in order.
$validator = [];
if ( !is_array( $validator_obj_arr ) ) {
$validator_obj_arr = [ $validator_obj_arr ];
}
foreach ( $validator_obj_arr as $key => $validator_obj ) {
//Sometimes a Factory object is passed in, so we have to pull the ->Validator property from that if it happens.
if ( is_a( $validator_obj, 'Validator' ) == false && isset( $validator_obj->Validator ) && is_a( $validator_obj->Validator, 'Validator' ) ) {
$validator_obj = $validator_obj->Validator;
}
if ( $this->getProtocolVersion() == 1 ) { //Don't return any warnings and therefore don't put errors in its own array element.
if ( $validator_obj->isError() === true ) {
$validator = $validator_obj->getErrorsArray( $record_label );
break;
}
} else {
if ( $validator_obj->isError() === true ) {
$validator['error'] = $validator_obj->getErrorsArray( $record_label );
break;
} else {
//Check for primary validator warnings next.
if ( $validator_obj->isWarning() === true ) {
$validator['warning'] = $validator_obj->getWarningsArray( $record_label );
break;
}
}
}
}
if ( count( $validator ) > 0 ) {
return $validator;
}
return false;
}
/**
* @param object|array|bool $validator
* @param array $validator_stats
* @param int $key
* @param array|bool $save_result
* @param bool $user_generic_status_batch_id
* @return array
*/
function handleRecordValidationResults( $validator, $validator_stats, $key, $save_result, $user_generic_status_batch_id = false, $system_job_queue = false ) {
global $config_vars;
if ( isset( $config_vars['other']['enable_job_queue'] ) && $config_vars['other']['enable_job_queue'] != true ) { //If the job queue is disabled, force system_job_queue flag to always be false so we don't trigger a spinner in the UI.
$system_job_queue = false;
}
if ( $validator_stats['valid_records'] > 0 && $validator_stats['total_records'] == $validator_stats['valid_records'] ) {
if ( $validator_stats['total_records'] == 1 ) {
return $this->returnHandler( $save_result[$key], true, false, false, false, $user_generic_status_batch_id, false, $system_job_queue ); //Single valid record
} else {
return $this->returnHandler( true, 'SUCCESS', TTi18n::getText( 'MULTIPLE RECORDS SAVED' ), $save_result, $validator_stats, $user_generic_status_batch_id, false, $system_job_queue ); //Multiple valid records
}
} else {
return $this->returnHandler( false, 'VALIDATION', TTi18n::getText( 'INVALID DATA' ), $validator, $validator_stats, $user_generic_status_batch_id, false, $system_job_queue );
}
}
//
// **IMPORTANT** Functions below this are the only functions that are designed to be called remotely from the API, and therefore should be whitelisted.
// These need to be added to API.inc.php in isWhiteListedAPICall()
//
/**
* Bridge to main class getOptions factory.
* @param bool $name
* @param string|int $parent
* @return array|bool
*/
function getOptions( $name = false, $parent = null ) {
if ( $name != '' ) {
if ( method_exists( $this->getMainClassObject(), 'getOptions' ) ) {
return $this->getMainClassObject()->getOptions( $name, $parent );
} else {
Debug::Text( 'getOptions() function does not exist for object: ' . get_class( $this->getMainClassObject() ), __FILE__, __LINE__, __METHOD__, 10 );
}
} else {
Debug::Text( 'ERROR: Name not provided, unable to return data...', __FILE__, __LINE__, __METHOD__, 10 );
}
return false;
}
/**
* Bridge multiple batched requests to main class getOptions factory.
* @param array $requested_options
* @return array
*/
function getOptionsBatch( $requested_options = [] ) {
$retarr = [];
if ( is_array( $requested_options ) && count( $requested_options ) > 0 ) {
foreach ( $requested_options as $option => $parent ) {
$retarr[$option] = $this->getOptions( $option, $parent );
}
}
return $retarr;
}
/**
* Download a result_set as a csv.
* @param string $format
* @param string $file_name
* @param array $result
* @param array $filter_columns
* @return array|bool
*/
function exportRecords( $format, $file_name, $result, $filter_columns ) {
if ( isset( $result[0] ) && is_array( $result[0] ) && is_array( $filter_columns ) && count( $filter_columns ) > 0 ) {
$columns = Misc::arrayIntersectByKey( array_keys( $filter_columns ), Misc::trimSortPrefix( $this->getOptions( 'columns' ) ) );
$file_extension = $format;
$mime_type = 'application/' . $format;
$output = '';
if ( $format == 'csv' ) {
$output = Misc::Array2CSV( $result, $columns, false );
}
$this->getProgressBarObject()->stop( $this->getAPIMessageID() );
if ( $output !== false ) {
Misc::APIFileDownload( $file_name . '.' . $file_extension, $mime_type, $output );
return null; //Do not send return value (even TRUE/FALSE), otherwise it could get appending to the end of the downloaded file.
} else {
return $this->returnHandler( false, 'VALIDATION', TTi18n::getText( 'ERROR: No data to export...' ) );
}
}
return $this->returnHandler( true ); //No records returned.
}
//
// **IMPORTANT** Functions above this are the only functions that are designed to be called remotely from the API, and therefore should be whitelisted.
// These need to be added to API.inc.php in isWhiteListedAPICall()
//
}
?>