如何在纯 PHP 中查询 MySQL 数据库
纯 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.php:
Settings
类,用于在页面之间保留数据 - querymysql_class_Db.php:
Db
类,所有数据库交互都集中在此 - querymysql_class_H.php:
H
类,包含各种静态函数 - querymysql_class_MyList.php:
MyList
类,用于处理值列表。
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 以及上述代码中的 Settings
和 Processor
类。让我们从 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
类,因此我们已经看到了支持类 Settings
、Db
和 H
。我将在对配置文件进行一些说明后解释它们。
配置文件
配置文件 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->whereParmsArray
!where
子句字符串包含诸如
.... AND thisField = :thisField AND ....
以及 whereParmsArray
包含
whereParmsArray['thisField'] = <actual value>
因此,where
字符串中的 :-fields
与 whereParmsArray
中的键匹配。这使得在 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 可能不是存储很容易变得相当多的数据的最佳方式。也许最好将参数按会话存储在数据库中。
- 在详细信息页面上添加非键字段的更新选项(数据可能有数据库约束)。添加
insert
和delete
并不容易。 - 添加对更多 SQL 数据类型的支持。如果您知道 BLOB 字段始终包含 .jpg 图像,您可以在详细信息页面上显示该图像。
- 添加对另一个 DBMS 的支持很容易,只要该 DBMS 受 PDO 支持。
结论
我希望我已经说服您,构建这样一个查询工具并不困难。该代码为您提供了一个完整的工具,但用途有限。将此代码的某些部分集成到您自己的应用程序中更有意义。
撰写本文的一个意外好处是,它让我重新考虑了我的一些编程习惯:因此,这个过程改进了我其他的代码。因此,如果您想改进您的代码,请写一篇文章!
历史
- 2022 年 2 月 18 日:初始版本