cache public ?array $exposedFields; // catalog of exposed fields instead of cacheMap public int $rowsAffected = 0; // count of the number of rows affected by a query protected bool $isMigration; // bypasses some operations for migration requests protected int $cacheExpiry; // seconds class object remains cached before expiring protected int $queryThreshold; // number in milliseconds to flag a query as slow protected int $queryWarning; // number in milliseconds to flag a query as a slow warning public ?array $queryResults = null; // contains return values from various API directives protected WriteResult $bwResult; // contains the $results object from bulk write operations protected string $templateName; // named of the template used to instantiate the class protected string $templateClass; // contains the template class name // public array $templateReport; // copy of the template report generated by the factory protected gacMeta $meta; // contains the meta-data definition protected array $metaPayload; // holds the current request meta payload data array public string $dbService; // defines the current database service in use public string $schema; // contains the template report/schema-report for the class public ?string $strQuery = null; // stores the last query executed public array $auditCreateQueries = []; // stores list of create queries for audit/journaling public array $auditUndoQueries = []; // stores list of undo queries for audit/journaling public ?array $auditRecordList = []; // list of record guids impacted by audit/journaling event public array $tokenList = []; // list of tokens affected by a query, for cache-updates public string $strSubCollectionQuery; // stores the last sub-collection query executed public string $restoreQuery; // stores the restore query for journaling protected array $subCollections; // stores the array(array) of sub-collections definitions protected array $validStates; // a list of valid states protected array $validStatus; // a list of valid statuses protected bool $skipReadAudit = false; // excludes database reads from audit if set to true public ?gacErrorLogger $logger = null; // logger class container for the core object protected object $connection; // container for the database resource/connection public string $collectionName; // container for the current table name or view public string $queryTable; // can only be a table name (not a view) protected int $recordLimit; // dynamic per class: number of records returned per query public array $migrationConfig; // stores the migration information data from the template public ?array $warehouseConfig; // stores the warehouse information data from the template private string $res = 'CORE: '; // logger id // note that the state/status values cover the health of the object - not the object's data! public ?string $state; // the state of the current instantiation public bool $status = false; // boolean indicator of health of current instantiation public string $eventGUID = ''; // identifies the broker event public string $sessionGUID = ''; // container for the current session GUID public string $userGUID = ''; // container for the userGUID, if needed public ?object $objUser = null; // container for the user object class public ?object $objSession = null; // container for the session object class public string $client; // defines requesting client entity (System, Batch, etc.) public bool $isWHRequest = false; // defines if the current operation is a WH request // public string $whType = ''; // defines the level of warehousing (cool, cold, etc.) public gatAudit $templateAudit; // container for the audit template public gatJournaling $templateJournal; // container for the journal template public array $recordAudit; // array container for the audit record data public array $recordJournal; // array container for the journal record data // CORE-1013 ------------------------------------------------------------------------------------------------------- private gacBrokerClient $brokerClient; // defines broker client object for remote service requests public ?object $template = null; // may contain a copy of the template file being processed // CORE-1013 ------------------------------------------------------------------------------------------------------- /** * gaaNamasteCore constructor. * * the NamasteCore abstraction constructor sets-up the logger (if not already done so in the parent class), * and then loads several configuration settings into member properties, as well as loading some key constants * that are used through-out the framework. It also scans the template directory and builds a list of valid * templates. * * @author mike@givingassistant.org * @version 1.0 * * @param string $_eventGUID * * HISTORY: * ======== * 06-15-17 mks CORE-447: original coding * 09-13-17 mks CORE-561: added event guid as param for easier invocation * 03-12-18 mks Fixing declarations of array params init'ing them to arrays instead of nulls - this will * squelch error warnings in the PHP log. * 03-04-19 mks DB-116: deprecated $cacheKeys for cacheMapping v2 * 01-03-20 mks DB-150: pacifying call to squelchIDEWarning() * */ public function __construct(string $_eventGUID = null) { // DO NOT PUT A LOGGER_EVENT CALL IN THIS CONSTRUCTOR! register_shutdown_function([$this, STRING_DESTRUCTOR]); if (!isset($this->logger)) { try { $this->logger = new gacErrorLogger($_eventGUID); } catch (Throwable $t) { consoleLog($this->res, CON_ERROR, ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage()); return; } } $this->squelchIDEWarnings(); // init some unused params to keep the IDE calm // set-up the database timer parameters if (gasConfig::$settings[CONFIG_DATABASE][CONFIG_DATABASE_QUERY_TIMERS]) { if (isset(gasConfig::$settings[CONFIG_DATABASE][CONFIG_DATABASE_QUERY_TIMERS]) and gasConfig::$settings[CONFIG_DATABASE][CONFIG_DATABASE_QUERY_TIMERS]) { if (isset(gasConfig::$settings[CONFIG_DATABASE][CONFIG_DATABASE_TIMER_SLOW_QUERY_ALERT])) { $this->queryThreshold = intval(gasConfig::$settings[CONFIG_DATABASE][CONFIG_DATABASE_TIMER_SLOW_QUERY_ALERT]); if (isset(gasConfig::$settings[CONFIG_DATABASE][CONFIG_DATABASE_QUERY_TIMER_WARNINGS]) and gasConfig::$settings[CONFIG_DATABASE][CONFIG_DATABASE_QUERY_TIMER_WARNINGS]) { $this->queryWarning = gasConfig::$settings[CONFIG_DATABASE][CONFIG_DATABASE_TIMER_SLOW_QUERY_WARNING]; } elseif (!isset(gasConfig::$settings[CONFIG_DATABASE][CONFIG_DATABASE_QUERY_TIMER_WARNINGS])) { $this->queryWarning = $this->queryThreshold * NUMBER_DEF_HWM; } } else { // todo - systemEvent::framework configuration violation $this->useTimers = 0; } } } $this->debug = gasConfig::$settings[CONFIG_DEBUG]; $this->event = DB_EVENT_NONE; $this->hiddenColumns = [DB_PKEY, MONGO_ID]; $this->sysEvents = []; $this->data = array(); $this->validTemplates = gasStatic::loadValidTemplateNames(); $this->loadStates(); $this->loadStatus(); $this->data = []; $this->count = 0; $this->strQuery = null; $this->collectionName = NONE; } /** * remoteFetchRequest() -- public method * * This method requires one input parameter -- an array, that is a copy of the fetch-request submitted to the * read broker. * * The method returns either a null, indicating a processing error was raised. Otherwise, we return a uncompressed * data payload received by the remote service. * * We instantiate a broker client based on the current class' template service and we validate the service - if the * service of the current class is the appServer, we deny the request because the appServer shouldn't farm out * these request since all fetch requests are directed to the appServer. * * If the service is not (yet) supported (tercero) or unknown, generate an error and return. * * Once we pass validation, then we generate a new payload request and submit it to the remote service, process * the return payload and pass that back to the calling client. * * * @author mike@givingassistant.org * @version 1.0 * * @param array $_request * @return array|null * * HISTORY: * ======== * 06-06-18 mks CORE-1013: original coding * 07-30-20 mks DB-142: tercero support cut; these events are now handled in functions::validateMetaData() * */ public function remoteFetchRequest(array $_request): ?array { // lvar init $this->state = STATE_DATA_ERROR; $this->status = false; // remember, the class object is already instantiated -- we "know" where this request will be sent... // validation if (!is_array($_request)) { $this->eventMessages[] = ERROR_DATA_ARRAY_NOT_ARRAY . BROKER_REQUEST; return null; } if (!isset($_request[BROKER_DATA])) { $this->eventMessages[] = ERROR_ARRAY_KEY_404 . BROKER_DATA; return null; } elseif (!isset($_request[BROKER_META_DATA])) { $this->eventMessages[] = ERROR_ARRAY_KEY_404 . BROKER_META_DATA; return null; } switch ($this->template->service) { case CONFIG_DATABASE_SERVICE_APPSERVER : $this->eventMessages[] = ERROR_RSR_APPSERVER; $this->state = STATE_SCHEMA_ERROR; return null; break; case CONFIG_DATABASE_SERVICE_ADMIN : $this->brokerClient = new gacBrokerClient(BROKER_QUEUE_AO, basename(__METHOD__) . AT . __LINE__); break; case CONFIG_DATABASE_SERVICE_SEGUNDO : $this->brokerClient = new gacBrokerClient(BROKER_QUEUE_WH, basename(__METHOD__) . AT . __LINE__); break; default : $this->eventMessages[] = sprintf(ERROR_RSR_NOT_DEF, $this->template->service); $this->state = STATE_TEMPLATE_ERROR; return null; break; } // if we get to this point, we have a valid broker client - create the appropriate payload and submit $meta = $_request[BROKER_META_DATA]; if (!isset($meta[META_CLIENT])) $meta[META_CLIENT] = CLIENT_SYSTEM; if (!isset($meta[META_SYSTEM_NOTES])) $meta[META_SYSTEM_NOTES] = INFO_INTERNAL_REQUEST; $request = [ BROKER_REQUEST => BROKER_REQUEST_REMOTE_FETCH, BROKER_DATA => $_request[BROKER_DATA], BROKER_META_DATA => $meta ]; $payload = gzcompress(json_encode($request)); // publish the request... try { $response = $this->brokerClient->call($payload); if (is_object($this->brokerClient)) $this->brokerClient->__destruct(); unset($this->brokerClient); return (json_decode(gzuncompress($response), true)); // don't judge } catch (Throwable $t) { $msg = ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage(); $this->eventMessages[] = $msg; if (isset($this->logger) and $this->logger->available) { $this->logger->warn($msg); } else { consoleLog($this->res, CON_ERROR, $msg); } } return null; } /** * loadStatus() -- private method * * this method takes no input parameters. * * the method loads a list of valid statuses into an array container and returns. * * Programmer's Note: * ------------------ * If you change an existing status, you must update the status value here. * If you delete an existing status, you must delete the status here. * If you add a new status, you must add the status here. * * todo: create the "system" table/collection and pull values from there * * * @author mike@givingassistant.org * @version 1.0 * * * HISTORY: * ======== * 06-20-17 mks original coding * */ private function loadStatus(): void { $this->validStatus = [ STATUS_PENDING, STATUS_ACTIVE, STATUS_DELETED, STATUS_SUSPENDED, STATUS_EXPIRED, STATUS_REVOKED, STATUS_LOCKED, STATUS_COMPLETE, STATUS_IN_PROGRESS, STATUS_ABANDONED, STATUS_INVALID, STATUS_VALID, STATUS_REJECTED, STATUS_APPROVED, STATUS_FAILED, STATUS_CLOSED, STATUS_SENT, STATUS_REQUEST_SENT, STATUS_INACTIVE, STATUS_NEW, STATUS_FAKE ]; } /** * setState() -- protected method * * this method sets the state of the current class instantiation. * * otherwise, the current class' state is set to the passed value: * $_newState one of the defined error states * * method will also generate a back-trace informational message, add the message to both the diagnostics member * array and as output to the log. This is optionally done if the second input parameter is explicitly * over-rode by the calling client under the assumption that the state change was the result of a detected * error or other problem. * * If the $_newState is invalid, set the status flag to false, log an error, and return. * * * @author mike@givingassistant.org * @version 1.0 * * @param string $_newState * @param bool $_dbt * * HISTORY: * ======== * 06-20-17 mks original coding * */ protected function setState(string $_newState, bool $_dbt = false): void { if ($_dbt) { $backTrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); $msg = $backTrace[0][ERROR_FILE] . '.' . $backTrace[1][ERROR_FUNCTION]; $msg .= '(' . $backTrace[0][ERROR_LINE] . ').' . $backTrace[2][ERROR_CLASS] . '(' . $backTrace[2][ERROR_LINE] . ')' . COLON . $_newState; //var_dump($backTrace); //echo 'class: ' . $this->class . PHP_EOL; //var_dump($msg); if (isset($this->logger) and $this->logger->available) $this->logger->error($msg); else consoleLog($this->res, CON_ERROR, $msg); $this->eventMessages[] = $msg; unset($backTrace); } if (empty($this->validStates) or !is_array($this->validStates)) $this->loadStates(); if (!in_array($_newState, $this->validStates)) { $msg = ERROR_INVALID_STATE . $_newState; $this->eventMessages[] = $msg; if (isset($this->logger) and $this->logger->available) $this->logger->error($msg); else consoleLog($this->res, CON_ERROR, $msg); $this->status = false; return; } $this->state = $_newState; } /** * loadStates -- private class method * * this method loads a list of the current, valid, states into a member array for validation. * * the method requires no input parameters, and the return is explicit in the setting of the class member. * * Programmer's Note: * ------------------ * If you change an existing state, use the refactor option. * If you delete an existing state, or add a new state, remember you need to update this list. * * todo: again, this data should come from a table and not the constants list for maintainability * * * @author mike@givingassistant.org * @version 1.0 * * * HISTORY: * ======== * 06-20-17 mks original coding * 09-28-17 mks CORE-572: updated for cache-fetch state, added sort feature * */ private function loadStates() { $this->validStates = [ STATE_SUCCESS, STATE_VALIDATION_ERROR, STATE_SCHEMA_ERROR, STATE_NOT_FOUND, STATE_DOES_NOT_EXIST, STATE_ALREADY_EXISTS, STATE_ALREADY_PROCESSED, STATE_REQUEST_REJECTED, STATE_DATA_ERROR, STATE_CRYPTO_ERROR, STATE_SECURITY_ERROR, STATE_CLASS_INSTANTIATION_ERROR, STATE_GUID_ERROR, STATE_TEMPLATE_ERROR, STATE_META_ERROR, STATE_FRAMEWORK_WARNING, STATE_FRAMEWORK_FAIL, STATE_FAIL, STATE_FILE_ERROR, STATE_FILE_NOT_FOUND, STATE_PENDING, STATE_RESOURCE_ERROR, STATE_RESOURCE_ERROR_MONGO, STATE_RESOURCE_ERROR_RABBIT, STATE_RESOURCE_ERROR_CACHE, STATE_RESOURCE_ERROR_XML, STATE_DUPLICATE_RECORD, STATE_DATA_TYPE_ERROR, STATE_FILESYSTEM_ERROR, STATE_AUTH_ERROR, STATE_DB_ERROR, STATE_AJ_ERROR, STATE_AMQP_ERROR, STATE_CACHE_ERROR, STATE_LOCKED, STATE_ACCOUNT_LOCKED, STATE_ACCOUNT_NOT_LOCKED, STATE_TIMER, STATE_STATUS, STATE_BLACK_LIST, STATE_NOT_WHITE_LIST, STATE_MAIL_FAIL, STATE_BIN_FAIL, STATE_JSON_FAIL, STATE_ENCODE_FAIL, STATE_DECODE_FAIL, STATE_INDEX_ERROR, STATE_CACHE_FETCH, STATE_NOT_SUPPORTED ]; sort($this->validStates, SORT_STRING); } /** * getColumn() -- public method * * this is one way data is fetched outside of the instantiation stack, since $data is a protected member variable, * by the framework. * * this method allows the retrieval of a single value within a matrix of data that's determined by both the * name of the column and the indexed row. * * this method will ONLY work with the defined column names -- it will NOT work with kepMapped column names. * * There's a wee bit o' flexibility built-in to the method in that, if the key passed does not have the class * extension appended, this method will append the extension prior to referencing the matrix value. * * if any of the validation checks fail, or if the key is not found in the current classes' data matrix, * then a null is returned to the calling client, the class member state/status variables are set accordingly, * and the diagnostics array is populated if there was a non-NotFound error generated. * * If the data was located in the matrix, then the data is returned (as-is) to the calling client. Note that * there is no restriction on the type($data) returned. * * @author mike@givingassistant.org * @version 1.0 * * @param string $_key -- the column name (extension optional) * @param int $_row -- which row of the data matrix to return (defaults to the first record) * @return null | mixed -- returns either a null or the data found at the referenced intersection * * HISTORY: * ======== * 06-21-17 mks original coding * */ public function getColumn(?string $_key, int $_row = 0) { $this->state = STATE_VALIDATION_ERROR; $this->status = false; $value = null; if (empty($this->data)) { $this->state = STATE_DOES_NOT_EXIST; $this->eventMessages[] = ERROR_DATA_404; } elseif (!is_array($this->data)) { $this->eventMessages[] = ERROR_DATA_FIELD_NOT_MEMBER; } elseif (($_row + 1) > $this->count) { $this->eventMessages[] = ERROR_DATA_INCONSISTENT_COUNT; } elseif (empty($_key)) { $this->eventMessages[] = ERROR_DATA_INPUT_EMPTY . STRING_KEY; } elseif (!is_string($_key)) { $this->eventMessages[] = ERROR_DATA_INVALID_FORMAT . COLON . STRING_KEY; } elseif (empty($this->ext)) { $this->eventMessages[] = ERROR_CLASS_EXT_404; } else { try { // if the key was passed without an extension, then append it $_key = $this->addExtension($_key); } catch (TypeError $t) { $msg = ERROR_TYPE_EXCEPTION . COLON . $t->getMessage(); $this->eventMessages[] = $msg; if (isset($this->logger) and $this->logger->available) $this->logger->warn($msg); else consoleLog($this->res, CON_ERROR, $msg); return null; } if (is_null($_key)) return $_key; // support for Boolean(false) content (false !== null) if (false === @$this->data[$_row][$_key]) { $value = false; } elseif (isset($this->data[$_row][$_key])) { $value = $this->data[$_row][$_key]; // $value = (empty($this->data[$_row][$_key]) and is_null($this->data[$_row][$_key])) ? null : $this->data[$_row][$_key]; } if (!is_null($value) or false === $value) { $this->state = STATE_SUCCESS; $this->status = true; return $value ; } else { $this->state = STATE_NOT_FOUND; } } return null; } /** * addExtension() -- public method * * this method requires one input-parameter: * * $_key - the key field which should be a collection column name * * we're not going to validate the key name as being a part of the current fieldList as there may some point where * I just want to add an extension to some string. * * we are going to throw a fatal error (to the logs) if the current instantiation does not have a defined * class extension. * * If the string passed-in to the method ($_key) is a mongo id (_id) or already has the extension appended, * then return the same value passed-in. Otherwise, append the extension to the current string. * * @author mike@givingassistant.org * @version 1.0 * * @param $_key * @return null|string * * HISTORY: * ======== * 06-21-17 mks original coding * */ public function addExtension($_key): ?string { if (empty($this->ext)) { if (isset($this->logger) and $this->logger->available) $this->logger->warn(ERROR_CLASS_EXT_404); else consoleLog($this->res, CON_ERROR, ERROR_CLASS_EXT_404); $this->eventMessages[] = ERROR_CLASS_EXT_404; $this->state = STATE_FRAMEWORK_WARNING; $this->status = false; return null; } elseif ($_key != MONGO_ID and $_key != DB_HISTORY) { if ((strlen($_key) < strlen($this->ext)) or (false === @strpos($_key, $this->ext, (strlen($_key) - NUMBER_LEN_CLASS_EXT)))) $_key = $_key . $this->ext; } return($_key); } /** * removeExtension() -- public function * * This function requires a single input parameter: * * $_key -- the field name that will have the extension dropped * * The method does a check to see if the incoming string has the class extension explicitly in the last-four * chars of the string. If so, then it's removed and returned to the calling client. Otherwise, the input string * is returned. * * * @author mike@givingassistant.org * @version 1.0 * * @param string $_key * @return string * * * HISTORY: * ======== * 04-21-20 mks ECI-107: original coding * */ public function removeExtension(string $_key):string { if (true === @strpos($_key, $this->ext, (strlen($_key) - NUMBER_LEN_CLASS_EXT))) { return rtrim($_key, $this->ext); } return $_key; } /** * getData() -- public method * * This is the public-facing method for fetching data from the protected $data member. Note that this process * involved invoking the scrubber method which filters out the $hiddenColumns from the data-set. * * Note: * ----- * Multiple records in $data is fully supported * * @author mike@givingassistant.org * @version 1.0 * * @return array|null * * HISTORY: * ======== * 06-22-17 mks original coding * 08-14-17 mks CORE-493: removed meta-param, support for DB_HISTORY * 02-28-19 mks DB-116: dropped support for the _clean param, now returning scrubbed-data as a pass-through * */ public function getData(): ?array { if (empty($this->data)) return null; if (!is_array($this->data)) { $msg = ERROR_DATA_ARRAY_NOT_ARRAY . STRING_DATA; $this->eventMessages[] = $msg; if (isset($this->logger) and $this->logger->available) $this->logger->error($msg); else consoleLog($this->res, CON_ERROR, $msg); return null; } // this function is always called and encodes known binary data columns for transport over AMQP wire $this->makeDataAQMPSafe(); try { return $this->dataScrub($this->data); } catch (TypeError $t) { $msg = ERROR_TYPE_EXCEPTION . COLON . $t->getMessage(); $this->eventMessages[] = $msg; if (isset($this->logger) and $this->logger->available) $this->logger->error($msg); else consoleLog($this->res, CON_ERROR, $msg); return null; } } /** * getCK() -- public method * * This method has no inputs. * * This method is called when the $data array contains cacheKeys and not class data and is provided to skip * field validation that occurs when processing a normal data payload. * * This method returns whatever is stored in the $data member since the member is protected and cannot be * accessed outside of the class stack. * * * @author mike@givingassistant.org * @version 1.0 * * @return null|array -- returns whatever is stored in $data without evaluation * * * HISTORY: * ======== * 03-05-19 mks DB-116: original coding * */ public function getCK(): ?array { return $this->data; } /** @noinspection PhpUnused */ /** * getRecord() -- public method * * There are times, albeit mostly internally, when we just want to retrieve a single record from a data payload. * * There are two input parameters to the method: * * $_row -- an integer value which determines which row of the current data payload to return * $_clean -- a boolean value that tells us to clean the returned record or not * * To avoid stripping extensions from the data, cache-mapping, filtering, etc., you must explicitly pass the * boolean value (false) to the second parameter. * * The method validates the $_row as (a) an integer, (b) that the count is valid, and (c) within range of the * current count. If any of these checks fail, then the eventMessages stack is populated with the corresponding * error and, if debug is active, the failure is logged, and a null value is returned to the calling client. * * Otherwise, we grab the referenced record from the current $data payload, check the clean and clean if * required. The record is then returned back (as an array) to the calling client. * * * NOTE: * ----- * The array (0) offset is adjusted in this method so that the calling client doesn't have to. * * * @author mike@givingassistant.org * @version 1.0 * * @param int $_row * @param bool $_clean * @return array|null * * * HISTORY: * ======== * 05-16-18 mks _INF-188: original coding * */ public function getRecord(int $_row, bool $_clean = true): ?array { $error = null; $record = null; if (!is_integer($_row)) $error = ERROR_DATA_TYPE_MISMATCH . COLON . ERROR_STUB_EXPECTING . DATA_TYPE_INTEGER . COLON . ERROR_STUB_RECEIVED . $_row; elseif (!isset($this->count) or $this->count == 0) $error = ERROR_DATA_ARRAY_EMPTY . COLON . STRING_DATA; elseif ($_row < $this->count or $_row > $this->count) $error = ERROR_DATA_RANGE . COLON . $_row . SLASH . $this->count; if (!is_null($error)) { $this->eventMessages[] = $error; if ($this->debug) $this->logger->debug($error); return null; } if (!is_bool($_clean)) $_clean = true; $record[] = $this->data[($_row - 1)]; if ($_clean) { try { $this->dataScrub($record); } catch (TypeError $t) { $msg = ERROR_TYPE_EXCEPTION . COLON . $t->getMessage(); $this->eventMessages[] = $msg; if (isset($this->logger) and $this->logger->available) $this->logger->error($msg); else consoleLog($this->res, CON_ERROR, $msg); return null; } } return $record[0]; } /** * makeDataAMQPSafe() -- public method * * this method is intended to take the existing class data and, if the class has binary fields set, then * we'll examine each binary field within every tuple of data. If the specified field has binary data, * then we're going to base-64 encode the data and replace the binary element with the encoded string making * the data tuple safe for transport for AMQP. * * there are no input parameters to the method and the method is type void. * * @author mike@givingassistant.org * @version 1.0 * * HISTORY: * ======== * 06-22-17 mks original coding * */ public function makeDataAQMPSafe() { if (empty($this->binaryFields)) return; for ($index = 0, $limit = $this->count; $index < $limit; $index++) { foreach ($this->binaryFields as $binaryField) { try { $field = $this->getColumn($binaryField, $index); if ($this->debug and isset($this->logger) and $this->logger->available) $this->logger->debug(STRING_PROCESSING_BIN_FIELD . $binaryField); else consoleLog($this->res, CON_ERROR, STRING_PROCESSING_BIN_FIELD . $binaryField); if (!empty($field->bin) and isBinStr($field->bin)) { $field = base64_encode($field->bin); // can't call setData() b/c type mismatch will cause rejection $this->data[$index][$binaryField] = $field; } elseif (empty($field)) { if (isset($this->logger) and $this->logger->available) $this->logger->data(ERROR_BIN_FIELD_404 . $field); else consoleLog($this->res, CON_ERROR, ERROR_BIN_FIELD_404 . $field); } } catch (Throwable $t) { $msg = ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage(); $this->eventMessages[] = $msg; if (isset($this->logger) and $this->logger->available) $this->logger->error($msg); else consoleLog($this->res, CON_ERROR, $msg); return; } } } } /** * dataScrub() -- private method * * dataScrub() has traditionally been a problem child, especially with large data sets. For historians, a copy * of the original (mostly) version is stored in the deprecated folder. * * This method was cleaned-up because it was notoriously slow and cache-mapping was moved to the broker-services * level. As such, we no longer care about "cleaning" the data which mostly consisted of stripping off the * extension from the array keys. * * Now, scrubbing happens with the array_diff_key() where we're saving the record sans the IDs from the various * schemas. And, because we're using the array_diff_key() method, we're no longer looking at every column, * every value in a record, including the recursive transversing. So, yeah, faster. * * Also, we're making a copy of the input parameter because, during testing, I learned that your results * can be "inconsistent" when you unset() a call-by-reference parameter. (You reset the param pointers on * the reset() call resetting any changed previously made.) * * * @author mikegivingassistant.org * @version 1.0 * * @param array $_data * @return array|null * * HISTORY: * ======== * 06-22-17 mks original coding * 08-14-17 mks CORE-493: removing meta param support (DB_HISTORY no longer supported) * 02-28-19 mks DB-116: removed extension filtering, replaced key-value pair queries with vector-level * filtering for quicker transversing of the record array. Input param $data is no * longer a call-by-reference param and we're explicitly returning a copy of the * modified array back to the calling client. * */ private function dataScrub(array $_data): ?array { if (empty($_data)) { $msg = ERROR_DATA_MISSING_ARRAY . STRING_DATA; $this->eventMessages[] = $msg; if (isset($this->logger) and $this->logger->available) $this->logger->error($msg); else consoleLog($this->res, CON_ERROR, $msg); return null; } // make copies of data we're going to be manipulating $dataCopy = null; $hiddenFields = $this->hiddenColumns; // if the current instantiation is PDO, add PKEY_ID plus the class extension, to hiddenFields if ($this->schema == CONFIG_SCHEMA_MYSQL and isset($this->ext)) $hiddenFields[] = PKEY_ID . $this->ext; // remove the hidden columns from a record if they exist for ($index = 0, $last = count($_data); $index < $last; $index++) $dataCopy[$index] = array_diff_key($_data[$index], $hiddenFields); return $dataCopy; } /** * setData() -- protected method * * the setData method is probably one of the most critical core routines in the framework. Because the $data * class member is a protected variable, it cannot be modified outside of the abstraction stack. This method, * then, provides a means of accessing the $data sub-array and allows the framework to set values within the * sub-array, each of which corresponds to a collection column. * * The $data sub-array is designed to be a two-dimensional vector representing actual data in a collection * or table. The first sub-script is the tuple (row) of the data, and the second subscript represents * the collection/table column. * * The $data elements (columns) are always keyed using the current class' table extension. Always. * * The input parameters to the method are as follows: * * $_key the name of the column to be set * $_value the value to set the column to * $_row which tuple of data to set (defaults to 0th tuple) * * The method does not assume that the calling client appended the class extension to the the $_key, so we're * going to make the following checks before allowing the assignment to occur: * * --- if the key, as is, is in the current fieldList, then save the key to a temp variable * --- if the key, with the appended extension, is in the current field list, save the key to a temp variable * --- if the key is a mapped key value, the save the referencing-index value (as the key) to a temp variable * * Example: * -------- * $_key = 'lname' .... $this->fieldList['lname_usr'] is the desired column * --- check 1 will fail because: 'lname' != 'lname_usr' * --- check 2 will succeed because '(lname . _usr') == 'lname_usr' * --- check 3 will not execute * * $_key = 'lastName' .... $this-fieldList['lname_usr'] is the desired column * --- check 1 will fail because 'lastName' != 'lname_usr' * --- check 2 will fail because ('lastName . _usr') != 'lname_usr' * --- check 3 will succeed because 'lastName' == $this->cacheMap['lname_usr'] => 'lastName' * * If we find a matching key, then the next step in validation is to make sure that the value (input parameter 2) * is the same TYPE as what's indicated in the class's fieldTypes array. * * If we found the right key, and the value is consistent, then assign the column to the new value for the $_row * indicator (parameter 3). * * The method makes use of the class' state, status and diagnostics members as a diagnostic aid if the assignment * request fails. It's the client's responsibility to evaluate the return value and take the appropriate action * although the method will generate log messages (non-production) if the data is a type-mismatch or the key * is not found. * * @author mike@givingassistant.org * @version 1.0 * * @param $_key * @param $_value * @param $_row * @return void * * HISTORY: * ======== * 07-13-17 mks original coding * 08-18-17 mks CORE-500: fixed PHP Notice for subCollection evaluation * 09-04-18 mks DB-48: squelching some output limiting it to debug mode * 02-19-18 mks DB-116: deprecated cache-mapping sections * 11-06-20 mks DB-171: squelching cachemap log errors if there's no cachemap for the class * */ protected function setData(string $_key, $_value, int $_row = 0): void { $this->state = STATE_VALIDATION_ERROR; $this->status = false; $loggerAvailable = (isset($this->logger) and $this->logger->available); // if the current row index is greater than the count of the number of rows currently stored, then only // only accept the row index if the value is equal to either the count or the count + 1 (new record) if ($_row > ($this->count + 1)) { $msg = ERROR_DATA_RANGE . ' _row > count'; $this->eventMessages[] = $msg; if ($loggerAvailable) $this->logger->data($msg); else consoleLog($this->res, CON_ERROR, $msg); } elseif (empty($_key)) { $this->eventMessages[] = ERROR_DATA_INPUT_EMPTY; if ($loggerAvailable) $this->logger->data(ERROR_DATA_INPUT_EMPTY . ' missing _key'); else consoleLog($this->res, CON_ERROR, ERROR_DATA_INPUT_EMPTY . ' missing _key'); } elseif (empty($_value) and ($_value !== false) and ($_value !== 0) and (!is_null($_value))) { $this->eventMessages[] = ERROR_DATA_INPUT_EMPTY; if ($loggerAvailable) $this->logger->data(ERROR_DATA_INPUT_EMPTY . ' missing value for key: ' . $_key); else consoleLog($this->res, CON_ERROR, ERROR_DATA_INPUT_EMPTY . ' missing value for key: ' . $_key); } elseif (empty($this->ext)) { $this->eventMessages[] = ERROR_CLASS_EXT_404; if ($loggerAvailable) $this->logger->error(ERROR_CLASS_EXT_404); else consoleLog($this->res, CON_ERROR, ERROR_CLASS_EXT_404); $this->state = STATE_CLASS_INSTANTIATION_ERROR; } else { $dataKey = false; // check to see if the key is in the field list index: if (in_array($_key, $this->fieldList)) { $dataKey = $_key; } elseif (in_array(($_key . $this->ext), $this->fieldList)) { $dataKey = ($_key . $this->ext); } elseif ($this->useCache and !empty($this->cacheMap)) { $dataKey = array_search($_key, $this->cacheMap); } if ($dataKey !== false) { $this->status = true; $this->state = STATE_SUCCESS; if (is_array($_value) and $this->fieldTypes[$dataKey] == DATA_TYPE_OBJECT) { $_value = (object)$_value; } $dataValue = gettype($_value); $fieldValue = $this->fieldTypes[$dataKey]; // try to recover simple (int <-> string) mismatches if (gettype($_value) == $this->fieldTypes[$dataKey]) { $this->data[$_row][$dataKey] = $_value; $this->count = count($this->data); } else { if (($dataValue == DATA_TYPE_INTEGER or $dataValue == DATA_TYPE_DOUBLE) and ($fieldValue == DATA_TYPE_STRING)) { $_value = (string)$_value; $this->data[$_row][$dataKey] = $_value; if ($this->debug) { // courtesy notice if debugging is on $msg = sprintf(__LINE__ . COLON_NS . ERROR_DATA_FORCE_CAST, $dataKey, $dataValue, $fieldValue); $this->eventMessages[] = $msg; consoleLog($this->res, CON_ERROR, $msg); } } elseif ($dataValue == DATA_TYPE_STRING and ($fieldValue == DATA_TYPE_INTEGER or $fieldValue == DATA_TYPE_DOUBLE)) { $_value = ($fieldValue == DATA_TYPE_DOUBLE) ? floatval($_value) : intval($_value); $this->data[$_row][$dataKey] = $_value; if ($this->debug) { // courtesy notice if debugging is on $msg = sprintf(__LINE__ . COLON_NS . ERROR_DATA_FORCE_CAST, $dataKey, $dataValue, $fieldValue); $this->eventMessages[] = $msg; consoleLog($this->res, CON_ERROR, $msg); } } else { // we can't allow the data to be added to the collection b/c of type mismatch $msg = sprintf(ERROR_DATA_TYPE_MISMATCH_DETAILS, $dataKey, $this->fieldTypes[$dataKey], gettype($_value) . COLON . $_value); $this->eventMessages[] = $msg; if ($loggerAvailable) $this->logger->error($msg); else consoleLog($this->res, CON_ERROR, $msg); $msg = ERROR_DATA_FIELD_DROPPED . $_key; $this->eventMessages[] = $msg; if ($loggerAvailable) $this->logger->error($msg); else consoleLog($this->res, CON_ERROR, $msg); $this->state = STATE_DATA_TYPE_ERROR; } } } elseif (count($this->cacheMap)) { $msg = sprintf(ERROR_DATA_INVALID_CLASS_KEY, $_key, $this->class); if ($loggerAvailable) $this->logger->error($msg); else consoleLog($this->res, CON_ERROR, $msg); $this->state = STATE_NOT_FOUND; } } } /** * loadPayloadData() -- protected method * * loadPayloadData is the method we use to initialize an instantiation class object with payload data and meta-data. * * It's protected so that it can be accessed only from within the framework stack. * * the method requires two inputs: * * $_data -- associative array of key->value pairs that match to the column values of the current collection * $_event -- string that defines the event, defaults to DB create but can be any legit event * * each $_data element is parsed via the call to the setData() protected method (below) and, if found, will be * inserted into the current $data array at $count position. * * if at least one $_data element was added to the current class, then we're going to call the protected method * metaTransfer() (above) to save/add the meta data to the current record. Upon return from this method, we'll * update the class count member variable to indicate the addition of a new record to the class $data. * * While the state/status members are populated with data determining processing success, the method explicitly * returns a boolean value to make processing in the client more efficient. * * * @author mike@givingassistant.org * @version 1.0 * * @param $_data * @param $_event * @return bool * * * HISTORY: * ======== * 07-13-17 mks CORE-464: original coding * 10-20-17 mks CORE-585: moved from mongo instantiation class to core, added condition for checking * schema (mongo) before invoking sub-collection processing * 09-01-18 mks DB-48: improved for-loop processing to avoid infinite looping which is generally bad * */ protected function loadPayloadData(array $_data, string $_event = DB_EVENT_CREATE): bool { $loggerAvailable = isset($this->logger) and $this->logger->available; $this->state = STATE_VALIDATION_ERROR; $this->status = false; $tmpDataContainer = null; // make sure we have data to save if (empty($_data)) { $this->logger->error(ERROR_DATA_404); $this->eventMessages[] = ERROR_DATA_404; return false; } elseif (!@is_array($_data[0])) { // make sure $_data is in array(array()) format $this->logger->data(ERROR_DATA_INVALID_FORMAT); $this->eventMessages[] = ERROR_DATA_INVALID_FORMAT; return false; } if (empty($this->metaPayload) or !is_array($this->metaPayload)) { $this->logger->error(ERROR_DATA_META_404); $this->eventMessages[] = ERROR_DATA_META_404; return false; } // process the $_data payload // verify that the elements are valid keys... for ($index = 0, $max = count($_data); $index < $max; $index++) { foreach ($_data[$index] as $key => &$value) { if (is_null($value)) { unset($_data[$index][$key]); } try { // determine if this is a sub-collection field for mongo schema if ($this->schema == TEMPLATE_DB_MONGO and is_array($value)) { if (!empty($this->subCollections) and (array_key_exists($key, $this->subCollections) or (array_key_exists($key . $this->ext, $this->subCollections)))) { $this->validateSubCollectionColumnNames($key, $value); } } // cache-mapping happens in setData()... $this->setData($key, $value, $index); } catch (Throwable $t) { $msg = ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage(); $this->eventMessages[] = $msg; if ($loggerAvailable) $this->logger->error($msg); else consoleLog($this->res, CON_ERROR, $msg); $this->state = STATE_FRAMEWORK_WARNING; return false; } } } if (empty($this->data)) { $msg = basename(__METHOD__) . AT . __LINE__ . COLON . ERROR_DATA_PROCESSING; if ($loggerAvailable) $this->logger->error($msg); else consoleLog($this->res, CON_ERROR, $msg); $this->eventMessages[] = $msg; $this->state = STATE_DATA_ERROR; return false; } else { $this->metaPayload[META_SESSION_DATE] = time(); $this->metaPayload[META_SESSION_EVENT] = $_event; $this->state = STATE_SUCCESS; $this->status = true; } return true; } /** * newRecordDataInjections() -- protected method * * This method was moved from the mongoDB instantiation class into the core as it's generic and can/should be used * by any schema. * * The method takes no input parameters as it's required/assumed that the $data member has already been * pre-populated with validated (cacheMapped/typed) records. If the $data member is empty, the class state/status * members will be set accordingly and the method will return a false to the client. * * The method spins through all of the records currently stored in $data and ensures that none of the records * already have a GUID record in the payload -- if they do, then processing stops immediately and control is * returned with an error flag to the calling client. The logic being that records with GUIDs already present * are records that should be updated, not created -- therefore, Namaste will reject the entire request. * * Data that is injected into each record in this method is: * * event GUID as pulled from the meta payload * a record GUID * the created date * the default status * * Additionally, if the current class is a mongo class, and there is a sub-collection defined for the class, and * if the sub-collection exists as a series of records, then for each sub-collection record, insert a GUID. * * * The method will also return a false result if there's no data to be processed. * * Warnings will also be generated, but processing will be allowed to continue, if the meta-data payload is missing * the EVENT guid (this is a framework error as this guid is supposed to be created and inserted into the meta * payload by the receiving event broker), or if the data template is missing the event guid from the declared * field list. * * The last task prior to returning, on a successful process, is to set the record count and the state/status * member variables. * * * @author mike@givingassistant.org * @version 1.0 * * @return bool * * * HISTORY: * ======== * 10-20-17 mks CORE-585: original coding * 03-24-18 mks CORE-852: support for mysql-destination during migration * 05-04-18 mks _INF-188: support for creating wh records * */ protected function newRecordDataInjections(): bool { // todo - put some intelligence here to save un-saved records and update already-saved records instead of rejecting // make sure all records are new "to be created" records if (!empty($this->data) and is_array($this->data) and !$this->isWHRequest) { foreach ($this->data as &$record) { if (array_key_exists((DB_TOKEN . $this->ext), $record) and !$this->isMigration) { // cannot create a new record if there already exists an ID field $this->state = STATE_ALREADY_EXISTS; $this->eventMessages[] = ERROR_DATA_CREATE_PRE_EXISTS . $record[(DB_TOKEN . $this->ext)]; return false; } // if the date-created field is a class member and is not set, then add it if (in_array((DB_CREATED . $this->ext), $this->fieldList) and (!isset($record[(DB_CREATED . $this->ext)]) or (empty($record[(DB_CREATED . $this->ext)])))) { switch ($this->schema) { case TEMPLATE_DB_MONGO : $record[(DB_CREATED . $this->ext)] = time(); break; case TEMPLATE_DB_PDO : $record[(DB_CREATED . $this->ext)] = date("Y-m-d H:i:s"); break; default : $msg = sprintf(ERROR_SCHEMA_NOT_SUPPORTED, $this->schema) . COLON . __METHOD__; $this->eventMessages[] = $msg; if (isset($this->logger) and $this->logger->available) $this->logger->info($msg); $this->state = STATE_SCHEMA_ERROR; $this->status = false; return false; break; } } // generate the db-token GUID... if ($this->useToken and (!isset($record[(DB_TOKEN . $this->ext)]) or empty($record[(DB_TOKEN . $this->ext)]))) { // todo: check for duplicate token -- make this a unique function so $data isn't over-written $record[(DB_TOKEN . $this->ext)] = guid(); } // set the status to the default for the class, if status is part of the field list: if (in_array((DB_STATUS . $this->ext), $this->fieldList) and empty($record[(DB_STATUS . $this->ext)])) { $record[(DB_STATUS . $this->ext)] = (!empty($this->defaultStatus)) ? $this->defaultStatus : STATUS_ACTIVE; } // CORE-556: check if sub-collections are supported, and if data exists, add a GUID value to // every sub-collection record if one does not already exist if ($this->schema == TEMPLATE_DB_MONGO and !empty($this->subCollections)) { foreach ($this->subCollections as $key => $value) { if (array_key_exists($key, $record) and is_array($record[$key])) { foreach ($record[$key] as &$subRecord) { if (!array_key_exists((DB_TOKEN . $this->ext), $subRecord)) { $subRecord[(DB_TOKEN . $this->ext)] = guid(); } } } } } // CORE-529: add the event guid to the record data if (in_array((DB_EVENT_GUID . $this->ext), $this->fieldList) and !isset($record[(DB_EVENT_GUID . $this->ext)])) { if (!isset($this->eventGUID) or empty($this->eventGUID)) { if (!isset($this->metaPayload[META_EVENT_GUID])) { $msg = ERROR_DATA_META_EG_404; $this->eventMessages[] = $msg; $this->logger->error($msg); } else { $record[(DB_EVENT_GUID . $this->ext)] = $this->metaPayload[META_EVENT_GUID]; } } else { $record[(DB_EVENT_GUID . $this->ext)] = $this->eventGUID; } } elseif (!in_array((DB_EVENT_GUID . $this->ext), $this->fieldList)) { // generate a warning and an eventMessage if the template is missing the event guid declaration $msg = ERROR_TEMPLATE_EG_DECL_404 . $this->class; $this->eventMessages[] = $msg; $this->logger->error($msg); } } } elseif (!empty($this->data) and is_array($this->data) and $this->isWHRequest) { // this is a data warehouse request -- all data fields should already be present so that all that's // required to do with the payload is inject the WH_TOKEN... foreach ($this->data as &$record) { switch ($this->schema) { case TEMPLATE_DB_MONGO : $record[(DB_WH_CREATED . $this->ext)] = time(); break; case TEMPLATE_DB_PDO : $record[(DB_WH_CREATED . $this->ext)] = date("Y-m-d H:i:s"); break; default : $msg = sprintf(ERROR_SCHEMA_NOT_SUPPORTED, $this->schema) . COLON . __METHOD__ . COLON_NS . __LINE__; $this->eventMessages[] = $msg; if (isset($this->logger) and $this->logger->available) $this->logger->error($msg); $this->state = STATE_SCHEMA_ERROR; $this->status = false; return false; break; } $record[(DB_WH_EVENT_GUID . $this->ext)] = $this->metaPayload[META_EVENT_GUID]; $record[(DB_WH_TOKEN . $this->ext)] = guid(); } } else { $this->eventMessages[] = ERROR_DATA_MISSING_ARRAY; if ($this->debug) $this->logger->debug(sprintf(STUB_LOC, basename(__FILE__), __METHOD__, __LINE__) . COLON . ERROR_DATA_MISSING_ARRAY); $this->eventMessages[] = sprintf(STUB_LOC, basename(__FILE__), __METHOD__, __LINE__) . COLON . ERROR_DATA_MISSING_ARRAY; $this->state = STATE_DATA_ERROR; $this->status = false; return false; } $this->count = count($this->data); $this->state = STATE_SUCCESS; $this->status = true; return true; } /** * cmFieldValidation() -- protected class method * * This method is used to qualify a field value, represented by the input parameter, a string, as a valid member * of the current table/collection. * * We do this by first looking at if the class uses the cache-map and, if so, then we expect (demand) that the * passed $_field be located in the current cacheMap -- and we return the referencing key as a result. * * If the $_field is in the exposed field list, or if the field is in the current fieldList, then return the * field as valid. * * Otherwise, in all other cases, return a null value to the calling client so as to flag $_field as a non- * member of the current class. * * @author mike@givingassistant.org * @version 1.0 * * @param string $_field * @return null|string * * HISTORY: * ======== * 10-09-17 mks CORE-584: original coding * 05-07-19 mks DB-116: suppressing PHP Warnings * */ protected function cmFieldValidation(string $_field): ?string { if ($this->useCache and is_array($this->cacheMap) and !in_array($_field, $this->cacheMap)) { // we're using cache-map but the array key is not in the cache map $msg = sprintf(ERROR_CACHE_KEY_404, $_field, $this->class); $this->eventMessages[] = $msg; if ($this->debug) $this->logger->debug($msg); return null; } elseif ($this->useCache and is_array($this->cacheMap) and in_array($_field, $this->cacheMap)) { return (array_search($_field, $this->cacheMap)); } elseif (!$this->useCache and in_array($_field, $this->cacheMap)) { return (is_array($this->cacheMap) and array_search($_field, $this->cacheMap)); } elseif ((is_array($this->exposedFields) and in_array($_field, $this->exposedFields)) or in_array($_field, $this->fieldList)) { return $_field; } elseif ((is_array($this->exposedFields) and in_array(($_field . $this->ext), $this->exposedFields)) or in_array(($_field . $this->ext), $this->fieldList)) { return $_field . $this->ext; } else { $msg = ERROR_KEY_404 . $_field; $this->eventMessages[] = $msg; if ($this->debug) $this->logger->debug($msg); return null; } } /** * validateSubCollectionColumnNames() - protected method * * this method is a recursive method which validates the column names in a mongo sub-collection. * * todo: evaluate ddb subC potential and, if null, move this up to mongo instantiation class * * the method accepts two input parameters - one is the current key (sub-collection name) and the other is * the indexed-array referenced by the key. * * while we're spinning through the indexed array (_value), if the type of value is an array and if the array * key is a sub-collection, then we have found a nested sub-collection and the function will invoke itself * by passing-in the new key and value sub-array. * * if a key is not in the sub-collection list, then remove it from the input data and record the removal in * the system log and in the class diagnostics. * * @author mike@givingassistant.org * @version 1.0 * * @param $_key * @param $_value * * HISTORY: * ======== * 07-13-17 mks CORE-464: original coding * */ protected function validateSubCollectionColumnNames($_key, &$_value) { // spin through the sub-collection array and toss invalid fields if (array_key_exists($_key, $this->subCollections)) { if (is_array($_value)) { foreach ($_value as $record) { foreach ($record as $key => $value) { // todo - fix this code and use for a recursive call for nested sub-collections... // if (is_array($value) and array_key_exists($k, $this->subCollections)) { // $this->validateSubCollectionColumnNames($k, $v); if (!in_array($key, $this->subCollections[$_key])) { $msg = ERROR_SUB_COLLECTION_NOT_MEMBER . $key; $this->logger->data($msg); $this->eventMessages[] = $msg; unset($_value[$key]); } } } } else { $this->logger->data(ERROR_SUB_COLLECTION_404); $this->eventMessages[] = ERROR_SUB_COLLECTION_404; } } } /** * softDeleteStatusInjection() -- protected method * * This method is called by the _fetchRecord() method in the instantiation classes. The method requires one * parameter: * * $_query -- this is the STRING_QUERY_DATA array embedded in the query payload submitted to the broker. * * If the current class supports soft-deletes, and if the query did not include a filter for the record status, * (e.g.: do not return deleted records) then we're going to inject that filter into the existing query. * * The method returns two parameters within an array -- the position of each parameter is relevant as the calling * client should make the following assignment/invocation: * * list($query, $injection) = $this->hardDeleteStatusInjection($query); * * The reason why we inject the status filter is that classes for records that support soft-deletes should not * be able to access records that were deleted either via the database or from a cache-fetch. * * NOTE: * ----- * Although you will be tempted to, do not remove the "= null" from the $_query parameter in the function * declaration -- it's permissible for this parameter to either be an array or null (to allow for blanket * 'select *' type queries) -- allowing for the parameter to default to null works. * * * @author mike@givingassistant.org * @version 1.0 * * @param array|null $_query * @return array * * HISTORY: * ======== * 09-26-17 mks CORE-572: original coding (moved from gacMongo::_fetchRecords() and modified) * 10-11-17 mks CORE-584: updated default for $_query argument so that null queries won't cause crash * 05-03-18 mks _INF-188: compensated for edge case where there may not be a cache map in a class that * supports soft-deletes * 04-01-20 mks PD-21: refactored processing algorithm - if hard-deletes for the current class are not * enabled, then process the query looking for the presence of a STATUS identifier * 11-05-20 mks DB-171: changed conditional statement processing order to ensure test for query being * null is the first evaluation tested * */ protected function softDeleteStatusInjection(array $_query = null): array { // if the current class supports soft-deletes ($this->useDeletes == false)... if (!$this->useDeletes) { // if there's a status field present in the non-null query, then we can return without processing if (is_null($_query)) { // query is empty so inject a status field... $_query[(DB_STATUS . $this->ext)] = [OPERAND_NULL => [OPERATOR_DNE => [STATUS_DELETED]]]; return [$_query, true]; } elseif (array_key_exists(DB_STATUS . $this->ext, $_query) or array_key_exists(DB_STATUS, $_query) or array_key_exists(CM_TST_FIELD_TEST_STATUS . $this->ext, $_query) or array_key_exists(CM_TST_FIELD_TEST_STATUS, $_query)) { return [$_query, false]; } else { // query is not empty so, if there's only one "query" in the query array, inject the AND operand // to bind the two fields in the query if (!array_key_exists(MONGO_AND, $_query)) { $_query[(DB_STATUS . $this->ext)] = [OPERAND_NULL => [OPERATOR_DNE => [STATUS_DELETED]]]; $_query[OPERAND_AND] = null; return [$_query, true]; } else { // spin though the $_query to detect the AND operand and inject the status field before the row // where the "AND" appears $newQuery = []; for($index = 0, $max = count($_query); $index < $max; $index++) { if (key($_query[$index]) == OPERAND_AND) { $newQuery[DB_STATUS . $this->ext] = [OPERAND_NULL => [OPERATOR_DNE => [STATUS_DELETED]]]; } $newQuery[] = $_query[$index]; } $_query = $newQuery; } return [$_query, true]; } } return[$_query, false]; } /** * doLogQueryTimer() -- public method * * this core method is responsible for logging a query timer to the database. * * a few checks have to be satisfied before a log message is generated and send to the error service: * * 1. the current class is using timers * 2. the current db service isn't using the remote logger * 3. the appserver user-timers setting is on * * if all of the above requirements are met, then we'll generate a timer event and send it to the error service, * specifying that this is a metric event (via the last parameter to the error service method). * * The exception to the above is if the framework has a value configured for queryThreshold and if the query * exceeded the threshold, then we'll log the slow-query event. * * If the current timer value exceeds the threshold (set in the configuration file) for slow queries, then * adjust the header output to show this is a query timer alert * * there are two parameters as input to the method: * * $_query -- the query to be logged * $_totalTime -- the amount of time to execute the query * * method returns no value nor does it update the class' state/status member variables * * @author mike@givingassistant.org * @version 1.0 * * @param $_query * @param $_totalTime * * HISTORY: * ======== * 06-30-17 mks original coding * */ public function doLogQueryTimer(string $_query, float $_totalTime) { // $sysEvents = true; // $sysEvents = false; // todo delete me once we have systemEvent support $data = null; $stackBT = null; // $objSystemEvent = new gacSystemEvents('', $this->metaPayload); // if (!$objSystemEvent->status) { // $this->logger->warn(ERROR_SCHEMA_INSTANTIATION . TEMPLATE_CLASS_SYS_EV); // $sysEvents = false; // } if ($this->useTimers) { if (($_totalTime * NUMBER_MS_PER_SEC) >= $this->queryThreshold) { // slow query event $timerHeading = '*** SLOW QUERY ALERT ***'; // if ($sysEvents) { // $data[MONGO_SYSTEM_EVENT_SEV] = EVENT_SEV_HIGH; // $data[MONGO_SYSTEM_EVENT_TYPE] = EVENT_TYPE_TIMER; // $data[MONGO_SYSTEM_EVENT_NAME] = EVENT_NAME_TIMER_SLOW_QUERY; // $data[MONGO_SYSTEM_EVENT_KEY] = STRING_DATA; // $data[MONGO_SYSTEM_EVENT_QUERY] = $_query; // $data[MONGO_SYSTEM_EVENT_QUERY_TIMER] = floatval($_totalTime); // $data[MONGO_SYSTEM_EVENT_VALUE] = [ // STRING_QUERY => $_query, // STRING_TOTAL_TIME => $_totalTime // ]; // $data[MONGO_LOG_CLASS] = $this->class; // if (isset($this->eventGUID)) $data[MONGO_SYSTEM_EVENT_EVENT_GUID] = $this->eventGUID; // } } elseif (($_totalTime * NUMBER_MS_PER_SEC) > $this->queryWarning) { // slow query warning $timerHeading = '~~~ SLOW QUERY WARNING ~~~'; // if ($sysEvents) { // $data[MONGO_SYSTEM_EVENT_SEV] = EVENT_SEV_LOW; // $data[MONGO_SYSTEM_EVENT_TYPE] = EVENT_TYPE_TIMER; // $data[MONGO_SYSTEM_EVENT_NAME] = EVENT_NAME_TIMER_QUERY_WARN; // $data[MONGO_SYSTEM_EVENT_KEY] = STRING_DATA; // $data[MONGO_SYSTEM_EVENT_QUERY] = $_query; // $data[MONGO_SYSTEM_EVENT_QUERY_TIMER] = floatval($_totalTime); // $data[MONGO_SYSTEM_EVENT_VALUE] = [ // STRING_QUERY => $_query, // STRING_TOTAL_TIME => $_totalTime // ]; // $data[MONGO_LOG_CLASS] = $this->class; // if (isset($this->eventGUID)) $data[MONGO_SYSTEM_EVENT_EVENT_GUID] = $this->eventGUID; // } } else { // regular query logging $timerHeading = 'QUERY TIMER'; // if ($sysEvents) { // $data[MONGO_SYSTEM_EVENT_SEV] = EVENT_SEV_LOW; // $data[MONGO_SYSTEM_EVENT_TYPE] = EVENT_TYPE_TIMER; // $data[MONGO_SYSTEM_EVENT_NAME] = EVENT_NAME_TIMER_QUERY; // $data[MONGO_SYSTEM_EVENT_KEY] = STRING_DATA; // $data[MONGO_SYSTEM_EVENT_QUERY] = $_query; // $data[MONGO_SYSTEM_EVENT_QUERY_TIMER] = floatval($_totalTime); // $data[MONGO_SYSTEM_EVENT_VALUE] = [ // STRING_QUERY => $_query, // STRING_TOTAL_TIME => $_totalTime // ]; // $data[MONGO_LOG_CLASS] = $this->class; // if (isset($this->eventGUID)) $data[MONGO_SYSTEM_EVENT_EVENT_GUID] = $this->eventGUID; // } } $logMsg = $timerHeading . HTML_LINE_BREAK; $logMsg .= strtoupper($this->schema . STRING_QUERY_EXEC_TIME . $_totalTime . ' seconds' . HTML_LINE_BREAK); $logMsg .= $_query . HTML_LINE_BREAK; $this->logger->metrics($logMsg, $_totalTime, $stackBT); if (!is_null($stackBT)) $data[STRING_DATA] = $stackBT; // if ($sysEvents and !is_null($data)) { // $data[MONGO_SYSTEM_EVENT_DESC] = $logMsg; // $objSystemEvent->saveEventArray([$data]); // } } } /** * addMeta() -- public method * * This method was written as a means of being able to add to the meta-data for a class, post-factory instantiation. * Since $metaPayload is a protected class, this public-facing method allows us to safely add meta-data elements * to the meta-data payload of the current class object. * * This method is normally used only in testing and in unit-testing. * * The method requires two input parameters, the key-value pairs for the new meta-data property. The key is * validated for it's name and the value is validated for the type assigned to the key. If both pass validation, * then we'll add the new field to the protected property and return a boolean-true. * * Otherwise, we're going to generate an error message, log it, and and push it on to the eventMessages stack * for the current class, and then return a boolean-false. * * * @author mike@givingassistant.org * @version 1.0 * * @param string $_key * @param mixed $_value * @return bool * * HISTORY: * ======== * 07-25-17 mks original coding * */ public function addMeta(string $_key, $_value): bool { $rd = false; if (array_key_exists($_key, $this->meta->fields)) { if (gettype($_value) == $this->meta->fields[$_key]) { $this->metaPayload[$_key] = $_value; $rd = true; } else { $msg = sprintf(ERROR_UNK_META_TYPE, gettype($_value), $_key); $this->logger->data($msg); $this->eventMessages[] = $msg; } } else { $msg = sprintf(ERROR_DATA_META_REJECTED_FOR_CLASS, $_key, $this->class); $this->logger->data($msg); $this->eventMessages[] = $msg; } return ($rd); } /** * removeMeta() -- public method * * This method allows us to cleanly remove an element from the class member $metaPayload since the class member * is protected. * * If the element is removed from the class member, we'll return a Boolean(true) -- if not, we return a * Boolean(false). * * There is one input parameter to the method: * $_key -- the key (element) to be removed from the class member * * * @author mike@givingassistant.org * @version 1.0 * * @param string $_key * @return bool * * * HISTORY: * ======== * 03-22-19 mks DB-116:original coding * */ public function removeMeta(string $_key): bool { if (array_key_exists($_key, $this->meta->fields)) { unset($this->metaPayload[$_key]); return true; } return false; } /** * replaceMeta() -- public method * * This method requires a single input parameter - an associative array of meta data that will immediately, * without validation or testing, overwrite the existing meta data payload already stored in the class. * * This method is considered to be a system-level method and should not be called casually. * * * @author mike@givingassistant.org * @version 1.0 * * @param array $_meta * * * HISTORY: * ======== * 01-09-19 mks DB-80: original coding * */ public function replaceMeta(array $_meta): void { $this->metaPayload = $_meta; } /** @noinspection PhpUnused */ /** * addMetaTemplate() -- public function * * when I switched Namaste to the gacFactory class build model, I needed a way to save the meta template as there * are times when the framework needs to inject new fields into the meta data stored in $this->metaPayload. * * Saving the meta template class to the local instantiation provides the framework with the authoritative data * to answer these types of questions. * * The input parameter, which is required, is the meta-data template object instantiated in the parent factory * class -- this method was written b/c the $meta member is protected and cannot be accessed by the factory class. * * tl;dr: * * $this->meta --- holds the meta template class definitions * $this->metaPayload --- holds the meta data payload received in the broker request * * @author mike@givingassistant.org * @version 1.0 * * @param gacMeta $_metaTemplate * * HISTORY: * ======== * 05-04-18 mks _INF-188: original coding * 12-13-18 mks DB-71: adjusting the properties of template values based on meta members * 02-04-18 mks DB-107: added audit-client to disable-audit check * 05-21-19 mks DB-116: fixed PHP Notices * */ public function addMetaTemplate(gacMeta $_metaTemplate): void { if (isset($this->metaPayload[META_DONUT_FILTER])) { $this->eventMessages[] = sprintf(INFO_META_OVERRIDE, STRING_CACHE) . $this->class; $this->useCache = false; } if (isset($this->metaPayload[META_SKIP_AUDIT]) or (isset($this->metaPayload[META_CLIENT]) and $this->metaPayload[META_CLIENT] == CLIENT_AUDIT)) { $this->eventMessages[] = sprintf(INFO_META_OVERRIDE, STRING_AUDIT) . $this->class; $this->useAuditing = AUDIT_NOT_ENABLED; $this->useJournaling = false; } $this->meta = $_metaTemplate; } /** * loadBrokerAuditData() -- public method * * This method should only be invoked by the AdminIN broker on an audit event. The purpose of the method * is to load the systemEvent token (generated in the broker event) into each record passed into the audit * data payload. The data payload is then passed into a core function to add the array into the $data member. * * Note that if the function is called outside of the admin environment, the request is immediate rejected with * an error returned. * * There are three input parameters to the method: * * $_data -- this should be $request[BROKER_DATA] culled from broker event processing * $_sysEventGUID -- this is the systemEvent record token that we'll inject into each audit record (back-tracking) * $_es -- call-by-reference parameter that records generated errors and passes them back to the calling client * * The method returns a boolean to indicate if we were able to successfully build the audit $data member. * * * @author mike@givingassistant.org * @version 1.0 * * @param array $_data * @param string $_sysEventGUID * @param array $_es * @return bool * * * HISTORY: * ======== * 10-18-18 mks DB-72: original coding * */ public function loadBrokerAuditData(array $_data, string $_sysEventGUID, array &$_es): bool { if (!boolval($this->isServiceLocal(ENV_ADMIN))) { $_es[] = ERROR_ENV_INVALID . gasConfig::$settings[CONFIG_ID][CONFIG_ID_ENV]; return false; } if (empty($_data)) { $_es[] = ERROR_DATA_ARRAY_EMPTY; return false; } if (!validateGUID($_sysEventGUID)) { $_es[] = ERROR_INVALID_GUID . $_sysEventGUID; return false; } // inject the systemEvent GUID for back-tracking foreach ($_data as &$record) $record[AUDIT_SYS_EV_GUID] = $_sysEventGUID; // import the broker data into the audit widget if (!$this->addData($_data)) { $_es[] = sprintf(ERROR_DATA_IMPORT, EVENT_TYPE_AUDIT, $this->class); return false; } return true; } /** * FQCN() -- public method * * Fully-Qualified-Column-Name (FQCN) * * This method requires a single input parameter, a string, which should be one of the following values: * * 1. A fully-qualified field name with extension as it appears in the database * 2. A field name, sans extension, but is still a valid column name * 3. A cache-mapped value representing the regular column name * * If any of the above are true, then return a string containing the fully-qualified-column-name back to the * calling client. * * If none of the above are true, a null is returned. * * * @author mike@givingassistant.org * @version 1.0 * * @param string $_fieldName * @return string|null * * * HISTORY: * ======== * 04-07-20 mks ECI-107: original coding * */ public function FQCN(string $_fieldName):?string { // is this fieldName ok on it's own? if (in_array($_fieldName, $this->fieldList)) return $_fieldName; // or is it lacking an extension? if (in_array($_fieldName . $this->ext, $this->fieldList)) return $_fieldName . $this->ext; // or is it in the cache map? if (!empty($this->cacheMap) and in_array($_fieldName, $this->cacheMap)) return (false === array_search($_fieldName, $this->cacheMap)) ? null : array_search($_fieldName, $this->cacheMap); // $_fieldName does not exist as a valid field with/without extension, or as a cache-mapped value so... return null; } /** * getMetaDataPayload() -- public method * * The metaPayload member is declared private for this class -- however, some Namaste system processing requires * access to the meta-data payload while out-of-scope of the class. This method permits those functions access * to the meta payload by returning the meta-data as an array if it's defined and assigned to the member variable. * * Otherwise, the function returns a null value to the calling client. * * * @author mike@givingassistant.org * @version 1.0 * * @return array|null * * * HISTORY: * ======== * 02-15-18 mks _INF-139: original coding * */ public function getMetaDataPayload(): ?array { return (isset($this->metaPayload) ? $this->metaPayload : null); } // non-cache-mapped data containers public function adjustSubCFieldNames(array &$_subC):bool { for ($index = 0, $max = count($_subC); $index < $max; $index++) { foreach ($_subC[$index] as $key => $value) { if (!is_array($value)) { if (!is_null($newKey = $this->addExtension($key))) { $_subC[$index][$newKey] = $value; unset($_subC[$index][$key]); } else return false; } elseif (!$this->adjustSubCFieldNames($value)) return false; } } return true; } /** * addData() -- public method * * This method enables the framework to process a data-ball instead of populating a data record, one field * at a time via setData() -- which this method ends-up calling anyway... * * The input parameter is an array of data. Please note that this should be an array of rows - even if you are * submitting one-row of data, it must be encapsulated within a container array. * * For every row in the data ball, parse each field and if that field exists in the current classes' field list, * then add that element to the current data property. * * NOTE: The data field type validation occurs in setData(). * * Method returns a boolean value indicating success or failure for the request. If no data was copied into the * data property, then a false is returned. * * NOTE: THIS IS A DESTRUCTIVE OPERATION -- WHATEVER INFO STORED IN $data PRIOR TO THIS CALL WILL BE DESTROYED! * * @author mike@givingassistant.org * @version 1.0 * * @param array $_data * @return bool * * HISTORY: * ======== * 08-16-17 mks CORE-500: original coding * 02-21-18 mks _INF-130: added condition for fields that may already have the extension appended to the * key...the data fields take this form as a function of data migration so should * be allowed to proceed. * 01-05-21 mks DB-180: Updating member $count for the data reset, better exception handling * */ public function addData(array $_data): bool { if (!is_array($_data)) { $this->eventMessages[] = ERROR_DATA_ARRAY_EMPTY . COLON . STRING_DATA; return(false); } $this->data = []; // kaboom, it's gone $this->count = 0; for ($index = 0, $max = count($_data); $index < $max; $index++) { foreach ($_data[$index] as $key => $value) { if (in_array(($key . $this->ext), $this->fieldList) or in_array($key, $this->fieldList)) { try { $this->setData($key, $value, $index); // setData() handles both key formats (w|w/o ext) } catch (TypeError | Throwable $t) { $hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__); @handleExceptionMessaging($hdr, $t->getMessage(), $foo, true); $this->eventMessages[] = ERROR_TYPE_EXCEPTION; return false; } } else { $this->eventMessages[] = sprintf(ERROR_DATA_INVALID_CLASS_KEY, $key, $this->class); } } } if (empty($this->data)) return false; $this->count = count($this->data); return true; } /** * replaceData() -- public method * * This method was written for the cacheKey process where we're, post-processing, replacing all the $data with * the cacheKey payload (That was the intended purpose of this method.) because $data is a protected member. * * There is one input parameter to the method which must be an array -- this array will overwrite the current * contents of $data, replacing it. * * The method itself returns void. * * * @author mike@givingassistant.org * @version 1.0 * * @param array $_data -- array of content to replace what ever is currently stored in the $data * * * HISTORY: * ======== * 04-16-19 mks DB-116: original coding * */ public function replaceData(array $_data): void { $this->data = $_data; } /** * removeData() -- public method * * This method clears out the existing $data property and resents dependent fields. The method has no input * parameters and returns void. * * The method is provided to provide an approved and painless way to reset the class $data. * * * @author mike@givingassistant.org * @version 1.0 * * HISTORY: * ======== * 08-21-17 mks CORE-500: original coding * */ public function removeData(): void { $this->data = []; $this->count = 0; $this->strQuery = ''; $this->queryResults = []; } /** * setRecordLimit() -- public method * * This method should only be called during data migration. Data migration, as of this writing, is the only * valid process which should be able to override the record-limit constraints set in the XML configuration. * * This method was necessary because $recordLimit is a protected variable and this method acts as a gateway to * provide access to the variable outside of the protected-class stack. * * The method requires a single input parameter which must be of type integer -- calling clients should exception- * wrap calls to this method to be safe. * * The method returns a Boolean indicating if the record limit was over-written or not. The record limit will * only be over-written if the value of the input parameter ($_limit) is greater than the zero allowing us to test * migrations with ridiculously-small payloads or, during real-runs, bump the limit up to processing thousands * of records with each request. * * * @author mike@givingassistant.org * @version 1.0 * * @param int $_limit * @return bool * * * HISTORY: * ======== * 01-31-18 mks _INF-139: original coding * */ public function setRecordLimit(int $_limit): bool { if ($_limit > 0) { $this->recordLimit = $_limit; return true; } return false; // $this->recordLimit = ($_limit != $this->recordLimit) ? $_limit : $this->recordLimit; // return ($this->recordLimit === $_limit); } /** @noinspection PhpUnused */ /** * getRecordLimit() -- public function * * Since $recordLimit member is private, we need a subroutine to expose it. If the member is not set, then set * it to the configured default. * * * @author mike@givingassistant.org * @version 1.0 * * @return int * * * HISTORY: * ======== * 05-24-18 mks _INF-188: original coding * */ public function getRecordLimit(): int { if (!isset($this->recordLimit)) $this->recordLimit = gasConfig::$settings[CONFIG_DATABASE][CONFIG_DATABASE_QUERY_RECORD_LIMIT]; return ($this->recordLimit); } /** * returnFilteredData() -- protected method * * This method requires that the existing property: $data already have a data-set assigned. We'll validate the * $data member as an array and that it contains at least one record. * * Otherwise there are no input parameters for the method. * * The method returns a boolean value which indicates the success of the operation: * * We're going to first evaluate whether or not we're going to use cache for the return payload. If we do, then * the process becomes one of caching the records and returning a cache-key. If one record is processed, we'll * return a cacheKey using the token for that record. If there is more than one record to be cached, then * we'll cache all the records under a random guid and return that as the cache key. * * Cache settings can be overridden in the meta payload variable META_DO_CACHE... if the value is set to 1, then * the data will be cached regardless or the class setting. If the value is set to 0, then the data will NOT be * cached, regardless of the class setting. * * If we're not using cache, then we're going to check to see if the class has exposedFields defined and, if so, * we'll filter the return data set through via this list. * * If the caching has been overridden, but the class has caching enabled, we're still going to cacheMap the data. * That feature of schema obfuscation cannot be bypassed. * * * @author mike@givingassistant.org * @version 1.0 * * @return bool * * HISTORY: * ======== * 07-17-17 mks CORE-464: initial coding (moved out of mongodb instantiation class) * 08-14-17 mks CORE-493: removed support for getData() meta parameter * 08-18-17 mks CORE-500: fixed NOTICE violation on accessing $this->metaPayload[META_DO_CACHE] * 10-12-17 mks CORE-584: allowing bypass of cache with META_DO_CACHE = 0 and group-caching $data if * more than one record exists in the current data payload...function now returns * a boolean value to indicate success or fail in processing. * 05-03-18 mks _INF-188: fixed bug where we were assuming data always has the class extension appended * 11-20-18 mks DB-63: override data "cleaning" on getData() for non-namaste (admin) classes * 02-15-19 mks DB-116: Refactored: removed cache-management code which is replaced by cache-processing * at the broker-services level. Eliminating a level of cursive processing in-favor * of an array function to improve performance. * */ protected function returnFilteredData(): bool { $res = false; if (empty($this->data)) { $this->eventMessages[] = ERROR_DATA_404; if ($this->debug) $this->logger->debug(ERROR_DATA_404); return $res; } elseif (!is_array($this->data)) { $msg = ERROR_DATA_ARRAY_NOT_ARRAY . STRING_DATA; $this->eventMessages[] = $msg; if ($this->debug) $this->logger->debug($msg); return $res; } // clean the data -- but only if this is a namaste class template - and filter all schemas against exposed // fields while generating the tokenList which is now required for all CRUD operations. // Also establishes the class record $count and $recordsReturned member values. try { $counter = 0; $diff = []; $rd = $this->getData(); if (!empty($this->exposedFields)) { // if we're limiting the fields, then filter them here: foreach ($rd as &$record) { $diff[] = array_diff_key($record, $this->exposedFields); if (in_array(DB_TOKEN, $record)) $this->tokenList[] = $record[DB_TOKEN]; elseif (in_array((DB_TOKEN . $this->ext), $record)) $this->tokenList[] = $record[(DB_TOKEN . $this->ext)]; else $counter++; } if ($counter) { $msg = sprintf(ERROR_GRID_MISSING_TOKENS, count($rd), $counter); $this->eventMessages[] = $msg; $this->logger->data($msg); } // if we ended up here with NO records in our diff array, do the error-processing thing if (!empty($diff)) { $this->data = $diff; unset($diff); $res = true; } else { $msg = sprintf(ERROR_DATA_ARRAY_FAIL, STRING_DIFFERENCE); $this->eventMessages[] = $msg; $this->logger->data($msg); $this->data = []; $res = false; } } else { $this->data = (count($rd)) ? $rd : null; $res = (bool) count($this->data); } unset($rd); // data has been cleaned... } catch (Throwable $t) { $msg = ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage(); $this->eventMessages[] = $msg; if (isset($this->logger) and $this->logger->available) $this->logger->error($msg); else consoleLog($this->res, CON_ERROR, $msg); $res = false; } $this->count = count($this->data); $this->recordsReturned = $this->count; return $res; } /** * getBWResults() -- public method * * because the BW (bulk-result) class member is protected, we need a gateway method what will return a copy * of the write-results to the calling client. This should only be called during the data migration event. * * if the member is not set, then a null will be returned instead of the mongoDB WriteResult object. * * * @author mike@givingassistant.org * @version 1.0 * * @return WriteResult|null * * HISTORY: * ======== * 02-05-18 mks _INF-139: original coding * */ public function getBWResults(): ?MongoDB\Driver\WriteResult { return (isset($this->bwResult)) ? $this->bwResult : null; } /** * insertField() -- public gateway method * * This is a gateway method that allows other classes to invoke setData() -- which is a protected function and * causes the spontaneous birth of all kinds of kittens when we try to call it directly during migration from * a gacFactory class widget. * * The method takes three parameters which just magically happen to coincide with the setData() params. * * The method returns a Boolean value which is passed back from the setData() method. * * * @author mike@givingassistant.org * @version 1.0 * * @param string $_field * @param $_value * @param int $_row * @param array $_errs * @param string $_res * @return bool * * * HISTORY: * ======== * 02-06-18 mks _INF-139: original coding * */ public function insertField(string $_field, $_value, int $_row = 0, array &$_errs = null, string $_res = 'MGRA: '): bool { try { $this->setData($_field, $_value, $_row); } catch (TypeError $t) { $msg = ERROR_TYPE_EXCEPTION . COLON . $t->getMessage(); $this->eventMessages[] = $msg; if (isset($this->logger) and $this->logger->available) $this->logger->error($msg); else consoleLog($this->res, CON_ERROR, $msg); return false; } if (!$this->status) { $msg = sprintf(ERROR_DATA_ADD_FAIL, MWH_NUM_RECS_MOVED, $this->class); if (isset($this->logger) and $this->logger->available) $this->logger->error($msg); else consoleLog($_res, CON_ERROR, $msg); $_errs[] = $msg; return false; } return true; } /** * launchAudit() -- public method * * This method was originally embedded in the adminBrokerIn code, in the BROKER_REQUEST_ADMIN_AUDIT_CREATE event. * It was moved to the core so as to reduce the overall code-footprint (and memory consumption) of the broker. * * This method requires the following parameters: * * $_req -- this is a copy of the broker request, ($request, not $_request) built during event pre-check * $_hj -- boolean value for $haveJournal initialized in the broker * $_seg -- string containing the system-event guid that was generated in the broker code * $_jd -- array containing the journal data payload that was originally sent to the broker event * * The function returns a boolean to indicate if processing was successful or not. Any error messages raised * will be copy to the class's eventMessages[] container and will be echo'd to the logger. * * ALGORITHM: * ---------- * First, load the broker audit data which does some of the payload validation and is also responsible for * injecting the system-event guid into the audit payload. If this request fails, processing ends. * * Next, we're going to call the function to create (save) the audit record and, if successful, pull the audit * token assigned to the newly-created record. * * Next, we'll instantiate a new journal record (if journaling is enabled for this data class) and we'll call * a method to create the journal data payload and save the journal record. * * Next, we save the journal record which has been linked back to the audit record. * * If any errors are raised, or if any methods fail, during the above processing, we generate appropriate error * messages and output them to both the eventMessages[] container and to the error log. * * The entire method, except for lvar initialization, is wrapped in an exception handler. * * * @author mike@givingassistant.org * @version 1.0 * * @param array $_req -- copy of the broker event $request array * @param bool $_hj -- does this class have journaling enabled? * @param string $_seg -- copy of the current system-event guid for the audit event * @param array $_jd -- copy of the journal-data published as part of the broker event data * @return bool * * * HISTORY: * ======== * 02-11-19 mks DB-100: moved from adminBrokerIn and refactored for local ($this->) processing * */ public function launchAudit(array $_req, bool $_hj, string $_seg, array $_jd): bool { $status = false; $errorList = []; try { if (!$this->loadBrokerAuditData($_req[BROKER_DATA], $_seg, $errorList)) { if (!empty($errorList)) { foreach ($errorList as $error) $this->logger->error($error); if (empty($this->eventMessages)) $this->eventMessages = $errorList; else $this->eventMessages = array_merge($this->eventMessages, $errorList); } $msg = sprintf(ERROR_DATA_IMPORT, SYSTEM_EVENT_DATA, $this->class); $this->eventMessages[] = $msg; $this->logger->error($msg); } else { // we've successfully created the audit object...save the audit data $this->_createRecord([], DATA_AUDT); if ($this->status) { $auditToken = $this->getColumn(DB_TOKEN); // DB-74: check to see if there is journaling data -- add new data and save if ($_hj) { $_req[BROKER_META_DATA][META_TEMPLATE] = TEMPLATE_CLASS_JOURNAL; $objJournal = new gacFactory($_req[BROKER_META_DATA], FACTORY_EVENT_NEW_CLASS, '', $errorList); if ($objJournal->status) { $jData = $objJournal->widget->template->buildJournalData($_seg, $auditToken, $_jd, $errorList); if (is_null($jData)) { $this->eventMessages[] = ERROR_JOURNAL_BUILD_FAIL; $this->logger->error(ERROR_JOURNAL_BUILD_FAIL); } else { $objJournal->widget->_createRecord($jData, DATA_AUDT); if ($objJournal->widget->status) { $status = true; } else { if (!empty($errorList)) { if (empty($this->eventMessages)) $this->eventMessages = $errorList; else $this->eventMessages = array_merge($this->eventMessages, $errorList); } elseif (!empty($objJournal->widget->eventMessages)) { if (empty($this->eventMessages)) $this->eventMessages = $errorList; else $this->eventMessages = array_merge($this->eventMessages, $errorList); } else { $msg = ERROR_TEMPLATE_INSTANTIATE . TEMPLATE_CLASS_JOURNAL; $this->eventMessages[] = $msg; $this->logger->error($msg); } $this->logger->error(ERROR_JOURNAL_GENERIC_FAIL); $this->eventMessages[] = ERROR_JOURNAL_GENERIC_FAIL; } } } else { if (!empty($errorList)) if (empty($this->eventMessages)) $this->eventMessages = $errorList; else $this->eventMessages = array_merge($this->eventMessages, $errorList); if (!empty($objJournal->eventMessages)) if (empty($this->eventMessages)) $this->eventMessages = $errorList; else $this->eventMessages = array_merge($this->eventMessages, $errorList); $msg = ERROR_TEMPLATE_INSTANTIATE . TEMPLATE_CLASS_JOURNAL; $this->eventMessages[] = $msg; $this->logger->error($msg); } if (is_object($objJournal)) $objJournal->__destruct(); unset($objJournal); } else { // successful audit event without a corresponding journaling event $status = true; } } else { if (!empty($errorList)) { if (empty($this->eventMessages)) $this->eventMessages = $errorList; else $this->eventMessages = array_merge($this->eventMessages, $errorList); } $msg = ERROR_AUDIT_CREATE; $this->eventMessages[] = $msg; $this->logger->error($msg); } } } catch (TypeError $t) { $msg = ERROR_TYPE_EXCEPTION . COLON . $t->getMessage(); $errorList[] = $msg; $this->logger->error($msg); } return $status; } /** * restoreAuditRecord() -- public method * * This method requires a single input parameter to the method and that's and empty array value into which we'll * populate copies of the before and after records for an implicit return back to the calling client. * * The method returns either a null or a boolean and the result should be parsed by the calling client. Only if * the restoration was completely successful will we return a Boolean(true) value. In all cases of error, we'll * post an appropriate message into the eventMessages member. * * The method also requires that the class be pre-loaded with the audit record as we're going to pull query * discriminant value from the $data member. * * First, we confirm that this method is exec'ing only on the admin service. If not, return immediately. * * Next, we're going to submit a request to namaste readBroker to pull a copy of the original record - we've also * set the skip-audit flag during lvar initialization so that no further audit records are created off this * recovery event. We also set the DONUT filter so that we disable caching and return the literal record. This * record will be returned implicitly in the call-by-reference parameter under the key STRING_ORIGINAL. * * If we're able to pull the original record from the audit data, we next fetch the journal record based on * audit guid (which is legit b/c audit <--1:1--> journal) and, if not found -- we return with a message indicating * that the namaste record class does not support journaling... otherwise, grab the restore query from the journal * record. * * Instantiate a write-broker client back to namaste and submit the journal-stored query to the write broker. * Evaluate the return payload and evaluate. If there was an error returned, mirror the error messages in to the * current error container and return false. * * If the journal query executed successfully, submit a new remote fetch request for the record. There will be one * of three outcomes from the remote fetch: * 1. there was an error executing the request (merger error messages and return false) * 2. the updated record was not found (when we remove records) -- populate the $_data container and return true * 3. the updated record was fetched -- populate the $_data container and return true * * * @author mike@givingassistant.org * @version 1.0 * * * @param array $_data * @return bool * * * HISTORY: * ======== * 12-13-18 mks DB-71: original coding completed and tested * 02-04-18 mks DB-107: fixed sub-collection initialization ternary comparison that assumed we'd always * have user data in the meta payload. * fixed typo in comparison statement (a[foo == bar]) => (a[foo] == bar) * 02-05-19 mks DB-107: detecting schema and loading table name from audit record - if remote fetch of * data record is PDO, we want to NOT use the default template ... instead, set the * view template to the audit-template which SHOULD be defined for the class. * 09-08-20 mks DB-168: updated for XML changes definitively declaring service locality * */ public function restoreAuditRecord(array &$_data): bool { // ensure that this method ONLY executes on admin if (!gasConfig::$settings[CONFIG_ADMIN][CONFIG_IS_LOCAL]) { $this->eventMessages[] = sprintf(ERROR_ENV_INVALID2, CONFIG_ADMIN); return false; } // set-up some local vars $auditGUID = $this->getColumn(DB_TOKEN); $recordGUID = $this->getColumn(AUDIT_RECORD_TOKEN); $template = $this->getColumn(AUDIT_TEMPLATE); $tableName = $this->getColumn(AUDIT_COLLECTION); $schema = $this->getColumn(AUDIT_SCHEMA); $meta = $this->metaPayload; $meta[META_TEMPLATE] = $template; $meta[META_SKIP_AUDIT] = 1; // set the flag to disable further audit tracking on the target record $meta[META_DONUT_FILTER] = 1; // set the flag to return an unfiltered (not cache-mapped) record $meta[META_CLIENT] = CLIENT_AUDIT; // instantiate a broker read client (brc) $brc = new gacBrokerClient(BROKER_QUEUE_R, basename(__FILE__ . COLON . __LINE__)); if (!$brc->status) { $msg = ERROR_BROKER_CLIENT_DECLARE . BROKER_QUEUE_R; $this->eventMessages[] = $msg; return false; } // first, we need to build a query to fetch the namaste record from the read broker -- we have to add a fake // status filter so that we can pull soft-deleted records if needed // $badQuery = [ DB_TOKEN => [ OPERAND_NULL => [ OPERATOR_EQ => [2] ]]]; $fetchQuery = [ DB_TOKEN => [ OPERAND_NULL => [ OPERATOR_EQ => [ $recordGUID ]]], DB_STATUS => [ OPERAND_NULL => [ OPERATOR_DNE => [ STATUS_FAKE ]]], OPERAND_AND => null ]; $request = [ BROKER_REQUEST => BROKER_REQUEST_FETCH, BROKER_DATA => [ STRING_QUERY_DATA => $fetchQuery ], BROKER_META_DATA => $meta ]; if ($schema == STRING_PDO) { $request[BROKER_DATA][STRING_FROM_DATA] = PDO_VIEW_AUDIT . $tableName; } $payload = gzcompress(json_encode($request)); $response = $brc->call($payload); $response = json_decode(gzuncompress($response), true); if (!$response[PAYLOAD_STATUS] or $response[PAYLOAD_STATE] == STATE_NOT_FOUND) { $msg = sprintf(ERROR_UT_BROKER_EVENT_FAIL, BROKER_REQUEST_FETCH, $response[PAYLOAD_STATE]); $this->eventMessages[] = $msg; $this->logger->error($msg); $this->logger->error(json_encode($request) . STRING_GUID_KEY . COLON . $recordGUID . COMMA . 'class: ' . $template); $this->eventMessages = array_merge($this->eventMessages, $response[PAYLOAD_DIAGNOSTICS]); if (is_object($brc)) $brc->__destruct(); unset($brc); $this->state = $response[PAYLOAD_STATE]; $this->status = $response[PAYLOAD_STATUS]; return false; } // copy over the record prior to the update $_data[STRING_ORIGINAL] = $response[PAYLOAD_RESULTS][STRING_QUERY_RESULTS][0]; // DON'T GO PAST THIS POINT WHEN DEBUGGING! // start to build the data payload by grabbing the journal record for the restore query $meta[META_TEMPLATE] = TEMPLATE_CLASS_JOURNAL; $objFactory = new gacFactory($meta, FACTORY_EVENT_NEW_CLASS, '', $this->eventMessages); if (!$objFactory->status) { $this->eventMessages[] = ERROR_FACTORY_LOAD . COLON . TEMPLATE_CLASS_JOURNAL; if (is_object($objFactory)) $objFactory->__destruct(); unset($objFactory); return false; } $objJournal = $objFactory->widget; if (is_object($objFactory)) $objFactory->__destruct(); unset($objFactory); // build a query to fetch the journal record based on the audit token $query = [ JOURNAL_AUD_TOK => [ OPERAND_NULL => [ OPERATOR_EQ => [ $auditGUID ]]]]; $projection = [ JOURNAL_RESTORE_QUERY, DB_TOKEN ]; // fetch the restore query and the journal record token only and store in $data $objJournal->_fetchRecords([ STRING_QUERY_DATA => $query, STRING_RETURN_DATA => $projection]); if (!$objJournal->status and $objJournal->state != STATE_NOT_FOUND) { $hdr = basename(__METHOD__) . AT . __LINE__ . COLON; $this->eventMessages[] = ($this->schema == CONFIG_SCHEMA_MONGO) ? ERROR_NOSQL_FETCH : ERROR_PDO_FETCH; $this->eventMessages[] = json_encode($query); $this->logger->error($hdr . json_encode($query)); if (is_object($objJournal)) $objJournal->__destruct(); unset($objJournal); return false; } // pull the pertinent data from the fetch and clear the object of data for re-use $thisEventGUID = $this->metaPayload[META_EVENT_GUID]; $query = (array) $objJournal->getColumn(JOURNAL_RESTORE_QUERY); $journalToken = $objJournal->getColumn(DB_TOKEN); $objJournal->removeData(); // instantiate a remote broker write client (bwc) to publish the journal spin-back request $bwc = new gacBrokerClient(BROKER_QUEUE_W, __METHOD__ . AT . __LINE__); if (!$bwc->status) { $this->eventMessages[] = basename(__FILE__) . DOT . __METHOD__ . AT . __LINE__; $this->eventMessages[] = ERROR_BROKER_CLIENT_DECLARE . BROKER_QUEUE_W; if (is_object($bwc)) $bwc->__destruct(); if (is_object($objJournal)) $objJournal->__destruct(); unset($bwc, $objJournal); return false; } // replace the meta GUID in the original query with the current event GUID $query[BROKER_META_DATA][META_EVENT_GUID] = $thisEventGUID; // set the meta template to the target (namaste) call $query[BROKER_META_DATA][META_TEMPLATE] = $template; // reset the client and the event $query[BROKER_META_DATA][META_CLIENT] = CLIENT_AUDIT; $query[BROKER_META_DATA][META_SESSION_EVENT] = EVENT_NAME_AUDIT_UPDATE; $query[BROKER_META_DATA][META_DONUT_FILTER] = 1; // publish the roll-back query (taken from the journal record) request to the targeted data (namaste) class $rc = json_decode(gzuncompress($bwc->call(gzcompress(json_encode($query)))), true); if (is_object($bwc)) $bwc->__destruct(); unset($bwc); if ($rc[PAYLOAD_STATUS] === true) { // successfully updated the target record -- now update the journal record's history section $subCollectionRecord = [ JOURNAL_HISTORY_DATE_RESTORED => time(), JOURNAL_HISTORY_RESTORED_EVENT_GUID => $thisEventGUID, JOURNAL_HISTORY_RESTORED_REASON => $this->metaPayload[META_SYSTEM_NOTES], JOURNAL_HISTORY_RESTORED_BY => (isset($this->metaPayload[META_USER_GUID]) ? $this->metaPayload[META_USER_GUID] : ((isset($this->metaPayload[META_USER_INFO])) ? $this->metaPayload[META_USER_INFO] : STRING_NOT_DEFINED)) ]; $payload = [ STRING_GUID_KEY => $journalToken, STRING_SUBC_FIELD => JOURNAL_HISTORY, STRING_DATA => $subCollectionRecord ]; $objJournal->pushSubCollectionEvent($payload); // failed to update journaling sub-collection record if (!$objJournal->status) { $msg = sprintf(ERROR_SUB_C_INSERT_FAIL, $objJournal->class); $this->logger->warn($msg); $this->eventMessages[] = $msg; if (is_object($objJournal)) $objJournal->__destruct(); unset($objJournal); return false; } // submit a broker request to fetch the "new" record $query = [ DB_TOKEN => [ OPERAND_NULL => [ OPERATOR_EQ => [ $recordGUID ]]]]; $meta[META_TEMPLATE] = $template; $request = [ BROKER_REQUEST => BROKER_REQUEST_FETCH, BROKER_DATA => [ STRING_QUERY_DATA => $query ], BROKER_META_DATA => $meta ]; $rc = json_decode(gzuncompress($brc->call(gzcompress(json_encode($request)))), true); if (is_object($brc)) $brc->__destruct(); unset($brc); if (!$rc[PAYLOAD_STATUS]) { $this->eventMessages[] = ERROR_BROKER_FETCH; return false; } elseif ($rc[PAYLOAD_STATE] == STATE_NOT_FOUND) { $_data[STRING_CHANGED] = INFO_RECORD_NOT_FOUND; } else { $_data[STRING_CHANGED] = $rc[PAYLOAD_RESULTS]; } } else { // we were unable to restore the targeted record back to it's journal'd state $this->eventMessages[] = ERROR_JOURNAL_REQ_BOMBED; $this->logger->error(ERROR_JOURNAL_REQ_BOMBED); if (!empty($rc[PAYLOAD_DIAGNOSTICS])) $this->eventMessages = array_merge($this->eventMessages, $rc[PAYLOAD_DIAGNOSTICS]); $this->state = STATE_AJ_ERROR; return false; } return true; } /** * registerAuditEvent() -- protected method * * This method is a gateway method for various private methods here in the core. It's intention is to route the * request, passed as the only input parameter to the method, to the correct handler following validation. * * If an invalid request is received, an error is generated and a bool(false) is returned to the calling client. * * Otherwise, the method invokes a the handler method passing that return value, also a boolean, back to the * calling client. * * * @author mike@givingassistant.org * @version 1.0 * * @param string $_eventType * @return bool * * * HISTORY: * ======== * 10-18-18 mks DB-67: original coding * 04-02-19 mks DB-116: check for audit-spawned event so we can bypass making a redundant call * */ protected function registerAuditEvent(string $_eventType): bool { if (empty($_eventType)) { $this->eventMessages[] = ERROR_PARAM_404 . STRING_EVENT_TYPE; if ($this->debug) $this->logger->debug(ERROR_PARAM_404 . STRING_EVENT_TYPE); return false; } if (isset($this->metaPayload[META_AUDIT_EVENT]) and $this->metaPayload[META_AUDIT_EVENT] !== 1) { // if this value is set, then we're not going to register another audit event, inferring that the event // itself was originally an audit-recovery request. return true; } switch ($_eventType) { case EVENT_NAME_AUDIT_CREATE : return $this->auditEventCreate(); break; case EVENT_NAME_AUDIT_FETCH : return $this->auditEventFetch(); break; case EVENT_NAME_AUDIT_UPDATE : return $this->auditEventUpdate(); break; case EVENT_NAME_AUDIT_DELETE : return $this->auditEventDelete(); break; default : $msg = STRING_EVENT_TYPE . ' ' . ERROR_DATA_INVALID_KEY . $_eventType; $this->eventMessages[] = $msg; if ($this->debug) $this->logger->debug($msg); return false; break; } } /** * cloneSquelch() -- protected method * * This method has no input parameters and returns void. * * The method requires that a cloned object already exist and we use that object to invoke this method which * toggles off several class member features. * * The main goal of this method, then, is to disable all the tracking features normally enabled in a data class * so as to not generate additional audit records. * * * @author mike@givingassistant.org * @version 1.0 * * * HISTORY: * ======== * 05-06-19 mks DB-116: original coding * */ protected function cloneSquelch(): void { $this->metaPayload[META_CLIENT] = CLIENT_AUDIT; $this->useAuditing = false; $this->useJournaling = false; $this->useCache = false; } /** * isTerceroLocal() -- protected method * * There is no input parameter to this method. * * The function returns a boolean value indicating whether or not the service is local or not. * * * @author mike@givingassistant.org * @version 1.0 * * @param string $_service -- should be one of the four defined environments * @return bool * * * HISTORY: * ======== * 08-31-20 mks DB-168: original coding * */ protected function isServiceLocal(string $_service): bool { $validServices = [ ENV_ADMIN, ENV_TERCERO, ENV_SEGUNDO, ENV_APPSERVER ]; if (!in_array($_service, $validServices)) { $this->eventMessages[] = ERROR_SERVICE_UNK . (!empty($_service) ? $_service : STRING_NOT_DEFINED); return false; } // check to see if $_service is local to the current instance if (isset(gasConfig::$settings[$_service][CONFIG_IS_LOCAL]) and (!gasConfig::$settings[$_service][CONFIG_IS_LOCAL])) { $hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__); $this->eventMessages[] = ERROR_LOCAL_SERVICE_404; $this->logger->warn($hdr . ERROR_LOCAL_SERVICE_404 . COLON . $_service); $this->logger->warn($hdr . STRING_CLASS_SERVICE . $this->dbService); $this->logger->warn($hdr . STRING_CLASS . COLON . $this->class); return false; } elseif (!isset(gasConfig::$settings[$_service][CONFIG_IS_LOCAL])) { // todo: security system event $hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__); $msg = ERROR_CONFIG_RESOURCE_404 . $_service; $this->eventMessages[] = ERROR_CONFIG_GENERIC; $this->logger->warn($hdr . $msg); return false; } return true; } /** * auditEventUpdate() -- private method * * This method is invoked for a data class that has auditing enabled and has just performed an update event. * * There are no input parameters to the method; the requirement is that the class be fully instantiated and * populated prior to the invocation of this event. * * Similar to the other methods, this audit-processing method builds the system-event data then injects the audit * and journaling data into the payload before submitting the entire hot, sweaty, mess to the adminIN broker * for processing. * * * @author mike@givingassistant.org * @version 1.0 * * @return bool * * * HISTORY: * ======== * 11-01-18 mks DB-68: original coding * 01-07-19 mks DB-79: fix for building the restore query in the journal record by removing the extension * text from the query keys * 01-23-19 mks DB-106: fixed bug in how the extension was being removed from the column name * 06-16-20 mks ECI-164: setting TLTI and client data explicitly for the audit event * */ private function auditEventUpdate(): bool { $data = $this->buildAuditDataPayload($this->strQuery, EVENT_NAME_AUDIT_UPDATE, STRING_AUDIT_DATA); //if (is_null($data)) { // echo PHP_EOL . 'DATA IS NULL!' . PHP_EOL; // var_export($this->eventMessages); // echo PHP_EOL; // echo 'query:' . PHP_EOL; // echo $this->strQuery . PHP_EOL; // echo 'json version:' . PHP_EOL; // echo json_encode($this->strQuery) . PHP_EOL; //} $meta = $this->metaPayload; $meta[META_TLTI] = STRING_CLASS_GAT; $meta[META_CLIENT] = CLIENT_SYSTEM; $meta[META_TEMPLATE] = TEMPLATE_CLASS_AUDIT; $meta[META_LIMIT_OVERRIDE] = 1; // generate the system event data $sysEvData[SYSTEM_EVENT_NAME] = EVENT_TYPE_AUDIT; $sysEvData[SYSTEM_EVENT_TYPE] = EVENT_NAME_AUDIT_UPDATE; $sysEvData[SYSTEM_EVENT_CLASS] = $this->class; $sysEvData[SYSTEM_EVENT_BROKER_EVENT] = BROKER_REQUEST_UPDATE; $sysEvData[SYSTEM_EVENT_OGUID] = $this->metaPayload[META_EVENT_GUID]; $sysEvData[SYSTEM_EVENT_CODE_LOC] = sprintf(STUB_LOC, basename(__FILE__), __METHOD__, __LINE__); // piggyback the systemEvent data in the audit payload (will be extracted on the admin service side) $data[SYSTEM_EVENT_DATA] = $sysEvData; // check if journaling is enabled - if so, add the journal data to the payload if ($this->useJournaling) { if (empty($this->auditRecordList) or !is_array($this->auditRecordList)) { $msg = sprintf(ERROR_AUDIT_DATA_404, STRING_JOURNAL_TOKEN_LIST, STRING_JOURNAL); $this->eventMessages[] = $msg; $this->logger->warn($msg); consoleLog($this->res, CON_SYSTEM, $msg); } else { foreach ($this->auditRecordList as $record) { $newRecord = []; foreach ($record as $key => $value) { if (false !== stripos($key, $this->ext)) { $newKey = str_replace($this->ext, '', $key); $newRecord[$newKey] = $value; } else { $newRecord[$key] = $value; } } $data[STRING_JOURNAL_DATA][STRING_JOURNAL_TOKEN_LIST][] = $newRecord[DB_TOKEN]; $query = [ DB_TOKEN => [ OPERAND_NULL => [ OPERATOR_EQ => [ $newRecord[DB_TOKEN]]]]]; unset($newRecord[DB_TOKEN], $newRecord[$this->pKey]); $request = [ BROKER_REQUEST => BROKER_REQUEST_UPDATE, BROKER_DATA => [STRING_QUERY_DATA => $query, STRING_UPDATE_DATA => $newRecord], BROKER_META_DATA => $meta ]; $data[STRING_JOURNAL_DATA][STRING_JOURNAL_QUERY_LIST][] = $request; } } } // get the data payload ready for transport $payload = [ BROKER_REQUEST => BROKER_REQUEST_ADMIN_AUDIT_CREATE, BROKER_DATA => $data, BROKER_META_DATA => $meta, ]; $payload = gzcompress(json_encode($payload)); // next step -- call the method to publish the payload and return the results from that method to the client return ($this->publishAndPerish($payload, $sysEvData[SYSTEM_EVENT_CODE_LOC])); } /** * auditEventDelete() -- private method * * This method generates the system-event, audit, and journaling data (records) for the delete event. Once the * data payloads are generated, a broker client is instantiated and the request is published to the adminIN broker. * * There are no input parameters for the method -- all of the required data is set in class members prior to * this method being invoked. * * The return value is passed from the publishAndPerish() method which indicates if the request was successfully * processed or not. * * * @author mike@givingassistant.org * @version 1.0 * * @return bool * * * HISTORY: * ======== * 11-02-18 mks DB-70: original coding * 06-16-20 mks ECI-164: setting TLTI and client data explicitly for the audit event * */ private function auditEventDelete(): bool { $data = $this->buildAuditDataPayload($this->strQuery, EVENT_NAME_AUDIT_DELETE,STRING_AUDIT_DATA); $meta = $this->metaPayload; $meta[META_TLTI] = STRING_CLASS_GAT; $meta[META_CLIENT] = CLIENT_SYSTEM; $meta[META_TEMPLATE] = TEMPLATE_CLASS_AUDIT; $meta[META_LIMIT_OVERRIDE] = 1; // generate the system event data $sysEvData[SYSTEM_EVENT_NAME] = EVENT_TYPE_AUDIT; $sysEvData[SYSTEM_EVENT_TYPE] = EVENT_NAME_AUDIT_DELETE; $sysEvData[SYSTEM_EVENT_CLASS] = $this->class; $sysEvData[SYSTEM_EVENT_BROKER_EVENT] = BROKER_REQUEST_DELETE; $sysEvData[SYSTEM_EVENT_OGUID] = $this->metaPayload[META_EVENT_GUID]; $sysEvData[SYSTEM_EVENT_CODE_LOC] = sprintf(STUB_LOC, basename(__FILE__), __METHOD__, __LINE__); // piggyback the systemEvent data in the audit payload (will be extracted on the admin service side) $data[SYSTEM_EVENT_DATA] = $sysEvData; // check if journaling is enabled -- if so, add journal data to the payload if ($this->useJournaling) { if (empty($this->auditRecordList) or !is_array($this->auditRecordList)) { $msg = sprintf(ERROR_AUDIT_DATA_404, STRING_JOURNAL_TOKEN_LIST, STRING_JOURNAL); $this->eventMessages[] = $msg; $this->logger->warn($msg); consoleLog($this->res, CON_SYSTEM, $msg); return false; } else { foreach ($this->auditRecordList as $record) { $recordCopy = $record; // so we can update the record $guid = $recordCopy[(DB_TOKEN . $this->ext)]; // remove the mongo _id field from the record if it exists if (isset($recordCopy[$this->pKey])) unset($recordCopy[$this->pKey]); $data[STRING_JOURNAL_DATA][STRING_JOURNAL_TOKEN_LIST][] = $guid; if ($this->useDeletes === false) { // soft delete restore query $request = [ BROKER_REQUEST => BROKER_REQUEST_UPDATE, BROKER_DATA => [ STRING_QUERY_DATA => [ DB_TOKEN => [ OPERAND_NULL => [ OPERATOR_EQ => [$guid]]]], STRING_UPDATE_DATA => [ DB_STATUS => $recordCopy[(DB_STATUS . $this->ext)]] ], BROKER_META_DATA => $this->metaPayload ]; } else { // hard delete restore query $request = [ BROKER_REQUEST => BROKER_REQUEST_CREATE, BROKER_DATA => $record, BROKER_META_DATA => $this->metaPayload ]; } $data[STRING_JOURNAL_DATA][STRING_JOURNAL_QUERY_LIST][] = $request; } } } // get the data payload ready for transport $payload = [ BROKER_REQUEST => BROKER_REQUEST_ADMIN_AUDIT_CREATE, BROKER_DATA => $data, BROKER_META_DATA => $meta ]; $payload = gzcompress(json_encode($payload)); // next step -- call the method to publish the payload and return the results from that method to the client return ($this->publishAndPerish($payload, $sysEvData[SYSTEM_EVENT_CODE_LOC])); } /** * auditEventFetch() -- private method * * This method is generates systemEvent and Audit data payloads that are then published to the adminIN broker * on the admin service. * * There are no input parameters to the method as it's assumed the requisite data members are pre-populated * and pre-validated by the calling client process(es). * * The method build the data and system event payloads, sets the event, then passes the data to another method * to publish the remote request -- this method returns a status (Boolean) value which is passed-through to the * calling client. * * * @author mike@givingassistant.org * @version 1.0 * * @return bool * * * HISTORY: * ======== * 10-20-18 mks DB-68: original coding * 06-16-20 mks ECI-164: setting TLTI and client data explicitly for the audit event * */ private function auditEventFetch(): bool { $data = $this->buildAuditDataPayload($this->strQuery, EVENT_NAME_AUDIT_FETCH); $meta = $this->metaPayload; $meta[META_TEMPLATE] = TEMPLATE_CLASS_AUDIT; $meta[META_TLTI] = STRING_CLASS_GAT; $meta[META_CLIENT] = CLIENT_SYSTEM; $meta[META_LIMIT_OVERRIDE] = 1; // generate the system event data $sysEvData[SYSTEM_EVENT_NAME] = EVENT_TYPE_AUDIT; $sysEvData[SYSTEM_EVENT_TYPE] = EVENT_NAME_AUDIT_FETCH; $sysEvData[SYSTEM_EVENT_CLASS] = $this->class; $sysEvData[SYSTEM_EVENT_BROKER_EVENT] = BROKER_REQUEST_FETCH; $sysEvData[SYSTEM_EVENT_OGUID] = $this->metaPayload[META_EVENT_GUID]; $sysEvData[SYSTEM_EVENT_CODE_LOC] = sprintf(STUB_LOC, basename(__FILE__), __METHOD__, __LINE__); // piggyback the systemEvent data in the audit payload (will be extracted on the admin service side) $data[SYSTEM_EVENT_DATA] = $sysEvData; // get the data payload ready for transport $payload = [ BROKER_REQUEST => BROKER_REQUEST_ADMIN_AUDIT_CREATE, BROKER_DATA => $data, BROKER_META_DATA => $meta ]; $payload = gzcompress(json_encode($payload)); // next step -- call the method to publish the payload and return the results from that method to the client return ($this->publishAndPerish($payload, $sysEvData[SYSTEM_EVENT_CODE_LOC])); } /** * auditEventCreate() -- private method * * This method is responsible for creating the data payload for both the systemEvent and the Audit (Create) * records for the current data class. * * There are no input parameters to the method as all data is expected to already be populated in the class * member variables. * * The method is meant to handle 1->N records in the current $data member. Note, please, that this is NOT the * audit or systemEvent class - this is the Namaste data class where auditing has been enabled. * * We spin through the records in $data creating an audit record for each... next we create a single record for * the systemEvent collection and, that data, is embedded within the indexed array as a key-value pair. * (Extracted in the broker processing admin-side and removed prior to creating the admin records.) * * Finally, we create/instantiate a broker client to AdminIn and publish the audit event request. Since adminIN * is non-RPC, we don't block on a response from the broker and just proceed to clean-up and return a bool(true) * to the calling client. * * * @author mike@givingassistant.org * @version 1.0 * * @return bool * * HISTORY: * ======== * 10-18-18 mks DB-67: original coding * 11-26-18 mks DB-55: fixed parameter passed to buildAuditDataPayload(): if array, json-ize the array * 06-16-20 mks ECI-164: setting TLTI and client data explicitly for the audit event * */ private function auditEventCreate(): bool { // generate the data payload that will be submitted to the audit event... $query = (is_array($this->auditCreateQueries)) ? json_encode($this->auditCreateQueries) : $this->auditCreateQueries; $data = $this->buildAuditDataPayload($query, EVENT_NAME_AUDIT_CREATE); $meta = $this->metaPayload; $meta[META_TEMPLATE] = TEMPLATE_CLASS_AUDIT; $meta[META_TLTI] = STRING_CLASS_GAT; $meta[META_CLIENT] = CLIENT_SYSTEM; $meta[META_LIMIT_OVERRIDE] = 1; // generate the system event data $sysEvData[SYSTEM_EVENT_NAME] = EVENT_TYPE_AUDIT; $sysEvData[SYSTEM_EVENT_TYPE] = EVENT_NAME_AUDIT_CREATE; $sysEvData[SYSTEM_EVENT_CLASS] = $this->class; $sysEvData[SYSTEM_EVENT_BROKER_EVENT] = BROKER_REQUEST_CREATE; $sysEvData[SYSTEM_EVENT_OGUID] = $this->metaPayload[META_EVENT_GUID]; $sysEvData[SYSTEM_EVENT_CODE_LOC] = sprintf(STUB_LOC, basename(__FILE__), __METHOD__, __LINE__); $sysEvData[SYSTEM_EVENT_KEY] = 'hasJournaling'; $sysEvData[SYSTEM_EVENT_VAL] = intval($this->useJournaling); // piggyback the systemEvent data in the audit payload (will be extracted on the admin service side) $data[SYSTEM_EVENT_DATA] = $sysEvData; // check if journaling is enabled - if so, add the journal data to the payload if ($this->useJournaling) { if (empty($this->auditRecordList) or !is_array($this->auditRecordList)) { $msg = sprintf(ERROR_AUDIT_DATA_404, STRING_JOURNAL_TOKEN_LIST, STRING_JOURNAL); $this->eventMessages[] = $msg; $this->logger->warn($msg); consoleLog($this->res, CON_SYSTEM, $msg); } else { $data[STRING_JOURNAL_DATA][STRING_JOURNAL_TOKEN_LIST] = $this->auditRecordList; } if (empty($this->auditUndoQueries) or !is_array($this->auditUndoQueries)) { $msg = sprintf(ERROR_AUDIT_DATA_404, STRING_JOURNAL_QUERY_LIST, STRING_JOURNAL); $this->eventMessages[] = $msg; $this->logger->warn($msg); consoleLog($this->res, CON_SYSTEM, $msg); } else { $data[STRING_JOURNAL_DATA][STRING_JOURNAL_QUERY_LIST] = $this->auditUndoQueries; } } // get the data payload ready for transport $payload = [ BROKER_REQUEST => BROKER_REQUEST_ADMIN_AUDIT_CREATE, BROKER_DATA => $data, BROKER_META_DATA => $meta ]; $payload = gzcompress(json_encode($payload)); // next step -- call the method to publish the payload and return the results from that method to the client return ($this->publishAndPerish($payload, $sysEvData[SYSTEM_EVENT_CODE_LOC])); } /** * publishAndPerish() -- private method * * This method is called by the CRUD audit events when a data payload needs to be published to the AdminIN broker. * * The method accepts the following parameters: * * $_payload -- this is the broker payload which has already been json-encoded and compressed * $_loc -- string text that identifies the origin of the calling client * * The method instantiates a gacAdminClient and publishes the request to the AdminIN broker -- since there is no * return payload, the boolean value this method returns to the calling client is based on the success of processing * and submitting the request only. * * If the payload was unsuccessful, for whatever reasons, error messages are generated and control is returned to * the calling client. * * * @author mike@givingassistant.org * @version 1.0 * * @param string $_payload * @param string $_loc * @return bool * * * HISTORY: * ======== * 10-20-18 mks DB-68: original coding * */ private function publishAndPerish(string $_payload, string $_loc): bool { try { $errMsg = ERROR_BROKER_CLIENT_DECLARE . CONFIG_ADMIN_BROKER_IN; $objAdminClient = new gacWorkQueueClient($_loc); if (!$objAdminClient->status) { $this->eventMessages[] = $errMsg; if ($this->debug and $this->logger->available) $this->logger->debug($errMsg); if (is_object($objAdminClient)) $objAdminClient->__destruct(); unset($objAdminClient); return false; } @$objAdminClient->call($_payload); // fire-n-forget broker if (is_object($objAdminClient)) $objAdminClient->__destruct(); unset($objAdminClient); // todo -- we can evaluate the success of the publish request by publishing a second request to fetch the newly-created audit record and return a bool based on the presence of the record return true; // true status only indicates that we published the event } catch (Throwable $t) { $this->eventMessages[] = $errMsg; $this->eventMessages[] = ERROR_THROWABLE_EXCEPTION; $this->eventMessages[] = $t->getMessage(); $this->logger->warn($errMsg); $this->logger->warn(ERROR_THROWABLE_EXCEPTION); $this->logger->warn($t->getMessage()); consoleLog($this->res, CON_SYSTEM, ERROR_THROWABLE_EXCEPTION . $t->getMessage()); if (isset($objAdminClient) and is_object($objAdminClient)) $objAdminClient->__destruct(); unset($objAdminClient); return false; } } /** * buildAuditDataPayload() -- private method * * This method is used by the audit CRUD routines and has the simple job of building the data payload for an * audit event to be submitted to the adminIn broker. * * The single input parameter is the query string. Note that for destructive queries, this should be json-ized * array containing the fully-functional broker payload that could be submitted, accepted and processed. For * the fetch queries, it should be the query string built. * * Method requires that the $data member be populated and that the data class is fully-instantiated. * * Once each audit record has been generated, one for each record touched during the CRUD request, we return the * array of audit-data records back to the calling client. * * If an error is raised, then a null value is returned and it's the responsibility of the calling client * to handle that return. * * * @author mike@givingassistant.org * @version 1.0 * * @param string $_query * @param string $_event -- defines which CRUD event is generating the audit event * @param string $_which -- defines which data source are we pulling from ($data or $auditRecordList) * @return array|null * * * HISTORY: * ======== * 10-29-18 mks DB-68: original coding * 11-06-18 mks DB-82: added the $_event parameter because d'oh! * */ private function buildAuditDataPayload(string $_query, string $_event, string $_which = STRING_DATA): ?array { $dbConfig = gasConfig::$settings[CONFIG_DATABASE][CONFIG_DATABASE_MONGODB][$this->dbService]; $data = null; $counter = 0; if ($_which != STRING_DATA and $_which != STRING_AUDIT_DATA) { $msg = ERROR_AUDIT_SOURCE; $this->eventMessages[] = $msg; if ($this->debug) $this->logger->debug($msg); return null; } else { $sourceData = ($_which == STRING_DATA) ? $this->getData() : $this->auditRecordList; } if (!is_array($sourceData) or empty($sourceData)) { $this->eventMessages[] = ERROR_AUDIT_NO_SOURCE; if ($this->debug) $this->logger->error(ERROR_AUDIT_NO_SOURCE); return $data; } // first step is to build the meta & data payloads that will be submitted to the systemEvent broker foreach ($sourceData as $record) { if (isset($this->metaPayload[META_SESSION_GUID])) $data[$counter][AUDIT_SESSION_GUID] = $this->metaPayload[META_SESSION_GUID]; if (isset($this->metaPayload[META_USER_GUID])) $data[$counter][AUDIT_USER_GUID] = $this->metaPayload[META_USER_GUID]; if (isset($this->metaPayload[META_SESSION_IP])) $data[$counter][AUDIT_SESSION_IP] = $this->metaPayload[META_SESSION_IP]; if (isset($this->metaPayload[META_CLIENT])) $data[$counter][AUDIT_ACCESS_CLIENT] = $this->metaPayload[META_CLIENT]; $data[$counter][AUDIT_SERVICE] = $this->dbService; $data[$counter][AUDIT_SCHEMA] = $this->schema; $data[$counter][AUDIT_DB] = gasConfig::$settings[CONFIG_ID][CONFIG_ID_ENV] . UDASH . $dbConfig[CONFIG_DATABASE_MONGODB_AUTH_SOURCE]; $data[$counter][AUDIT_COLLECTION] = $this->collectionName; $data[$counter][AUDIT_TEMPLATE] = $this->templateClass; $data[$counter][AUDIT_COLLECTION_EXT] = $this->ext; $data[$counter][AUDIT_RECORD_TOKEN] = $record[DB_TOKEN . $this->ext]; $data[$counter][AUDIT_SNAPSHOT] = base64_encode(json_encode($record)); $data[$counter][AUDIT_QUERY] = $_query; $data[$counter][AUDIT_OPERATION] = $_event; $data[$counter][AUDIT_ACCESS_ALLOWED] = true; $data[$counter][DB_EVENT_GUID] = $this->metaPayload[META_EVENT_GUID]; // fields that were not-defined at time of original coding todo: code this when data becomes available $data[$counter][AUDIT_ACCESS_USER] = STRING_NOT_DEFINED; $data[$counter++][AUDIT_USER_ROLE] = STRING_NOT_DEFINED; } return $data; } /** * squelchIDEWarnings() -- private class method * * This method is called once, by the class constructor, and is used to init some member variables so as to * suppress IDE warnings in the member-declaration section. Not all of these member variables are in-use as of * yet (although they will be, someday...) so they're stationed here, in this little null-value routine. * * * @author mike@givingassistant.org * @version 1.0 * * * HISTORY: * ======== * 01-03-20 mks DB-150: original coding * */ private function squelchIDEWarnings() { $this->recordAudit = []; $this->recordJournal = []; $this->strSubCollectionQuery = ''; $this->restoreQuery = ''; $this->returnData = []; $this->requiredFields = []; $this->indexOrder = []; $this->historyData = null; $this->recordLimit = (gasConfig::$settings[CONFIG_DATABASE][CONFIG_DATABASE_QUERY_RECORD_LIMIT]) ?? 100; $this->exposedFields = null; } /** * __destruct() -- public function * * class destructor * * @author mike@givingassistant.org * @version 1.0 * * HISTORY: * ======== * 06-15-17 mks original coding * */ public function __destruct() { // As of PHP 5.3.10 destructors are not run on shutdown caused by fatal errors. // // destructor is registered shut-down function in constructor -- so any recovery // efforts should go in this method. // there is no destructor method defined in the core abstraction class, hence // there is no call to that parent destructor in this class. } }