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 .= "
\n
\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 .= "
\n
\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() // } ?>