SANITIZE_ALLOW_DIGIT, // TYPO3_DEBUG_SAVE => SANITIZE_ALLOW_DIGIT, // TYPO3_FORM => SANITIZE_ALLOW_ALNUMX, // TYPO3_FE_USER => SANITIZE_ALLOW_ALNUMX, // TYPO3_FE_USER_UID => SANITIZE_ALLOW_DIGIT, // TYPO3_FE_USER_GROUP => SANITIZE_ALLOW_ALNUMX, CLIENT_SIP => SANITIZE_ALLOW_ALNUMX, CLIENT_TYPO3VARS => SANITIZE_ALLOW_ALNUMX, CLIENT_RECORD_ID => SANITIZE_ALLOW_DIGIT, CLIENT_KEY_SEM_ID => SANITIZE_ALLOW_DIGIT, CLIENT_KEY_SEM_ID_USER => SANITIZE_ALLOW_DIGIT, CLIENT_PAGE_ID => SANITIZE_ALLOW_DIGIT, CLIENT_PAGE_TYPE => SANITIZE_ALLOW_DIGIT, CLIENT_PAGE_LANGUAGE => SANITIZE_ALLOW_DIGIT, CLIENT_FORM => SANITIZE_ALLOW_ALNUMX, // Part of $_SERVER. Missing vars must be requested individual with the needed sanitize class. CLIENT_SCRIPT_URL => SANITIZE_ALLOW_ALNUMX, CLIENT_SCRIPT_URI => SANITIZE_ALLOW_ALNUMX, CLIENT_HTTP_HOST => SANITIZE_ALLOW_ALNUMX, CLIENT_HTTP_USER_AGENT => SANITIZE_ALLOW_ALNUMX, CLIENT_SERVER_NAME => SANITIZE_ALLOW_ALNUMX, CLIENT_SERVER_ADDRESS => SANITIZE_ALLOW_ALNUMX, CLIENT_SERVER_PORT => SANITIZE_ALLOW_DIGIT, CLIENT_REMOTE_ADDRESS => SANITIZE_ALLOW_ALNUMX, CLIENT_REQUEST_SCHEME => SANITIZE_ALLOW_ALNUMX, CLIENT_SCRIPT_FILENAME => SANITIZE_ALLOW_ALNUMX, CLIENT_QUERY_STRING => SANITIZE_ALLOW_ALL, CLIENT_REQUEST_URI => SANITIZE_ALLOW_ALL, CLIENT_SCRIPT_NAME => SANITIZE_ALLOW_ALNUMX, CLIENT_PHP_SELF => SANITIZE_ALLOW_ALNUMX, // CLIENT_UPLOAD_FILENAME => SANITIZE_ALLOW_ALLBUT, // SYSTEM_DBUSER => SANITIZE_ALLOW_ALNUMX, // SYSTEM_DBSERVER => SANITIZE_ALLOW_ALNUMX, // SYSTEM_DBPW => SANITIZE_ALLOW_ALL, // SYSTEM_DB => SANITIZE_ALLOW_ALNUMX, // SYSTEM_TESTDB => SANITIZE_ALLOW_ALNUMX, // SYSTEM_SESSIONNAME => SANITIZE_ALLOW_ALNUMX, // SYSTEM_DBH => SANITIZE_ALLOW_ALL, // SYSTEM_SQL_RAW => SANITIZE_ALLOW_ALL, // SYSTEM_SQL_FINAL => SANITIZE_ALLOW_ALL, // SYSTEM_SQL_COUNT => SANITIZE_ALLOW_DIGIT, // SYSTEM_SQL_PARAM_ARRAY => SANITIZE_ALLOW_ALL, // SIP_SIP => SANITIZE_ALLOW_ALNUMX, // SIP_RECORD_ID => SANITIZE_ALLOW_DIGIT, // SIP_FORM => SANITIZE_ALLOW_ALNUMX, // SIP_URLPARAM => SANITIZE_ALLOW_ALL ]; self::$sanitizeStore = [ STORE_FORM => true, STORE_SIP => false, STORE_RECORD => false, STORE_BEFORE => false, STORE_PARENT_RECORD => false, STORE_TABLE_DEFAULT => false, STORE_TABLE_COLUMN_TYPES => false, STORE_CLIENT => true, STORE_TYPO3 => false, STORE_VAR => false, STORE_ZERO => false, STORE_EMPTY => false, STORE_SYSTEM => false, STORE_EXTRA => false, STORE_LDAP => false, STORE_ADDITIONAL_FORM_ELEMENTS => false ]; self::fillSystemStore($fileConfigIni); self::fillStoreTypo3($bodytext); self::fillStoreClient(); self::fillStoreSip(); self::fillStoreExtra(); } /** * Fill the system store by reading config.qfq.ini. Also setup config defaults. * * @throws CodeException * @throws qfq\UserFormException */ private static function fillSystemStore($fileConfigIni = '') { $cfg = new Config(); $config = $cfg->readConfig($fileConfigIni); // Defaults Support::setIfNotSet($config, SYSTEM_DATE_FORMAT, 'yyyy-mm-dd'); Support::setIfNotSet($config, SYSTEM_SHOW_DEBUG_INFO, 'auto'); Support::setIfNotSet($config, F_BS_COLUMNS, '12'); Support::setIfNotSet($config, F_BS_LABEL_COLUMNS, '3'); Support::setIfNotSet($config, F_BS_INPUT_COLUMNS, '6'); Support::setIfNotSet($config, F_BS_NOTE_COLUMNS, '3'); Support::setIfNotSet($config, F_CLASS_PILL, 'qfq-color-grey-1'); Support::setIfNotSet($config, F_CLASS_BODY, 'qfq-color-grey-2'); Support::setIfNotSet($config, F_BUTTON_ON_CHANGE_CLASS, 'btn-info alert-info'); Support::setIfNotSet($config, SYSTEM_EDIT_FORM_PAGE, 'form'); $config = self::doSystemPath($config); $config = self::adjustConfig($config); self::checkMandatoryParameter($config); self::setStore($config, STORE_SYSTEM, true); } /** * QFQ might be called via Typo3 (index.php) or directly via AJAX (directory: api). The * @param array $config * @return array */ private function doSystemPath(array $config) { // SYSTEM_PATH_EXT: compute only if not already defined. if (!isset($config[SYSTEM_PATH_EXT]) || $config[SYSTEM_PATH_EXT] === '' || $config[SYSTEM_PATH_EXT][0] !== '/') { $relExtDir = '/typo3conf/ext/' . EXT_KEY; // If we are called through AJAX API (e.g. api/save.php), there is no TYPO3 environment. if (isset($_SERVER['SCRIPT_FILENAME'])) { $pos = strpos($_SERVER['SCRIPT_FILENAME'], $relExtDir); if ($pos === false && isset($GLOBALS['TYPO3_LOADED_EXT'][EXT_KEY]['ext_localconf.php'])) { // Typo3 extension: probably index.php $config[SYSTEM_PATH_EXT] = dirname($GLOBALS['TYPO3_LOADED_EXT'][EXT_KEY]['ext_localconf.php']); $config[SYSTEM_SITE_PATH] = dirname($_SERVER['SCRIPT_FILENAME']); } else { // API $config[SYSTEM_PATH_EXT] = substr($_SERVER['SCRIPT_FILENAME'], 0, $pos + strlen($relExtDir)); $config[SYSTEM_SITE_PATH] = substr($_SERVER['SCRIPT_FILENAME'], 0, $pos); } } else { // No $_SERVER >>this means phpUnit. $config[SYSTEM_SITE_PATH] = getcwd(); $config[SYSTEM_PATH_EXT] = getcwd(); } } return $config; } /** * Depending on some configuration value, update corresponding values. * * @param array $config * @return array */ private static function adjustConfig(array $config) { // Adjust config if ($config[SYSTEM_SHOW_DEBUG_INFO] === 'auto') { $rc = self::beUserLoggdIn(); if ($rc !== false) { $config[SYSTEM_SHOW_DEBUG_INFO] = $rc; } } // make SQL PATH absolute. This is necessary to work in different directories correctly. if (isset($config[SYSTEM_SQL_LOG]) && $config[SYSTEM_SQL_LOG][0] !== '/') { $config[SYSTEM_SQL_LOG] = $config[SYSTEM_PATH_EXT] . '/' . $config[SYSTEM_SQL_LOG]; } return $config; } /** * @return bool|string */ private static function beUserLoggdIn() { if (isset($GLOBALS["TSFE"])) { $rc = (isset($GLOBALS["TSFE"]->beUserLogin) && $GLOBALS["TSFE"]->beUserLogin === true) ? 'yes' : 'no'; } else { $rc = false; } return $rc; } /** * Iterate over all Parameter which have to exist in the config. Throw an array if any is missing. * * @param array $config * @throws UserFormException */ private static function checkMandatoryParameter(array $config) { // Check mandatory config vars. $names = array('DB_USER', 'DB_SERVER', 'DB_PASSWORD', 'DB_NAME', 'SQL_LOG', 'SQL_LOG_MODE'); foreach ($names as $name) { if (!isset($config[$name])) { throw new qfq\UserFormException ("Missing configuration in `config.ini`: $name", ERROR_MISSING_CONFIG_INI_VALUE); } } } /** * Set or overwrite a complete store. * * @param array $dataArray * @param $store * @param bool|false $flagOverwrite * @throws UserFormException * @throws \qfq\CodeException */ public static function setStore(array $dataArray, $store, $flagOverwrite = false) { // Check valid Storename if (!isset(self::$sanitizeStore)) throw new UserFormException("Unknown Store: $store", ERROR_UNNOWN_STORE); if ($store === STORE_ZERO) throw new CodeException("setVarArray() for STORE_ZERO is impossible - there are no values.", ERROR_SET_STORE_ZERO); if (!$flagOverwrite && isset(self::$raw[$store]) && count(self::$raw[$store]) > 0) { throw new CodeException("Raw values already been copied to store '$store'. Do this only one time.", ERROR_STORE_VALUE_ALREADY_CODPIED); } self::$raw[$store] = $dataArray; } /** * Copy the BodyText as well as some T3 specific vars to STORE_TYPO3. * Attention: if called through API, there is no T3 environment. The only values which are available are fe_user and fe_user_uid. * * @param $bodytext * @throws CodeException */ private static function fillStoreTypo3($bodytext) { // form=, showDebugBodyText=, 10.20.. $arr = KeyValueStringParser::parse($bodytext, "=", "\n"); if (isset($GLOBALS["TSFE"])) { if (isset($GLOBALS["TSFE"]->fe_user->user["username"])) { $arr[TYPO3_FE_USER] = $GLOBALS["TSFE"]->fe_user->user["username"]; } if (isset($GLOBALS["TSFE"]->fe_user->user["uid"])) { $feUid = $GLOBALS["TSFE"]->fe_user->user["uid"]; $arr[TYPO3_FE_USER_UID] = $GLOBALS["TSFE"]->fe_user->user["uid"]; } if (isset($GLOBALS["TSFE"]->fe_user->user["usergroup"])) { $arr[TYPO3_FE_USER_GROUP] = $GLOBALS["TSFE"]->fe_user->user["usergroup"]; } if (isset($GLOBALS["TSFE"]->page["uid"])) { $arr[TYPO3_TT_CONTENT_UID] = $GLOBALS["TSFE"]->page["uid"]; } if (isset($GLOBALS["TSFE"]->id)) { $arr[TYPO3_PAGE_ID] = $GLOBALS["TSFE"]->id; } if (isset($GLOBALS["TSFE"]->type)) { $arr[TYPO3_PAGE_TYPE] = $GLOBALS["TSFE"]->type; } if (isset($GLOBALS["TSFE"]->sys_language_uid)) { $arr[TYPO3_PAGE_LANGUAGE] = $GLOBALS["TSFE"]->sys_language_uid; } } else { // No T3 environment (called by API): restore from SESSION foreach ([SESSION_FE_USER, SESSION_FE_USER_UID, SESSION_FE_USER_GROUP] as $key) { if (isset($_SESSION[SESSION_NAME][$key])) { $arr[$key] = $_SESSION[SESSION_NAME][$key]; } } } self::setStore($arr, STORE_TYPO3, true); } /** * Fills the STORE_CLIENT * * @throws CodeException */ private static function fillStoreClient() { // copy GET and POST and SERVER Parameter. Priority: SERVER, POST, GET $arr = array(); if (isset($_GET)) $arr = array_merge($arr, $_GET); if (isset($_POST)) $arr = array_merge($arr, $_POST); // It's important to merge the SERVER array last: those entries shall overwrite client values. if (isset($_SERVER)) $arr = array_merge($arr, $_SERVER); $arr = \qfq\Sanitize::normalize($arr); self::setStore($arr, STORE_CLIENT, true); } /** * Fills the STORE_SIP. Reads therefore specified SIP, decode the values and stores them in STORE_SIP. * * @throws CodeException * @throws UserFormException */ private static function fillStoreSip() { self::$sip = new Sip(self::$phpUnit); $s = self::getVar(CLIENT_SIP, STORE_CLIENT); if ($s !== false) { // if session is given, copy values to store $param = self::$sip->getVarsFromSip($s); $param[SIP_SIP] = $s; $param[SIP_URLPARAM] = self::$sip->getQueryStringFromSip($s); // self::setVarArray(KeyValueStringParser::parse($param, "=", "&"), STORE_SIP); self::setStore($param, STORE_SIP, true); } } /** * Cycles through all stores in $useStore. * First match will return the found value. * During cycling: fill cache with requestet value and sanitize raw value. * * @param string $key * @param string $useStores f.e.: 'FSRD' * @param string $sanitizeClass * @param string $foundInStore Returns the name of the store where $key has been found. If $key is not found, return ''. * @return string a) if found: value, b) false * @throws \qfq\CodeException */ public static function getVar($key, $useStores = STORE_USE_DEFAULT, $sanitizeClass = '', &$foundInStore = '') { // no store specifed? if ($useStores === "" || $useStores === null) { $useStores = STORE_USE_DEFAULT; } // no sanitizeClass specified: take predefined (if exist) or default. if ($sanitizeClass === '' || $sanitizeClass === null) { $sanitizeClass = isset(self::$sanitizeClass[$key]) ? self::$sanitizeClass[$key] : SANITIZE_DEFAULT; } while ($useStores !== false) { $store = substr($useStores, 0, 1); // next store $finalKey = $key; if ($store == STORE_LDAP) { $finalKey = strtolower($key); // in STORE_LDAP all keys are lowercase } $foundInStore = $store; $useStores = substr($useStores, 1); // shift left remaining stores if (!isset(self::$raw[$store][$finalKey])) { switch ($store) { case STORE_ZERO: return 0; case STORE_EMPTY: return ''; case STORE_VAR: if ($finalKey === VAR_RANDOM) { return Support::randomAlphaNum(RANDOM_LENGTH); } else { continue 2; // no value provided, continue with while loop } break; default: continue 2; // no value provided, continue with while loop break; } } $rawVal = isset(self::$raw[$store][$finalKey]) ? self::$raw[$store][$finalKey] : null; if (self::$sanitizeStore[$store] && $sanitizeClass != '') { if ($sanitizeClass == SANITIZE_ALLOW_PATTERN || $sanitizeClass == SANITIZE_ALLOW_MIN_MAX || $sanitizeClass == SANITIZE_ALLOW_MIN_MAX_DATE) { // We do not have any pattern or min|max values at this point. For those who be affected, they already checked earlier. So set 'no check' $sanitizeClass = SANITIZE_ALLOW_ALL; } return \qfq\Sanitize::sanitize($rawVal, $sanitizeClass, '', SANATIZE_EMPTY_STRING); } else { return $rawVal; } } $foundInStore = ''; return false; } /** * Fills the STORE_EXTRA. * * @throws UserFormException * @throws \qfq\CodeException */ private static function fillStoreExtra() { $value = Session::get(STORE_EXTRA); if (!isset($_SESSION[SESSION_NAME][STORE_EXTRA]) || $_SESSION[SESSION_NAME][STORE_EXTRA] === null) { $value = false; } if ($value === false) { self::setStore(array(), STORE_EXTRA, true); } else { self::setStore($_SESSION[SESSION_NAME][STORE_EXTRA], STORE_EXTRA, true); } } /** * Returns a pointer to this Class. * * @param string $bodytext * @param bool|false $phpUnit * @param string $fileConfigIni * @return null|Store * @throws UserFormException * @throws \qfq\CodeException */ public static function getInstance($bodytext = '', $phpUnit = false, $fileConfigIni = '') { if ($phpUnit) { if (self::$instance !== null) { // fake to have a clean environment for the next test. self::unsetStore(STORE_TYPO3); self::fillStoreTypo3($bodytext); self::unsetStore(STORE_CLIENT); self::fillStoreClient(); } // Testing different config files means initialize completely if ($fileConfigIni != '') { self::$instance = null; } } // Design Pattern: Singleton if (self::$instance === null) { self::$phpUnit = $phpUnit; self::$instance = new self($bodytext, $fileConfigIni); } else { // Class Store seems to be presistent over multiple QFQ instantiation. Set bodytext again, with every new request (if bodytext is given). if ($bodytext !== '') self::fillStoreTypo3($bodytext); } // Disable TYPO3_DEBUG_SHOW_BODY_TEXT=1 if SYSTEM_SHOW_DEBUG_INFO!='yes' if (self::getVar(TYPO3_DEBUG_SHOW_BODY_TEXT, STORE_TYPO3) === '1' && self::getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM) !== 'yes' ) { self::setVar(TYPO3_DEBUG_SHOW_BODY_TEXT, '0', STORE_TYPO3); } return self::$instance; } /** * Deletes a store assigning a new empty array to it. * * @param $store * @throws UserFormException * @throws \qfq\CodeException */ public static function unsetStore($store) { // Check valid Storename if (!isset(self::$sanitizeStore)) throw new UserFormException("Unknown Store: $store", ERROR_UNNOWN_STORE); if ($store === STORE_ZERO) throw new CodeException("unsetStore() for STORE_ZERO is impossible - there are no values.", ERROR_SET_STORE_ZERO); if (isset(self::$raw[$store])) { self::$raw[$store] = array(); } } /** * Set's a single $key/$value pair $store. * * @param string $key * @param string|array $value * @param string $store * @param bool|true $overWrite * @throws UserFormException * @throws \qfq\CodeException */ public static function setVar($key, $value, $store, $overWrite = true) { // Check valid Storename if (!isset(self::$sanitizeStore)) throw new UserFormException("Unknown Store: $store", ERROR_UNNOWN_STORE); if ($store === STORE_ZERO) throw new CodeException("setVar() for STORE_ZERO is impossible - there are no values.", ERROR_SET_STORE_ZERO); if ($overWrite === false && isset(self::$raw[$store][$key])) { throw new UserFormException("Value of '$key' already set in store '$store'.", ERROR_STORE_KEY_EXIST); } self::$raw[$store][$key] = $value; // The STORE_EXTRA saves arrays and is persistent if ($store === STORE_EXTRA) { $store = Session::get(STORE_EXTRA); if ($store === false) { $store = array(); } $store[$key] = $value; Session::set(STORE_EXTRA, $store); } } /** * Create a SIP after a form load. This is necessary on forms without a sip and on forms with r=0 (new record). * * @param $formName * @throws CodeException */ public static function createSipAfterFormLoad($formName) { $recordId = self::getVar(CLIENT_RECORD_ID, STORE_TYPO3 . STORE_CLIENT); if ($recordId === false) { $recordId = 0; } // If there are existing SIP param, keep them by copying to the new SIP Param Array $tmpParam = self::getNonSystemSipParam(); $tmpParam[SIP_RECORD_ID] = $recordId; $tmpParam[SIP_FORM] = $formName; if ($recordId == 0) { // SIPs for 'new records' needs to be uniq per TAB! Therefore add a uniq parameter $tmpParam[SIP_MAKE_URLPARAM_UNIQ] = uniqid(); } // Construct fake urlparam $tmpUrlparam = OnArray::toString($tmpParam); // Create a fake SIP which has never been passed by URL - further processing might expect this to exist. $sip = self::getSipInstance()->queryStringToSip($tmpUrlparam, RETURN_SIP); self::setVar(CLIENT_SIP, $sip, STORE_CLIENT); // Store in SIP Store (cause it's empty until now). $tmpParam[SIP_SIP] = $sip; self::setStore($tmpParam, STORE_SIP, true); } /** * Return an array with non system SIP parameter. Take the whole STORE_SIP and search for non system parameter. * * @return array * @throws UserFormException * @throws \qfq\CodeException */ private static function getNonSystemSipParam() { $tmpParam = array(); $sipArray = self::getStore(STORE_SIP); foreach ($sipArray as $key => $value) { if ($key[0] === '_') { continue; } switch ($key) { case SIP_SIP: case SIP_RECORD_ID: case SIP_FORM; case SIP_URLPARAM: continue; default: $tmpParam[$key] = $value; } } return $tmpParam; } /** * Returns a complete $store. * * @param $store * @return array * @throws UserFormException * @throws \qfq\CodeException */ public static function getStore($store) { // Check valid Storename if (!isset(self::$sanitizeStore[$store])) throw new UserFormException("Unknown Store: $store", ERROR_UNNOWN_STORE); if ($store === STORE_ZERO) throw new CodeException("getStore() for STORE_ZERO is impossible - there are no values saved.", ERROR_GET_STORE_ZERO); if (isset(self::$raw[$store])) { return self::$raw[$store]; } return array(); } /** * Returns a pointer to this class. * * @return null|Sip */ public static function getSipInstance() { return self::$sip; } /** * Fills STORE_TABLE_DEFAULT and STORE_TABLE_COLUMN_TYPES * * @param $tableName * @throws CodeException */ public static function fillStoreTableDefaultColumnType($tableName) { $db = new qfq\Database(); $tableDefinition = $db->getTableDefinition($tableName); self::setStore(array_column($tableDefinition, 'Default', 'Field'), STORE_TABLE_DEFAULT, true); self::setStore(array_column($tableDefinition, 'Type', 'Field'), STORE_TABLE_COLUMN_TYPES, true); } /** * Saves a subset of STORE_TYPO3 vars as a SIP. The SIP will be transmitted as hidden form element. * * More docs: CODING.md > 'Faking the STORE_TYPO3 for API calls' * * @throws CodeException * @throws UserFormException */ public static function copyT3VarsToSip() { $tempArray = self::getStore(STORE_TYPO3); foreach ([TYPO3_FE_USER, TYPO3_FE_USER_UID, TYPO3_FE_USER_GROUP, TYPO3_TT_CONTENT_UID, TYPO3_PAGE_ID, TYPO3_PAGE_TYPE, TYPO3_PAGE_LANGUAGE, TYPO3_BE_USER_LOGGED_IN] as $key) { if (isset($tempArray[$key])) { $t3varsArray[$key] = $tempArray[$key]; } } $t3varsArray[TYPO3_BE_USER_LOGGED_IN] = (isset($GLOBALS["TSFE"]->beUserLogin) && $GLOBALS["TSFE"]->beUserLogin === true) ? 'yes' : 'no'; $t3varsString = KeyValueStringParser::unparse($t3varsArray, '=', '&'); $t3sip = self::$sip->queryStringToSip($t3varsString, RETURN_SIP); return $t3sip; } /** * Get stored STORE_TYPO3 vars from SIP and restore the store. * * More docs: CODING.md > 'Faking the STORE_TYPO3 for API calls' * * @param string $sipTypo3Vars */ public function fillTypo3StoreFromSip($sipTypo3Vars) { $t3vars = self::getStore(STORE_TYPO3); // Do nothing if STORE_TYPO3 is already filled if (isset($t3vars[TYPO3_TT_CONTENT_UID]) && $t3vars[TYPO3_TT_CONTENT_UID] != '0') { return; } $typo3VarsArray = self::$sip->getVarsFromSip($sipTypo3Vars); self::setStore($typo3VarsArray, STORE_TYPO3, true); // If necessary, update SYSTEM_SHOW_DEBUG_INFO if (self::getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM) == 'auto') { $val = ($typo3VarsArray[TYPO3_BE_USER_LOGGED_IN] == 'yes') ? 'yes' : 'no'; self::setVar(SYSTEM_SHOW_DEBUG_INFO, $val, STORE_SYSTEM); } } }