name = 'Configuration'; } public function defaultTask() { $this->layout = 'default'; $data = [ 'licenseKey' => $this->config->get('license_key'), 'blacklist' => $this->config->get('blacklist'), 'whitelist' => $this->config->get('whitelist'), 'blacklist_name' => $this->config->get('blacklist_name'), 'blacklist_name_length' => $this->config->get('blacklist_name_length', 0), ]; $this->breadcrumb[__('Configuration', 'acychecker')] = Url::completeLink('configuration'); $this->display($data); } public function save() { Form::checkToken(); $formData = Security::getVar('array', 'config', []); if (empty($formData)) { Router::redirect(Url::completeLink('configuration', false, true)); } $licenseKeyBeforeSave = $this->config->get('license_key'); $isLicenseKeyUpdated = isset($formData['license_key']) && $licenseKeyBeforeSave !== $formData['license_key']; $status = $this->config->save($formData); if ($status) { Message::enqueueMessage(__('Successfully saved', 'acychecker')); if ($isLicenseKeyUpdated) { // If we add a key or edit it, we try to attach it if (!empty($formData['license_key'])) { $this->attachLicenseKey(); } else { // If we remove a key, we unlink it $this->detachLicenseKey(); } } } else { Message::enqueueMessage(__('Error saving', 'acychecker'), 'error'); } Router::redirect(Url::completeLink('configuration', false, true)); } public function ajaxCheckDB() { // Get the structure that the AcyChecker tables should have in the database $correctTablesStructure = $this->getCorrectTablesStructure(); // Get the current structure of the AcyChecker tables and tries to repair/create them if needed $currentTablesStructure = $this->getCurrentTablesStructure($correctTablesStructure); // Adds missing columns in AcyChecker tables and missing indexes / primary keys / constraints on the tables $this->fixCurrentStructure($correctTablesStructure, $currentTablesStructure); $result = ''; if (empty($this->messagesNoHtml)) { $result = ''; } else { $nbMessages = count($this->messagesNoHtml); foreach ($this->messagesNoHtml as $i => $oneMsg) { $result .= ''.$oneMsg['msg'].''; if ($i < $nbMessages) { $result .= '
'; } } } Ajax::sendAjaxResponse('', ['html' => $result]); } /** * Returns the structure that the AcyChecker tables should have in the database * * @return array */ private function getCorrectTablesStructure(): array { $correctTablesStructure = [ 'structure' => [], 'createTable' => [], 'indexes' => [], 'constraints' => [], ]; $queries = file_get_contents(ACYC_BACK.'tables.sql'); $tables = explode('CREATE TABLE IF NOT EXISTS ', $queries); // For each table, get its name, its column names and its indexes / primary key foreach ($tables as $oneTable) { if (strpos($oneTable, '`#__') !== 0) { continue; } $tableName = substr($oneTable, 1, strpos($oneTable, '`', 1) - 1); $correctTablesStructure['createTable'][$tableName] = 'CREATE TABLE IF NOT EXISTS '.$oneTable; $correctTablesStructure['indexes'][$tableName] = []; $correctTablesStructure['constraints'][$tableName] = []; $fields = explode("\n", $oneTable); foreach ($fields as $key => $oneField) { if (strpos($oneField, '#__') === 1) { continue; } $oneField = rtrim(trim($oneField), ','); // Find the column names and remember them if (substr($oneField, 0, 1) === '`') { $columnName = substr($oneField, 1, strpos($oneField, '`', 1) - 1); $correctTablesStructure['structure'][$tableName][$columnName] = trim($oneField, ','); continue; } // Remember the primary key and indexes of the table if (strpos($oneField, 'PRIMARY KEY') === 0) { $correctTablesStructure['indexes'][$tableName]['PRIMARY'] = $oneField; } elseif (strpos($oneField, 'INDEX') === 0) { $firstBackquotePos = strpos($oneField, '`'); $indexName = substr($oneField, $firstBackquotePos + 1, strpos($oneField, '`', $firstBackquotePos + 1) - $firstBackquotePos - 1); $correctTablesStructure['indexes'][$tableName][$indexName] = $oneField; } elseif (strpos($oneField, 'FOREIGN KEY') !== false) { preg_match('/(#__fk.*)\`/Uis', $fields[$key - 1], $matchesConstraints); preg_match('/(#__.*)\`\(`(.*)`\)/Uis', $fields[$key + 1], $matchesTable); preg_match('/\`(.*)\`/Uis', $oneField, $matchesColumn); if (!empty($matchesConstraints) && !empty($matchesTable) && !empty($matchesColumn)) { $correctTablesStructure['constraints'][$tableName][$matchesConstraints[1]] = [ 'table' => $matchesTable[1], 'column' => $matchesColumn[1], 'table_column' => $matchesTable[2], ]; } } } } $correctTablesStructure['tableNames'] = array_keys($correctTablesStructure['structure']); return $correctTablesStructure; } /** * Returns the current structure of the AcyChecker tables and tries to repair/create them if needed * * @param array $correctTablesStructure * * @return array */ private function getCurrentTablesStructure(array $correctTablesStructure): array { $currentTablesStructure = []; $existingTables = Database::getTableList(); foreach ($correctTablesStructure['tableNames'] as $oneTableName) { $tableNameWithPrefix = str_replace('#__', Database::getPrefix(), $oneTableName); if (in_array($tableNameWithPrefix, $existingTables)) { try { $columns = Database::loadObjectList('SHOW COLUMNS FROM '.$oneTableName); } catch (\Exception $e) { $columns = null; } } else { $columns = null; } if (!empty($columns)) { foreach ($columns as $oneField) { $currentTablesStructure[$oneTableName][$oneField->Field] = $oneField->Field; } continue; } // We didn't get the columns, the table crashed or doesn't exist $errorMessage = (isset($e) ? $e->getMessage() : substr(strip_tags(Database::getDBError()), 0, 200)); $this->messagesNoHtml[] = [ 'error' => false, 'color' => 'blue', 'msg' => sprintf(__('Could not load columns from the table %1$s : %2$s', 'acychecker'), $oneTableName, $errorMessage), ]; if (strpos($errorMessage, 'marked as crashed')) { try { $isError = Database::query('REPAIR TABLE '.$oneTableName); } catch (\Exception $e) { $isError = null; } if ($isError === null) { $errorMessage = (isset($e) ? $e->getMessage() : substr(strip_tags(Database::getDBError()), 0, 200)); $this->messagesNoHtml[] = [ 'error' => true, 'color' => 'red', 'msg' => sprintf(__('[ERROR]Could not repair the table %1$s : %2$s', 'acychecker'), $oneTableName, $errorMessage), ]; } else { $this->messagesNoHtml[] = [ 'error' => false, 'color' => 'green', 'msg' => sprintf(__('[OK]Problem solved: Table %s repaired', 'acychecker'), $oneTableName), ]; } continue; } else { try { // Create missing table $isError = Database::query($correctTablesStructure['createTable'][$oneTableName]); } catch (\Exception $e) { $isError = null; } if ($isError === null) { $errorMessage = (isset($e) ? $e->getMessage() : substr(strip_tags(Database::getDBError()), 0, 200)); $this->messagesNoHtml[] = [ 'error' => true, 'color' => 'red', 'msg' => sprintf(__('[ERROR]Could not create the table %1$s : %2$s', 'acychecker'), $oneTableName, $errorMessage), ]; } else { $this->messagesNoHtml[] = [ 'error' => false, 'color' => 'green', 'msg' => sprintf(__('[OK]Problem solved: Table %s created', 'acychecker'), $oneTableName), ]; } } } return $currentTablesStructure; } /** * Adds missing columns in AcyChecker tables and missing indexes / primary keys on the tables * * @param array $correctTablesStructure * @param array $currentTablesStructure * * @return void */ private function fixCurrentStructure(array $correctTablesStructure, array $currentTablesStructure) { foreach ($correctTablesStructure['tableNames'] as $oneTableName) { if (empty($currentTablesStructure[$oneTableName])) { continue; } $this->addMissingColumns($correctTablesStructure['structure'][$oneTableName], $currentTablesStructure[$oneTableName], $oneTableName); $this->removeExtraColumns($correctTablesStructure['structure'][$oneTableName], $currentTablesStructure[$oneTableName], $oneTableName); $this->fixDefaultValues($correctTablesStructure['structure'][$oneTableName], $oneTableName); $this->addMissingTableKeys($correctTablesStructure['indexes'][$oneTableName], $oneTableName); $this->addMissingTableConstraints($correctTablesStructure['constraints'][$oneTableName], $oneTableName); } } /** * Add missing columns in an AcyChecker table * * @param array $correctTableColumns * @param array $currentTableColumnNames * @param string $oneTableName * * @return void */ private function addMissingColumns(array $correctTableColumns, array $currentTableColumnNames, string $oneTableName) { $idealColumnNames = array_keys($correctTableColumns); $missingColumns = array_diff($idealColumnNames, $currentTableColumnNames); if (empty($missingColumns)) { return; } foreach ($missingColumns as $oneColumn) { $this->messagesNoHtml[] = [ 'error' => false, 'color' => 'blue', 'msg' => sprintf(__('Column %1$s missing in %2$s', 'acychecker'), $oneColumn, $oneTableName), ]; try { $isError = Database::query('ALTER TABLE '.$oneTableName.' ADD '.$correctTableColumns[$oneColumn]); } catch (\Exception $e) { $isError = null; } if ($isError === null) { $errorMessage = (isset($e) ? $e->getMessage() : substr(strip_tags(Database::getDBError()), 0, 200)); $this->messagesNoHtml[] = [ 'error' => true, 'color' => 'red', 'msg' => sprintf(__('[ERROR]Could not add the column %1$s on the table %2$s : %3$s', 'acychecker'), $oneColumn, $oneTableName, $errorMessage), ]; } else { $this->messagesNoHtml[] = [ 'error' => false, 'color' => 'green', 'msg' => sprintf(__('[OK]Problem solved: Added %1$s in %2$s', 'acychecker'), $oneColumn, $oneTableName), ]; } } } private function removeExtraColumns(array $correctTableColumns, array $currentTableColumnNames, string $oneTableName) { $idealColumnNames = array_keys($correctTableColumns); $extraColumns = array_diff($currentTableColumnNames, $idealColumnNames); if (empty($extraColumns)) { return; } foreach ($extraColumns as $oneColumn) { $this->messagesNoHtml[] = [ 'error' => false, 'color' => 'blue', 'msg' => sprintf(__('Extra column %1$s detected in table %2$s', 'acychecker'), $oneColumn, $oneTableName), ]; try { $isError = Database::query('ALTER TABLE '.$oneTableName.' DROP COLUMN `'.Database::secureDBColumn($oneColumn).'`'); } catch (\Exception $e) { $isError = null; } if ($isError === null) { $errorMessage = (isset($e) ? $e->getMessage() : substr(strip_tags(Database::getDBError()), 0, 200)); $this->messagesNoHtml[] = [ 'error' => true, 'color' => 'red', 'msg' => sprintf(__('[ERROR]Could not remove the column %1$s from the table %2$s: %3$s', 'acychecker'), $oneColumn, $oneTableName, $errorMessage), ]; } else { $this->messagesNoHtml[] = [ 'error' => false, 'color' => 'green', 'msg' => sprintf(__('[OK]Problem solved: Removed column %1$s from table %2$s', 'acychecker'), $oneColumn, $oneTableName), ]; } } } private function fixDefaultValues($correctTableColumns, $oneTableName) { try { $currentTableColumns = Database::loadObjectList( 'SELECT COLUMN_NAME, COLUMN_DEFAULT, IS_NULLABLE, COLUMN_TYPE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '.Database::escapeDB($oneTableName), 'COLUMN_NAME' ); if (empty($currentTableColumns)) { //TODO return; } } catch (\Exception $e) { $this->messagesNoHtml[] = [ 'error' => true, 'color' => 'orange', 'msg' => sprintf(__('[ERROR]Could not get current columns for table %1$s to check for default value: %2$s', 'acychecker'), $oneTableName, $e->getMessage()), ]; return; } foreach ($correctTableColumns as $oneColumn => $oneColumnDefinition) { $defaultValue = ''; if (preg_match('#DEFAULT ([^ ]+)$#Ui', $oneColumnDefinition, $matches)) { $defaultValue = $matches[1]; } if (strlen($defaultValue) === 0) { continue; } // if current value is surrounded by double quotes, replace them by quotes before comparing if (!empty($currentTableColumns[$oneColumn]->COLUMN_DEFAULT) && substr($currentTableColumns[$oneColumn]->COLUMN_DEFAULT, 0, 1) === '"') { $currentTableColumns[$oneColumn]->COLUMN_DEFAULT = '\''.substr($currentTableColumns[$oneColumn]->COLUMN_DEFAULT, 1, -1).'\''; } if (!empty($currentTableColumns[$oneColumn]->COLUMN_DEFAULT) && $currentTableColumns[$oneColumn]->COLUMN_DEFAULT === $defaultValue) { continue; } $this->messagesNoHtml[] = [ 'error' => false, 'color' => 'blue', 'msg' => sprintf(__('The default value for the column %1$s in the table %2$s is not correct', 'acychecker'), $oneColumn, $oneTableName), ]; try { $isError = Database::query('ALTER TABLE '.$oneTableName.' CHANGE `'.Database::secureDBColumn($oneColumn).'` '.$oneColumnDefinition); } catch (\Exception $e) { $isError = null; } if ($isError === null) { $errorMessage = (isset($e) ? $e->getMessage() : substr(strip_tags(Database::getDBError()), 0, 200)); $this->messagesNoHtml[] = [ 'error' => true, 'color' => 'red', 'msg' => sprintf(__('[ERROR]Could not update the default value for the column %1$s in the table %2$s: %3$s', 'acychecker'), $oneColumn, $oneTableName, $errorMessage), ]; } else { $this->messagesNoHtml[] = [ 'error' => false, 'color' => 'green', 'msg' => sprintf(__('[OK]Problem solved: Updated the default value for the column %1$s in the table %2$s', 'acychecker'), $oneColumn, $oneTableName), ]; } } } /** * Adds the missing indexes / primary keys on an AcyChecker table * * @param array $correctTableIndexes * @param string $oneTableName * * @return void */ private function addMissingTableKeys(array $correctTableIndexes, string $oneTableName) { // Add missing index and primary keys $results = Database::loadObjectList('SHOW INDEX FROM '.$oneTableName, 'Key_name'); if (empty($results)) { $results = []; } foreach ($correctTableIndexes as $name => $query) { $name = Database::prepareQuery($name); if (in_array($name, array_keys($results))) { continue; } // The index / primary key is missing, add it $keyName = $name === 'PRIMARY' ? 'primary key' : 'index '.$name; $this->messagesNoHtml[] = [ 'error' => false, 'color' => 'blue', 'msg' => sprintf(__('%1$s missing in %2$s', 'acychecker'), $keyName, $oneTableName), ]; try { $isError = Database::query('ALTER TABLE '.$oneTableName.' ADD '.$query); } catch (\Exception $e) { $isError = null; } if ($isError === null) { $errorMessage = (isset($e) ? $e->getMessage() : substr(strip_tags(Database::getDBError()), 0, 200)); $this->messagesNoHtml[] = [ 'error' => true, 'color' => 'red', 'msg' => sprintf(__('[ERROR]Could not add the %1$s on the table %2$s : %3$s', 'acychecker'), $keyName, $oneTableName, $errorMessage), ]; } else { $this->messagesNoHtml[] = [ 'error' => false, 'color' => 'green', 'msg' => sprintf(__('[OK]Problem solved: Added %1$s to %2$s', 'acychecker'), $keyName, $oneTableName), ]; } } } /** * Adds or fixes the table's foreign keys * * @param array $correctTableConstraints * @param string $oneTableName * * @return void */ private function addMissingTableConstraints(array $correctTableConstraints, string $oneTableName) { if (empty($correctTableConstraints)) { return; } $tableNameQuery = str_replace('#__', Database::getPrefix(), $oneTableName); $databaseName = Database::loadResult('SELECT DATABASE();'); $foreignKeys = Database::loadObjectList( 'SELECT i.TABLE_NAME, i.CONSTRAINT_TYPE, i.CONSTRAINT_NAME, k.REFERENCED_TABLE_NAME, k.REFERENCED_COLUMN_NAME, k.COLUMN_NAME FROM information_schema.TABLE_CONSTRAINTS AS i LEFT JOIN information_schema.KEY_COLUMN_USAGE AS k ON i.CONSTRAINT_NAME = k.CONSTRAINT_NAME WHERE i.TABLE_NAME = '.Database::escapeDB($tableNameQuery).' AND i.CONSTRAINT_TYPE = "FOREIGN KEY" AND i.TABLE_SCHEMA = '.Database::escapeDB($databaseName), 'CONSTRAINT_NAME' ); Database::query('SET foreign_key_checks = 0'); foreach ($correctTableConstraints as $constraintName => $constraintInfo) { $constraintTableNamePrefix = str_replace('#__', Database::getPrefix(), $constraintInfo['table']); $constraintName = str_replace('#__', Database::getPrefix(), $constraintName); if (!empty($foreignKeys[$constraintName]) && $foreignKeys[$constraintName]->REFERENCED_TABLE_NAME === $constraintTableNamePrefix && $foreignKeys[$constraintName]->REFERENCED_COLUMN_NAME === $constraintInfo['table_column'] && $foreignKeys[$constraintName]->COLUMN_NAME === $constraintInfo['column']) { continue; } $this->messagesNoHtml[] = [ 'error' => false, 'color' => 'blue', 'msg' => sprintf(__('Foreign key %1$s not well set for table %2$s', 'acychecker'), $constraintName, $oneTableName), ]; // The foreign key exists, but it is incorrect. We remove it then add the correct one if (!empty($foreignKeys[$constraintName])) { try { $isError = Database::query('ALTER TABLE `'.$oneTableName.'` DROP FOREIGN KEY `'.$constraintName.'`'); } catch (\Exception $e) { $isError = null; } if ($isError === null) { $errorMessage = (isset($e) ? $e->getMessage() : substr(strip_tags(Database::getDBError()), 0, 200)); $this->messagesNoHtml[] = [ 'error' => true, 'color' => 'red', 'msg' => sprintf(__('[ERROR]Could not add the foreign key %1$s on the table %2$s : %3$s', 'acychecker'), $constraintName, $oneTableName, $errorMessage), ]; continue; } } // Add the missing foreign key try { $isError = Database::query( 'ALTER TABLE `'.$oneTableName.'` ADD CONSTRAINT `'.$constraintName.'` FOREIGN KEY (`'.$constraintInfo['column'].'`) REFERENCES `'.$constraintInfo['table'].'` (`'.$constraintInfo['table_column'].'`) ON DELETE NO ACTION ON UPDATE NO ACTION;' ); } catch (\Exception $e) { $isError = null; } if ($isError === null) { $errorMessage = (isset($e) ? $e->getMessage() : substr(strip_tags(Database::getDBError()), 0, 200)); $this->messagesNoHtml[] = [ 'error' => true, 'color' => 'red', 'msg' => sprintf(__('[ERROR]Could not add the foreign key %1$s on the table %2$s : %3$s', 'acychecker'), $constraintName, $oneTableName, $errorMessage), ]; } else { $this->messagesNoHtml[] = [ 'error' => false, 'color' => 'green', 'msg' => sprintf(__('[OK]Problem solved: Added foreign key %1$s to table %2$s', 'acychecker'), $constraintName, $oneTableName), ]; } } Database::query('SET foreign_key_checks = 1'); } public function seeLogs() { SecurityService::noCache(); $type = Security::getVar('cmd', 'type'); $types = [ 'batch' => 'batch_tests', 'callback' => 'callback_controller', 'individual' => 'individual_tests', 'single' => 'single_test', ]; if (!in_array($type, array_keys($types))) { exit; } $reportPath = DebugService::getLogPath($types[$type].'.log'); if (file_exists($reportPath)) { try { $lines = 5000; $f = fopen($reportPath, 'rb'); fseek($f, -1, SEEK_END); if (fread($f, 1) != "\n") { $lines -= 1; } $report = ''; while (ftell($f) > 0 && $lines >= 0) { $seek = min(ftell($f), 4096); // Figure out how far back we should jump fseek($f, -$seek, SEEK_CUR); $report = ($chunk = fread($f, $seek)).$report; // Get the line fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR); $lines -= substr_count($chunk, "\n"); // Move to previous line } while ($lines++ < 0) { $report = substr($report, strpos($report, "\n") + 1); } fclose($f); } catch (\Exception $e) { $report = ''; } } if (empty($report)) { $report = __('The log file is empty', 'acychecker'); } echo nl2br($report); exit; } public function deleteLogs() { $type = Security::getVar('cmd', 'type'); $types = [ 'batch' => 'batch_tests', 'callback' => 'callback_controller', 'individual' => 'individual_tests', 'single' => 'single_test', ]; if (!in_array($type, array_keys($types))) { exit; } $reportPath = DebugService::getLogPath($types[$type].'.log'); if (file_exists($reportPath)) { unlink($reportPath); Message::enqueueMessage(__('Logs deleted', 'acychecker')); } Router::redirect(Url::completeLink('configuration', false, true)); } public function attachLicenseKey() { $formData = Security::getVar('array', 'config', []); $licenseKey = $formData['license_key']; $this->config->save(['license_key' => $licenseKey]); if (empty($licenseKey)) { Message::enqueueMessage(__('Please set a valid license key', 'acychecker'), 'error'); } else { $apiService = new ApiService(); $result = $apiService->getCredits(); if (empty($result['success'])) { Message::enqueueMessage($result['message'], 'error'); } } if (!empty($result['success'])) { $newConfig = [ 'license_key' => $licenseKey, 'credits_used_batch' => $result['data']['credits_used_batch'], 'credits_used_simple' => $result['data']['remaining_credits_simple'], 'remaining_credits_batch' => $result['data']['remaining_credits_batch'], 'remaining_credits_simple' => $result['data']['remaining_credits_simple'], 'license_level' => $result['data']['license_level'], 'license_last_check' => time(), 'license_end_date' => $result['data']['end_date'], ]; } else { $newConfig = [ 'license_key' => '', 'credits_used_batch' => 0, 'remaining_credits_batch' => 0, 'credits_used_simple' => 0, 'remaining_credits_simple' => 0, ]; } $this->config->save($newConfig); Router::redirect(Url::completeLink('configuration', false, true)); } public function detachLicenseKey() { $newConfig = [ 'license_key' => '', 'credits_used_batch' => 0, 'remaining_credits_batch' => 0, 'credits_used_simple' => 0, 'remaining_credits_simple' => 0, 'license_level' => '', 'license_last_check' => time(), 'license_end_date' => '', ]; $this->config->save($newConfig); Router::redirect(Url::completeLink('configuration', false, true)); } }