This is useful for identifying which submit button actually
submitted the form.
*/
/**
* @param string $prefix
* @return null|string
*/
static function findSubmitButton( $prefix = 'action' ) {
// search post vars, then get vars.
$queries = [ $_POST, $_GET ];
foreach ( $queries as $query ) {
foreach ( $query as $key => $value ) {
//Debug::Text('Key: '. $key .' Value: '. $value, __FILE__, __LINE__, __METHOD__, 10);
$newvar = explode( ':', $key, 2 );
//Debug::Text('Explode 0: '. $newvar[0] .' 1: '. $newvar[1], __FILE__, __LINE__, __METHOD__, 10);
if ( isset( $newvar[0] ) && isset( $newvar[1] ) && $newvar[0] === $prefix ) {
$val = $newvar[1];
// input type=image stupidly appends _x and _y.
if ( substr( $val, ( strlen( $val ) - 2 ) ) === '_x' ) {
$val = substr( $val, 0, ( strlen( $val ) - 2 ) );
}
//Debug::Text('Found Button: '. $val, __FILE__, __LINE__, __METHOD__, 10);
return strtolower( $val );
}
}
unset( $value ); //code standards
}
return null;
}
/**
* @param bool $text_keys
* @return array
*/
static function getSortDirectionArray( $text_keys = false ) {
if ( $text_keys === true ) {
return [ 'asc' => 'ASC', 'desc' => 'DESC' ];
} else {
return [ 1 => 'ASC', -1 => 'DESC' ];
}
}
/**
* This function totals arrays where the data wanting to be totaled is deep in a multi-dimentional array.
* Usually a row array just before its passed to smarty.
* @param $array
* @param null $element
* @param null $decimals
* @param bool $include_non_numeric
* @return array|bool
*/
static function ArrayAssocSum( $array, $element = null, $decimals = null, $include_non_numeric = false ) {
if ( !is_array( $array ) ) {
return false;
}
$retarr = [];
$totals = [];
foreach ( $array as $value ) {
if ( isset( $element ) && isset( $value[$element] ) ) {
foreach ( $value[$element] as $sum_key => $sum_value ) {
if ( !isset( $totals[$sum_key] ) ) {
$totals[$sum_key] = 0;
}
$totals[$sum_key] += $sum_value;
}
} else {
//Debug::text(' Array Element not set: ', __FILE__, __LINE__, __METHOD__, 10);
foreach ( $value as $sum_key => $sum_value ) {
if ( !isset( $totals[$sum_key] ) ) {
$totals[$sum_key] = 0;
}
//Both $totals[$sum_key] and $sum_value need to be numeric to add them to each other.
if ( !is_numeric( $sum_value ) || !is_numeric( $totals[$sum_key] ) ) {
if ( $include_non_numeric == true && $sum_value != '' ) {
$totals[$sum_key] = $sum_value;
}
} else {
$totals[$sum_key] = bcadd( $totals[$sum_key], $sum_value );
}
//Debug::text(' Sum: '. $totals[$sum_key] .' Key: '. $sum_key .' This Value: '. $sum_value, __FILE__, __LINE__, __METHOD__, 10);
}
}
}
//format totals
if ( $decimals !== null ) {
foreach ( $totals as $retarr_key => $retarr_value ) {
//Debug::text(' Number Formatting: '. $retarr_value, __FILE__, __LINE__, __METHOD__, 10);
$retarr[$retarr_key] = number_format( $retarr_value, $decimals, '.', '' );
}
} else {
return $totals;
}
unset( $totals );
return $retarr;
}
/**
* This function is similar to a SQL group by clause, only its done on a AssocArray
* Pass it a row array just before you send it to smarty.
* @param $array
* @param $group_by_elements
* @param array $ignore_elements
* @return array
*/
static function ArrayGroupBy( $array, $group_by_elements, $ignore_elements = [] ) {
if ( !is_array( $group_by_elements ) ) {
$group_by_elements = [ $group_by_elements ];
}
if ( isset( $ignore_elements ) && is_array( $ignore_elements ) ) {
foreach ( $group_by_elements as $group_by_element ) {
//Remove the group by element from the ignore elements.
unset( $ignore_elements[$group_by_element] );
}
}
$retarr = [];
if ( is_array( $array ) ) {
foreach ( $array as $row ) {
$group_by_key_val = null;
foreach ( $group_by_elements as $group_by_element ) {
if ( isset( $row[$group_by_element] ) ) {
$group_by_key_val .= $row[$group_by_element];
}
}
//Debug::Text('Group By Key Val: '. $group_by_key_val, __FILE__, __LINE__, __METHOD__, 10);
if ( !isset( $retarr[$group_by_key_val] ) ) {
$retarr[$group_by_key_val] = [];
}
foreach ( $row as $key => $val ) {
//Debug::text(' Key: '. $key .' Value: '. $val, __FILE__, __LINE__, __METHOD__, 10);
if ( in_array( $key, $group_by_elements ) ) {
$retarr[$group_by_key_val][$key] = $val;
} else if ( !in_array( $key, $ignore_elements ) ) {
if ( isset( $retarr[$group_by_key_val][$key] ) ) {
$retarr[$group_by_key_val][$key] = Misc::MoneyRound( bcadd( $retarr[$group_by_key_val][$key], $val ) );
//Debug::text(' Adding Value: '. $val .' For: '. $retarr[$group_by_key_val][$key], __FILE__, __LINE__, __METHOD__, 10);
} else {
//Debug::text(' Setting Value: '. $val, __FILE__, __LINE__, __METHOD__, 10);
$retarr[$group_by_key_val][$key] = $val;
}
}
}
}
}
return $retarr;
}
/**
* @param $arr
* @return bool|float|int
*/
static function ArrayAvg( $arr ) {
if ( ( !is_array( $arr ) ) || ( !count( $arr ) > 0 ) ) {
return false;
}
return ( array_sum( $arr ) / count( $arr ) );
}
/**
* @param $prepend_arr
* @param $arr
* @return array|bool
*/
static function prependArray( $prepend_arr, $arr ) {
if ( !is_array( $prepend_arr ) && is_array( $arr ) ) {
return $arr;
} else if ( is_array( $prepend_arr ) && !is_array( $arr ) ) {
return $prepend_arr;
} else if ( !is_array( $prepend_arr ) && !is_array( $arr ) ) {
return false;
}
$retarr = $prepend_arr;
foreach ( $arr as $key => $value ) {
//Don't overwrite entries from the prepend array.
if ( !isset( $retarr[$key] ) ) {
$retarr[$key] = $value;
}
}
return $retarr;
}
/**
* @param null $input
* @param null $columnKey
* @param null $indexKey
* @return array|bool|null
*/
static function arrayColumn( $input = null, $columnKey = null, $indexKey = null ) {
return array_column( (array)$input, $columnKey, $indexKey );
}
/**
* @param $array
* @param bool $preserve
* @param array $r
* @return array
*/
static function flattenArray( $array, $preserve = false, $r = [] ) {
foreach ( $array as $key => $value ) {
if ( is_array( $value ) ) {
foreach ( $value as $k => $v ) {
if ( is_array( $v ) ) {
$tmp = $v;
unset( $value[$k] );
}
}
if ( $preserve == true ) {
$r[$key] = $value;
} else {
$r[] = $value;
}
}
$r = isset( $tmp ) ? self::flattenArray( $tmp, $preserve, $r ) : $r;
}
return $r;
}
/**
* Flattens an array by only a single level, ie: [ 'level1' => [ 0 => [ 'key' => 'val' ] ] ] becomes: [ 0 => [ 'key' => 'val' ] ]
* @param $array
* @return array
*/
static function flattenArrayOneLevel( $array ) {
$retarr = [];
if ( is_array( $array ) && !empty($array) ) {
foreach( $array as $key => $value ) {
if ( is_array( $value ) ) {
$retarr = $retarr + $value; //Merge array and maintain indexes.
}
}
}
return $retarr;
}
/*
When passed an array of input_keys, and an array of output_key => output_values,
this function will return all the output_key => output_value pairs where
input_key == output_key
*/
/**
* @param $keys
* @param $options
* @return array|null
*/
static function arrayIntersectByKey( $keys, $options ) {
if ( is_array( $keys ) && is_array( $options ) ) {
$retarr = [];
foreach ( $keys as $key ) {
if ( isset( $options[$key] ) && $key !== false ) { //Ignore boolean FALSE, so the Root group isn't always selected.
$retarr[$key] = $options[$key];
}
}
if ( empty( $retarr ) == false ) {
return $retarr;
}
}
//Return NULL because if we return FALSE smarty will enter a
//"blank" option into select boxes.
return null;
}
/*
When passed an associative array from a ListFactory, ie:
array( 0 => array( <...Data ..> ),
1 => array( <...Data ..> ),
2 => array( <...Data ..> ),
... )
this function will return an associative array of only the key=>value
pairs that intersect across all rows.
*/
/**
* @param $rows
* @return bool|mixed
*/
static function arrayIntersectByRow( $rows ) {
if ( !is_array( $rows ) ) {
return false;
}
if ( count( $rows ) < 2 ) {
return false;
}
//Debug::Arr($rows, 'Intersected/Common Data', __FILE__, __LINE__, __METHOD__, 10);
$retval = false;
if ( isset( $rows[0] ) ) {
$retval = @call_user_func_array( 'array_intersect_assoc', $rows );
// The '@' cannot be removed, Some of the array_* functions that compare elements in
// multiple arrays do so by (string)$elem1 === (string)$elem2 If $elem1 or $elem2 is an
// array, then the array to string notice is thrown, $rows is an array and its every
// element is also an array, but its element may have one element is still an array, if
// so, the array to string notice will be produced. this case may be like this:
// array(
// array('a'), array(
// array('a'),
// ),
// );
// Put a "@" in front to prevent the error, otherwise, the Flex will not work properly.
//Debug::Arr($retval, 'Intersected/Common Data', __FILE__, __LINE__, __METHOD__, 10);
}
return $retval;
}
/**
* Returns the most common values for each key (column) in the rows.
* @param $rows
* @param null $filter_columns Specific columns to get common data for.
* @return array|bool
*/
static function arrayCommonValuesForEachKey( $rows, $filter_columns = null ) {
if ( !is_array( $rows ) ) {
return false;
}
$retarr = [];
if ( is_array( $filter_columns ) ) {
$array_keys = $filter_columns;
} else {
$array_keys = array_keys( $rows[0] );
}
foreach ( $array_keys as $array_key ) {
$counted_column_values = @array_count_values( Misc::arrayColumn( $rows, $array_key ) );
arsort( $counted_column_values );
$retarr[$array_key] = current( array_slice( array_keys( $counted_column_values ), 0, 1, true ) );
unset( $counted_column_values );
}
Debug::Arr( $retarr, 'Most common values for each key: ', __FILE__, __LINE__, __METHOD__, 10 );
return $retarr;
}
/*
Returns all the output_key => output_value pairs where
the input_keys are not present in output array keys.
*/
/**
* @param $keys
* @param $options
* @return array|null
*/
static function arrayDiffByKey( $keys, $options ) {
if ( is_array( $keys ) && is_array( $options ) ) {
$retarr = [];
foreach ( $options as $key => $value ) {
if ( !in_array( $key, $keys, true ) ) { //Use strict we ignore boolean FALSE, so the Root group isn't always selected.
$retarr[$key] = $options[$key];
}
}
unset( $value ); //code standards
if ( empty( $retarr ) == false ) {
return $retarr;
}
}
//Return NULL because if we return FALSE smarty will enter a
//"blank" option into select boxes.
return null;
}
/**
* This only merges arrays where the array keys must already exist.
* @param array $array1
* @param array $array2
* @return array
*/
static function arrayMergeRecursiveDistinct( array $array1, array $array2 ) {
$merged = $array1;
foreach ( $array2 as $key => &$value ) {
if ( is_array( $value ) && isset( $merged[$key] ) && is_array( $merged[$key] ) ) {
$merged[$key] = self::arrayMergeRecursiveDistinct( $merged[$key], $value );
} else {
$merged[$key] = $value;
}
}
return $merged;
}
/**
* Merges arrays with overwriting whereas PHP standard array_merge_recursive does not overwrites but combines.
* @param array $array1
* @param array $array2
* @return array
*/
static function arrayMergeRecursive( array $array1, array $array2 ) {
foreach ( $array2 as $key => $value ) {
if ( array_key_exists( $key, $array1 ) && is_array( $value ) ) {
$array1[$key] = self::arrayMergeRecursive( $array1[$key], $array2[$key] );
} else {
$array1[$key] = $value;
}
}
return $array1;
}
/**
* @param $array1
* @param $array2
* @return array|bool
*/
static function arrayDiffAssocRecursive( $array1, $array2 ) {
$difference = [];
if ( is_array( $array1 ) ) {
foreach ( $array1 as $key => $value ) {
if ( is_array( $value ) ) {
if ( !isset( $array2[$key] ) ) {
$difference[$key] = $value;
} else if ( !is_array( $array2[$key] ) ) {
$difference[$key] = $value;
} else {
$new_diff = self::arrayDiffAssocRecursive( $value, $array2[$key] );
if ( $new_diff !== false ) {
$difference[$key] = $new_diff;
}
}
} else if ( !isset( $array2[$key] ) || $array2[$key] != $value ) {
$difference[$key] = $value;
}
}
}
if ( empty( $difference ) ) {
return false;
}
return $difference;
}
/**
* @param $arr
* @return mixed
*/
static function arrayCommonValue( $arr ) {
$arr_count = array_count_values( $arr );
arsort( $arr_count );
return key( $arr_count );
}
/**
* Case insensitive array_unique().
* @param $array
* @return array
*/
static function arrayIUnique( $array ) {
return array_intersect_key( $array, array_unique( array_map( 'strtolower', $array ) ) );
}
/**
* Adds prefix to all array keys, mainly for reportings and joining array data together to avoid conflicting keys.
* @param $prefix
* @param $arr
* @param null $ignore_elements
* @return array
*/
static function addKeyPrefix( $prefix, $arr, $ignore_elements = null ) {
if ( is_array( $arr ) ) {
$retarr = [];
foreach ( $arr as $key => $value ) {
if ( !is_array( $ignore_elements ) || ( is_array( $ignore_elements ) && !in_array( $key, $ignore_elements ) ) ) {
$retarr[$prefix . $key] = $value;
} else {
$retarr[$key] = $value;
}
}
if ( empty( $retarr ) == false ) {
return $retarr;
}
}
//Don't return FALSE, as this can create array( 0 => FALSE ) arrays if we then cast it to an array, which corrupts some report data.
// Instead just return the original variable that was passed in (likely NULL)
return $arr;
}
/**
* Removes prefix to all array keys, mainly for reportings and joining array data together to avoid conflicting keys.
* @param $prefix
* @param $arr
* @param null $ignore_elements
* @return array|bool
*/
static function removeKeyPrefix( $prefix, $arr, $ignore_elements = null ) {
if ( is_array( $arr ) ) {
$retarr = [];
foreach ( $arr as $key => $value ) {
if ( !is_array( $ignore_elements ) || ( is_array( $ignore_elements ) && !in_array( $key, $ignore_elements ) ) ) {
$retarr[self::strReplaceOnce( $prefix, '', $key )] = $value;
} else {
$retarr[$key] = $value;
}
}
if ( empty( $retarr ) == false ) {
return $retarr;
}
}
return false;
}
/**
* Adds sort prefixes to an array maintaining the original order. Primarily used because Flex likes to reorded arrays with string keys.
* @param $arr
* @param int $begin_counter
* @return array
*/
static function addSortPrefix( $arr, $begin_counter = 1 ) {
if ( is_array( $arr ) ) {
$retarr = [];
$i = $begin_counter;
foreach ( $arr as $key => $value ) {
$sort_prefix = null;
if ( substr( $key, 0, 1 ) != '-' ) {
$sort_prefix = '-' . str_pad( $i, 4, 0, STR_PAD_LEFT ) . '-';
}
$retarr[$sort_prefix . $key] = $value;
$i++;
}
if ( empty( $retarr ) == false ) {
return $retarr;
}
}
//Return original variable if we can't add any sort prefixes. This prevents array_merge() from fatal erroring if we return false due to it not being an array.
return $arr;
}
/**
* Removes sort prefixes from an array.
* @param $value
* @param bool $trim_arr_value
* @return array|mixed
*/
static function trimSortPrefix( $value, $trim_arr_value = false ) {
if ( $value != '' ) {
$trim_sort_prefix_regex = '/^-[0-9]{3,9}-/i';
$retval = [];
if ( is_array( $value ) && count( $value ) > 0 ) {
foreach ( $value as $key => $val ) {
if ( $trim_arr_value == true ) {
$retval[$key] = preg_replace( $trim_sort_prefix_regex, '', $val );
} else {
$retval[preg_replace( $trim_sort_prefix_regex, '', $key )] = $val;
}
}
} else {
$retval = preg_replace( $trim_sort_prefix_regex, '', $value );
}
if ( empty( $retval ) == false ) {
return $retval;
}
}
return $value;
}
/**
* @param $str_pattern
* @param $str_replacement
* @param $string
* @return mixed
*/
static function strReplaceOnce( $str_pattern, $str_replacement, $string ) {
if ( strpos( $string, $str_pattern ) !== false ) {
return substr_replace( $string, $str_replacement, strpos( $string, $str_pattern ), strlen( $str_pattern ) );
}
return $string;
}
/**
* @param $file_name
* @param $type
* @param $size
* @return bool
*/
static function FileDownloadHeader( $file_name, $type, $size ) {
if ( $file_name == '' || $size == '' ) {
return false;
}
Debug::Text( 'Downloading File: '. $file_name .' Type: '. $type .' Size: '. $size, __FILE__, __LINE__, __METHOD__, 10 );
Header( 'Content-Type: ' . $type );
//Header('Content-disposition: inline; filename='.$file_name); //Displays document inline (in browser window) if available
Header( 'Content-Disposition: attachment; filename="' . $file_name . '"' ); //Forces document to download
Header( 'Content-Length: ' . $size );
return true;
}
/**
* This function helps sending binary data to the client for saving/viewing as a file.
* @param $file_name
* @param $type
* @param $data
* @return bool
* @noinspection PhpInconsistentReturnPointsInspection
*/
static function APIFileDownload( $file_name, $type, $data ) {
if ( $file_name == '' || $data == '' ) {
return false;
}
if ( is_array( $data ) ) {
return false;
}
$size = strlen( $data );
self::FileDownloadHeader( $file_name, $type, $size );
echo $data;
//Don't return any TRUE/FALSE here as it could end up in the file.
}
/**
* value should be a float and not a string. be sure to run this before TTi18n currency or number formatter due to foreign numeric formatting for decimal being a comma.
* @param $value float
* @param int $minimum_decimals
* @return string
*/
static function removeTrailingZeros( $value, $minimum_decimals = 2 ) {
//Remove trailing zeros after the decimal, leave a minimum of X though.
//*NOTE: This should always be passed in a float, so we don't need to worry about locales or TTi18n::getDecimalSymbol(), since we don't set LC_NUMERIC anymore.
// If you are running into problems traced to here, try casting to float first.
// If a casted float value is float(50), there won't be a decimal place, so make sure we handle those cases too.
if ( is_float( $value ) || strpos( $value, '.' ) !== false ) {
$trimmed_value = (float)$value;
if ( strpos( $trimmed_value, '.' ) !== false ) {
$tmp_minimum_decimals = strlen( (int)strrev( $trimmed_value ) );
} else {
$tmp_minimum_decimals = 0;
}
if ( $tmp_minimum_decimals > $minimum_decimals ) {
$minimum_decimals = $tmp_minimum_decimals;
}
return number_format( $trimmed_value, $minimum_decimals, '.', '' );
}
return $value;
}
/**
* Compares two float values for equality and greater/less than. Required because floats should never be compared directly due to epsilon differences
* For example: (float)845.92 + (float)14.3 != (float)860.22 -- Yet it does as far as a human is concerned.
* @param $float1
* @param $float2
* @param string $operator
* @return bool
*/
static function compareFloat( $float1, $float2, $operator = '==' ) {
$retval = false;
$bc_comp_result = bccomp( $float1, $float2 );
switch ( $operator ) {
case '==':
if ( bccomp( $float1, $float2 ) === 0 ) {
$retval = true;
}
break;
case '>=':
if ( $bc_comp_result >= 0 ) {
$retval = true;
}
break;
case '<=':
if ( $bc_comp_result <= 0 ) {
$retval = true;
}
break;
case '>':
if ( $bc_comp_result === 1 ) {
$retval = true;
}
break;
case '<':
if ( $bc_comp_result === -1 ) {
$retval = true;
}
break;
}
return $retval;
}
/**
* Just a number format that looks like currency without currency symbol
* can maybe be replaced by TTi18n::numberFormat()
*
* @param $value
* @param bool $pretty
* @return string
*/
static function MoneyFormat( $value, $pretty = true ) {
if ( $pretty === true ) {
$thousand_sep = TTi18n::getThousandsSymbol();
} else {
$thousand_sep = '';
}
return number_format( (float)$value, 2, TTi18n::getDecimalSymbol(), $thousand_sep );
}
/**
* Round currency value without formatting it. In most cases where Misc::MoneyFormat( $var, FALSE ) is used, this should be used instead.
*
* @param float|int $value
* @param int $decimals
* @param null|CurrencyFactory $currency_obj
* @return float|int
*/
static function MoneyRound( $value, $decimals = 2, $currency_obj = null ) {
if ( is_object( $currency_obj ) ) {
$retval = $currency_obj->round( $value );
} else {
//When using round() it returns a float, so large values like 100000000000000000000.00 get converted to scientific notation when passed to bcmath() due to the string conversion. Use number_format() instead.
//$retval = round( $value, $decimals );
//Could use bcadd( $value, 0, $decimals ) to round larger values perhaps?
$retval = number_format( (float)$value, $decimals, '.', '' );
}
return $retval;
}
/**
* @param $value
* @param int $decimals
* @param null $currency_obj
* @return string
*/
static function MoneyRoundDifference( $value, $decimals = 2, $currency_obj = null ) {
$rounded_value = Misc::MoneyRound( $value, $decimals, $currency_obj );
$rounding_diff = bcsub( $rounded_value, $value );
Debug::Text( 'Input Value: ' . $value .' Rounded Value: '. $rounding_diff .' Diff: '. $rounding_diff, __FILE__, __LINE__, __METHOD__, 10 );
return $rounding_diff;
}
/**
* Removes vowels from the string always keeping the first and last letter.
* @param $str
* @return bool|string
*/
static function abbreviateString( $str ) {
$vowels = [ 'a', 'e', 'i', 'o', 'u' ];
$retarr = [];
$words = explode( ' ', trim( $str ) );
if ( is_array( $words ) ) {
foreach ( $words as $word ) {
$first_letter_in_word = substr( $word, 0, 1 );
$last_letter_in_word = substr( $word, -1, 1 );
$word = str_ireplace( $vowels, '', trim( $word ) );
if ( substr( $word, 0, 1 ) != $first_letter_in_word ) {
$word = $first_letter_in_word . $word;
}
if ( substr( $word, -1, 1 ) != $last_letter_in_word ) {
$word .= $last_letter_in_word;
}
$retarr[] = $word;
}
return implode( ' ', $retarr );
}
return false;
}
/**
* @param $str
* @param $length
* @param int $start
* @param bool $abbreviate
* @return string
*/
static function TruncateString( $str, $length, $start = 0, $abbreviate = false ) {
if ( strlen( $str ) > $length ) {
if ( $abbreviate == true ) {
//Try abbreviating it first.
$retval = trim( substr( self::abbreviateString( $str ), $start, $length ) );
if ( strlen( $retval ) > $length ) {
$retval .= '...';
}
} else {
$retval = trim( substr( trim( $str ), $start, $length ) ) . '...';
}
} else {
$retval = $str;
}
return $retval;
}
/**
* @param $bool
* @return string
*/
static function HumanBoolean( $bool ) {
if ( $bool == true ) {
return 'Yes';
} else {
return 'No';
}
}
/**
* @param float|int|string $float
* @return int
*/
static function getBeforeDecimal( $float ) {
$float = (float)$float;
//Locale agnostic, so we can handle decimal separators that are commas.
if ( strpos( $float, ',' ) !== false ) {
$separator = ',';
} else {
$separator = '.';
}
$split_float = explode( $separator, (float)$float );
return (int)$split_float[0];
}
/**
* @param float|int|string $float
* @param bool $format_number
* @return int
*/
static function getAfterDecimal( $float, $format_number = true ) {
if ( $format_number == true ) {
$float = Misc::MoneyRound( $float );
}
//Locale agnostic, so we can handle decimal separators that are commas.
if ( strpos( $float, ',' ) !== false ) {
$separator = ',';
} else {
$separator = '.';
}
$split_float = explode( $separator, $float );
if ( isset( $split_float[1] ) ) {
return (int)$split_float[1];
} else {
return 0;
}
}
/**
* @param $value
* @return mixed
*/
static function removeDecimal( $value ) {
return str_replace( '.', '', number_format( $value, 2, '.', '' ) );
}
/**
* Encode integer to a alphanumeric value that is reversible.
* @param $int
* @return string
*/
static function encodeInteger( $int ) {
if ( $int != '' ) {
return strtoupper( base_convert( strrev( str_pad( $int, 11, 0, STR_PAD_LEFT ) ), 10, 36 ) );
}
return $int;
}
/**
* @param $str
* @param int $max
* @return int
*/
static function decodeInteger( $str, $max = 2147483646 ) {
$retval = (int)str_pad( strrev( base_convert( $str, 36, 10 ) ), 11, 0, STR_PAD_RIGHT );
if ( $retval > $max ) { //This helps prevent out of range errors in SQL queries.
Debug::Text( 'Decoding string to int, exceeded max: ' . $str . ' Max: ' . $max, __FILE__, __LINE__, __METHOD__, 10 );
$retval = 0;
}
return $retval;
}
/**
* @param $current
* @param $maximum
* @param int $precision
* @return float|int
*/
static function calculatePercent( $current, $maximum, $precision = 0 ) {
if ( $maximum == 0 ) {
return 100;
}
$percent = round( ( ( $current / $maximum ) * 100 ), (int)$precision );
if ( $precision == 0 ) {
$percent = (int)$percent;
}
return $percent;
}
//
/**
* Takes an array with columns, and a 2nd array with column names to sum.
* @param $data
* @param $sum_elements
* @return bool|int|string
*/
static function sumMultipleColumns( $data, $sum_elements ) {
if ( !is_array( $data ) ) {
return false;
}
if ( !is_array( $sum_elements ) ) {
return false;
}
$retval = 0;
foreach ( $sum_elements as $sum_element ) {
if ( isset( $data[$sum_element] ) ) {
$retval = bcadd( $retval, $data[$sum_element] );
//Debug::Text('Found Element in Source Data: '. $sum_element .' Retval: '. $retval, __FILE__, __LINE__, __METHOD__, 10);
}
}
return $retval;
}
/**
* @param $amount
* @param $element
* @param array $include_elements
* @param array $exclude_elements
* @return int
*/
static function calculateIncludeExcludeAmount( $amount, $element, $include_elements = [], $exclude_elements = [] ) {
//Make sure the element isnt in both include and exclude.
if ( in_array( $element, $include_elements ) && !in_array( $element, $exclude_elements ) ) {
return $amount;
} else if ( in_array( $element, $exclude_elements ) && !in_array( $element, $include_elements ) ) {
return ( $amount * -1 );
} else {
return 0;
}
}
/**
* @param $data
* @param array $include_elements
* @param array $exclude_elements
* @return bool|int|string
*/
static function calculateMultipleColumns( $data, $include_elements = [], $exclude_elements = [] ) {
if ( !is_array( $data ) ) {
return false;
}
$retval = 0;
if ( is_array( $include_elements ) ) {
foreach ( $include_elements as $include_element ) {
if ( isset( $data[$include_element] ) ) {
$retval = bcadd( $retval, $data[$include_element] );
//Debug::Text('Found Element in Source Data: '. $sum_element .' Retval: '. $retval, __FILE__, __LINE__, __METHOD__, 10);
}
}
}
if ( is_array( $exclude_elements ) ) {
foreach ( $exclude_elements as $exclude_element ) {
if ( isset( $data[$exclude_element] ) ) {
$retval = bcsub( $retval, $data[$exclude_element] );
//Debug::Text('Found Element in Source Data: '. $sum_element .' Retval: '. $retval, __FILE__, __LINE__, __METHOD__, 10);
}
}
}
return $retval;
}
/**
* @param $array
* @param $element
* @param int $start
* @return mixed
*/
static function getPointerFromArray( $array, $element, $start = 1 ) {
//Debug::Arr($array, 'Source Array: ', __FILE__, __LINE__, __METHOD__, 10);
//Debug::Text('Searching for Element: '. $element, __FILE__, __LINE__, __METHOD__, 10);
$keys = array_keys( $array );
//Debug::Arr($keys, 'Source Array Keys: ', __FILE__, __LINE__, __METHOD__, 10);
//Debug::Text($keys, 'Source Array Keys: ', __FILE__, __LINE__, __METHOD__, 10);
$key = array_search( $element, $keys );
if ( $key !== false ) {
$key = ( $key + $start );
}
//Debug::Arr($key, 'Result: ', __FILE__, __LINE__, __METHOD__, 10);
return $key;
}
/**
* @param $coord
* @param $adjust_coord
* @return mixed
*/
static function AdjustXY( $coord, $adjust_coord ) {
return ( $coord + $adjust_coord );
}
/**
* @param $hex
* @param bool $asString
* @return array|string
*/
static function hex2rgb( $hex, $asString = true ) {
// strip off any leading #
if ( 0 === strpos( $hex, '#' ) ) {
$hex = substr( $hex, 1 );
} else {
if ( 0 === strpos( $hex, '&H' ) ) {
$hex = substr( $hex, 2 );
}
}
// break into hex 3-tuple
$cutpoint = ( ceil( ( strlen( $hex ) / 2 ) ) - 1 );
$rgb = explode( ':', wordwrap( $hex, $cutpoint, ':', $cutpoint ), 3 );
// convert each tuple to decimal
$rgb[0] = ( isset( $rgb[0] ) ? hexdec( $rgb[0] ) : 0 );
$rgb[1] = ( isset( $rgb[1] ) ? hexdec( $rgb[1] ) : 0 );
$rgb[2] = ( isset( $rgb[2] ) ? hexdec( $rgb[2] ) : 0 );
return ( $asString ? "{$rgb[0]} {$rgb[1]} {$rgb[2]}" : $rgb );
}
/**
* Mititage CSV Injection attacks: See below links for more information:
* [1] https://www.owasp.org/index.php/CSV_Excel_Macro_Injection
* [2] https://hackerone.com/reports/72785
* [3] https://hackerone.com/reports/90131
* @param $input
* @return mixed
*/
static function escapeCSVTriggerChars( $input ) {
$input = trim( $input );
$first_char = substr( $input, 0, 1 );
$trigger_chars = [ '=', '-', '+', '|' ];
//Be sure to ignore negative numbers/dollar amounts here, as its not expected when using the API to retrieve such data and there shouldn't be risk of injections attacks that are all numeric.
if ( !is_numeric( $input ) && in_array( $first_char, $trigger_chars ) ) {
$retval = '\'' . $input; //Prepend with single quote "'" to force it to text.
} else {
$retval = $input;
}
return str_replace( '|', '\|', $retval ); //Make sure pipes are escaped anywhere in the string.
}
/**
* @param $data
* @param null $columns
* @param bool $ignore_last_row
* @param bool $include_header
* @param string $eol
* @return bool|null|string
*/
static function Array2CSV( $data, $columns = null, $ignore_last_row = true, $include_header = true, $eol = "\n" ) {
if ( is_array( $columns ) && count( $columns ) > 0 ) { //If data is FALSE or not an array, we still want to output some CSV encoded data.
if ( $ignore_last_row === true ) {
array_pop( $data );
}
//Header
if ( $include_header == true ) {
$row_header = [];
foreach ( $columns as $column_name ) {
$row_header[] = $column_name;
}
$out = '"' . implode( '","', $row_header ) . '"' . $eol;
} else {
$out = null;
}
if ( is_array( $data ) && count( $data ) > 0 ) {
foreach ( $data as $rows ) {
$row_values = [];
foreach ( $columns as $column_key => $column_name ) {
if ( isset( $rows[$column_key] ) ) {
$row_values[] = str_replace( "\"", "\"\"", Misc::escapeCSVTriggerChars( $rows[$column_key] ) );
} else {
//Make sure we insert blank columns to keep proper order of values.
$row_values[] = null;
}
}
$out .= '"' . implode( '","', $row_values ) . '"' . $eol;
unset( $row_values );
}
}
return $out;
}
return false;
}
/**
* @param $data
* @param null $columns
* @return bool|null|string
*/
static function Array2JSON( $data, $columns = null ) {
if ( is_array( $columns ) && count( $columns ) > 0 ) { //If data is FALSE or not an array, we still want to output some JSON encoded data.
$out = [];
if ( is_array( $data ) && count( $data ) > 0 ) {
foreach ( $data as $rows ) {
$row_values = [];
foreach ( $columns as $column_key => $column_name ) {
if ( isset( $rows[$column_key] ) ) {
$row_values[$column_name] = $rows[$column_key];
}
}
$out[] = $row_values;
unset( $row_values );
}
}
return json_encode( $out, JSON_PRETTY_PRINT );
}
return false;
}
/**
* @param $data
* @return bool
*/
static function isJSON( $data ) {
if ( is_string( $data ) ) {
json_decode( $data );
return ( json_last_error() == JSON_ERROR_NONE );
}
return false;
}
/**
* @param $data
* @param null $columns
* @param null $column_format
* @param bool $ignore_last_row
* @param bool $include_xml_header
* @param string $root_element_name
* @param string $row_element_name
* @return bool|null|string
*/
static function Array2XML( $data, $columns = null, $column_format = null, $ignore_last_row = true, $include_xml_header = false, $root_element_name = 'data', $row_element_name = 'row' ) {
if ( is_array( $columns ) && count( $columns ) > 0 ) { //If data is FALSE or not an array, we still want to output some XML encoded data.
if ( $ignore_last_row === true ) {
array_pop( $data );
}
//Debug::Arr($column_format, 'Column Format: ', __FILE__, __LINE__, __METHOD__, 10);
$out = null;
if ( $include_xml_header == true ) {
$out .= '' . "\n";
}
$out .= '' . "\n";
$out .= ' ' . "\n";
$out .= ' ' . "\n";
$out .= ' ' . "\n";
$out .= ' ' . "\n";
$out .= ' ' . "\n";
$out .= ' ' . "\n";
foreach ( $columns as $column_key => $column_name ) {
$data_type = 'string';
if ( is_array( $column_format ) && isset( $column_format[$column_key] ) ) {
switch ( $column_format[$column_key] ) {
case 'report_date':
$data_type = 'string';
break;
case 'currency':
case 'percent':
case 'numeric':
$data_type = 'decimal';
break;
case 'time_unit':
$data_type = 'decimal';
break;
case 'date_stamp':
$data_type = 'date';
break;
case 'time':
$data_type = 'time';
break;
case 'time_stamp':
$data_type = 'dateTime';
break;
case 'boolean':
$data_type = 'string';
break;
default:
$data_type = 'string';
break;
}
}
$out .= ' ' . "\n";
}
unset( $column_name ); //code standards
$out .= ' ' . "\n";
$out .= ' ' . "\n";
$out .= ' ' . "\n";
$out .= ' ' . "\n";
$out .= ' ' . "\n";
$out .= ' ' . "\n";
$out .= '' . "\n";
if ( $root_element_name != '' ) {
$out .= '<' . $root_element_name . '>' . "\n";
}
if ( is_array( $data ) && count( $data ) > 0 ) {
foreach ( $data as $rows ) {
$out .= '<' . $row_element_name . '>' . "\n";
foreach ( $columns as $column_key => $column_name ) {
if ( isset( $rows[$column_key] ) ) {
$out .= ' <' . $column_key . '>' . $rows[$column_key] . '' . $column_key . '>' . "\n";
}
}
$out .= '' . $row_element_name . '>' . "\n";
}
}
if ( $root_element_name != '' ) {
$out .= '' . $root_element_name . '>' . "\n";
}
//Debug::Arr($out, 'XML: ', __FILE__, __LINE__, __METHOD__, 10);
return $out;
}
return false;
}
/**
* @param $factory_arr
* @param $filter_data
* @param $output_file
*/
static function Export2XML( $factory_arr, $filter_data, $output_file ) {
global $global_class_map;
$global_exclude_arr = [
'Factory',
'FactoryListIterator',
'SystemSettingFactory',
'CronJobFactory',
'CompanyUserCountFactory',
'HelpFactory',
'HelpGroupControlFactory',
'HelpGroupFactory',
'HierarchyFactory',
'HierarchyShareFactory',
'JobUserAllowFactory',
'JobItemAllowFactory',
'PolicyGroupAccrualPolicyFactory',
'PolicyGroupOverTimePolicyFactory',
'PolicyGroupPremiumPolicyFactory',
'PolicyGroupRoundIntervalPolicyFactory',
'ProductTaxPolicyProductFactory',
];
$dependency_tree = new DependencyTree();
$i = 0;
$global_class_dependancy_map = [];
foreach ( $global_class_map as $class => $file ) {
if ( stripos( $class, 'Factory' ) !== false
&& stripos( $class, 'API' ) === false && stripos( $class, 'ListFactory' ) === false && stripos( $class, 'Report' ) === false
&& !in_array( $class, $global_exclude_arr )
) {
if ( isset( $global_class_dependancy_map[$class] ) ) {
$dependency_tree->addNode( $class, $global_class_dependancy_map[$class], $class, $i );
} else {
$dependency_tree->addNode( $class, [], $class, $i );
}
}
$i++;
}
unset( $file ); //code standards
$ordered_factory_arr = $dependency_tree->getAllNodesInOrder();
//Debug::Arr($ordered_factory_arr, 'Ordered Factory List: ', __FILE__, __LINE__, __METHOD__, 10);
if ( is_array( $factory_arr ) && count( $factory_arr ) > 0 ) {
Debug::Arr( $factory_arr, 'Factory Filter: ', __FILE__, __LINE__, __METHOD__, 10 );
$filtered_factory_arr = [];
foreach ( $ordered_factory_arr as $factory ) {
if ( in_array( $factory, $factory_arr ) ) {
$filtered_factory_arr[] = $factory;
} // else { //Debug::Text('Removing factory: '. $factory .' due to filter...', __FILE__, __LINE__, __METHOD__, 10);
}
} else {
Debug::Text( 'Not filtering factory...', __FILE__, __LINE__, __METHOD__, 10 );
$filtered_factory_arr = $ordered_factory_arr;
}
unset( $ordered_factory_arr );
if ( isset( $filtered_factory_arr ) && count( $filtered_factory_arr ) > 0 ) {
@unlink( $output_file );
$fp = bzopen( $output_file, 'w' );
Debug::Arr( $filtered_factory_arr, 'Filtered/Ordered Factory List: ', __FILE__, __LINE__, __METHOD__, 10 );
Debug::Text( 'Exporting data...', __FILE__, __LINE__, __METHOD__, 10 );
foreach ( $filtered_factory_arr as $factory ) {
$class = str_replace( 'Factory', 'ListFactory', $factory );
$lf = new $class;
Debug::Text( 'Exporting ListFactory: ' . $factory . ' Memory Usage: ' . memory_get_usage() . ' Peak: ' . memory_get_peak_usage( true ), __FILE__, __LINE__, __METHOD__, 10 );
self::ExportListFactory2XML( $lf, $filter_data, $fp );
unset( $lf );
}
bzclose( $fp );
} else {
Debug::Text( 'No data to export...', __FILE__, __LINE__, __METHOD__, 10 );
}
}
///**
// * @param $lf
// * @param $filter_data
// * @param $file_pointer
// * @return bool
// * @noinspection PhpUndefinedConstantInspection
// */
//static function ExportListFactory2XML( $lf, $filter_data, $file_pointer ) {
// require_once( Environment::getBasePath() . 'classes/pear/XML/Serializer.php' );
//
// $serializer = new XML_Serializer( [
// XML_SERIALIZER_OPTION_INDENT => ' ',
// XML_SERIALIZER_OPTION_RETURN_RESULT => true,
// 'linebreak' => "\n",
// 'typeHints' => true,
// 'encoding' => 'UTF-8',
// 'rootName' => get_parent_class( $lf ),
// ]
// );
//
// $lf->getByCompanyId( $filter_data['company_id'] );
// if ( $lf->getRecordCount() > 0 ) {
// Debug::Text( 'Exporting ' . $lf->getRecordCount() . ' rows...', __FILE__, __LINE__, __METHOD__, 10 );
// foreach ( $lf as $obj ) {
// if ( isset( $obj->data ) ) {
// $result = $serializer->serialize( $obj->data );
// bzwrite( $file_pointer, $result . "\n" );
// //Debug::Arr($result, 'Data: ', __FILE__, __LINE__, __METHOD__, 10);
// } else {
// Debug::Text( 'Object \'data\' variable does not exist, cant export...', __FILE__, __LINE__, __METHOD__, 10 );
// }
// }
// unset( $result, $obj, $serializer );
// } else {
// Debug::Text( 'No rows to export...', __FILE__, __LINE__, __METHOD__, 10 );
//
// return false;
// }
//
// return true;
//}
/**
* @param $arr
* @param $search_key
* @param $search_value
* @return bool
*/
static function inArrayByKeyAndValue( $arr, $search_key, $search_value ) {
if ( !is_array( $arr ) && $search_key != '' && $search_value != '' ) {
return false;
}
//Debug::Text('Search Key: '. $search_key .' Search Value: '. $search_value, __FILE__, __LINE__, __METHOD__, 10);
//Debug::Arr($arr, 'Hay Stack: ', __FILE__, __LINE__, __METHOD__, 10);
foreach ( $arr as $arr_value ) {
if ( isset( $arr_value[$search_key] ) ) {
if ( $arr_value[$search_key] == $search_value ) {
return true;
}
}
}
return false;
}
/**
* This function is used to quickly preset array key => value pairs so we don't
* have to have so many isset() checks throughout the code.
* @param $arr
* @param $keys
* @param null $preset_value
* @return array|bool
*/
static function preSetArrayValues( $arr, $keys, $preset_value = null ) {
if ( ( $arr == '' || is_bool( $arr ) || is_null( $arr ) || is_array( $arr ) || is_object( $arr ) ) && is_array( $keys ) ) {
if ( !is_array( $arr ) && !is_object( $arr ) ) { //Avoid PHP deprecation warning: Automatic conversion of false to array is deprecated
$arr = [];
}
foreach ( $keys as $key ) {
if ( is_object( $arr ) ) {
if ( !isset( $arr->$key ) ) {
$arr->$key = $preset_value;
}
} else {
if ( !isset( $arr[$key] ) ) {
$arr[$key] = $preset_value;
}
}
}
} else {
Debug::Arr( $arr, 'ERROR: Unable to initialize preset array values! Current variable is: ', __FILE__, __LINE__, __METHOD__, 10 );
}
return $arr;
}
/**
* @param $file_name
* @param bool $buffer
* @param bool $keep_charset
* @param string $unknown_type
* @return bool|mixed|string
*/
static function getMimeType( $file_name, $buffer = false, $keep_charset = false, $unknown_type = 'application/octet-stream' ) {
if ( function_exists( 'finfo_buffer' ) ) { //finfo extension in PHP v5.3+
if ( $buffer == false && file_exists( $file_name ) ) {
//Its a filename passed in.
$finfo = finfo_open( FILEINFO_MIME_TYPE ); // return mime type ala mimetype extension
$retval = finfo_file( $finfo, $file_name );
finfo_close( $finfo );
} else if ( $buffer == true && $file_name != '' ) {
//Its a string buffer;
$finfo = new finfo( FILEINFO_MIME );
$retval = $finfo->buffer( $file_name );
}
if ( isset( $retval ) ) {
if ( $keep_charset == false ) {
$split_retval = explode( ';', $retval );
if ( is_array( $split_retval ) && isset( $split_retval[0] ) ) {
$retval = $split_retval[0];
}
}
Debug::text( 'MimeType: ' . $retval, __FILE__, __LINE__, __METHOD__, 10 );
return $retval;
}
} else {
//Attempt to detect mime type with PEAR MIME class.
if ( $buffer == false && file_exists( $file_name ) ) {
$retval = MIME_Type::autoDetect( $file_name );
if ( is_object( $retval ) ) { //MimeType failed.
//Attempt to detect mime type manually when finfo extension and PEAR Mime Type is not installed (windows)
$extension = strtolower( pathinfo( $file_name, PATHINFO_EXTENSION ) );
switch ( $extension ) {
case 'jpg':
$retval = 'image/jpeg';
break;
case 'png':
$retval = 'image/png';
break;
case 'gif':
$retval = 'image/gif';
break;
default:
$retval = $unknown_type;
break;
}
}
return $retval;
}
}
return false;
}
/**
* @param $file
* @param bool $skip_blank_lines
* @return int
*/
static function countLinesInFile( $file, $skip_blank_lines = true ) {
ini_set( 'auto_detect_line_endings', true ); //PHP can have problems detecting Mac/OSX line endings in some case, this should help solve that.
$line_count = 0;
$skipped_lines = 0;
$handle = fopen( $file, 'r' );
while ( !feof( $handle ) ) {
$line = fgets( $handle, 4096 );
if ( $skip_blank_lines == true && $line != '' && ( trim( $line ) == '' || trim( $line, ',' ) == "\n" ) ) { //Ignore lines that are all commas (ie: ",,,,,,,") which can often happen at the end of a CSV file that rows were deleted from.
$skipped_lines++;
} else {
$line_count += substr_count( $line, "\n" );
}
}
fclose( $handle );
ini_set( 'auto_detect_line_endings', false );
Debug::text( 'File has total lines: ' . $line_count . ' Blank Lines: ' . $skipped_lines, __FILE__, __LINE__, __METHOD__, 10 );
return $line_count;
}
/**
* Converts the first sheet in an Excel file to CSV.
* @param $xls_file
* @return bool
*/
static function convertExcelToCSV( $xls_file ) {
Debug::text( 'Attempting Excel to CSV conversion of: ' . $xls_file, __FILE__, __LINE__, __METHOD__, 10 );
//mime_content_type might not work properly on Windows. So if its not available just accept any file type.
if ( function_exists( 'mime_content_type' ) ) {
$mime_type = mime_content_type( $xls_file );
if ( $mime_type !== false && !in_array( $mime_type, [ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-excel', 'application/vnd.oasis.opendocument.spreadsheet' ] ) ) { //This should match upload_file.php
Debug::text( 'Invalid MIME TYPE: ' . $mime_type, __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
}
try {
$spreadsheet = PhpOffice\PhpSpreadsheet\IOFactory::load( $xls_file );
// Export to CSV file.
$writer = PhpOffice\PhpSpreadsheet\IOFactory::createWriter( $spreadsheet, 'Csv' );
$writer->setSheetIndex( 0 ); // Select which sheet to export.
$writer->setDelimiter( ',' ); // Set delimiter.
$writer->save( $xls_file );
unset( $spreadsheet, $writer );
} catch ( Exception $e ) {
return false;
}
return true;
}
/**
* Detects the delimiter of a CSV file (can be semicolon, comma or pipe) by trying every delimiter, then
* counting how many potential columns could be found with this delimiter and removing the delimiter from array of
* only one columns could be created (without a working limiter you'll always have "one" column: the entire row).
* The delimiter that created the most columns is returned.
*
* @param string $file path to the CSV file
* @return string|null nullable delimiter
* @throws \Exception
*/
public static function detectFileDelimiter( $file ) {
$delimiters = [
',' => 0,
';' => 0,
':' => 0,
'|' => 0,
"\t" => 0, //Tab
//Can't use space, as column headers will often have many spaces for multiple names.
];
$handle = fopen( $file, 'r' );
$first_line = fgets( $handle );
fclose( $handle );
foreach ( $delimiters as $delimiter_character => $delimiter_count ) {
$found_columns_with_this_delimiter = count( str_getcsv( $first_line, $delimiter_character ) );
if ( $found_columns_with_this_delimiter > 1 ) {
$delimiters[$delimiter_character] = $found_columns_with_this_delimiter;
} else {
unset( $delimiters[$delimiter_character] );
}
}
if ( !empty( $delimiters ) ) {
return array_search( max( $delimiters ), $delimiters );
}
return false; //The CSV delimiter could not been found. Should be semicolon, comma or pipe!
}
/**
* @param $file
* @param bool $head
* @param bool $first_column
* @param string $delim
* @param int $len
* @param null $max_lines
* @return array|bool
*/
static function parseCSV( $file, $head = false, $first_column = false, $delim = ',', $len = 9216, $max_lines = null ) {
if ( !file_exists( $file ) ) {
Debug::text( 'Files does not exist: ' . $file, __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
//mime_content_type might not work properly on Windows. So if its not available just accept any file type.
if ( function_exists( 'mime_content_type' ) ) {
$mime_type = mime_content_type( $file );
if ( $mime_type !== false && !in_array( $mime_type, [ 'text/plain', 'plain/text', 'text/comma-separated-values', 'text/csv', 'application/csv', 'text/anytext', 'text/x-c', 'application/octet-stream' ] ) ) { //This should match upload_file.php
Debug::text( 'Invalid MIME TYPE: ' . $mime_type, __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
}
//If no delimiter is specified, try to detect it automatically.
if ( empty( $delim ) ) {
$delim = Misc::detectFileDelimiter( $file );
Debug::text( ' Detected delimiter as: ' . $delim, __FILE__, __LINE__, __METHOD__, 10 );
if ( $delim === false ) {
Debug::text( ' Delimiter is empty, unable to parse file!', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
}
$return = [];
$handle = fopen( $file, 'r' );
if ( $head !== false ) {
if ( $first_column !== false ) {
while ( ( $header = fgetcsv( $handle, $len, $delim ) ) !== false ) {
if ( $header[0] == $first_column ) {
$found_header = true;
break;
}
}
if ( $found_header !== true ) {
return false;
}
} else {
$header = fgetcsv( $handle, $len, $delim );
}
}
//Excel adds a Byte Order Mark (BOM) to the beginning of files with UTF-8 characters. That needs to be stripped off otherwise it looks like a space and columns don't match up.
if ( isset( $header ) && isset( $header[0] ) ) {
$header[0] = str_replace( "\xEF\xBB\xBF", '', $header[0] );
}
$i = 1;
while ( ( $data = fgetcsv( $handle, $len, $delim ) ) !== false ) {
if ( $data !== [ null ] ) { // Ignore blank lines
//Skip lines with commas (columns), but *all* columns are blank. The raw line would look like this: ,,,,,,,,,,,... OR "","","","","","",...
if ( strlen( implode( '', $data ) ) == 0 ) {
continue;
}
if ( $head == true && isset( $header ) ) {
$row = [];
foreach ( $header as $key => $heading ) {
$row[trim( $heading )] = ( isset( $data[$key] ) ) ? $data[$key] : '';
}
$return[] = $row;
} else {
$return[] = $data;
}
if ( $max_lines !== null && $max_lines != '' && $i == $max_lines ) {
break;
}
$i++;
}
}
fclose( $handle );
ini_set( 'auto_detect_line_endings', false );
if ( empty( $return ) ) {
$return = false;
}
return $return;
}
/**
* @param $column_map
* @param $csv_arr
* @return array|bool
*/
static function importApplyColumnMap( $column_map, $csv_arr ) {
if ( !is_array( $column_map ) ) {
return false;
}
if ( !is_array( $csv_arr ) ) {
return false;
}
$retarr = [];
foreach ( $column_map as $map_arr ) {
$timetrex_column = $map_arr['timetrex_column'];
$csv_column = $map_arr['csv_column'];
$default_value = $map_arr['default_value'];
if ( isset( $csv_arr[$csv_column] ) && $csv_arr[$csv_column] != '' ) {
$retarr[$timetrex_column] = trim( $csv_arr[$csv_column] );
//echo "NOT using default value: ". $default_value ."\n";
} else if ( $default_value != '' ) {
//echo "using Default value! ". $default_value ."\n";
$retarr[$timetrex_column] = trim( $default_value );
}
}
if ( empty( $retarr ) == false ) {
return $retarr;
}
return false;
}
/**
* censor part of a string for purposes of displaying things like SIN, bank accounts, credit card numbers.
* @param $str
* @param string $censor_char
* @param int|null $min_first_chunk_size
* @param int|null $max_first_chunk_size
* @param int|null $min_last_chunk_size
* @param int|null $max_last_chunk_size
* @return bool|string
*/
static function censorString( $str, $censor_char = '*', $min_first_chunk_size = null, $max_first_chunk_size = null, $min_last_chunk_size = null, $max_last_chunk_size = null ) {
$length = strlen( $str );
if ( $length == 0 ) {
return $str;
}
if ( $str != '' ) {
if ( $length < 3 || $length <= ( $min_first_chunk_size + $min_last_chunk_size + 2 ) ) {
return str_repeat( $censor_char, $length ); //Default to all censored.
} else {
$first_chunk_size = ( floor( $length / 3 ) );
$last_chunk_size = ( floor( $length / 3 ) );
if ( $min_first_chunk_size != null && $first_chunk_size < $min_first_chunk_size ) {
$first_chunk_size = $min_first_chunk_size;
}
if ( $max_first_chunk_size != null && $first_chunk_size > $max_first_chunk_size ) {
$first_chunk_size = $max_first_chunk_size;
}
if ( $min_last_chunk_size != null && $last_chunk_size < $min_last_chunk_size ) {
$last_chunk_size = $min_last_chunk_size;
}
if ( $max_last_chunk_size != null && $last_chunk_size > $max_last_chunk_size ) {
$last_chunk_size = $max_last_chunk_size;
}
//Grab the first 1, and last 4 digits.
$first_chunk = substr( $str, 0, $first_chunk_size );
$last_chunk = substr( $str, ( $last_chunk_size * -1 ) );
$middle_chunk_size = ( $length - ( $first_chunk_size + $last_chunk_size ) );
$retval = $first_chunk . str_repeat( $censor_char, $middle_chunk_size ) . $last_chunk;
return $retval;
}
}
return false;
}
/**
* @param $str
* @param null $salt
* @param null $key
* @return bool|string
*/
static function encrypt( $str, $salt = null, $key = null ) {
if ( $str == '' || $str === false || empty( $str ) ) {
return false;
}
if ( $key == null || $key == '' ) {
global $config_vars;
if ( isset( $config_vars['other']['salt'] ) && $config_vars['other']['salt'] != '' ) {
$key = $config_vars['other']['salt'];
}
}
$key .= $salt;
$strong_seed = true; //passed by ref so we need it as a variable.
$iv = openssl_random_pseudo_bytes( openssl_cipher_iv_length( 'AES-256-CTR' ), $strong_seed );
$encrypted_data = '2:' . base64_encode( $iv ) . ':' . base64_encode( openssl_encrypt( trim( $str ), 'AES-256-CTR', $key, 0, $iv ) );
return $encrypted_data;
}
/**
* We can't reliably test that decryption was successful, so type checking should be done in the calling function. (see RemittanceDestinationAccountFactory::getValue3())
* @param $str
* @param null $salt
* @param null $key
* @return bool|string
*/
static function decrypt( $str, $salt = null, $key = null ) {
if ( $key == null || $key == '' ) {
global $config_vars;
if ( isset( $config_vars['other']['salt'] ) && $config_vars['other']['salt'] != '' ) {
$key = $config_vars['other']['salt'];
}
}
$key .= $salt;
if ( $str == '' ) {
return false;
}
$version = 1;
if ( strpos( $str, ':' ) !== false ) {
$bits = explode( ':', $str );
$version = $bits[0];
$iv = base64_decode( $bits[1] );
if ( isset( $bits[2] ) ) {
$encrypted_string = $bits[2];
} else {
$encrypted_string = $str;
}
unset( $bits );
if ( !isset( $version ) || $version == '' || !isset( $iv ) || $iv == '' ) {
Debug::Arr( $encrypted_string, 'ERROR: Required encryption data is blank: ' . $str, __FILE__, __LINE__, __METHOD__, 10 );
return $str; //allow for returning the unencrypted values that contain colons;
}
} else {
$encrypted_string = $str;
}
//Check to make sure $encrypted_string is base64_encoded.
if ( base64_encode( base64_decode( $encrypted_string, true ) ) !== $encrypted_string ) {
Debug::Arr( $encrypted_string, 'ERROR: String is not base64_encoded...', __FILE__, __LINE__, __METHOD__, 10 );
return $str; //allow for unencrypted values
} else {
$encrypted_string = base64_decode( $encrypted_string );
switch ( $version ) {
case 1:
//backwards compatibility for v1 encryption.
if ( function_exists( 'mcrypt_module_open' ) ) {
$td = @mcrypt_module_open( 'tripledes', '', 'ecb', '' );
$iv = @mcrypt_create_iv( mcrypt_enc_get_iv_size( $td ), MCRYPT_RAND );
$max_key_size = @mcrypt_enc_get_key_size( $td );
@mcrypt_generic_init( $td, substr( $key, 0, $max_key_size ), $iv );
$unencrypted_data = rtrim( @mdecrypt_generic( $td, $encrypted_string ) );
@mcrypt_generic_deinit( $td );
@mcrypt_module_close( $td );
} else {
Debug::Text( 'ERROR: MCRYPT extension is not installed!', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
break;
case 2: //'AES-256-CTR'
default:
$unencrypted_data = openssl_decrypt( $encrypted_string, 'AES-256-CTR', $key, null, $iv );
break;
}
}
if ( $unencrypted_data != '' && ctype_print( $unencrypted_data ) === false ) { //Check for only ASCII characters.
Debug::Text( ' ERROR: Unencrypted data is not ASCII characters, decryption likely failed! Raw Data: '. $unencrypted_data, __FILE__, __LINE__, __METHOD__, 10 );
$unencrypted_data = null;
}
/**
* We can't reliably test that decryption was successful, so type checking should be done in the calling function. (see RemittanceDestinationAccountFactory::getValue3())
*/
return $unencrypted_data;
}
/**
* @param $values
* @param null $name
* @param bool $assoc
* @param bool $object
* @return string
*/
static function getJSArray( $values, $name = null, $assoc = false, $object = false ) {
if ( $name != '' && (bool)$assoc == true ) {
$retval = 'new Array();';
if ( is_array( $values ) && count( $values ) > 0 ) {
foreach ( $values as $key => $value ) {
$retval .= $name . '[\'' . $key . '\']=\'' . $value . '\';';
}
}
} else if ( $name != '' && (bool)$object == true ) { //For multidimensional objects.
$retval = ' {';
if ( is_array( $values ) && count( $values ) > 0 ) {
foreach ( $values as $key => $value ) {
$retval .= $key . ': ';
if ( is_array( $value ) ) {
$retval .= '{';
foreach ( $value as $key2 => $value2 ) {
$retval .= $key2 . ': \'' . $value2 . '\', ';
}
$retval .= '}, ';
} else {
$retval .= $key . ': \'' . $value . '\', ';
}
}
}
$retval .= '} ';
} else {
$retval = 'new Array("';
if ( is_array( $values ) && count( $values ) > 0 ) {
$retval .= implode( '","', $values );
}
$retval .= '");';
}
return $retval;
}
/**
* Uses the internal array pointer to get array neighnors.
* @param $arr
* @param $key
* @param string $neighbor
* @return array
*/
static function getArrayNeighbors( $arr, $key, $neighbor = 'both' ) {
$neighbor = strtolower( $neighbor );
//Neighor can be: Prev, Next, Both
$retarr = [ 'prev' => false, 'next' => false ];
$keys = array_keys( $arr );
$key_indexes = array_flip( $keys );
if ( $neighbor == 'prev' || $neighbor == 'both' ) {
if ( isset( $keys[( $key_indexes[$key] - 1 )] ) ) {
$retarr['prev'] = $keys[( $key_indexes[$key] - 1 )];
}
}
if ( $neighbor == 'next' || $neighbor == 'both' ) {
if ( isset( $keys[( $key_indexes[$key] + 1 )] ) ) {
$retarr['next'] = $keys[( $key_indexes[$key] + 1 )];
}
}
//next($arr);
return $retarr;
}
/**
* @return string
*/
static function getURLProtocol() {
$retval = 'http';
if ( Misc::isSSL() == true ) {
$retval .= 's';
}
return $retval;
}
/**
* @param $url
* @return bool|mixed
*/
static function getRemoteHTTPFileSize( $url ) {
if ( function_exists( 'curl_exec' ) ) {
Debug::Text( 'Using CURL for HTTP...', __FILE__, __LINE__, __METHOD__, 10 );
$curl = curl_init();
//Don't require SSL verification, as the SSL certs may be out-of-date: http://stackoverflow.com/questions/316099/cant-connect-to-https-site-using-curl-returns-0-length-content-instead-what-c
curl_setopt( $curl, CURLOPT_SSL_VERIFYPEER, false );
curl_setopt( $curl, CURLOPT_SSL_VERIFYHOST, false );
curl_setopt( $curl, CURLOPT_URL, $url );
// Issue a HEAD request and follow any redirects.
curl_setopt( $curl, CURLOPT_NOBODY, true );
curl_setopt( $curl, CURLOPT_HEADER, true );
curl_setopt( $curl, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $curl, CURLOPT_FOLLOWLOCATION, true );
curl_setopt( $curl, CURLOPT_USERAGENT, APPLICATION_NAME . ' ' . APPLICATION_VERSION );
curl_exec( $curl );
$size = curl_getinfo( $curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD );
curl_close( $curl );
return $size;
} else {
Debug::Text( 'Using PHP streams for HTTP...', __FILE__, __LINE__, __METHOD__, 10 );
$headers = @get_headers( $url, 1 );
if ( $headers === false ) { //Failure downloading headers from URL.
return false;
}
$headers = array_change_key_case( $headers );
if ( isset( $headers[0] ) && stripos( $headers[0], '404 Not Found' ) !== false ) {
return false;
}
$retval = isset( $headers['content-length'] ) ? $headers['content-length'] : false;
return $retval;
}
}
/**
* @param $url
* @param $file_name
* @param $timeout int Seconds
* @return bool|int
*/
static function downloadHTTPFile( $url, $file_name, $timeout = 7200 ) {
Debug::Text( 'Downloading: ' . $url . ' To: ' . $file_name, __FILE__, __LINE__, __METHOD__, 10 );
if ( function_exists( 'curl_exec' ) ) {
Debug::Text( 'Using CURL for HTTP...', __FILE__, __LINE__, __METHOD__, 10 );
if ( is_writable( dirname( $file_name ) ) == true && ( file_exists( $file_name ) == false || ( file_exists( $file_name ) == true && is_writable( $file_name ) ) ) ) {
// Open file to write
$fp = @fopen( $file_name, 'w+' );
if ( $fp !== false ) {
$curl = curl_init();
//Don't require SSL verification, as the SSL certs may be out-of-date: http://stackoverflow.com/questions/316099/cant-connect-to-https-site-using-curl-returns-0-length-content-instead-what-c
curl_setopt( $curl, CURLOPT_SSL_VERIFYPEER, false );
curl_setopt( $curl, CURLOPT_SSL_VERIFYHOST, false );
curl_setopt( $curl, CURLOPT_URL, $url );
curl_setopt( $curl, CURLOPT_FOLLOWLOCATION, true );
curl_setopt( $curl, CURLOPT_RETURNTRANSFER, false ); // Set return transfer to false
curl_setopt( $curl, CURLOPT_BINARYTRANSFER, true );
curl_setopt( $curl, CURLOPT_CONNECTTIMEOUT, 10 );
curl_setopt( $curl, CURLOPT_TIMEOUT, $timeout ); //0=never timeout.
curl_setopt( $curl, CURLOPT_FILE, $fp ); // Write data to local file
curl_exec( $curl );
curl_close( $curl );
fclose( $fp );
if ( file_exists( $file_name ) ) {
$file_size = filesize( $file_name );
if ( $file_size > 0 ) {
Debug::Text( ' Successfully downloaded... Size: ' . $file_size, __FILE__, __LINE__, __METHOD__, 10 );
return (int)$file_size;
}
}
Debug::Text( 'ERROR: File download failed: ' . $file_name, __FILE__, __LINE__, __METHOD__, 10 );
return false;
} else {
Debug::Text( 'ERROR: Unable to open file for download/writing, likely permission problem?: ' . $file_name .' Error: '. Debug::getLastPHPErrorMessage(), __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
} else {
Debug::Text( 'ERROR: Download directory/file not writable, likely permission problem?: ' . $file_name, __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
} else {
Debug::Text( 'Using PHP streams for HTTP...', __FILE__, __LINE__, __METHOD__, 10 );
$retval = @file_put_contents( $file_name, fopen( $url, 'r' ) );
if ( $retval === false ) {
Debug::Text( 'ERROR: Unable to save/download file, likely permission or network access problem?: ' . $file_name .' Error: '. Debug::getLastPHPErrorMessage(), __FILE__, __LINE__, __METHOD__, 10 );
}
return $retval;
}
}
/**
* @return string
*/
static function getEmailDomain() {
global $config_vars;
if ( isset( $config_vars['mail']['email_domain'] ) && $config_vars['mail']['email_domain'] != '' ) {
$domain = $config_vars['mail']['email_domain'];
} else if ( isset( $config_vars['other']['email_domain'] ) && $config_vars['other']['email_domain'] != '' ) {
$domain = $config_vars['other']['email_domain'];
} else {
Debug::Text( 'No From Email Domain set, falling back to regular hostname...', __FILE__, __LINE__, __METHOD__, 10 );
$domain = self::getHostName( false );
}
return $domain;
}
/**
* @return string
*/
static function getEmailLocalPart() {
global $config_vars;
if ( isset( $config_vars['mail']['email_local_part'] ) && $config_vars['mail']['email_local_part'] != '' ) {
$local_part = $config_vars['mail']['email_local_part'];
} else if ( isset( $config_vars['other']['email_local_part'] ) && $config_vars['other']['email_local_part'] != '' ) {
$local_part = $config_vars['other']['email_local_part'];
} else {
Debug::Text( 'No Email Local Part set, falling back to default...', __FILE__, __LINE__, __METHOD__, 10 );
$local_part = 'DoNotReply';
}
return $local_part;
}
/**
* @param null $email
* @return string
*/
static function getEmailReturnPathLocalPart( $email = null ) {
global $config_vars;
if ( isset( $config_vars['other']['email_return_path_local_part'] ) && $config_vars['other']['email_return_path_local_part'] != '' ) {
$local_part = $config_vars['other']['email_return_path_local_part'];
} else {
Debug::Text( 'No Email Local Part set, falling back to default...', __FILE__, __LINE__, __METHOD__, 10 );
$local_part = self::getEmailLocalPart();
}
//In case we need to put the original TO address in the bounce local part.
//This could be an array in some cases.
// if ( $email != '' ) {
// $local_part .= '+';
// }
return $local_part;
}
/**
* Checks if the domain the user is seeing in their browser matches the configured domain that should be used.
* If not we can then do a redirect.
* @return bool
*/
static function checkValidDomain() {
global $config_vars;
if ( PRODUCTION == true && isset( $config_vars['other']['enable_csrf_validation'] ) && $config_vars['other']['enable_csrf_validation'] == true ) {
//Use HTTP_HOST rather than getHostName() as the same site can be referenced with multiple different host names
//Especially considering on-site installs that default to 'localhost'
//If deployment ondemand is set, then we assume SERVER_NAME is correct and revert to using that instead of HTTP_HOST which has potential to be forged.
//Apache's UseCanonicalName On configuration directive can help ensure the SERVER_NAME is always correct and not masked.
if ( DEPLOYMENT_ON_DEMAND == false && isset( $_SERVER['HTTP_HOST'] ) ) {
$host_name = $_SERVER['HTTP_HOST'];
} else if ( isset( $_SERVER['SERVER_NAME'] ) ) {
$host_name = $_SERVER['SERVER_NAME'];
} else if ( isset( $_SERVER['HOSTNAME'] ) ) {
$host_name = $_SERVER['HOSTNAME'];
} else {
$host_name = '';
}
if ( isset( $config_vars['other']['hostname'] ) && $config_vars['other']['hostname'] != '' ) {
$search_result = strpos( $config_vars['other']['hostname'], $host_name );
if ( $search_result === false || (int)$search_result >= 8 ) { //Check to see if .ini hostname is found within SERVER_NAME in less than the first 8 chars, so we ignore https://.
$redirect_url = Misc::getURLProtocol() . '://' . Misc::getHostName() . Environment::getDefaultInterfaceBaseURL();
Debug::Text( 'Web Server Hostname: ' . $host_name . ' does not match .ini specified hostname: ' . $config_vars['other']['hostname'] . ' Redirect: ' . $redirect_url, __FILE__, __LINE__, __METHOD__, 10 );
$rl = TTNew( 'RateLimit' ); /** @var RateLimit $rl */
$rl->setID( 'authentication_' . Misc::getRemoteIPAddress() );
$rl->setAllowedCalls( 5 );
$rl->setTimeFrame( 60 ); //1 minute
sleep( 1 ); //Help prevent fast redirect loops.
if ( $rl->check() == false ) {
Debug::Text( 'ERROR: Excessive redirects... sending to down for maintenance page to stop the loop: ' . Misc::getRemoteIPAddress() . ' for up to 1 minutes...', __FILE__, __LINE__, __METHOD__, 10 );
Redirect::Page( URLBuilder::getURL( [ 'exception' => 'domain_redirect_loop' ], Environment::getBaseURL() . 'html5/DownForMaintenance.php' ) );
} else {
Redirect::Page( URLBuilder::getURL( null, $redirect_url ) );
}
}
//else {
// Debug::Text( 'Domain matches!', __FILE__, __LINE__, __METHOD__, 10);
//}
}
}
return true;
}
/**
* @param $host_name
* @return string
*/
static function getHostNameWithoutSubDomain( $host_name ) {
$split_host_name = explode( '.', $host_name );
if ( count( $split_host_name ) > 2 ) {
unset( $split_host_name[0] );
return implode( '.', $split_host_name );
}
return $host_name;
}
/**
* @param bool $include_port
* @return string
*/
static function getHostName( $include_port = true ) {
global $config_vars;
$server_port = null;
//Can't use the SERVER_PORT if its going through a proxy, as the server port and proxy port could be different, but we don't know what the proxy port is in all cases.
if ( isset( $_SERVER['SERVER_PORT'] )
&& isset( $config_vars['other']['proxy_protocol_header_name'] ) && $config_vars['other']['proxy_protocol_header_name'] != '' && !isset($_SERVER[$config_vars['other']['proxy_protocol_header_name']]) ) {
$server_port = ':' . (int)$_SERVER['SERVER_PORT'];
}
if ( defined( 'DEPLOYMENT_ON_DEMAND' ) && DEPLOYMENT_ON_DEMAND == true && isset( $config_vars['other']['hostname'] ) && $config_vars['other']['hostname'] != '' ) {
$server_domain = $config_vars['other']['hostname'];
} else {
//Try server hostname/servername first, than fallback on .ini hostname setting.
//If the admin sets the hostname in the .ini file, always use that, as the servers hostname from the CLI could be incorrect.
if ( isset( $config_vars['other']['hostname'] ) && $config_vars['other']['hostname'] != '' ) {
$server_domain = trim( $config_vars['other']['hostname'] ); //Trim gets rid of newlines that could get into the .ini file. WARNING(2): Header may not contain more than a single header, new line detected
if ( strpos( $server_domain, ':' ) === false ) {
//Add port if its not already specified.
$server_domain .= $server_port;
}
} else if ( isset( $_SERVER['HTTP_HOST'] ) ) { //Use HTTP_HOST instead of SERVER_NAME first so it includes any custom ports.
$server_domain = $_SERVER['HTTP_HOST'];
} else if ( isset( $_SERVER['SERVER_NAME'] ) ) {
$server_domain = $_SERVER['SERVER_NAME'] . $server_port;
} else if ( isset( $_SERVER['HOSTNAME'] ) ) {
$server_domain = $_SERVER['HOSTNAME'] . $server_port;
} else {
Debug::Text( 'Unable to determine hostname, falling back to localhost...', __FILE__, __LINE__, __METHOD__, 10 );
$server_domain = 'localhost' . $server_port;
}
}
if ( $include_port == false && $server_port != '' ) {
//strip off port, important for sending emails.
$server_domain = str_replace( $server_port, '', $server_domain );
}
return $server_domain;
}
/**
* @param $database_host_string
* @return array
*/
static function parseDatabaseHostString( $database_host_string ) {
$retarr = [];
$db_hosts = explode( ',', str_replace( ' ', '', $database_host_string ) );
if ( is_array( $db_hosts ) ) {
$i = 0;
foreach ( $db_hosts as $db_host ) {
$db_host_split = explode( '#', $db_host );
$db_host = $db_host_split[0];
$weight = ( isset( $db_host_split[1] ) ) ? $db_host_split[1] : 1;
$retarr[] = [ $db_host, ( $i == 0 ) ? 'write' : 'readonly', $weight ];
$i++;
}
}
//Debug::Arr( $retarr, 'Parsed Database Connections: ', __FILE__, __LINE__, __METHOD__, 1);
return $retarr;
}
/**
* @param $address
* @param int $port
* @param int $timeout
* @return bool
*/
static function isOpenPort( $address, $port = 80, $timeout = 3 ) {
$checkport = @fsockopen( $address, $port, $errnum, $errstr, $timeout ); //The 2 is the time of ping in secs
//Check if port is closed or open...
if ( $checkport == false ) {
return false;
}
return true;
}
/**
* Accepts a search_str and key=>val array that it searches through, to return the array key of the closest fuzzy match.
* @param $search_str
* @param $search_arr
* @param int $minimum_percent_match
* @param bool $return_all_matches
* @return array|bool|mixed
*/
static function findClosestMatch( $search_str, $search_arr, $minimum_percent_match = 0, $return_all_matches = false ) {
if ( $search_str == '' ) {
return false;
}
if ( !is_array( $search_arr ) || count( $search_arr ) == 0 ) {
return false;
}
$matches = [];
foreach ( $search_arr as $key => $search_val ) {
similar_text( strtolower( $search_str ), strtolower( $search_val ), $percent );
if ( $percent >= $minimum_percent_match ) {
$matches[$key] = $percent;
}
}
if ( empty( $matches ) == false ) {
arsort( $matches );
if ( $return_all_matches == true ) {
return $matches;
}
//Debug::Arr( $search_arr, 'Search Str: '. $search_str .' Search Array: ', __FILE__, __LINE__, __METHOD__, 10);
//Debug::Arr( $matches, 'Matches: ', __FILE__, __LINE__, __METHOD__, 10);
reset( $matches );
return key( $matches );
}
//Debug::Text('No match found for: '. $search_str, __FILE__, __LINE__, __METHOD__, 10);
return false;
}
/**
* Converts a number between 0 and 25 to the corresponding letter.
* @param $number
* @return bool|string
*/
static function NumberToLetter( $number ) {
if ( $number > 25 ) {
return false;
}
return chr( ( $number + 65 ) );
}
/**
* @param $value
* @param int $old_min
* @param int $old_max
* @param int $new_min
* @param int $new_max
* @return float|int
*/
static function reScaleRange( $value, $old_min = 1, $old_max = 5, $new_min = 1, $new_max = 10 ) {
if ( $value === '' || $value === null ) {
return $value;
} else {
$retval = ( ( ( ( $value - $old_min ) * ( $new_max - $new_min ) ) / ( $old_max - $old_min ) ) + $new_min );
return $retval;
}
}
/**
* @param $var
* @param null $default
* @return null
*/
static function issetOr( &$var, $default = null ) {
if ( isset( $var ) ) {
return $var;
}
return $default;
}
/**
* @param $first_name
* @param $middle_name
* @param $last_name
* @param bool $reverse
* @param bool $include_middle
* @return bool|string
*/
static function getFullName( $first_name, $middle_name, $last_name, $reverse = false, $include_middle = true ) {
if ( $first_name != '' && $last_name != '' ) {
if ( $reverse === true ) {
$retval = $last_name . ', ' . $first_name;
if ( $include_middle == true && $middle_name != '' ) {
$retval .= ' ' . $middle_name[0] . '.'; //Use just the middle initial.
}
} else {
$retval = $first_name;
if ( $include_middle == true && $middle_name != '' ) {
$retval .= ' ' . $middle_name[0] . '.'; //Use just the middle initial.
}
$retval .= ' ' . $last_name;
}
return $retval;
}
return false;
}
/**
* @param $city
* @param $province
* @param $postal_code
* @return string
*/
static function getCityAndProvinceAndPostalCode( $city, $province, $postal_code ) {
$retval = '';
if ( $city != '' ) {
$retval .= $city;
}
if ( $province != '' && $province != '00' ) {
if ( $retval != '' ) {
$retval .= ',';
}
$retval .= ' ' . $province;
}
if ( $postal_code != '' ) {
$retval .= ' ' . strtoupper( $postal_code );
}
return $retval;
}
/**
* Caller ID numbers can come in in all sorts of forms:
* 2505551234
* 12505551234
* +12505551234
* (250) 555-1234
* Parse out just the digits, and use only the last 10 digits.
* Currently this will not support international numbers
* @param $number
* @return bool|string
*/
static function parseCallerID( $number ) {
$validator = new Validator();
$retval = substr( $validator->stripNonNumeric( $number ), -10, 10 );
return $retval;
}
/**
* @param $name
* @param bool $strict
* @return bool|string
*/
static function generateCopyName( $name, $strict = false, $max_length = 49 ) {
$name = str_replace( TTi18n::getText( 'Copy of' ), '', $name );
if ( $strict === true ) {
$retval = TTi18n::getText( 'Copy of' ) . ' ' . $name;
} else {
$retval = TTi18n::getText( 'Copy of' ) . ' ' . $name . ' [' . rand( 1, 99 ) . ']';
}
$retval = substr( $retval, 0, $max_length ); //Make sure the name doesn't get too long.
return $retval;
}
/**
* @param $from
* @param $name
* @param bool $strict
* @return bool|string
*/
static function generateShareName( $from, $name, $strict = false ) {
if ( $strict === true ) {
$retval = $name . ' (' . TTi18n::getText( 'Shared by' ) . ': ' . $from . ')';
} else {
$retval = $name . ' (' . TTi18n::getText( 'Shared by' ) . ': ' . $from . ') [' . rand( 1, 99 ) . ']';
}
$retval = substr( $retval, 0, 99 ); //Make sure the name doesn't get too long.
return $retval;
}
/** Delete all files in directory
* @param $path string directory to clean
* @param $recursive boolean delete files in subdirs
* @param bool $del_dirs
* @param bool $del_root
* @param $exclude_regex_filter string regex to exclude paths
* @return bool
* @access public
*/
static function cleanDir( $path, $recursive = false, $del_dirs = false, $del_root = false, $exclude_regex_filter = null ) {
$result = true;
if ( $path == '' ) {
Debug::Text( 'Path is blank, unable to clean...', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
$dir = @dir( $path ); //Get directory class object.
if ( !is_object( $dir ) ) {
Debug::Text( 'Unable to open path for cleaning: ' . $path, __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
Debug::Text( 'Cleaning: ' . $path . ' Exclude Regex: ' . $exclude_regex_filter, __FILE__, __LINE__, __METHOD__, 10 );
while ( $file = $dir->read() ) {
if ( $file === '.' || $file === '..' ) {
continue;
}
$full_file_name = $dir->path . DIRECTORY_SEPARATOR . $file;
if ( $exclude_regex_filter != '' && preg_match( '/' . $exclude_regex_filter . '/i', $full_file_name ) == 1 ) {
continue;
}
if ( is_dir( $full_file_name ) && $recursive == true ) {
$result = self::cleanDir( $full_file_name, $recursive, $del_dirs, $del_dirs, $exclude_regex_filter );
} else if ( is_file( $full_file_name ) ) {
//$result = @unlink( $full_file_name );
//if ( $result == false ) {
// Debug::Text( ' Failed Deleting: ' . $full_file_name, __FILE__, __LINE__, __METHOD__, 10 );
//}
$result = Misc::unlink( $full_file_name ); //Use our own unlink function so it does a rename before delete on Windows.
}
}
$dir->close();
if ( $del_root == true ) {
//Debug::Text('Deleting Dir: '. $dir->path, __FILE__, __LINE__, __METHOD__, 10);
$result = @rmdir( $dir->path );
}
clearstatcache(); //Clear any stat cache when done.
return $result;
}
/**
* Unlink (Delete) a file.
* @param $file_name
* @return bool
*/
static function unlink( $file_name ) {
if ( !is_file( $file_name ) ) {
return false;
}
//On Windows, unlink() is async, therefore rename the file first before deleting it,
//That will allow a new file with the original file name to immediately be put in its place without triggering an "access denied" or "file exists" error.
if ( PHP_OS == 'WINNT' ) {
$tmp_file_name = $file_name . str_replace( '.', '', uniqid( '_pending_del_', true ) );
$result = @rename( $file_name, $tmp_file_name );
if ( $result == true ) {
$file_name = $tmp_file_name; //If the rename was successfull, set the new file name so it can be deleted below.
} else {
Debug::Text( ' Failed renaming: ' . $file_name . ' Reason: ' . Debug::getLastPHPErrorMessage(), __FILE__, __LINE__, __METHOD__, 10 );
}
}
$result = @unlink( $file_name );
if ( $result == false ) {
Debug::Text( ' Failed Deleting: ' . $file_name .' Reason: '. Debug::getLastPHPErrorMessage(), __FILE__, __LINE__, __METHOD__, 10 );
}
return $result;
}
/**
* Checks to see if the directory $path and $recurse_parent_levels number of parent paths are empty, and deletes them all going *UP* the tree. This will not go *DOWN* the tree.
* @param $path
* @param int $recurse_parent_levels
* @return bool
*/
static function deleteEmptyParentDirectory( $path, $recurse_parent_levels = 0 ) {
if ( $path == '' ) {
Debug::Text( 'Path is empty: ' . $path, __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
if ( !is_dir( $path ) ) {
Debug::Text( 'Path is not a directory: ' . $path, __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
$fs_iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $path, FilesystemIterator::SKIP_DOTS ), RecursiveIteratorIterator::CHILD_FIRST );
if ( $fs_iterator->valid() == false ) {
Debug::Text( 'Deleting Empty Directory: ' . $path, __FILE__, __LINE__, __METHOD__, 10 );
$parent_dir = realpath( $path . DIRECTORY_SEPARATOR . '..' ); //Need to get parent directory before its deleted, otherwise realpath() fails.
rmdir( $path );
if ( $recurse_parent_levels > 0 ) {
return self::deleteEmptyParentDirectory( $parent_dir, ( $recurse_parent_levels - 1 ) );
}
} else {
Debug::Text( 'Skipping Non-Empty Directory: ' . $path, __FILE__, __LINE__, __METHOD__, 10 );
}
return true;
}
/**
* Deletes all empty directories in $path and underneath (down) from it.
* @param $path
* @return bool
*/
static function deleteEmptyChildDirectory( $path ) {
if ( $path == '' ) {
Debug::Text( 'Path is empty: ' . $path, __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
if ( !is_dir( $path ) ) {
Debug::Text( 'Path is not a directory: ' . $path, __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
$files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $path, FilesystemIterator::SKIP_DOTS ), RecursiveIteratorIterator::CHILD_FIRST );
foreach ( $files as $file_obj ) {
if ( $file_obj->isDir() == true ) {
Misc::deleteEmptyParentDirectory( $file_obj->getPathName() );
}
}
return true;
}
/**
* Renames a file or directory. If rename fails for some reason, attempt a copy instead as that might work, specifically on windows where if the file is in use.
* Might fix possible "Access is denied. (code: 5)" errors on Windows when using PHP v5.2 (https://bugs.php.net/bug.php?id=43817)
* On Windows a directory containing a file formerly opened within the same script may also cause access denied errors in some versions of PHP.
* @param string $old_name
* @param string $new_name
* @return bool
*/
static function rename( $old_name, $new_name ) {
$new_dir = dirname( $new_name );
if ( file_exists( $new_dir ) == false ) {
@mkdir( $new_dir, 0755, true );
}
//Some operating systems (ie: Windows) may not allow renaming if the destination already exists, so check for that is the case and delete it first.
// **NOTE: We can't delete/unlink the new file first if it exists, because if the later rename/copy fails due to access denied, the file will be gone.
// Instead we need rename it, so we can rename it back if needed.
if ( file_exists( $new_name ) ) {
$tmp_new_name = $new_name .'pending_delete';
if ( @rename( $new_name, $tmp_new_name ) == false ) {
Debug::Text( 'ERROR: Unable to rename existing destination file: ' . $new_name .' Message: '. Debug::getLastPHPErrorMessage(), __FILE__, __LINE__, __METHOD__, 10 );
unset( $tmp_new_name );
}
}
if ( @rename( $old_name, $new_name ) == false ) {
Debug::Text( 'ERROR: Unable to rename: ' . $old_name . ' to: ' . $new_name .' Message: '. Debug::getLastPHPErrorMessage(), __FILE__, __LINE__, __METHOD__, 10 );
if ( is_dir( $old_name ) == false && @copy( $old_name, $new_name ) == true ) {
if ( @unlink( $old_name ) == false ) {
Debug::Text( 'ERROR: Unable to unlink after copy: ' . $old_name . ' to: ' . $new_name .' Message: '. Debug::getLastPHPErrorMessage(), __FILE__, __LINE__, __METHOD__, 10 );
}
$retval = true;
} else {
Debug::Text( 'ERROR: Unable to copy after rename failure: ' . $old_name . ' to: ' . $new_name .' Message: '. Debug::getLastPHPErrorMessage(), __FILE__, __LINE__, __METHOD__, 10 );
$retval = false;
}
} else {
$retval = true;
}
if ( isset( $tmp_new_name ) ) {
if ( $retval == true ) {
//Delete the temporary "pending_delete" file
@unlink( $tmp_new_name );
} else {
//Rename the temporary "pending_delete" file back to its original name so it doesn't disappear.
if ( @rename( $tmp_new_name, $new_name ) == false ) {
Debug::Text( 'ERROR: Unable to rename pending_delete file back to original: ' . $new_name .' Message: '. Debug::getLastPHPErrorMessage(), __FILE__, __LINE__, __METHOD__, 10 );
}
}
}
return $retval;
}
/**
* @param $start_dir
* @param null $regex_filter
* @param bool $recurse
* @return array|bool
*/
static function getFileList( $start_dir, $regex_filter = null, $recurse = false ) {
$files = [];
if ( is_dir( $start_dir ) && is_readable( $start_dir ) ) {
$fh = opendir( $start_dir );
while ( ( $file = readdir( $fh ) ) !== false ) {
// loop through the files, skipping . and .., and recursing if necessary
// If for some reason $file is blank, it could cause an infinite loop like: "C:\TimeTrex\cache\upgrade_staging\latest_version\interface\html5\views\payroll\pay_stub_transaction\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"
if ( $file == '' || strcmp( $file, '.' ) == 0 || strcmp( $file, '..' ) == 0 ) {
continue;
}
$filepath = $start_dir . DIRECTORY_SEPARATOR . $file;
if ( is_dir( $filepath ) && $recurse == true ) {
Debug::Text( ' Recursing into dir: ' . $filepath, __FILE__, __LINE__, __METHOD__, 10 );
$tmp_files = self::getFileList( $filepath, $regex_filter, true );
if ( $tmp_files != false && is_array( $tmp_files ) ) {
$files = array_merge( $files, $tmp_files );
}
unset( $tmp_files );
} else if ( !is_dir( $filepath ) ) {
if ( $regex_filter == '*' || preg_match( '/' . $regex_filter . '/i', $file ) == 1 ) {
//Debug::Text(' Match: Dir: '. $start_dir .' File: '. $filepath, __FILE__, __LINE__, __METHOD__, 10);
if ( is_readable( $filepath ) ) {
array_push( $files, $filepath );
} else {
Debug::Text( ' Matching file is not read/writable: ' . $filepath, __FILE__, __LINE__, __METHOD__, 10 );
}
} // else { //Debug::Text(' NO Match: Dir: '. $start_dir .' File: '. $filepath, __FILE__, __LINE__, __METHOD__, 10);
}
}
closedir( $fh );
sort( $files );
} else {
// false if the function was called with an invalid non-directory argument
$files = false;
}
//Debug::Arr( $files, 'Matching files: ', __FILE__, __LINE__, __METHOD__, 10);
return $files;
}
/**
* @param $child_dir
* @param $parent_dir
* @return bool
*/
static function isSubDirectory( $child_dir, $parent_dir ) {
//Make sure directories always end in trailing slash, otherwise paths like this will fail:
// Child: /var/www/TimeTrex Parent: /var/www/TimeTrexTest
$child_dir = rtrim( $child_dir, DIRECTORY_SEPARATOR ) . DIRECTORY_SEPARATOR;
$parent_dir = rtrim( $parent_dir, DIRECTORY_SEPARATOR ) . DIRECTORY_SEPARATOR;
if ( strpos( $child_dir, $parent_dir ) === 0 ) {
return true;
} else {
//When using realpath(), if the path does not exist it will return FALSE. In that case it can never be a sub directory.
$real_child_dir = realpath( $child_dir );
$real_parent_dir = realpath( $parent_dir );
if ( $real_child_dir !== false && $real_parent_dir !== false && strpos( $real_child_dir . DIRECTORY_SEPARATOR, $real_parent_dir . DIRECTORY_SEPARATOR ) === 0 ) { //Test realpaths incase they are relative or have "../" in them.
return true;
}
}
return false;
}
/**
* @param object|array $obj
* @return array
*/
static function convertObjectToArray( $obj ) {
if ( is_object( $obj ) ) {
$obj = get_object_vars( $obj );
}
if ( is_array( $obj ) ) {
return array_map( [ 'Misc', __FUNCTION__ ], $obj );
} else {
return $obj;
}
}
/**
* @param $val
* @return int|string
*/
static function getBytesFromSize( $val ) {
$val = trim( $val );
switch ( strtolower( substr( $val, -1 ) ) ) {
case 'm':
$val = ( (int)substr( $val, 0, -1 ) * 1048576 );
break;
case 'k':
$val = ( (int)substr( $val, 0, -1 ) * 1024 );
break;
case 'g':
$val = ( (int)substr( $val, 0, -1 ) * 1073741824 );
break;
case 'b':
switch ( strtolower( substr( $val, -2, 1 ) ) ) {
case 'm':
$val = ( (int)substr( $val, 0, -2 ) * 1048576 );
break;
case 'k':
$val = ( (int)substr( $val, 0, -2 ) * 1024 );
break;
case 'g':
$val = ( (int)substr( $val, 0, -2 ) * 1073741824 );
break;
default:
break;
}
break;
default:
break;
}
return $val;
}
/**
* @return int|string
*/
static function getSystemMemoryInfo() {
if ( OPERATING_SYSTEM == 'LINUX' ) {
$memory_file = '/proc/meminfo';
if ( @file_exists( $memory_file ) && is_readable( $memory_file ) ) {
$buffer = file_get_contents( $memory_file );
preg_match( '/MemFree:\s+([0-9]+) kB/im', $buffer, $mem_free_match );
if ( isset( $mem_free_match[1] ) ) {
$mem_free = Misc::getBytesFromSize( (int)$mem_free_match[1] . 'K' );
unset( $mem_free_match );
}
preg_match( '/Cached:\s+([0-9]+) kB/im', $buffer, $mem_cached_match );
if ( isset( $mem_cached_match[1] ) ) {
$mem_cached = Misc::getBytesFromSize( (int)$mem_cached_match[1] . 'K' );
unset( $mem_cached_match );
}
Debug::Text( ' Memory Info: Free: ' . $mem_free . 'b Cached: ' . $mem_cached . 'b', __FILE__, __LINE__, __METHOD__, 10 );
return ( $mem_free + ( $mem_cached * ( 1 / 2 ) ) ); //Only allow up to 1/2 of cached memory to be used.
}
} else if ( OPERATING_SYSTEM == 'WIN' ) {
//Windows can use the following commands:
//wmic computersystem get TotalPhysicalMemory
//wmic OS get FreePhysicalMemory /Value
//This seems to take about 250ms on Windows 7.
$command = 'wmic OS get FreePhysicalMemory';
exec( $command, $output, $retcode );
if ( isset( $output[1] ) && is_numeric( trim( $output[1] ) ) ) {
$retval = ( $output[1] * 1024 ); //Convert from MB to bytes.
Debug::Text( ' Memory Info: Total Physical: ' . $retval . 'b', __FILE__, __LINE__, __METHOD__, 10 );
return $retval;
}
}
return PHP_INT_MAX; //If not linux, return large number, this is in Bytes.
}
/**
* @return int|mixed
*/
static function getSystemLoad() {
if ( OPERATING_SYSTEM == 'LINUX' ) {
$loadavg_file = '/proc/loadavg';
if ( @file_exists( $loadavg_file ) && is_readable( $loadavg_file ) ) {
//$buffer = '0 0 0';
$buffer = file_get_contents( $loadavg_file ); //In rare cases this can fail to be read.
if ( $buffer !== false ) {
$load = explode( ' ', $buffer );
//$retval = max((float)$load[0], (float)$load[1], (float)$load[2]);
//$retval = max( (float)$load[0], (float)$load[1] ); //Only consider 1 and 5 minute load averages, so we don't block cron/reports for more than 5 minutes.
$retval = (float)$load[0]; //Only consider 1 minute load averages, so we don't block cron/reports for more than 1 minute.
//Debug::text(' Load Average: '. $retval, __FILE__, __LINE__, __METHOD__, 10);
return $retval;
}
}
}
//While the below works to get CPU LoadPercent (between 0-100) on Windows, its extremely slow and can take upwords of 2-5 seconds to complete.
//else if ( OPERATING_SYSTEM == 'WIN' ) {
// $command = 'wmic cpu get LoadPercentage';
// exec( $command, $output, $exit_code );
// if ( $exit_code == 0 && is_array( $output ) ) {
// if ( isset( $output[1] ) ) {
// return (int)$output[1];
// }
// }
//}
return 0;
}
static function getMaxSystemLoad() {
global $config_vars;
if ( isset( $config_vars['other']['max_cron_system_load'] ) ) {
$retval = $config_vars['other']['max_cron_system_load'];
} else {
$retval = 128;
}
return $retval;
}
/**
* @return bool
*/
static function isSystemLoadValid( $multiplier = 1, $disable_debug_log = false ) {
$max_system_load = Misc::getMaxSystemLoad();
$current_system_load = Misc::getSystemLoad();
//The below works if we are getting CPU LoadPercent from Windows, but currently its too slow.
//if ( OPERATING_SYSTEM == 'WIN' ) {
// //Convert Windows load percent to a max load similar to Linux.
// $current_system_load = ( $current_system_load * $config_vars['other']['max_cron_system_load'] / 100 );
//}
if ( $current_system_load <= ( $max_system_load * $multiplier ) ) {
if ( $disable_debug_log == false ) {
Debug::text( 'Load average within valid limits: Current: ' . $current_system_load . ' Max: ' . $max_system_load . ' Multiplier: ' . $multiplier, __FILE__, __LINE__, __METHOD__, 10 );
}
return true;
}
Debug::text( ' WARNING: LIMIT REACHED... Load average NOT within valid limits: Current: ' . $current_system_load . ' Max: ' . $max_system_load .' Multiplier: '. $multiplier, __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
/**
* @param $email
* @param object $user_obj
* @return string
*/
static function formatEmailAddress( $email, $user_obj ) {
if ( !is_object( $user_obj ) ) {
return $email;
}
$email = '"' . $user_obj->getFirstName() . ' ' . $user_obj->getLastName() . '" <' . $email . '>';
return $email;
}
/**
* Parses an RFC822 Email Address ( "John Doe" ) into its separate components.
* @param $input
* @param bool $return_just_key
* @return array|bool
*/
static function parseRFC822EmailAddress( $input, $return_just_key = false ) {
if ( strstr( $input, '<>' ) !== false ) { //Check for <> together, as that means no email address is specified.
return false;
}
if ( function_exists( 'imap_rfc822_parse_adrlist' ) ) {
$parsed_data = @imap_rfc822_parse_adrlist( $input, 'unknown.local' );
//Debug::Arr( $parsed_data, 'Parsed Email Data From: ' . $input, __FILE__, __LINE__, __METHOD__, 10 );
if ( is_array( $parsed_data ) && count( $parsed_data ) > 0 ) {
$parsed_data = $parsed_data[0];
if ( $parsed_data->host != 'unknown.local' ) {
$retarr = [];
$retarr['email'] = $parsed_data->mailbox . '@' . $parsed_data->host;
if ( isset( $parsed_data->personal ) ) {
$retarr['full_name'] = $parsed_data->personal;
$split_name = explode( ' ', $parsed_data->personal );
if ( $split_name !== false ) {
if ( isset( $split_name[0] ) ) {
$retarr['first_name'] = $split_name[0];
}
if ( isset( $split_name[( count( $split_name ) - 1 )] ) ) {
$retarr['last_name'] = $split_name[( count( $split_name ) - 1 )];
}
}
}
if ( $return_just_key != '' ) {
if ( isset( $retarr[$return_just_key] ) ) {
return $retarr[$return_just_key];
}
return false;
} else {
return $retarr;
}
}
}
} else {
Debug::Text( 'ERROR: PHP IMAP extension is not installed...', __FILE__, __LINE__, __METHOD__, 10 );
}
return false;
}
/**
* @param $subject
* @param $body
* @param null $attachments
* @param bool $force
* @return bool
*/
static function sendSystemMail( $subject, $body, $attachments = null, $force = false ) {
if ( $subject == '' || $body == '' ) {
return false;
}
if ( function_exists( 'getTTProductEdition' ) == false || ( getTTProductEdition() >= TT_PRODUCT_PROFESSIONAL && DEPLOYMENT_ON_DEMAND == true ) ) {
$allowed_calls = 500;
} else if ( getTTProductEdition() >= TT_PRODUCT_PROFESSIONAL ) {
$allowed_calls = 100;
} else {
$allowed_calls = 25;
}
$rl = new RateLimit;
$rl->setID( 'system_mail_' . Misc::getRemoteIPAddress() );
$rl->setAllowedCalls( $allowed_calls );
$rl->setTimeFrame( 86400 ); //24hrs
if ( $rl->check() == false ) {
Debug::Text( 'Excessive system emails... Preventing error reports from: ' . Misc::getRemoteIPAddress() . ' for up to 24hrs...', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
$registration_key = 'N/A';
try {
//If during an install/schema upgrade a SQL error has occurred, the transaction will be aborted and cause the below select to fail.
//To avoid an infinite loop, always check that the transaction hasn't already failed.
global $db, $disable_database_connection;
if ( ( !isset( $disable_database_connection ) || ( isset( $disable_database_connection ) && $disable_database_connection != true ) ) && is_object( $db ) && $db->hasFailedTrans() == false ) {
//Check to make sure "system_setting" table exists, as a PHP error being triggered and sending an email before the database schema is setup could prevent error reports from being sent.
$install_obj = new Install();
$install_obj->setDatabaseConnection( $db ); //Default Connection
if ( $install_obj->checkTableExists( 'system_setting' ) == true ) {
$registration_key = SystemSettingFactory::getSystemSettingValueByKey( 'registration_key' );
}
}
} catch ( Exception $e ) {
Debug::Text( 'Error getting registration key!', __FILE__, __LINE__, __METHOD__, 1 );
}
$to = 'errors@timetrex.com';
global $config_vars;
if ( isset( $config_vars['other']['system_admin_email'] ) ) {
if ( $config_vars['other']['system_admin_email'] != '' ) {
$to = $config_vars['other']['system_admin_email'];
} else {
return false;
}
}
$from = APPLICATION_NAME . '@' . Misc::getHostName( false );
$headers = [
'From' => $from,
'Subject' => $subject,
'X-Relay-For-Key' => $registration_key,
];
$mail = new TTMail();
$mail->setTo( $to );
$mail->setHeaders( $headers );
@$mail->getMIMEObject()->setTXTBody( $body );
if ( is_array( $attachments ) ) {
foreach ( $attachments as $attachment ) {
if ( isset( $attachment['data'] ) && isset( $attachment['mime_type'] ) && isset( $attachment['file_name'] ) ) {
@$mail->getMIMEObject()->addAttachment( $attachment['data'], $attachment['mime_type'], $attachment['file_name'], false );
}
}
}
$mail->setBody( $mail->getMIMEObject()->get( $mail->default_mime_config ) );
$retval = $mail->Send( $force );
return $retval;
}
/**
* @return bool
*/
static function isCurrentOSUserRoot() {
$user = self::getCurrentOSUser();
if ( $user == 'root' ) {
return true;
}
return false;
}
/**
* @return bool
*/
static function getCurrentOSUser() {
if ( OPERATING_SYSTEM == 'LINUX' ) {
if ( function_exists( 'posix_geteuid' ) && function_exists( 'posix_getpwuid' ) ) {
$user = posix_getpwuid( posix_geteuid() );
Debug::text( 'Running as OS User: ' . $user['name'], __FILE__, __LINE__, __METHOD__, 9 );
return $user['name'];
} else {
Debug::text( 'POSIX extension not installed, unable to determine webserver user...', __FILE__, __LINE__, __METHOD__, 9 );
}
}
return false;
}
/**
* @param $uid
* @return bool
*/
static function setProcessUID( $uid ) {
if ( OPERATING_SYSTEM == 'LINUX' ) {
if ( function_exists( 'posix_setuid' ) ) {
if ( $uid > 0 ) {
Debug::text( 'WARNING: Downgrading process UID to: ' . $uid, __FILE__, __LINE__, __METHOD__, 9 );
return posix_setuid( $uid );
} else {
Debug::text( 'UID is invalid or 0 (root), skipping...', __FILE__, __LINE__, __METHOD__, 9 );
}
}
}
return false;
}
/**
* @return bool
*/
static function findWebServerOSUser() {
if ( OPERATING_SYSTEM == 'LINUX' ) {
if ( function_exists( 'posix_getpwnam' ) ) {
$users = [ 'www-data', 'apache', 'wwwrun' ]; //Debian/Ubuntu: www-data, CentOS/Fedora/RHEL: apache, SUSE: wwwrun
foreach ( $users as $tmp_user ) {
$user_data = posix_getpwnam( $tmp_user );
if ( $user_data !== false && isset( $user_data['uid'] ) && isset( $user_data['name'] ) ) {
//return array('uid' => $user_data['uid'], 'name' => $user_data['name']);
Debug::text( 'Found web server user: ' . $tmp_user . ' UID: ' . $user_data['uid'], __FILE__, __LINE__, __METHOD__, 9 );
return $user_data['uid'];
}
}
}
}
Debug::text( 'No web server user found...', __FILE__, __LINE__, __METHOD__, 9 );
return false;
}
/**
* @param bool $email_notification
* @return bool
*/
static function disableCaching( $email_notification = true ) {
//In case the cache directory does not exist, disabling caching can prevent errors from occurring or punches to be missed.
//So this should be enabled even for ON-DEMAND services just in case.
if ( PRODUCTION == true ) {
$tmp_config_vars = [];
//Disable caching to prevent stale cache data from being read, and further cache errors.
$install_obj = new Install();
$tmp_config_vars['cache']['enable'] = 'FALSE';
$write_config_result = $install_obj->writeConfigFile( $tmp_config_vars );
unset( $install_obj );
if ( $email_notification == true ) {
if ( $write_config_result == true ) {
$subject = APPLICATION_NAME . ' - Error!';
$body = 'ERROR writing cache file, likely due to incorrect operating system permissions, disabling caching to prevent data corruption. This may result in ' . APPLICATION_NAME . ' performing slowly.' . "\n\n";
$body .= Debug::getOutput();
} else {
$subject = APPLICATION_NAME . ' - Error!';
$body = 'ERROR writing config file, likely due to incorrect operating system permissions conflicts. Please correction permissions so ' . APPLICATION_NAME . ' can operate correctly.' . "\n\n";
$body .= Debug::getOutput();
}
return self::sendSystemMail( $subject, $body );
}
return true;
}
return false;
}
/**
* @param $address1
* @param $address2
* @param $city
* @param $province
* @param $postal_code
* @param $country
* @param string $service
* @return bool|string
*/
static function getMapURL( $address1, $address2, $city, $province, $postal_code, $country, $service = 'google' ) {
if ( $address1 == '' && $address2 == '' ) {
return false;
}
$url = null;
//Expand the country code to the full country name?
if ( strlen( $country ) == 2 ) {
$cf = TTnew( 'CompanyFactory' ); /** @var CompanyFactory $cf */
$long_country = Option::getByKey( $country, $cf->getOptions( 'country' ) );
if ( $long_country != '' ) {
$country = $long_country;
}
}
if ( $service == 'google' ) {
$base_url = 'maps.google.com/?z=16&q=';
$url = $base_url . urlencode( $address1 . ' ' . $city . ' ' . $province . ' ' . $postal_code . ' ' . $country );
}
if ( $url != '' ) {
return 'http://' . $url;
}
return false;
}
/**
* @param $email
* @param bool $check_dns
* @param bool $error_level
* @param bool $return_raw_result
* @return bool
* @noinspection PhpUndefinedConstantInspection
* @noinspection PhpUndefinedFunctionInspection
*/
static function isEmail( $email, $check_dns = true, $error_level = true, $return_raw_result = false, $enable_smtp_verification = false ) {
if ( !function_exists( 'is_email' ) ) {
require_once( Environment::getBasePath() . '/classes/misc/is_email.php' );
}
if ( $enable_smtp_verification == true ) {
$raw_result = is_email( $email, false, 7 ); //Check basic email address formatting and Skip DNS checks here, as the below SMTP checks will handle that for us.
if ( $raw_result === ISEMAIL_VALID ) {
Debug::Text( 'Attempting SMTP check of email address: ' . $email, __FILE__, __LINE__, __METHOD__, 10 );
try {
$smtp_verification_start = microtime( true );
$smtp_validator = new SMTPValidateEmail\Validator();
$smtp_validator->setConnectTimeout( 3 ); //3 seconds.
$smtp_results = $smtp_validator->validate( $email, Misc::getEmailLocalPart() . '@' . Misc::getEmailDomain() );
if ( isset( $smtp_results[$email] ) && $smtp_results[$email] === true ) {
$raw_result = 0; //Success
if ( ( microtime( true ) - $smtp_verification_start ) > 15 ) {
Debug::Arr( $smtp_results, 'SMTP Email Verification Success, but slow: ', __FILE__, __LINE__, __METHOD__, 10 );
Debug::Arr( $smtp_validator->getLog(), 'SMTP Email Verification Log: ', __FILE__, __LINE__, __METHOD__, 10 );
}
} else {
$raw_result = 99; //Use validation message from key = 99.
if ( Debug::getVerbosity() >= 10 ) {
Debug::Arr( $smtp_results, 'SMTP Email Verification Failed: ', __FILE__, __LINE__, __METHOD__, 10 );
// Get log data (log data is always collected)
Debug::Arr( $smtp_validator->getLog(), 'SMTP Email Verification Log: ', __FILE__, __LINE__, __METHOD__, 10 );
}
}
unset( $smtp_validator, $smtp_results );
} catch ( Exception $e ) {
$raw_result = 99; //Use validation message from key = 99.
Debug::Text( 'SMTP Email Verification Failed with exception: ' . $e->getMessage(), __FILE__, __LINE__, __METHOD__, 10 );
Debug::Arr( $smtp_validator->getLog(), 'SMTP Email Verification Log: ', __FILE__, __LINE__, __METHOD__, 10 );
}
}
} else {
$raw_result = is_email( $email, $check_dns, $error_level );
}
if ( $return_raw_result === true ) {
if ( $raw_result !== ISEMAIL_VALID ) {
Debug::Text( 'Result Code: ' . $raw_result, __FILE__, __LINE__, __METHOD__, 10 );
}
return $raw_result;
} else {
if ( $raw_result === ISEMAIL_VALID ) {
return true;
} else {
Debug::Text( 'Result Code: ' . $raw_result, __FILE__, __LINE__, __METHOD__, 10 );
}
return false;
}
}
/**
* @return int
*/
static function getCurrentCompanyProductEdition() {
//Attempt to get the edition of the currently logged in users company, so we can better tailor the columns to them.
$product_edition_id = getTTProductEdition();
if ( $product_edition_id >= TT_PRODUCT_PROFESSIONAL ) {
global $current_company;
if ( isset( $current_company ) && is_object( $current_company ) ) {
$product_edition_id = $current_company->getProductEdition();
}
}
return $product_edition_id;
}
/**
* @return bool
*/
static function redirectMobileBrowser() {
$desktop = 0;
extract( FormVariables::GetVariables( [ 'desktop' ] ) );
if ( !isset( $desktop ) ) {
$desktop = 0;
}
// ?desktop=1 must be sent in cases like password reset email links to prevent the user from being redirected to the QuickPunch login page when trying to reset passwords.
// Unfortunately when using #!m=... we can't detect what page they are really trying to go to on the server side.
// Don't redirect search engines either.
if ( getTTProductEdition() != TT_PRODUCT_COMMUNITY && Misc::isSearchEngineBrowser() == false && $desktop != 1 ) {
$browser = self::detectMobileBrowser();
if ( $browser == true ) {
Redirect::Page( URLBuilder::getURL( null, Environment::getBaseURL() . '/html5/quick_punch/' ) );
}
} else {
Debug::Text( 'Desktop browser override: ' . (int)$desktop, __FILE__, __LINE__, __METHOD__, 10 );
}
return false;
}
/**
* @return bool
*/
static function redirectUnSupportedBrowser() {
if ( self::isUnSupportedBrowser() == true ) {
$registration_key = SystemSettingFactory::getSystemSettingValueByKey( 'registration_key' );
if ( $registration_key == '' ) {
$registration_key = 'UNKNOWN';
}
Redirect::Page( URLBuilder::getURL( [ 'tt_version' => APPLICATION_VERSION, 'tt_edition' => getTTProductEdition(), 'registration_key' => $registration_key ], 'https://www.timetrex.com/supported-web-browsers' ) );
}
return true;
}
/**
* @param null $useragent
* @return bool
*/
static function isUnSupportedBrowser( $useragent = null ) {
if ( $useragent == '' ) {
if ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
$useragent = $_SERVER['HTTP_USER_AGENT'];
} else {
return false;
}
}
$retval = false;
if ( !class_exists( 'Browser', false ) ) {
require_once ( Environment::getBasePath() . 'vendor' . DIRECTORY_SEPARATOR . 'cbschuld' . DIRECTORY_SEPARATOR . 'browser.php' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'Browser.php' );
}
$browser = new Browser( $useragent );
if ( $browser->isRobot() == true ) { //Never redirect robots, as GoogleBot sometimes appears as Chrome v41.
Debug::Text( 'Detected Robot: ' . $browser->getBrowser() . ' Version: ' . $browser->getVersion() . ' User Agent: ' . $useragent, __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
//This is for the full web interface - Need full ES6 support: https://caniuse.com/#feat=es6 *and* Webpack Terser needs to fully support it too.
//IE (All Versions)
//Edge < 80 - https://en.wikipedia.org/wiki/Microsoft_Edge
//Firefox < 78 (52 is latest version on Windows XP)
//Chrome < 79 (49 is latest version on Windows XP)
//Safari < 12 - https://en.wikipedia.org/wiki/Safari_version_history (v10 supports ES6 mostly, but v10 & v11 have incompatibilities with Terser unless work arounds are used)
//Opera < 79 https://help.opera.com/en/opera-version-history/
//
// iOS v10 (Safari v10) [Oldest device is about iPhone 7]
if ( $browser->isMobile() == false && $browser->isTablet() == false ) { //Firefox on iOS has versions are much lower than on Android or iOS. ie: v28
if ( $browser->getBrowser() == Browser::BROWSER_IE ) { //All versions of IE.
$retval = true;
}
if ( $browser->getBrowser() == Browser::BROWSER_EDGE && version_compare( $browser->getVersion(), 80, '<' ) ) {
$retval = true;
}
if ( $browser->getBrowser() == Browser::BROWSER_FIREFOX && version_compare( $browser->getVersion(), 78, '<' ) ) {
$retval = true;
}
if ( $browser->getBrowser() == Browser::BROWSER_CHROME && version_compare( $browser->getVersion(), 79, '<' ) ) {
$retval = true;
}
if ( $browser->getBrowser() == Browser::BROWSER_SAFARI && version_compare( $browser->getVersion(), 12, '<' ) ) {
$retval = true;
}
if ( $browser->getBrowser() == Browser::BROWSER_OPERA && version_compare( $browser->getVersion(), 79, '<' ) ) {
$retval = true;
}
} else {
if ( $browser->getBrowser() == Browser::BROWSER_SAFARI && version_compare( $browser->getVersion(), 12, '<' ) ) { //Different Safari versions on iOS vs. Desktop.
$retval = true;
}
if ( $browser->getBrowser() == Browser::BROWSER_CHROME && version_compare( $browser->getVersion(), 79, '<' ) ) {
$retval = true;
}
if ( $browser->getBrowser() == Browser::BROWSER_FIREFOX && version_compare( $browser->getVersion(), 78, '<' ) ) {
$retval = true;
}
if ( $browser->getBrowser() == Browser::BROWSER_SAMSUNG && version_compare( $browser->getVersion(), 7.2, '<' ) ) {
$retval = true;
}
//Example user agent strings that we can't get a version from:
// Mozilla/5.0 (iPad; CPU OS 9_3_5 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Mobile/13G36
// Mozilla/5.0 (iPad; CPU OS 9_3_5 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13G36 Safari/601.1
if ( ( $browser->getBrowser() == Browser::BROWSER_IPAD || $browser->getBrowser() == Browser::BROWSER_IPHONE || $browser->getBrowser() == Browser::BROWSER_IPOD ) ) {
if ( $browser->getVersion() == 'unknown' || version_compare( $browser->getVersion(), 10, '<' ) ) { //Different Safari versions on iOS vs. Desktop.
$retval = true;
}
}
}
if ( $retval == true ) {
Debug::Text( 'Unsupported Browser: ' . $browser->getBrowser() . ' Version: ' . $browser->getVersion(), __FILE__, __LINE__, __METHOD__, 10 );
}
return $retval;
}
/**
* @param null $useragent
* @return bool
*/
static function isSearchEngineBrowser( $useragent = null ) {
if ( $useragent == '' ) {
if ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
$useragent = $_SERVER['HTTP_USER_AGENT'];
} else {
return false;
}
}
$retval = false;
if ( !class_exists( 'Browser', false ) ) {
require_once ( Environment::getBasePath() . 'vendor' . DIRECTORY_SEPARATOR . 'cbschuld' . DIRECTORY_SEPARATOR . 'browser.php' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'Browser.php' );
}
$browser = new Browser( $useragent );
if ( $browser->getBrowser() == Browser::BROWSER_GOOGLEBOT || $browser->getBrowser() == Browser::BROWSER_BINGBOT || $browser->getBrowser() == Browser::BROWSER_SLURP ) {
$retval = true;
}
return $retval;
}
/**
* @param null $useragent
* @return bool|string
*/
static function detectMobileBrowser( $useragent = null, $return_platform = false ) {
if ( $useragent == '' ) {
if ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
$useragent = $_SERVER['HTTP_USER_AGENT'];
} else {
return false;
}
}
$retval = false;
if ( !class_exists( 'Browser', false ) ) {
require_once ( Environment::getBasePath() . 'vendor' . DIRECTORY_SEPARATOR . 'cbschuld' . DIRECTORY_SEPARATOR . 'browser.php' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'Browser.php' );
}
$browser = new Browser( $useragent );
if ( $browser->isMobile() == true || $browser->isTablet() == true ) {
if ( $return_platform == true ) {
if ( $browser->getPlatform() == Browser::PLATFORM_IPHONE || $browser->getPlatform() == Browser::PLATFORM_IPAD || $browser->getPlatform() == Browser::PLATFORM_IPOD ) {
$retval = 'ios';
} elseif ( $browser->getPlatform() == Browser::PLATFORM_ANDROID ) {
$retval = 'android';
} else {
$retval = true;
}
} else {
$retval = true;
}
}
Debug::Text( 'User Agent: ' . $useragent . ' Retval: ' . $retval, __FILE__, __LINE__, __METHOD__, 10 );
return $retval;
}
/**
* Take an amount and a distribution array of key => value pairs, value being a decimal percent (ie: 0.50 for 50%)
* return an array with the same keys and resulting distribution between them.
* Adding any remainder to the last key is the fastest.
* @param $amount
* @param $percent_arr
* @param string $remainder_operation
* @param int $precision
* @return array|bool
*/
static function PercentDistribution( $amount, $percent_arr, $remainder_operation = 'last', $precision = 2 ) {
//$percent_arr = array(
// 'key1' => 0.505,
// 'key2' => 0.495,
//);
if ( is_array( $percent_arr ) && count( $percent_arr ) > 0 ) {
$retarr = [];
$total = 0;
foreach ( $percent_arr as $key => $distribution_percent ) {
$distribution_amount = bcmul( $amount, $distribution_percent, $precision );
$retarr[$key] = $distribution_amount;
$total = bcadd( $total, $distribution_amount, $precision );
}
//Add any remainder to the last key.
if ( $total != $amount ) {
$remainder_amount = bcsub( $amount, $total, $precision );
//Debug::Text('Found remainder: '. $remainder_amount, __FILE__, __LINE__, __METHOD__, 10);
if ( $remainder_operation == 'first' ) {
reset( $retarr );
$key = key( $retarr );
}
$retarr[$key] = bcadd( $retarr[$key], $remainder_amount, $precision );
}
//Debug::Text('Amount: '. $amount .' Total (After Remainder): '. array_sum( $retarr ), __FILE__, __LINE__, __METHOD__, 10);
return $retarr;
}
return false;
}
/**
* Change the case of all values in an array
* @param $input
* @param int $case
* @return array|bool
*/
static function arrayChangeValueCase( $input, $case = CASE_LOWER ) {
switch ( $case ) {
case CASE_LOWER:
return array_map( 'strtolower', $input );
break;
case CASE_UPPER:
return array_map( 'strtoupper', $input );
break;
default:
trigger_error( 'Case is not valid, CASE_LOWER or CASE_UPPER only', E_USER_ERROR ); //E_USER_ERROR stops execution.
}
}
/**
* Checks to see if a file/directory is writable.
* @param $path
* @return bool
*/
static function isWritable( $path ) {
//Debug::text( 'File: ' . $path, __FILE__, __LINE__, __METHOD__, 10 );
if ( file_exists( $path ) ) {
if ( substr( $path, -1 ) == DIRECTORY_SEPARATOR || substr( $path, -1 ) == '.' || is_dir( $path ) ) {
//Debug::text( 'File is directory: ' . $path, __FILE__, __LINE__, __METHOD__, 10 );
return self::isWritable( $path . DIRECTORY_SEPARATOR . uniqid( mt_rand() ) . '.tmp' ); //Try to write a temporary file to the directory to ensure it can be written and deleted.
}
$f = @fopen( $path, 'r+' );
if ( $f == false ) {
//Debug::text( 'File is NOT writable: ' . $path, __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
fclose( $f );
return true;
} else {
//Debug::text( 'File does not exists...', __FILE__, __LINE__, __METHOD__, 10 );
$f = @fopen( $path, 'w' );
if ( $f == false ) {
Debug::text( 'File is NOT writable: ' . $path, __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
fclose( $f );
if ( @unlink( $path ) == false ) { //This could error if create but not delete permission exists.
Debug::text( 'File can be created, but not deleted: ' . $path, __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
return true;
}
}
/**
* @return bool
*/
static function isMobileAppUserAgent() {
if ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
$useragent = $_SERVER['HTTP_USER_AGENT'];
if ( strpos( $useragent, 'TimeTrex Mobile App' ) !== false ) {
return true;
}
}
return false;
}
/**
* @return false|mixed|string
*/
static function getMobileAppClientVersion() {
if ( isset( $_SERVER['HTTP_USER_AGENT'] ) && $_SERVER['HTTP_USER_AGENT'] != '' ) {
$parsed_user_agent = Misc::parseMobileAppUserAgent();
if ( isset( $parsed_user_agent['app_version'] ) ) {
return $parsed_user_agent['app_version'];
}
} else if ( isset( $_GET['v'] ) && $_GET['v'] != '' ) { //Is v=X the API version or the User Agent (App) version? This does not appear to be used on recent production app versions.
return $_GET['v'];
}
return false;
}
/**
* @return false|mixed|string
*/
static function parseMobileAppUserAgent( $user_agent = null ) {
if ( $user_agent == '' ) {
$user_agent = $_SERVER['HTTP_USER_AGENT'];
}
$retarr = [];
if ( $user_agent != '' && preg_match( '/App: v([\d]{1,2}\.[\d]{1,2}\.[\d]{1,2})(; StationType: (\d{2}); OS: (.*); OSVersion: (.*); OSArch: (.*); DeviceModel: (.*))*/i', $user_agent, $matches ) == 1 ) {
if ( isset($matches[1]) ) {
$retarr['app_version'] = $matches[1];
}
if ( isset($matches[3]) ) {
$retarr['station_type'] = $matches[3];
}
if ( isset($matches[4]) ) {
$retarr['os_type'] = strtolower( $matches[4] );
}
if ( isset($matches[5]) ) {
$retarr['os_version'] = $matches[5];
}
if ( isset($matches[6]) ) {
$retarr['os_arch'] = strtolower( $matches[6] );
}
if ( isset($matches[7]) ) {
$retarr['device_model'] = strtolower( $matches[7] );
}
}
return $retarr;
}
/**
* Get city, province, country based on IP address.
* @param $ip_address
* @return false|string
*/
static function getLocationOfIPAddress( $ip_address = null ) {
if ( $ip_address == '' ) {
$ip_address = Misc::getRemoteIPAddress();
}
$geo_ip = new GeoIP();
$city = $geo_ip->getCity( $ip_address );
$province = $geo_ip->getRegionCode( $ip_address );
$country = $geo_ip->getCountryName( $ip_address );
//$retval = Misc::formatAddress( '', false, false, $city, $province, false, $country, 'oneline' );
$retval = '';
if ( $country != '' ) {
$retval = $country;
}
if ( $province != '' ) {
$retval = $province .', '. $retval;
}
if ( $city != '' ) {
$retval = $city .', '. $retval;
}
if ( $retval == '' ) {
$retval = 'Unknown';
}
return $retval;
}
/**
* @return bool
*/
static function getRemoteIPAddress() {
global $config_vars;
if ( isset( $config_vars['other']['proxy_ip_address_header_name'] ) && $config_vars['other']['proxy_ip_address_header_name'] != '' ) {
$header_name = $config_vars['other']['proxy_ip_address_header_name'];
}
if ( isset( $header_name ) && isset( $_SERVER[$header_name] ) && $_SERVER[$header_name] != '' ) {
//Debug::text('Remote IP: '. $_SERVER['REMOTE_ADDR'] .' Behind Proxy IP: '. $_SERVER[$header_name], __FILE__, __LINE__, __METHOD__, 10);
//Make sure we handle it if multiple IP addresses are returned due to multiple proxies.
$comma_pos = strpos( $_SERVER[$header_name], ',' );
if ( $comma_pos !== false ) {
$_SERVER[$header_name] = substr( $_SERVER[$header_name], 0, $comma_pos );
}
$retval = $_SERVER[$header_name];
} else if ( isset( $_SERVER['REMOTE_ADDR'] ) ) {
//Debug::text('Remote IP: '. $_SERVER['REMOTE_ADDR'], __FILE__, __LINE__, __METHOD__, 10);
$retval = $_SERVER['REMOTE_ADDR'];
}
if ( isset( $retval ) && $retval != '' ) {
//Ensure IP address is actually valid before returning it.
// This can help avoid X_FORWARDED_FOR header attacks being used for SQL injections and such.
if ( filter_var( $retval, FILTER_VALIDATE_IP ) === false ) {
Debug::text( 'ERROR: Invalid Remote IP Address: ' . $retval, __FILE__, __LINE__, __METHOD__, 10 );
} else {
return $retval;
}
}
return false;
}
/**
* @param bool $ignore_force_ssl
* @return bool
*/
static function isSSL( $ignore_force_ssl = false ) {
global $config_vars;
if ( isset( $config_vars['other']['proxy_protocol_header_name'] ) && $config_vars['other']['proxy_protocol_header_name'] != '' ) {
$header_name = $config_vars['other']['proxy_protocol_header_name']; //'HTTP_X_FORWARDED_PROTO'; //X-Forwarded-Proto;
}
//ignore_force_ssl is used for things like cookies where we need to determine if SSL is *currently* in use, vs. if we want it to be used or not.
if ( $ignore_force_ssl == false && isset( $config_vars['other']['force_ssl'] ) && ( $config_vars['other']['force_ssl'] == true ) ) {
return true;
} else if (
( isset( $_SERVER['HTTPS'] ) && ( strtolower( $_SERVER['HTTPS'] ) == 'on' || $_SERVER['HTTPS'] == '1' ) )
||
//Handle load balancer/proxy forwarding with SSL offloading.
//FIXME: Similar to X_FORWARDED_FOR, this can have a comma and contain multiple protocols.
( isset( $header_name ) && isset( $_SERVER[$header_name] ) && strtolower( $_SERVER[$header_name] ) == 'https' )
) {
return true;
} else if ( isset( $_SERVER['SERVER_PORT'] ) && ( $_SERVER['SERVER_PORT'] == '443' ) ) {
return true;
}
return false;
}
/**
* @param $version1
* @param $version2
* @param $operator
* @return mixed
*/
static function MajorVersionCompare( $version1, $version2, $operator ) {
$tmp_version1 = explode( '.', $version1 ); //Return first two dot versions.
array_pop( $tmp_version1 );
$version1 = implode( '.', $tmp_version1 );
$tmp_version2 = explode( '.', $version2 ); //Return first two dot versions.
array_pop( $tmp_version2 );
$version2 = implode( '.', $tmp_version2 );
//Debug::Text('Comparing: Version1: '. $version1 .' Version2: '. $version2 .' Operator: '. $operator, __FILE__, __LINE__, __METHOD__, 10);
return version_compare( $version1, $version2, $operator );
}
/**
* @param $primary_company
* @param $system_settings
* @return string
*/
static function getInstanceIdentificationString( $primary_company, $system_settings ) {
$version_string = [];
$version_string[] = 'Company:';
$version_string[] = ( is_object( $primary_company ) ) ? $primary_company->getName() : 'N/A';
$version_string[] = 'Edition: ' . getTTProductEditionName();
$version_string[] = 'Key:';
$version_string[] = ( isset( $system_settings ) && isset( $system_settings['registration_key'] ) ) ? $system_settings['registration_key'] : 'N/A';
$version_string[] = 'Version: ' . APPLICATION_VERSION;
$version_string[] = '('. ( ( isset( $system_settings ) && isset( $system_settings['system_version_install_date'] ) ) ? TTDate::getDate('DATE+TIME', $system_settings['system_version_install_date'] ) : 'N/A' ) .')';
$version_string[] = 'Production Mode: ' . (int)PRODUCTION;
return implode( ' ', $version_string );
}
/**
* Removes the word "the" from the beginning of strings and optionally places it at the end.
* Primarily for client/company names like: The XYZ Company -> XYZ Company, The
* Should often be used to sanitize metaphones.
* @param $str
* @param bool $add_to_end
* @return bool|string
*/
static function stripThe( $str, $add_to_end = false ) {
if ( stripos( $str, 'The ' ) === 0 ) {
$retval = substr( $str, 4 );
if ( $add_to_end == true ) {
$retval .= ', The';
}
return $retval;
}
return $str;
}
/**
* Remove any HTML special char (before its encoded) from the string
* Useful for things like government forms submitted in XML.
* @param $str
* @return mixed
*/
static function stripHTMLSpecialChars( $str ) {
return str_replace( [ '&', '"', '\'', '>', '<' ], '', $str );
}
/**
* Sanitizes a string or array of strings against XSS attacks. Used before passing data to HTML emails or back to the user in HTML.
* @param string|array $string
* @return array|string
*/
static function escapeHTML( $string ) {
if ( is_array( $string ) ) {
foreach( $string as $key => $value ) {
$retval[$key] = htmlspecialchars( $value, ENT_QUOTES, 'UTF-8' );
}
} else {
$retval = htmlspecialchars( $string, ENT_QUOTES, 'UTF-8' );
}
return $retval;
}
/**
* @param $file_data
* @return bool
*/
static function checkValidImage( $file_data ) {
$mime_type = Misc::getMimeType( $file_data, true );
if ( strpos( $mime_type, 'image' ) !== false ) {
$file_size = strlen( $file_data );
//use getimagesize() to make sure image isn't too large and actually is an image.
$size = getimagesizefromstring( $file_data );
Debug::Arr( $size, 'Mime Type: ' . $mime_type . ' Bytes: ' . $file_size . ' Size: ', __FILE__, __LINE__, __METHOD__, 10 );
if ( isset( $size ) && isset( $size[0] ) && isset( $size[1] ) ) {
$bytes_to_image_size_ratio = ( $file_size / ( $size[0] * $size[1] ) );
Debug::Text( 'Bytes to image ratio: ' . $bytes_to_image_size_ratio, __FILE__, __LINE__, __METHOD__, 10 );
//UNFINISHED!
return true;
}
return false;
}
Debug::Text( 'Not a image, unable to process: Mime Type: ' . $mime_type, __FILE__, __LINE__, __METHOD__, 10 );
return true; //Isnt an image, don't bother processing...
}
static function formatPhoneNumber( $phone_number, $country, $prefix_country_code = null ) {
$retval = $phone_number;
//Strip phone to just digits.
$sanitized_phone_number = preg_replace('/[^0-9]/', '', $phone_number );
if ( strtoupper( $country ) == 'US' || strtoupper( $country ) == 'CA' ) {
$retval = '('. substr($sanitized_phone_number, -10, -7) . ') ' . substr($sanitized_phone_number, -7, -4) . '-' . substr($sanitized_phone_number, -4);
if ( $prefix_country_code === true ) {
$retval = '+'.$prefix_country_code .' '. $retval;
}
}
return $retval;
}
/**
* @param $name
* @param bool $address1
* @param bool $address2
* @param bool $city
* @param bool $province
* @param bool $postal_code
* @param bool $country
* @param string $format
* @return string
*/
static function formatAddress( $name, $address1 = false, $address2 = false, $city = false, $province = false, $postal_code = false, $country = false, $format = null ) {
$format = ( is_string( $format ) ? strtolower( $format ) : $format );
$retarr = [];
$city_arr = [];
if ( $name != '' ) {
$retarr[] = $name;
}
if ( $format == 'multiline_condensed' ) { //Try to reduce the number of lines the address appears on for tight spaces like checks or windowed envelopes.
$address = '';
if ( $address1 != '' ) {
$address = $address1;
}
if ( $address2 != '' ) {
$address .= ' ' . $address2;
}
if ( $address != '' ) {
$retarr[] = $address;
}
} else {
if ( $address1 != '' ) {
$retarr[] = $address1;
}
if ( $address2 != '' ) {
$retarr[] = $address2;
}
}
if ( $city != '' ) {
if ( $province != '' ) {
$city .= ',';
}
$city_arr[] = $city;
}
if ( $province != '' ) {
$city_arr[] = $province;
}
if ( $postal_code != '' ) {
$city_arr[] = $postal_code;
}
if ( empty( $city_arr ) == false ) {
$retarr[] = implode( ' ', $city_arr );
}
if ( $country != '' ) {
$retarr[] = $country;
}
if ( $format == 'oneline' ) {
return implode( ' ', $retarr );
} else {
return implode( "\n", $retarr );
}
}
/**
* @return string
*/
static function getUniqueID() {
global $config_vars;
if ( isset( $config_vars['other']['salt'] ) && $config_vars['other']['salt'] != '' ) {
$salt = $config_vars['other']['salt'];
} else {
$salt = uniqid( dechex( mt_rand() ), true );
}
if ( function_exists( 'openssl_random_pseudo_bytes' ) ) {
$retval = $salt . bin2hex( openssl_random_pseudo_bytes( 128 ) );
} else {
$retval = uniqid( $salt . dechex( mt_rand() ), true );
}
return $retval;
}
/**
* Strips any mention of path and file names from a string to avoid information exposure vulnerabilities.
* @param $str string
* @return array|string|string[]|null
*/
static function stripPathAndFileNames( $str ) {
$retval = preg_replace( '/(\/.*?\.\S*)/', '*CENSORED*', $str ); //Strip all non-alpha numeric characters or underscores.
return $retval;
}
/**
* Sanitize strings to be used in file names by converting spaces to underscore and removing non-alpha numeric characters
* @param $file_name
* @return mixed|string|string[]|null
*/
static function sanitizeFileName( $file_name ) {
$retval = str_replace( ' ', '_', strtolower( Misc::stripDirectoryTraversal( trim( $file_name ) ) ) ); //Switch all spaces to underscores
//Strip off file extension so we can sanitize just the file name part then append it to the end later on.
$extension = pathinfo( $retval, PATHINFO_EXTENSION );
if ( $extension != '' ) {
$retval = pathinfo( $retval, PATHINFO_FILENAME );
}
$retval = preg_replace( '/[^0-9a-z\-_]/', '', $retval ); //Strip all non-alpha numeric characters or underscores. Ensure that no shell escaping is required after the result of this too.
if ( $extension != '' ) {
$retval .= '.'.$extension;
}
return $retval;
}
/**
* Removes any sort of directory traversal attacks from a file name, ie: '../../myfile.pdf'
* @param $file_name
* @return mixed|string|string[]|null
*/
static function stripDirectoryTraversal( $file_name ) {
$retval = str_replace( [ '/', '\\' ], '', $file_name );
return $retval;
}
/**
* zips an array of files and returns a file array for download
* @param $file_array
* @param bool $zip_file_name
* @param bool $ignore_single_file
* @return array|bool
*/
static function zip( $file_array, $zip_file_name = false, $ignore_single_file = false ) {
if ( !is_array( $file_array ) || count( $file_array ) == 0 ) {
return $file_array;
}
if ( $ignore_single_file == true && ( count( $file_array ) == 1 || key_exists( 'file_name', $file_array ) ) ) {
//if there's just one file don't bother zipping it.
foreach ( $file_array as $file ) {
return $file;
}
} else {
if ( $zip_file_name == '' ) {
$file_path_info = pathinfo( $file_array[key( $file_array )]['file_name'] );
$zip_file_name = $file_path_info['filename'] . '.zip';
}
global $config_vars;
$tmp_file = tempnam( $config_vars['cache']['dir'], 'zip_' );
$zip = new ZipArchive();
$result = $zip->open( $tmp_file, ZIPARCHIVE::CREATE );
Debug::Text( 'Creating new zip file for download: ' . $zip_file_name . ' File Open Result: ' . $result, __FILE__, __LINE__, __METHOD__, 10 );
$total_zipped_files = 0;
foreach ( $file_array as $file ) {
if ( isset( $file['file_name'] ) && isset( $file['data'] ) ) {
$zip->addFromString( $file['file_name'], $file['data'] );
$total_zipped_files++;
}
}
$zip->close();
$zip_file_contents = file_get_contents( $tmp_file );
unlink( $tmp_file );
if ( $total_zipped_files > 0 ) {
$ret_arr = [ 'file_name' => $zip_file_name, 'mime_type' => 'application/zip', 'data' => $zip_file_contents ];
return $ret_arr;
} else {
return false; //No ZIP files to return...
}
}
return false;
}
/**
* @param $amount
* @param $limit
* @return int
*/
static function getAmountToLimit( $amount, $limit ) {
if ( $amount == 0 || $amount === '' || $amount === null || $amount === false || $amount === true || Misc::compareFloat( $amount, 0, '==' ) ) {
return 0;
}
//If no limit is specified, just return the amount.
if ( $limit == 0 || $limit === '' || $limit === null || $limit === false || $limit === true || Misc::compareFloat( $limit, 0, '==' ) ) {
return $amount;
}
//Cases:
// Positive Amount, 0 Limit -- Always return the amount as if there is no limit. (handled above)
// Positive Amount, Positive Limit -- Handle up to limit
// Positive Amount, Negative Limit -- Always return 0 as they cross 0 and by definition have already crossed the limit.
//
// Negative Amount, 0 Limit -- Always return the amount as if there is no limit. (handled above)
// Negative Amount, Positive Limit -- Always return 0 as they cross 0 and by definition have already crossed the limit.
// Negative Amount, Negative Limit -- Handle down to limit
//$retval = 0;
if ( $amount > 0 && $limit < 0 ) {
$retval = 0;
} else if ( $amount < 0 && $limit > 0 ) {
$retval = 0;
} else {
if ( $amount >= 0 ) {
if ( $amount >= $limit ) {
//Amount is greater than limit, just use limit.
$retval = $limit;
} else {
$retval = $amount;
}
} else {
if ( $amount <= $limit ) {
//Amount is less than limit, just use limit.
$retval = $limit;
} else {
$retval = $amount;
}
}
}
return $retval;
}
/**
* Returns an adjusted current amount, and the amount under a limit and over a limit.
* Ideal for calculating wages up to a maximum limit and increasing the YTD amount in a loop. For example social security wage limits.
* @param $current_amount float Current amount
* @param $ytd_amount float Running balance leading up to maximum limit
* @param $ytd_amount_limit float Overall maximum limit
* @return array|int[]
*/
static function getAmountAroundLimit( $current_amount, $ytd_amount, $ytd_amount_limit ) {
if ( $ytd_amount < $ytd_amount_limit ) {
$ytd_amount_over_ytd_amount_limit = bcadd( $current_amount, $ytd_amount );
if ( $ytd_amount_over_ytd_amount_limit > $ytd_amount_limit ) {
$retarr = [ 'adjusted_amount' => bcsub( $ytd_amount_limit, $ytd_amount ), 'under_limit' => 0, 'over_limit' => bcsub( bcadd( $ytd_amount, $current_amount ), $ytd_amount_limit ) ];
} else {
$retarr = [ 'adjusted_amount' => $current_amount, 'under_limit' => bcsub( $ytd_amount_limit, bcadd( $ytd_amount, $current_amount ) ), 'over_limit' => 0 ];
}
} elseif ( $ytd_amount == $ytd_amount_limit ) {
if ( $current_amount >= 0 ) {
$retarr = [ 'adjusted_amount' => 0, 'under_limit' => 0, 'over_limit' => $current_amount ];
} else {
$retarr = [ 'adjusted_amount' => $current_amount, 'under_limit' => abs( $current_amount ), 'over_limit' => 0 ];
}
} elseif ( $ytd_amount > $ytd_amount_limit ) {
if ( $current_amount >= 0 ) {
$retarr = array( 'adjusted_amount' => 0, 'under_limit' => 0, 'over_limit' => bcsub( $ytd_amount, $ytd_amount_limit ) );
} else {
$ytd_amount_under_ytd_amount_limit = bcadd( $current_amount, $ytd_amount );
if ( $ytd_amount_under_ytd_amount_limit < $ytd_amount_limit ) {
$retarr = [ 'adjusted_amount' => bcsub( bcadd( $ytd_amount, $current_amount ), $ytd_amount_limit ), 'under_limit' => abs( bcsub( bcadd( $ytd_amount, $current_amount ), $ytd_amount_limit ) ), 'over_limit' => 0 ];
} else {
$retarr = [ 'adjusted_amount' => 0, 'under_limit' => 0, 'over_limit' => bcsub( bcadd( $ytd_amount, $current_amount ), $ytd_amount_limit ) ];
}
}
}
return $retarr;
}
/**
* This is can be used to handle YTD amounts.
* @param $amount
* @param $limit
* @return string
*/
static function getAmountDifferenceToLimit( $amount, $limit ) {
//If no limit is specified, just return the amount.
if ( $limit === '' || $limit === null || $limit === false || $limit === true ) {
return $amount;
}
if ( $amount < 0 && $limit > 0 ) {
$retval = bcadd( abs( $amount ), $limit ); //Return value that gets the amount to the limit.
} else if ( $amount > 0 && $limit < 0 ) {
$retval = bcadd( bcmul( $amount, -1 ), $limit ); //Return value that gets the amount to the limit.
} else {
$tmp_amount = self::getAmountToLimit( $amount, $limit );
$retval = bcsub( $limit, $tmp_amount );
}
return $retval;
}
/**
* Generic Retry handler with closures.
* @param $function Closure
* @param int $retry_max_attempts
* @param int $retry_sleep
* @param bool $continue_on_error
* @return mixed
* @throws Exception
*/
static function Retry( $function, $retry_max_attempts = 3, $retry_sleep = 1, $continue_on_error = false ) { //When changing function definition, also see APIFactory->RetryTransaction()
// Help mitigate function injection attacks due to the variable function call below $transaction_function() -- If we always insist on a closure its harder for an attacker to pass in phpinfo() as the function to call for example.
if ( !$function instanceof Closure ) {
Debug::text( 'ERROR: Retry function is not a closure, unable to execute!', __FILE__, __LINE__, __METHOD__, 10 );
return null;
}
$tmp_sleep = ( $retry_sleep * 1000000 );
$retry_attempts = 0;
while ( $retry_attempts < $retry_max_attempts ) {
try {
unset( $e ); //Clear any exceptions on retry.
Debug::text( '==================START: RETRY BLOCK===================================', __FILE__, __LINE__, __METHOD__, 10 );
$retval = $function(); //This function should call StartTransaction() at the beginning, and CommitTransaction() at the end.
Debug::text( '==================END: RETRY BLOCK=====================================', __FILE__, __LINE__, __METHOD__, 10 );
} catch ( Exception $e ) {
$random_sleep_interval = ( ceil( ( rand() / getrandmax() ) * ( ( $tmp_sleep * 0.33 ) * 2 ) - ( $tmp_sleep * 0.33 ) ) ); //+/- 33% of the sleep time.
Debug::text( 'WARNING: Retry block failed: Retry Attempt: ' . $retry_attempts . ' Sleep: ' . ( $tmp_sleep + $random_sleep_interval ) . '(' . $tmp_sleep . ') Code: ' . $e->getCode() . ' Message: ' . $e->getMessage(), __FILE__, __LINE__, __METHOD__, 10 );
Debug::text( '==================END: RETRY BLOCK===================================', __FILE__, __LINE__, __METHOD__, 10 );
if ( $retry_attempts < ( $retry_max_attempts - 1 ) ) { //Don't sleep on the last iteration as its serving no purpose.
usleep( $tmp_sleep + $random_sleep_interval );
}
$tmp_sleep = ( $tmp_sleep * 2 ); //Exponential back-off with 25% of retry sleep time as a random value.
$retry_attempts++;
continue;
}
break;
}
if ( isset( $e ) ) { //$retry_attempts >= $retry_max_attempts ) { //Allow retry_max_attempts to be set at 0 to prevent any retries and fail without an error.
Debug::text( 'ERROR: RETRY block failed after max attempts: ' . $retry_attempts . ' Max: ' . $retry_max_attempts, __FILE__, __LINE__, __METHOD__, 10 );
//If after all the retry attempts, if there is still an error, determine if should re-throw the exception or just continue.
if ( $continue_on_error != true ) {
throw $e;
}
}
if ( isset( $retval ) ) {
Debug::Arr( $retval, 'Returning Retval: ', __FILE__, __LINE__, __METHOD__, 10 );
return $retval;
}
return null;
}
/**
* Checks the max post size and file upload size, and returns the lower of the two.
* @return int
*/
static function getPHPMaxUploadSize() {
$max_post_size = (int)convertHumanSizeToBytes( ini_get( 'post_max_size' ) );
$max_upload_filesize = (int)convertHumanSizeToBytes( ini_get( 'upload_max_filesize' ) );
$max_size = ( $max_upload_filesize < $max_post_size ) ? $max_upload_filesize : $max_post_size;
return (int)$max_size;
}
/**
* @return bool
*/
static function doesRequestExceedPHPMaxPostSize() {
$retval = false;
if ( isset( $_SERVER['CONTENT_LENGTH'] ) && (int)$_SERVER['CONTENT_LENGTH'] > Misc::getPHPMaxUploadSize() ) {
$retval = true;
}
return $retval;
}
/**
* Returns all parsed feature flags as an array.
* @return array
*/
static function parseFeatureFlags() {
global $config_vars, $current_company;
//Force default values here.
$flag_arr = [
'support_chat' => true,
'job_queue' => false,
'custom_field' => true,
'custom_field_punch' => false,
];
//Community edition defaults custom fields to false.
if ( getTTProductEdition() == TT_PRODUCT_COMMUNITY ) { //Installed edition is community.
$flag_arr['custom_field'] = false;
}
//Get global feature flags first, so we can append company specific feature flags after that overwrite them.
$split_flags = [];
if ( isset( $config_vars ) && isset( $config_vars['other']['feature_flags'] ) && $config_vars['other']['feature_flags'] != '' ) {
$split_flags = explode( ',', $config_vars['other']['feature_flags'] );
}
if ( isset( $current_company ) && is_object( $current_company ) ) { //Currently logged in company edition.
if ( $current_company->getProductEdition() == TT_PRODUCT_COMMUNITY ) {
$flag_arr['custom_field'] = false;
}
//Get company specific feature flags that can overwrite any global ones.
if ( isset( $config_vars ) && isset( $config_vars['other']['company_feature_flags'] ) && is_array( $config_vars['other']['company_feature_flags'] ) && isset( $config_vars['other']['company_feature_flags'][$current_company->getId()] ) ) {
$split_flags = array_merge( $split_flags, explode( ',', $config_vars['other']['company_feature_flags'][$current_company->getId()] ) );
}
}
//Parse each individual feature flag, later ones in the list (ie: company specific) take priority over earlier ones.
if ( isset( $split_flags ) && is_array( $split_flags ) ) {
foreach( $split_flags as $split_flag ) {
$split_flag_values = explode( '=', $split_flag );
$tmp_key = strtolower( trim( $split_flag_values[0] ) );
$tmp_value = strtolower( trim( $split_flag_values[1] ) );
if ( !is_numeric( $tmp_value ) ) {
if ( $tmp_value == 'true' ) {
$tmp_value = true;
} else {
$tmp_value = false;
}
}
$flag_arr[ $tmp_key ] = $tmp_value;
}
}
return $flag_arr;
}
/**
* Returns the value of a specific feature flag, and if none is set returns the default value.
* @param $flag
* @param $default_value
* @return mixed|null
*/
static function getFeatureFlag( $flag = null, $default_value = null ) {
$flag_arr = Misc::parseFeatureFlags();
if ( isset( $flag_arr[$flag] ) ) {
return $flag_arr[$flag];
} else {
return $default_value;
}
}
/**
* Generate a shell command to pass thru the TT_CONFIG_FILE environment variable through to executed cron jobs or system job queue commands.
* @return string
*/
static function getEnvironmentVariableConfigFile() {
//Prepare environment prefix.
if ( OPERATING_SYSTEM == 'LINUX' && isset( $_SERVER['TT_CONFIG_FILE'] ) && $_SERVER['TT_CONFIG_FILE'] != '' ) {
$retval = 'export TT_CONFIG_FILE='. $_SERVER['TT_CONFIG_FILE'] .';';
} else {
$retval = '';
}
return $retval;
}
static function isPortOpen( $host, $port, $timeout = 3 ) {
$fp = @fsockopen( $host, $port, $errno, $errstr, $timeout );
if ( $fp == false ) {
Debug::Text( 'Port: '. $port .' to: '. $host .' is NOT open: ' . $errstr . ' (' . $errno . ')', __FILE__, __LINE__, __METHOD__, 10 );
$retval = false;
} else {
fclose( $fp );
$retval = true;
Debug::Text( 'Port: '. $port .' to: '. $host .' is open!', __FILE__, __LINE__, __METHOD__, 10 );
}
return $retval;
}
}
?>