65.9K
CodeProject 正在变化。 阅读更多。
Home

如何在纯 PHP 中查询 MySQL 数据库

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.38/5 (4投票s)

2022 年 2 月 18 日

CPOL

20分钟阅读

viewsIcon

8185

downloadIcon

194

纯 PHP 中查询 MySQL 数据库的完整代码

引言

运行代码从选择数据库页面开始,然后是图中所示的其他页面。您可以“按原样”使用代码,但本文的主要目的是展示如何构建一个查询工具。PHP 只是纯粹的 PHP,没有额外的库。代码使用了类、cookie、HTML 和 CSS、SQL SELECT 语句、SQL 注入(好吧,是尝试阻止它)和加密。所有这些都处于相当基础的水平。

本文假设您至少从远处看过一些 PHP、HTML、CSS 和 MySQL。如果没有,《PHP 入门分步教程》是一个极好的入门。或者浏览一下 W3schools 网站。

我使用的是 PHP 7.1.22,在 Windows 10 PC 上使用 PHP 内置的 Web 服务器 php.exe,以及 MySQL 8.0.12(和 MariaDB 10.3.28-MariaDB),但代码应该可以在 MySQL 5.x 数据库上运行。显然,您需要对数据库的表和视图具有 SELECT 权限。在 php.ini 文件中,启用 extension=php_pdo_mysql.dll。

关于表、视图及其字段的信息可以在两个 MySQL 表中找到

select * from information_schema.tables
	where table_schema = <your_database> and table_type in ('BASE TABLE', 'VIEW')
		order by TABLE_TYPE, TABLE_NAME

select * from information_schema.columns
	where table_Schema = <your_database> and table_name = <your_table>
		order by ORDINAL_POSITION

(MySQL 称之为“模式”,几乎所有其他人称之为“数据库”)。

代码

代码主要由五个页面的代码组成

  • querymysql_select_db.php:选择数据库的页面
  • querymysql_select_tables.php:选择表或视图的页面
  • querymysql_query.php:输入查询参数的页面;某些数据类型不受支持,主要是因为不清楚如何查询和显示它们
  • querymysql_browse.php:浏览结果行的列表的页面
  • querymysql_detail.php:查看特定行的所有数据的页面:此页面仅在表具有主键时可用,视图没有主键

以及一些支持性内容

  • querymysql_config.php:包含在所有页面中的配置文件
  • 4 个支持类的代码
  • querymysql.css:CSS 文件
  • querymysql_selection.php:用于更改浏览页面上显示的排序和字段的页面
  • querymysql_logout.php:登出页面

四个支持类是

  • querymysql_class_Settings.phpSettings 类,用于在页面之间保留数据
  • querymysql_class_Db.phpDb 类,所有数据库交互都集中在此
  • querymysql_class_H.phpH 类,包含各种静态函数
  • querymysql_class_MyList.phpMyList 类,用于处理值列表。

Using the Code

将源代码复制到同一目录,然后在浏览器中加载 querymysql_select_db.php

注释

  • 在查询和浏览页面上,您会找到用于选择将在浏览页面上显示的字段(默认:表的所有字段)和排序(默认:按表中出现顺序排列的主键字段;如果没有主键则为空)的按钮。单击这些按钮将导航到选择页面 querymysql_selection.php
  • 并非所有 SQL 数据类型都受支持:您无法查询不支持数据类型的字段,在浏览和详细信息页面上它们将被标记为不支持。
  • 如果在查询页面中填充了多个字段,则所有条件必须在检索中满足(条件之间为 AND)。
  • 浏览页面最多显示 1000 行(querymysql_config.php 中的 MAXROWS 设置)。
  • 在浏览页面上,最多显示字段的 30 个字符(querymysql_config.php 中的 MAXWIDTH 设置);将鼠标悬停在字段上会显示完整内容。
  • 如果没有主键(所有视图至少都没有主键),则详细信息页面不可用。
  • 小数点是 .(点)。
  • 日期以 SQL 格式 ccyy/mm/dd 显示(和查询!)
  • 在选择表页面上显示的 TABLE_ROWS 值并不总是准确的,并且对于视图不可用。
  • 我预计您自己数据库的所有字段都具有整理序列 utf8mb4_unicode_ci。当字段不是这样时,可能会出现查询问题。

代码解释

我将首先详细解释选择数据库页面以及支持代码。然后,我将重点介绍其他页面的功能。

很久以前,在我第一份工作中,理想是无我编程:这意味着你无法从代码中看出谁编写了它。那是一个编程语言和应用程序需求都有限的时代。我试图使代码尽可能简单(每步一页),并给每页相同的结构,但请注意,这只是在 PHP 中解决需求的一种方法。编程应该是快乐的:如果你有更好的方法,请根据自己的偏好调整代码。

选择数据库页面

此页面的目的是设置和保存连接到数据库的参数。

该页面由两个文件实现:主文件 querymysql_select_db.php 显示页面,提交后 querymysql_select_db_processor.php 中的 Processor 类用于处理输入。所有页面都具有相同的结构,即使 Processor 类只处理页面回退(如 querymysql_detail_processor.php)。我们需要保留的数据存储在 Sessions 类中。

选择数据库主页面

querymysql_select_db.php 的第一部分是

	// include the config file
 	require_once 'querymysql_config.php';;
	require_once 'querymysql_select_db_Processor.php';

	// instantiate the Settings class, which is the store for parameters
	$Settings = new Settings();

	// run the Processor if this page was submitted
	if ($_SERVER['REQUEST_METHOD'] === 'POST') {
		$Processor = new Processor($Settings);
	}

	// retrieve the previously entered data
	$dataRowArray = array();
	$dataRowArray['server'] = $Settings->get('server'); // empty string if not found
	$dataRowArray['database'] = $Settings->get('database');
	$dataRowArray['user'] = $Settings->get('user');
	$dataRowArray['password'] = $Settings->get('password');

后面是显示数据的 HTML。所以这里我们需要解释一下配置文件、HTML 以及上述代码中的 SettingsProcessor 类。让我们从 HTML 开始。

HTML 和 CSS

HTML 以一些或多或少标准的代码开头,然后是四个部分

  • header
  • (带有类的)字段部分
  • 按钮部分
  • footer

我们需要这些部分是因为字段部分和按钮部分是并排显示的。而且这使得代码更清晰!这些部分的属性定义在 CSS 文件 querymysql_css.css 中。我不是 CSS 专家,通过反复试验和互联网的大量帮助找到了设置。

回到 HTML

代码的 HTML 部分包含大量 PHP。在哪里编写直接的 HTML(可能夹杂一些 <?PHP my code ?>)以及在哪里在 PHP 中使用 echo,这有点随意。关于此 HTML 的几点说明

  • 如果在处理器类或其他页面中检测到错误(如连接失败),则会设置(并保存在 Settings 中)$errorMessage,现在它将在标题中显示。
    <p>.<b>
    	<?PHP echo $Settings->getAndClearError(); ?> 
    </b></p>
  • 请注意 HTML 代码末尾的 ShowCookies 按钮。
    <div style="position:relative; text-align:center">
    	<input type="submit" <?PHP if (DEBUG === 'NO') {echo 'hidden';} ?>
    		class="smallbutton" name="ShowCookies" value="ShowCookies">
    </div>

DEBUG 在配置文件中设置。如果为 YES,则显示此按钮,以便您可以检查 cookie 的内容。

选择数据库的 processor() 类

处理器类的代码是

class Processor
{
	private $Settings;
	
	public function __construct(Settings $aSettings) 
	{
		// most Processor classes have more methods
		// using $this->Settings to be consistent
		$this->Settings = $aSettings;

		if (isset($_POST['ShowCookies'])) { // for debugging
			$this->Settings->showCookies();
			die;
		}

		if (isset($_POST['Clear'])) {
			$this->Settings->set('server', '');
			$this->Settings->set('database', '');
			$this->Settings->set('user', '');
			$this->Settings->set('password', '');
			H::redirect('querymysql_select_db.php');
		}

		// save the data
		$this->Settings->set('server', $_POST['server']);
		$this->Settings->set('database', $_POST['database']);
		$this->Settings->set('user', $_POST['user']);
		$this->Settings->set('password', $_POST['password']);

		if (isset($_POST['Go'])) { // not really any other option possible, but be consistent
			// try to connect; because "false", normal error message if fails
			$Db = new Db($this->Settings, false); 
			if ($Db->errorMessage !== '') { // always check $Db->errorMessage
				$this->Settings->set('errorMessage', $Db->errorMessage);
				H::redirect('querymysql_select_db.php');
			}
			$resultArray = $Db->getVersion(); // to show on next pages
			if ($Db->errorMessage !== '') {
				$this->Settings->set('errorMessage', $Db->errorMessage);
				H::redirect('querymysql_select_db.php');
			}
			$version = $resultArray[0]['version'];
			$this->Settings->set('version', $version);

			$this->Settings->clear(); // clear stuff from previous db
			H::redirect('querymysql_select_table.php');
		}

		H::fatalError('unhandled POST');
	}
}

此类将输入保存在 Settings 中,然后尝试使用 Db 类连接到数据库。如果连接成功,我们将检索数据库版本并转到下一个选择表页面,但首先我们会清除之前设置的所有 cookie,以防您先查询了其他数据库。如果发生错误,我们会重定向到选择数据库页面。

我们在此代码中还看到了 H 类,因此我们已经看到了支持类 SettingsDbH。我将在对配置文件进行一些说明后解释它们。

配置文件

配置文件 querymysql_config.php 包含在每个页面中,并包含应用程序范围的常量和包含文件。请注意,DEBUG 设置为 'NO',您可能希望将其更改为 'YES'。

//	DEBUG must exist. if YES: 
//		button to show cookies on select db page
//		shows trace on fatalError

	define('DEBUG', 'NO');
//	define('DEBUG', 'YES');

支持类

Settings 类

cookie 用于在页面之间保留数据。根据手册:“一旦设置了 cookie,它们就可以在下一个页面加载时访问”。$settingsArray 用于在下一个页面加载之前,在同一页面内访问已设置的参数。

cookie 名称带有前缀“querymysql_”:可能还有其他应用程序设置 cookie(尤其是在 localhost 上)。如果您查询了一个表,然后转到另一个表,然后返回到第一个表,那么如果您看到第一个表中以前使用的查询参数,将会很方便。换句话说,我们应该为每个表存储查询参数(并在更改数据库时清除所有),还是只为当前表设置一组参数(并在更改表时清除它们)?我采用了第一种方法,因此查询参数还需要表名作为前缀(如果不清楚,当我们看到查询页面时就会清楚)。

构造函数将所有带有 querymysql_ 前缀的 cookie 读取到 $settingsArray 中。

	public function __construct()
	{
		foreach ($_COOKIE as $name => $value) {
			if (H::aStartsWithB($name, MYAPP)) {
				setcookie($name, $value, time() + TIMEOUT, '/'); // refresh the cookie's timeout

				// the settingsArray keys are without the MYAPP.'_' prefix
				$name = H::removeLeftAfromB(MYAPP.'_', $name);
				$this->settingsArray[$name] = H::decrypt($value);
			}
		}
	}

cookie 值已加密。原因在下面的 SQL 注入和加密部分进行了说明。get() 用于从此数组中检索参数,set() 用于更改、添加或从此数组中删除参数立即更新具有该参数名的 cookie。重要的是要立即设置 cookie。否则,我们将不得不使用类似 save() 的方法在每个页面的末尾进行保存。我会忘记,而且如果您使用浏览器的左箭头返回到上一页,它将不起作用。

获取参数时,如果未找到,则返回空字符串。当您将参数设置为空字符串时,它将从 $settingsArray 中移除,并且具有该名称的 cookie 也将被移除。

Db 类

所有数据库交互都集中在此类中。数据库检索返回零行或多行数据,但事情可能会出错,在这种情况下会设置 $errorMessage。因此,在每次调用方法后,我们都必须检查此 $errorMessage 是否不为空。

类的构造函数负责连接数据库。当从选择数据库页面调用时,错误(通常是“密码错误”)是正常情况,我们会重定向到该页面,并在那里显示错误消息(上面提到的 $Settings->getAndClearError())。当从其他页面调用时,错误几乎是致命的:数据库可能不再可用,但最可能的原因是 cookie 已过期,因此连接参数不再可用。因此,在这种特定情况下,我们假设是该错误并重定向到选择数据库页面。

除了检查连接外,选择数据库页面的处理器还执行第一次检索:通过 $Db->getVersion() 检索数据库版本。

    public function getVersion() : array
	{
		$sql = "select version() as version";
		return $this->retrieve($sql);
	}

此函数使用私有函数 retrieve() 进行实际工作。

	private function retrieve(string $aSql, array $aParameterArray = null) : array
	{
		try {
			$stmt = $this->conn->prepare($aSql);
			// execute, binding the values of $this->whereParmsArray[fieldName]
			// to the :fieldNames in the select clause
			$stmt->execute($aParameterArray);
			$tableArray = $stmt->fetchAll(PDO::FETCH_ASSOC);
		} catch (PDOException $e) {
			$this->errorMessage = 'Retrieve failed: ' . $e->getMessage().'; SQL: '.$aSql;
			$tableArray = array();
		}
		return $tableArray;
	}

注意 catch 中如何设置 errorMessage。对于此演示代码,完整的错误消息和 sql 显示在错误消息中,在更现实的场景中,您将记录错误消息和 sql,并返回更中性的消息。进一步请注意,retrieve() 返回零行或多行,每一行都是一个字段名和值的关联数组(还有其他获取选项,但这是从检索中获取的最像数据库的返回)。

getVersion() 中没有参数。为了保持一致性,我们始终使用准备好的语句,请参阅本文末尾关于 SQL 注入的部分。

getTables() 函数检索表和视图列表,getTableFields() 检索表的字段列表。更有趣的是 getTableData()getWhere():它们在浏览页面上使用,并在那里讨论。getRowData() 在详细信息页面中使用,用于从给定表中检索单行,使用给定的主键。

请注意,而不是使用单独的 $errorMessage,我们可以返回一个数组或一个错误消息。如果一个函数可以返回两个完全不同的东西,我会犯很多错误,但我承认我有时会忘记检查错误消息。

辅助类

querymysql_class_H.php 中的辅助类包含静态函数。首先是重定向到另一个页面的 redirect(),然后是 show(),它没有被使用,但在您更改代码时会很有用。

fatalError() 用于在发生错误时(1)无法进一步处理(2)并且应在测试阶段解决。否则,您希望知道它何时发生。当 DEBUG = 'YES' 时,您还将看到跟踪信息。在更高级的版本中,您将记录错误和跟踪,并向用户显示更中性的消息(例如“抱歉,请重试”)。H 类还包含一些用于操作字符串的函数,使用 fatalError() 检测错误的参数值。

在此类的进一步内容中,还有 encrypt()decrypt() 函数,然后是一些用于简化字符串处理的字符串函数,最后是特定应用程序函数。getPhpType() 将 MySQL 数据类型转换为“PHP 数据类型”:对于本文的 PHP 代码,MySQL 数据类型是 VARCHAR 还是 TEXT 没有区别。在本文中,并非所有数据类型都受支持,以限制代码量。

getPrimaryWhere(),在浏览页面上使用。getSelection() 在选择页面上使用。setSelectionDefault() 在查询页面上用于初始化排序和字段列表,在选择页面上用于重置为默认值。

MyList 类

SQL SELECT 语句中的字段列表是逗号分隔的字段名字符串。在此应用程序中,填充列表是在数组中完成的。当将列表保存到 Settings 和从中检索时,使用字符串。更改列表时,使用数组。所以这个“列表”有时应该表现为(逗号分隔的)字符串,有时表现为数组。这正是 MyList 类提供的。

每当有列表时,都会使用此类。有时它有点大材小用,但类使用得越多,类代码中的错误出现的几率就越大。add()remove()up()down() 函数在选择页面上使用。

选择表页面

此页面的目的是为查询页面选择和保存表名和表类型。

代码的第一部分是

 	require_once 'querymysql_config.php';;
	require_once 'querymysql_select_table_Processor.php';

	$Settings = new Settings();
	$currentTable = $Settings->get('currentTable'); // filled if set previously on this page
	
	if ($_SERVER['REQUEST_METHOD'] === 'POST') {
		$Processor = new Processor($Settings);
	}

	// get the list of tables
	$Db = new Db($Settings);
	$tableArray = $Db->getTables();
	$errorMessage = $Db->errorMessage;

$currentTable 用于在具有该表值的行上显示 *。$Db->getTables() 检索表和视图,这是首次使用参数绑定(:database)。

    public function getTables() : array
	{
		$sql = "select TABLE_TYPE, TABLE_NAME, TABLE_ROWS from information_schema.tables
			where table_Schema = :database and table_type in ('BASE TABLE', 'VIEW')
				order by TABLE_TYPE, TABLE_NAME";
		$parameterArray = array(':database' => $this->Settings->get('database'));
		return $this->retrieve($sql, $parameterArray);
	}

代码继续初始化 HTML 中的页面,然后遍历 $tableArray 以显示每张表的行。

HTML 中有两个您可能以前没见过的技巧。第一个是滚动到位置:如果您在此页面上选择一个表,转到查询页面,然后稍后返回到选择表页面,此页面将滚动到最后选择的表。实现此目的的代码是

echo '<div id="'.$fieldArray['TABLE_NAME'].'"></div>';

在此页面并且当从查询页面返回到此页面时,将表名(以 # 开头)添加到 URL。因此,当您从查询页面返回到此页面时,此页面将滚动到最后选择的表名并在该行上显示 *。

第二个技巧是隐藏选择按钮中的信息

	echo '<input type="submit" class="smallbutton"';
	if ($fieldArray['TABLE_TYPE'] === 'VIEW') {
	    echo ' name="view'.H::encrypt($fieldArray['TABLE_NAME']);
	} else {
	    echo ' name="table'.H::encrypt($fieldArray['TABLE_NAME']);
	}
	echo '" value="Select">';

表名需要加密。原因在下面的 SQL 注入和加密部分进行了说明。

处理器拆分类型和名称,并将它们保存在 Settings 类中,因此查询页面知道要从哪个表检索。

class Processor
{
	private $Settings;
	
	public function __construct(Settings $aSettings) 
	{
		$this->Settings = $aSettings;

		foreach ($_POST as $key => $dummy) { // $key is the button name
			
			if (mb_substr($key, 0, 4) === 'view') {
				$tableNameEncrtpted = H::removeLeftAfromB('view', $key);
				$this->Settings->set('currentType', 'view');
			} else {
				$tableNameEncrtpted = H::removeLeftAfromB('table', $key);
				$this->Settings->set('currentType', 'table');
			}
			$tableName = H::decrypt($tableNameEncrtpted);
			$this->Settings->set('currentTable', $tableName);
			H::redirect('querymysql_query.php');
		}
	}
}

我们不知道哪个 Select 按钮被点击了,所以我们不知道按钮的全名。这就是 foreach 的原因。这里只处理被点击的按钮。

查询页面

此页面的目的是输入并保存浏览页面的 WHERE 子句的数据。

WHERE 子句由一个或多个条件组成,用 AND 分隔(此版本不支持 OR)。一个条件由字段名、运算符和值组成(诸如 fielda in (b, c, d) 这样的条件在此版本中不支持)。

因此,我们通过 $Db->getTableFields($currentTable) 检索所有字段的列表,然后在 HTML 中为每个字段呈现一行,包含名称、运算符和值。

setOperator() 函数设置支持的运算符,对于此演示项目,只有少数几种可能性。如上所述,并非所有数据类型都受支持,如果不支持,您将看到文本“not supported”。

符合查询参数的数据检索是在浏览页面上完成的。

Processor 中,运算符和值会为每个字段保存。前面加上表名,以使查询参数特定于每个表。

		foreach ($this->fieldArray as $fieldNumber => $valueArray) {

			$fieldName = $valueArray['COLUMN_NAME'];
			
			if (H::getPhpType($valueArray['DATA_TYPE']) == 'not supported') {
				continue; // nothing to save
			}

			if ($_POST[$fieldName.'_value'] != '') {
				$this->Settings->set($currentTable.'_'.$fieldName.'_operator', 
                                $_POST[$fieldName.'_operator']);
				$this->Settings->set($currentTable.'_'.$fieldName.'_value', 
                                $_POST[$fieldName.'_value']);
				// is value valid?
				$errorMessage = $this->checkType($fieldName, $valueArray['DATA_TYPE'], 
                               $_POST[$fieldName.'_value']);
				if ($errorMessage != '') {
					$this->Settings->set('errorMessage', $errorMessage);
					H::redirect('querymysql_query.php');
				}
			}
		}

此页面还包含 **Sort** 和 **Choose Fields** 按钮。如果您点击其中一个,您将转到选择页面,下面将讨论。Sort 用于设置 ORDER BY ...,Choose Fields 用于设置 SELECT ... FROM 中的字段列表(及其出现顺序)。如果未点击,默认值将保存在 Settings 中,字段列表的所有字段以及排序的主键(或如果没有主键则为空排序)。这在 Processor 中完成,然后转到浏览页面。

	// set the fields and sort if not set
	if ($this->Settings->get($aTable.'_fields') === '') {
		H::setSelectionDefault($this->Settings, $aFieldArray, $aTable, 'fields');
	}	
	if ($this->Settings->get($aTable.'_sort') === '') {
		H::setSelectionDefault($this->Settings, $aFieldArray, $aTable, 'sort');
	}
	H::redirect('querymysql_browse.php');

浏览页面

此页面的目的是根据查询参数显示行。如果表有主键,则每行都有一个 Select 按钮。

页面调用 $Db->getTableData(),实际工作在此完成,并在此页面上显示返回的数组。如果没有主键,这是序列中的最后一页。如果有主键,每行都会有一个 **Select** 按钮。HTML 看起来很像选择表页面的 HTML。在选择表页面上,我们使用表名作为锚点链接和 Select 按钮的名称,现在我们使用 WHERE 子句作为主键。再次,我们需要加密 Select 按钮中的 WHERE 子句。当您从详细信息页面转到浏览页面时,您会在 URL 中看到主键的 WHERE SQL。此处没有 SQL 注入的风险,此 WHERE 仅用于滚动到浏览中的正确行。

在行的显示中,只显示 MAXWIDTH 个字符(配置文件中的设置)。如果您将鼠标悬停在字段上,将显示完整内容,使用 <abbr>

$Db->getTableData

getTableData() 是根据用户输入的查询参数检索实际数据的方​​法。这里我们使用准备好的语句,更多信息请参阅下面的 SQL 注入部分。数据检索如下所示:

 select <field list> from <my table> where <conditions> order by <sort list> limit <n>

<field list> 是默认值(所有字段)或在选择页面上设置的。<my table> 是在选择表页面上设置的 $currentTable。<sort list> 是默认值(主键字段或空)或在选择页面上设置的。<conditions> 是有趣的部分。

我们首先计算要检索的行数。我们不想检索 10,000 行完整数据。如果我们使用 limit <n>(config 文件中的 MAXROWS),我们仍然想知道实际上有多少行。计数的代码是

$wherePlus = ($this->getWhere($this->Settings, $aFieldArray) !== '')
	? ' where '.$this->getWhere($this->Settings, $aFieldArray) // where clause including 'where'
	: ''; // no where clause

// first get the number of rows 
$sql = 'select count(*) as rowcount from '.$aTable.$wherePlus;
$tableArray = $this->retrieve($sql, $this->whereParmsArray);
if ($this->errorMessage !== '') {
	return array();
}
$rowCount = $tableArray[0]['rowcount'];
if ($rowCount === 0) {
	$this->errorMessage = 'nothing found';
	return array();
}

第一行是 getWhere() 的调用,它生成 WHERE 子句,但不包括实际的“where ”。

private function getWhere(Settings $aSettings, array $aFieldArray) : string
{
	$currentTable = $aSettings->get('currentTable');
	$where = ''; // the where for the select
	$this->displayWhere = ''; // presentable where to show on browse page
		
	foreach ($aFieldArray as $fieldNumber => $valueArray) {
		$fieldName = $valueArray['COLUMN_NAME'];
		if ($aSettings->get($currentTable.'_'.$fieldName.'_value') == '') { 
			// value not set, so not relevant for where clause
			continue;
		}
		$operator = $aSettings->get($currentTable.'_'.$fieldName.'_operator');
		$value = $aSettings->get($currentTable.'_'.$fieldName.'_value');

		$phpType = H::getPhpType($valueArray['DATA_TYPE']);

		$operatorsList = new MyList('=, <, >');
			
		if ($phpType === 'string') {

			// mb_strtolower() and lower() in the following code help 
			// if collation of table not utf8mb4_unicode_ci 
			// but probably do not solve every collation issue
				
			if ($operatorsList->isInList($operator)) {
				// :fieldName goes in the where clause, 
				// the Value goes into $this->whereParmsArray
				$this->whereParmsArray[$fieldName] = mb_strtolower($value);
				$where = $where.' AND lower('.$fieldName.') '.$operator.' :'.$fieldName;
				$this->displayWhere = $this->displayWhere
					.' AND '.$fieldName.' '.$operator.' '.$value;
				continue;
			}

			if ($operator === 'starts with') {
				$this->whereParmsArray[$fieldName] = mb_strtolower($value.'%');
				$where = $where.' AND lower('.$fieldName.') like :'.$fieldName;
				$this->displayWhere = $this->displayWhere
					.' AND '.$fieldName.' like '.$value.'%';
				continue;
			}

			if ($operator === 'contains') {
				$this->whereParmsArray[$fieldName] = mb_strtolower('%'.$value.'%');
				$where = $where.' AND lower('.$fieldName.') like :'.$fieldName;
				$this->displayWhere = $this->displayWhere
					.' AND '.$fieldName.' like %'.$value.'%';
				continue;
			}
		}
			
		if ($phpType == 'numeric') {

			if ($operatorsList->isInList($operator)) {
				$this->whereParmsArray[$fieldName] = $value;
				$where = $where.' AND '.$fieldName.' '.$operator.' :'.$fieldName;
				$this->displayWhere = $this->displayWhere
					.' AND '.$fieldName.' '.$operator.' '.$value;
				continue;
			}
		}

		if ($phpType == 'date') {

			if ($operatorsList->isInList($operator)) {
				$this->whereParmsArray[$fieldName] = $value;
				$where = $where.' AND '.$fieldName.' '.$operator.' :'.$fieldName;
				$this->displayWhere = $this->displayWhere
					.' AND '.$fieldName.' '.$operator.' '.$value;
				continue;
			}
		}			
	}
	if ($where == '') {
		return '';
	}
	// remove the first AND
	$where = H::removeLeftAfromB(' AND ', $where);
	$this->displayWhere = H::removeLeftAfromB(' AND ', $this->displayWhere);
	return $where;
}

该函数返回 where 子句,并且还填充了 $this->whereParmsArraywhere 子句字符串包含诸如

.... AND thisField = :thisField AND ....

以及 whereParmsArray 包含

whereParmsArray['thisField'] = <actual value>

因此,where 字符串中的 :-fieldswhereParmsArray 中的键匹配。这使得在 execute 中绑定它们可以正常工作。检索到行数后,我们检索数据,限制为 MAXROWS(在 querymysql_config.php 中设置,您可以在那里更改值)。getTableData() 的第二部分是

		// now retrieve the data
		$sql = 'select '.$this->Settings->get($aTable.'_fields')
			.' from '.$aTable.$wherePlus;
		if ($this->Settings->get($aTable.'_sort') !== '') { // no sort if no primary key
			$sql = $sql.' order by '.$this->Settings->get($aTable.'_sort');
		}
		if ($rowCount > MAXROWS) { // MAXROWS is set in the config file
			$this->rowCountText = 'showing '.MAXROWS.' rows of '.$rowCount.' ';
			$sql = $sql.' limit '.MAXROWS;
		} else {
			$this->rowCountText = 'selected '.$rowCount.' rows ';
		}
		return $this->retrieve($sql, $this->whereParmsArray);

详细信息页面

此页面的目的是显示在浏览页面上选择的行的所有字段。

浏览页面将主键的 where 子句保存在 Settings 中,因此检索数据只需

    // retrieve the primary key SQL
	$primaryKeySql = $Settings->get('primaryKeySql');
	$PrimaryKeySqlCompressed = str_replace(' ', '', $primaryKeySql); // anchor link 
                                                                    // going back to browse
	// and get the data
	$rowArray = $Db->getRowData($currentTable, $primaryKeySql);
	$errorMessage = $Db->errorMessage;

此页面始终显示所有字段,即使您在浏览页面上使用 **Choose Fields** 按钮限制了显示的字段。长度大于 30 的字符串使用 <textarea> 显示。

选择页面

此页面的目的是更改和保存排序以及要在浏览页面中显示的字段。如果表具有主键,则不能从字段列表中删除主键字段:它们对于创建 **Select** 按钮中设置的用于主键的“where”是必需的。

可以从查询页面和浏览页面访问此页面。

登出页面

此页面的目的是清除此应用程序设置的所有 cookie。

SQL 注入和加密

在输入字段中操作 SQL

大多数 SQL 注入示例都描述了如何操作用户输入的字段中的 SQL。互联网上的普遍观点似乎是,绑定参数 足以防止 SQL 注入至少对于更常见的字符集和较新版本的 MySQL)。考虑选择表页面上的 getTables()

    public function getTables() : array
	{
		$sql = "select TABLE_TYPE, TABLE_NAME, TABLE_ROWS from information_schema.tables
			where table_Schema = :database and table_type in ('BASE TABLE', 'VIEW')
				order by TABLE_TYPE, TABLE_NAME";
		$parameterArray = array(':database' => $this->Settings->get('database'));
		return $this->retrieve($sql, $parameterArray);
	}    

这里,select 使用 :database,数据库名称在 $parameterArray 中设置。数据库名称由用户输入,因此应触发“risk-of-sql-injection-alert”。在此情况下,我想不到任何注入风险 SQL 的方法,但大多数黑客比我聪明得多,所以最好小心为上。因此,我们在所有检索中使用参数绑定。

更改浏览器中的 HTML

假设您在 SQL 中排除了某些表来检索表,例如,“users”表。恶意用户可能会猜到有一个“users”表,并试图通过两种方式访问该表。在选择表页面上,表名在 **Select** 按钮的 HTML 中。因此,第一种方法是切换到 HTML 并将其中一个表名更改为“users”。这就是为什么表名在 Select 按钮中被加密的原因。

浏览页面也存在同样的问题,其中 **Select** 按钮包含用于主键的 where 子句的 SQL。

更改 Cookie

选择表页面将表名保存在 cookie 中。当在查询页面时,同一用户可以更改表名 cookie 的值(使用简单的 PHP 脚本)为“users”然后刷新页面。这就是为什么这样的 cookie 需要加密,并且为了安全起见,所有 cookie 都被加密。

将名称更改为其他名称只是我能想到的一个例子。表名特别容易受到攻击,因为在浏览和详细信息页面上,您无法绑定它,绑定只对值有效。

后续步骤

为了可维护性,请根据自己的风格调整代码;更好的是,使用本文的元素来创建自己的代码。

如果您可以控制数据库,那么您可以进行很多改进。例如

  • 添加更多查询选项,例如“between”或“is one of”值列表
  • 详细信息页面默认排序不适用于视图,因为没有主键可以锚定。但是视图通常有一个(或一组)唯一的字段。您可以添加一个表来指定视图的这些键字段。
  • Cookie 可能不是存储很容易变得相当多的数据的最佳方式。也许最好将参数按会话存储在数据库中。
  • 在详细信息页面上添加非键字段的更新选项(数据可能有数据库约束)。添加 insertdelete 并不容易。
  • 添加对更多 SQL 数据类型的支持。如果您知道 BLOB 字段始终包含 .jpg 图像,您可以在详细信息页面上显示该图像。
  • 添加对另一个 DBMS 的支持很容易,只要该 DBMS 受 PDO 支持

结论

我希望我已经说服您,构建这样一个查询工具并不困难。该代码为您提供了一个完整的工具,但用途有限。将此代码的某些部分集成到您自己的应用程序中更有意义。

撰写本文的一个意外好处是,它让我重新考虑了我的一些编程习惯:因此,这个过程改进了我其他的代码。因此,如果您想改进您的代码,请写一篇文章!

历史

  • 2022 年 2 月 18 日:初始版本
© . All rights reserved.