在你开始前
在本教程中,您将学习如何使用HTTP身份验证,流文件以及如何在PHP中创建对象和异常。
关于本教程
本教程将完成您在本系列的第一部分有关学习PHP的过程中开始的简单工作流程应用程序。 您将添加HTTP身份验证,从非Web访问位置流式传输文档的功能以及异常处理。 您还将把一些应用程序组织到对象中。
总体而言,您将添加管理员批准文件的功能,使文件通常可供用户使用。 在此过程中,将讨论以下主题:
- 启用和使用基于浏览器的HTTP身份验证
- 从文件流数据
- 创建类和对象
- 使用对象方法和属性
- 创建和处理异常
- 根据请求页面控制对数据的访问
谁应该学习本教程?
本教程是一个由三部分组成的系列文章的第3部分,旨在在构建简单的工作流程应用程序时教您PHP编程的基础知识。 它适用于希望学习更多有关高级主题的开发人员,例如使用PHP进行面向对象的编程。 本教程还涉及HTTP身份验证,流传输,类和对象以及异常处理。
本教程假定您熟悉PHP的基本概念,例如语法,表单处理和访问数据库。 通过阅读“ 学习PHP,第1部分 ”和“ 学习PHP,第2部分 ”,然后选中“ 相关主题 ”,可以获得所需的所有信息。
先决条件
您需要安装并可用的Web服务器,PHP和数据库。 如果您拥有托管帐户,则只要服务器已安装PHP V5并有权访问MySQL数据库,就可以使用它。 否则,下载并安装以下软件包:
-
XAMPP
- 无论您使用的是Windows,Linux还是Mac,获取本教程所有必需软件的最简单方法是安装XAMPP,其中包括Web服务器,PHP和MySQL数据库引擎。 如果选择这种方式,请安装并运行控制面板以启动Apache和MySQL进程。 您还可以选择单独安装各个部件。 请记住,您将必须配置它们以使其协同工作-XAMPP已经完成了这一步骤。 网络服务器
- 如果选择不使用XAMPP,则Web服务器有多个选项。 如果使用PHP 5.4(在撰写本文时,XAMPP仅使用PHP 5.3.8),则可以使用内置的Web服务器进行测试。 但是,对于生产而言,我假设您使用的是Apache Web服务器2.x版。 PHP 5.x
- 如果您不使用XAMPP,则需要单独下载PHP5.x。 标准发行版包括本教程所需的一切。 随意下载二进制文件; 您d0不需要本教程的源代码(或者永远不需要,除非您想破解PHP本身)。 本教程是在PHP 5.3.8上编写和测试的。 的MySQL
- 该项目的一部分涉及将数据保存到数据库,因此您也将需要其中之一。 同样,如果安装XAMPP,则可以跳过此步骤,但是,如果选择这样做,则可以单独安装数据库。 在本教程中,我将重点介绍MySQL,因为它在PHP中非常常用。 如果选择这种方式,则可以下载并安装社区服务器。
到目前为止的故事
在本节中,我将回顾您在本系列中取得的进展,创建一个欢迎页面,并使用PHP创建一些限制。
现在的状况
在这些教程中,您一直在构建一个简单的工作流应用程序。 该应用程序使用户可以将文件上载到系统并查看这些文件以及管理员批准的文件。 到目前为止,您已经建立了:
- 一个注册页面,使用户能够通过输入唯一的用户名,电子邮件地址和密码来使用HTML表单来注册帐户。 您构建了PHP页面,该页面分析提交的数据,检查数据库以确保用户名是唯一的,并将注册保存在数据库中。
- 一个使用用户名和密码的登录页面,对它们进行数据库检查,如果有效,则在服务器上创建一个会话,以便服务器知道要显示的文件。
- 简单的界面元素,用于检测用户是否已登录以显示适当的选择。
- 一个上载页面,使用户可以通过浏览器将文件发送到服务器。 您还构建了获取该上载文件并将其保存到服务器的页面,然后使用文档对象模型(DOM)将有关该文件的信息添加到XML文件中以供以后检索。
- 显示功能使用另一种格式JavaScript对象表示法(JSON)来保存和显示数据。
您可以在“ 学习PHP,第2部分 ”中下载代表应用程序停止位置的文件。
你要做什么
在学习本教程之前,您将拥有一个完整的(尽管非常简单)工作流应用程序。 在本教程中,您将:
- 添加由Web服务器控制的HTTP身份验证。 您还将集成您的注册过程,以便将新用户添加到Web服务器。
- 将链接添加到显示可用文件的功能,以便用户下载它们。 您将创建一个函数,将这些文件从不可从Web访问的位置流式传输到浏览器。
- 确保用户从适当的页面下载文件。 您将使用以下事实:文件必须由应用程序流式传输,而不是由HTTP服务器简单地传输,以实现对用户下载文件的环境的控制。
- 创建一个表示文档的类,并使用面向对象的方法访问和下载它。
- 创建并使用自定义例外来帮助查明问题。
- 管理批准过程。
首先,您将对已有的东西公开展示。
欢迎页面
到目前为止,您已经集中精力构建应用程序的各个部分。 现在是时候将它们放在一起,所以从一个简单的欢迎页面开始,您可以将其用作访客的“着陆带”。 创建一个名为index.php的新文件,并添加清单1中的代码。
清单1.索引页面
<?php
session_start();
include ("top.txt");
include ("scripts.txt");
display_files();
include ("bottom.txt");
?>
启动会话后,以便稍后使用,页面的第一个include()
函数将加载页面的顶部界面元素(如果适用)。 第二个脚本加载您到目前为止创建的所有脚本,包括您在“ 学习PHP,第2部分 ”中创建的display_files()
函数,其中列出了当前用户上载或管理员批准的所有文件。 最后的包含内容是HTML页面的底部。
将文件与创建的其他文件保存在同一目录中。 例如,您可以将文件放在服务器的文档根目录中。 启动HTTP服务器后,可以通过将浏览器指向http://localhost/index.php来查看页面。
图1显示了简单页面。
图1.基本清单页面
限制文件访问
在下一部分中,您将学习控制身份验证的对象。 您首先需要设置一些限制。 此时,所有用户都可以查看所有文件,无论它们是否已批准,这都不是您想要的。 相反,您希望display_files()
仅向用户显示已批准的文件,除非用户是上传文件的用户。
打开scripts.txt
并在清单2中进行添加。
清单2.限制对文件的访问
for ($i = 0; $i < count($workflow["fileInfo"]); $i++) {
$thisFile = $workflow["fileInfo"][$i];
if (
($thisFile["approvedBy"] != null) ||
(
isset($_SESSION["username"]) &&
($thisFile["submittedBy"] == $_SESSION["username"])
)
) {
echo "<tr>";
echo "<td>" . $thisFile["fileName"] . "</td>";
echo "<td>" . $thisFile["submittedBy"] . "</td>";
echo "<td>" . $thisFile["size"] . "</td>";
echo "<td>" . $thisFile["status"] . "<td>";
echo "</tr>";
}
}
在清单2中 ,您结合了三种不同的条件来确定是否列出特定文件。 首先,如果文件被批准,则$thisFile["approvedBy"]
将具有一个值,因此该条件为true。 双竖线( ||
)的意思是“或”,因此,如果第一个测试出现错误,则条件的后半部分会获得第二次机会。
条件的后半部分也由两部分组成,但是由于您使用了双与号( &&
)(表示“和”),因此后半部分都必须为真。 第一个测试是查看会话是否知道用户名。 如果是这样,则用户名必须与$thisFile["submittedBy"]
值匹配。
如果总体条件评估为“真”(换句话说,如果文件被批准,或者用户登录并且是文件的创建者),系统将显示它。 如果不是,则不是。
因此,如果您尚未登录(可能必须重新启动浏览器进行测试),您应该会看到一个空白页面, 如图2所示 。
图2.基本列表页面,有限制
如果您只是启动浏览器,则应该会看到“ 注册”和“ 登录”链接,因为您尚未登录。在下一部分中,您将介绍处理该过程的另一种方法。
使用HTTP身份验证
在本部分中,您将为HTTP身份验证设置服务器,以便您的Web服务器可以控制PHP应用程序的登录过程。
HTTP认证
到目前为止,您使用了一个登录系统,其中用户在表单中输入用户名和密码,并且当用户提交表单时,将根据MySQL数据库检查该信息。 如果匹配,则应用程序在PHP中创建一个会话,并将用户名分配给$_SESSION
数组以供以后使用。
尽管此过程运行良好,但与其他系统集成时会遇到问题。 例如,如果您的工作流应用程序是Intranet的一部分,用户可以在其中使用其他系统的用户名登录,则您可能不希望他们再次登录。 相反,如果他们已经登录到其他地方,则希望他们已经登录时已经登录。 这被称为单点登录系统。
为此,您将切换到Web服务器实际控制登录过程的系统。 服务器不仅在页面中提供服务,还从浏览器的请求中检查用户名和密码,如果看不到它们,它会告诉浏览器弹出一个用户名和密码框,以便您输入该信息。 输入信息后,您将无需再次执行此操作,因为浏览器会将其与后续请求一起发送。
让我们从设置服务器开始。
启用HTTP验证
开始之前,请注意,如果使用的服务器不是Apache 2.X,则必须查看HTTP身份验证文档以了解如何进行设置。 XAMPP使用HTTP身份验证,因此,如果您使用XAMPP,则一切就绪。 (或者,您可以直接跳过此部分。您将构建适当的步骤,以便该应用程序可以使用两种身份验证。)
但是HTTP身份验证实际上如何工作? 首先,服务器知道它需要为每个目录提供什么样的安全性。 更改特定目录的一种方法是在服务器的主要配置中进行设置。 另一种方法是使用.htaccess文件,该文件包含有关文件所在目录的说明。
例如,您希望服务器确保所有访问您特定于用户的文件的用户都具有有效的用户名和密码。 首先,在您当前拥有文件的目录中创建一个名为loginin的目录。 例如,如果文件位于/ usr / local / apache2 / htdocs中,则可以创建/ usr / local / apache2 / htdocs / loggedin目录。
现在,您需要告诉服务器您想要覆盖该目录的整体安全性,因此打开httpd.conf文件,并将清单3中的代码添加到该文件中。
清单3.覆盖目录的安全性
<Directory /usr/local/apache2/htdocs/loggedin>
AllowOverride AuthConfig
</Directory>
(您应该使用正确的目录进行自己的设置。)
现在该准备实际目录了。
设置认证
接下来,创建一个新的文本文件,并将其保存在名为.htaccess的登录目录中。 将清单4中的代码添加到其中。
清单4.创建.htaccess文件
AuthName "Registered Users Only"
AuthType Basic
AuthUserFile /usr/local/apache2/password/.htpasswd
Require valid-user
在清单4中 , AuthName
是显示在用户名和密码框顶部的文本。 AuthType
指定您正在使用Basic
身份验证,这意味着您将以明文形式发送用户名和密码。 (如果创建高安全性应用程序,则需要研究其他选项。) AuthUserFile
是包含允许的用户名和密码的文件。 (您稍后将创建该文件。)最后, Require
指令使您可以指定实际看到此内容的人员。 在这里,您说将显示给任何有效的用户,但是您也可以选择要求特定的用户或用户组。
重新启动HTTP服务器,以便这些更改可以生效。
(对于XAMPP,如果已将其设置为服务,则可以从XAMPP控制菜单或从“服务”控制面板执行此操作。对于Apache V2.0的所有安装,还可以调用<APACHE_HOME>/bin/apachectl stop
,然后按<APACHE_HOME>/bin/apachectl start
。)
接下来,创建密码文件。
创建密码文件
为了使所有这些工作正常进行,您需要具有服务器可以检查的密码文件。 在“ 将新用户添加到密码文件”中 ,您将看到如何在PHP中操作该文件,但是现在,如果您有权访问命令行,则可以直接创建该文件。
首先,为您的.htpasswd文件选择一个位置。 它不应该是一个目录中的Web访问。 如果有人可以简单地下载和分析它,那不是很安全。 它也应该位于PHP可以写入的位置。 例如,您可以在apache2目录中创建一个密码目录。 无论选择哪个位置,请确保您的.htaccess文件中包含正确的信息。
要创建密码文件,您需要Apache随附的htpasswd应用程序。 如果使用XAMPP,请在<XAMPP_HOME>/apache/bin
查找。 在清单5中执行以下命令,替换您自己的目录和用户名: htpasswd -c /usr/local/apache2/password/.htpasswd roadnick
。
然后,提示您键入,然后重复密码,如清单5所示 。
清单5.创建.htpasswd文件
htpasswd -c /usr/local/apache2/password/.htpasswd NickChase
New password:
Re-type new password:
Adding password for user NickChase
-c
开关告诉服务器创建一个新文件,因此,在添加新用户后,该文件将如下所示: NickChase:IpoRzCGnsQv.Y
。
请注意,此版本的密码是加密的,从应用程序添加密码时必须牢记这一点。
现在,让我们来看一下它的作用。
在登录
要查看此操作,您需要访问受保护目录中的文件。 将uploadfile.php和uploadfile_action.php文件移到登录目录中,然后将index.php作为display_files.php复制到登录目录中。
在三个文件的每个文件中,更改include()
语句以说明新位置,如清单6所示 。
清单6.显示文件
<?php
session_start();
include ("../top.txt");
include ("../scripts.txt");
echo "Logged in user is ".$_SERVER['PHP_AUTH_USER'];
display_files();
include ("../bottom.txt");
?>
在这种情况下,您可以修复对包含文件的引用,但是还可以引用在浏览器发送用户名和密码时应设置的变量。 将您的浏览器指向http://localhost/loggedin/display_files.php即可查看实际操作。 如图3所示 ,您应该获得一个用户名和密码框。
图3.用户名和密码框
输入在创建密码文件中使用的用户名和密码以查看实际页面。
使用登录信息
此时,您已经输入了用户名和登录名,因此可以看到页面。 如图3所示,虽然消息显示用户已登录,但实际内容似乎并不相同。 您仍然会看到Register和Login链接,并且文件列表仍然仅显示管理员已批准的文件,而不显示当前用户已上传但仍在等待处理的文件, 如图4所示 。
图4.登录...
要解决这些问题,您有两种选择。 第一种是返回并重新编码应用程序引用用户名的每个实例,以查找$_SERVER['PHP_AUTH_USER']
而不是$_SESSION["username"]
。 但是,优秀的程序员本来就是懒惰的,所以这不是一个特别有吸引力的选择。
第二种选择是简单地基于$_SERVER['PHP_AUTH_USER']
设置$_SESSION["username"]
,以便一切将继续像以前一样工作。 您可以在启动新会话或加入现有会话之后立即在top.txt中执行此操作(请参见清单7 )。
清单7.设置当前用户
<?php
if (isset($_SESSION["username"])){
//Do nothing
} elseif (isset($_SERVER['PHP_AUTH_USER'])) {
$_SESSION["username"] = $_SERVER['PHP_AUTH_USER'];
}
?>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
...
使浏览器“忘记”您输入的用户名和密码的唯一方法是关闭浏览器,以便为$_SESSION["username"]
变量赋予优先级。 这样,您可以选择允许用户以其他人身份登录。 (您在这里不会这样做,但是可以选择。)
接下来,如果既未设置$_SESSION
变量也未设置$_SERVER
变量,则什么都不会发生,并且页面将继续运行,就好像用户未登录(是这种情况)。 进行这一简单的更改即可解决您的登录问题, 如图5所示 。
图5.更正的页面
修复界面
在添加新用户之前,您需要对top.txt进行几个快速修复,以适应新的结构。 一方面,您需要更改“ 登录”链接,以便它指向新保护的display_files.php文件,而不是指向您的旧login.php页面。 当用户尝试访问它时,浏览器将提供一种登录方式(请参见清单8 )。
清单8.调整导航
...
<div id="nav1">
<ul style='float: left'>
<li><a href="#" shape="rect">Home</a></li>
<li><a href="/uploadfile.php" shape="rect">Upload</a></li>
><li><a href="/loggedin/display_files.php" shape="rect">Files
</a></li>>
<?php
if (isset($_SESSION["username"]) || isset($username)){
if (isset($_SESSION["username"])){
$usernameToDisplay = $_SESSION["username"];
} else {
$usernameToDisplay = $username;
}
?>
><!--> <li><a href="logout.php" shape="rect">Logout</a>
</li> -->
<li><p style='color:white;'>
Welcome,
<b><?=$usernameToDisplay?></b>.</p></li>
<?php
} else {
?>
<li><a href="/registration.php" shape="rect">
Register</a></li>
<li><a href=">/loggedin/display_files.php"
shape="rect">Login</a></li>
<?php
}
?>
</ul>
</div>
...
请注意,除了修复登录参考并添加用于显示文件列表的新选项之外,我还注释了有关注销的消息,因为该主题不在本教程的讨论范围之内。
现在,您只需要将注册过程与密码文件集成在一起即可。
将新用户添加到密码文件
此过程的最后一步是将您的注册与.htpasswd文件集成在一起。 为此,将用户保存到数据库后,只需要将新条目添加到.htpasswd。 打开registration_action.php并添加清单9的内容。
清单9.注册时在服务器上创建用户
...
if ($checkUserStmt->rowCount() == 0) {
$stmt = $dbh->prepare("insert into users (username, email, password) ".
"values (?, ?, ?)");
$stmt->bindParam(1, $name);
$stmt->bindParam(2, $email);
$stmt->bindParam(3, $pword);
$name = $_POST["name"];
$email = $_POST["email"];
$pword = $passwords[0];
$stmt->execute();
$pwdfile = '/usr/local/apache2/password/.htpasswd';
if (is_file($pwdfile)) {
$opencode = "a";
} else {
$opencode = "w";
}
$fp = fopen($pwdfile, $opencode);
$pword_crypt = crypt($passwords[0]);
fwrite($fp, $_POST['name'] . ":" . $pword_crypt . "\n");
fclose($fp);
echo "<p>Thank you for registering!</p>";
} else {
echo "<p>There is already a user with that name: </p>";
...
开始之前,如果已经有了.htpasswd文件,请确保运行Apache的用户可以在Web服务器上对其进行写入。 如果没有,请确保用户可以写入适当的目录。
首先,检查文件是否存在,并使用该信息确定是要写入新文件还是将信息附加到现有文件。 知道之后,继续打开文件。
正如在创建密码文件中看到的那样,密码以加密形式存储,因此您可以使用crypt()
函数获取该字符串。 最后,将用户名和密码写到文件中并关闭文件。
要对此进行测试,请退出浏览器以清除所有缓存的密码,然后打开http://localhost/index.php。
单击注册并创建一个新帐户。 创建帐户完成后,请再次退出浏览器并尝试访问受保护的页面。 您的新用户名和密码应该可以使用。
使用流
现在,您已经设置了系统\,可以使用户实际下载可用文件了。 从一开始,这些文件就已存储在不可从Web访问的目录中,因此不存在指向它们的简单链接。 取而代之的是,在本节中,您将创建一个函数,该函数将文件从当前位置流式传输到浏览器。
什么是流?
现在,实际访问资源(例如文件)的方式取决于存储位置和存储方式。 访问位置文件与通过HTTP或FTP访问远程服务器上的文件非常不同。
幸运的是,PHP提供了流包装器 。 无论资源在哪里,都可以调用资源,并且如果PHP具有可用的包装器,它将找出如何进行该调用。
您可以通过打印stream_get_wrappers()
函数返回的数组的内容来找出可用的包装器,如清单10所示 。
清单10.显示可用的流包装器
<?php
print_r(stream_get_wrappers());
?>
对于查看数组的内容, print_r()
函数非常方便。 例如,您的系统可能会为您提供清单11 。
清单11.可用的流包装器
Array
(
[0] => php
[1] => file
[2] => http
[3] => ftp
)
这使您可以轻松地将文件存储在远程Web服务器或FTP服务器上,作为将文件存储为本地服务器上的替代方法。 您在本节中使用的代码仍然可以使用。
让我们来看看。
下载文件
为了让用户看到文件,浏览器必须接收它。 它还必须知道文件是什么才能正确显示。 您可以照顾这两个问题。 创建一个名为download_file.php的新文件,并将其保存在登录目录中。 添加清单12中的代码。
清单12.发送文件
<?php
include ("../scripts.txt");
$filetype = $_GET['filetype'];
$filename = $_GET['file'];
$filepath = UPLOADEDFILES.$filename;
if($stream = fopen($filepath, "rb")){
$file_contents = stream_get_contents($stream);
header("Content-type: ".$filetype);
print($file_contents);
}
?>
尽管功能强大,但是这里的过程实际上非常简单。 首先,打开文件进行读取和缓冲。 您实际上使用fopen()
函数所做的是创建一个表示文件的资源。 然后,您可以将该资源传递给stream_get_contents()
,后者将整个文件读出为单个字符串。
现在,您已经拥有了内容,可以将其发送到浏览器,但是浏览器将不知道如何处理它,并且可能将其显示为文本。 这对于文本文件来说很好,但是对图像甚至HTML文件来说都不太好。 因此,您不仅要发送原始消息,还需要将包含有关文件的Content-type
的信息(例如image/jpeg
的header
发送到浏览器。
最后,将文件的内容输出到浏览器。 收到Content-type
标头后,浏览器将知道如何处理它。
至于确定实际使用的文件和类型,您可以从$_GET
数组中读取这些文件和类型,因此可以将它们直接添加到URL中,如下所示:
http://localhost/loggedin/download_file.php?file=NoTooMiLogo.png&filetype=image/png
在浏览器中输入该URL(当然,使用适当的文件名和类型),以查看图6中的结果。
图6.下载文件
将链接添加到文件
因为下载页面所需的所有信息都可以添加到URL,所以添加链接使用户能够下载文件很简单。 使用display_files()
函数创建可用文件的显示,因此可以如清单13所示添加链接。
清单13.添加链接
...
for ($i = 0; $i < count($workflow["fileInfo"]); $i++){
$thisFile = $workflow["fileInfo"][$i];
echo "<tr>";
echo "<td><a href='/loggedin/download_file.php?file="
.$thisFile["fileName"].
"&filetype=".$thisFile["fileType"]."'>".$thisFile["fileName"]
."</a></td>";
echo "<td>".$thisFile["submittedBy"]."</td>";
echo "<td>".$thisFile["size"]."</td>";
echo "<td>".$thisFile["status"]."<td>";
echo "</tr>";
}
...
您可以在图7中看到结果。
图7.链接到文件
单击链接以验证文件。
接下来,您将研究如何将此过程封装到一个对象中。
使用物件
在本节中,您将探索object的用法。 到目前为止,您几乎完成的所有操作都是过程性的 ,这意味着您拥有一个从头到尾运行的脚本。 现在您将远离它。
什么是物体?
面向对象编程的中心概念是可以将“事物”表示为自给自足的捆绑包。 例如,电热水壶具有诸如其颜色和最高温度之类的特性,以及诸如加热水并自行关闭之类的功能。
如果要将该水壶表示为一个对象,则它还将具有诸如color
和maximumTemperature
类的属性以及一些功能或方法(如heatWater()
和turnOff()
。 如果您正在编写与水壶连接的程序,则只需调用水壶对象的heatWater()
方法,而不用担心它的实际完成方式。
为了使事情更加相关,您将创建一个对象,该对象表示要下载的文件。 它将具有属性,例如文件的名称和类型,以及方法,例如download()
。
说了这么多,我需要指出的是,您实际上并没有定义对象。 而是定义一个对象类 。 类充当该类型对象的一种“模板”。 然后,您创建该类的实例 ,该实例是对象。
让我们从创建实际的类开始。
创建WFDocument类
处理对象的第一步是创建对象所基于的类。 您可以将此定义添加到scripts.txt文件中,但是您正在尝试使代码更具可维护性,而不是更少。 因此,创建一个单独的文件WFDocument.php,并将其保存在主目录中。 添加清单14中的代码。
清单14.基本文档Object
<?php
include_once("scripts.txt");
class WFDocument {
function download($filename, $filetype) {
$filepath = UPLOADEDFILES.$filename;
if($stream = fopen($filepath, "rb")){
$file_contents = stream_get_contents($stream);
header("Content-type: ".$filetype);
print($file_contents);
}
}
}
?>
首先,您需要UPLOADEDFILES
常量,因此要包含scripts.txt文件。 接下来,创建实际的类。 WFDocument
类只有一个方法download()
,它与download_file.php中的代码相同,不同的是接收文件名和类型作为函数的输入,而不是直接从$_GET
数组中提取它们。 。
现在让我们看一下实例化此类。
调用WFDocument类型的对象
在本系列的第2部分中,当您使用DOM时,实际上已经实例化了几个对象,但是关于为什么或如何却很少提及。 我现在要补救。
打开download_file.php页面并更改代码,其内容如清单15所示 。
清单15.将文件信息发送到函数
<?php
include ("../WFDocument.php");
$filetype = $_GET['filetype'];
$filename = $_GET['file'];
$wfdocument = new WFDocument();
$wfdocument->download($filename, $filetype);
?>
首先,不是包含scripts.txt文件,而是包含WFDocument
类的定义,并将其放入WFDocument.php文件中。 (一些开发人员发现,简单地创建一个包含所有类的页面,然后包含该页面,而不是到处都包含单个类是有用的。)
现在,您可以创建一个新对象,可以使用new
关键字进行操作。 此行创建类型为WFDocument
的新对象,并将其分配给$wfdocument
变量。
一旦有了对该对象的引用,就可以调用其任何公共方法。 在这种情况下,只有一个方法download()
,您可以使用->
运算符来调用它。 基本上,此符号表示“使用属于该对象的方法(或属性)”。
保存文件并通过单击页面上的链接之一对其进行测试。 代码与之前完全相同。 唯一的区别是您如何称呼它。
创建属性
方法只是故事的一部分。 一个对象的全部要点是它被封装了。 它应该包含自己的所有信息,因此您可以将它们设置为对象的属性,而不是将名称和文件类型提供给download()
方法。 首先,您必须在类中创建它们(请参见清单16 )。
清单16.使用对象属性
<?php
include_once("../scripts.txt");
class WFDocument {
public $filename;
public $filetype;
function download() {
$filepath = UPLOADEDFILES.$this->filename;
if($stream = fopen($filepath, "rb")){
$file_contents = stream_get_contents($stream);
header("Content-type: ".$this->filetype);
print($file_contents);
}
}
}
?>
注意,您在函数外部声明了变量; 它们是类的一部分,而不是函数。 您还可以将它们声明为public
,这意味着您可以从类本身之外访问它们。 您还可以将属性设置为private
,这意味着您只能在类本身中使用它,也可以将其设置为protected
,这意味着您只能在该类或基于该类的任何类中使用它。 (如果您不熟悉这个想法,请稍等片刻。在创建自定义异常中 ,我将详细讨论这个概念继承 )。
最后,要引用对象属性,必须知道哪个对象拥有该属性。 在对象本身内,您可以只使用关键字$this
,它引用对象本身。 这样,您可以使用$this->filename
引用执行此代码的对象的filename
属性。
现在让我们看一下这些属性的设置值。
设定属性
您实际上不是设置信息到对象,而是想要设置对象的属性(请参见清单17 )。
清单17.设置对象属性
<?php
include ("../WFDocument.php");
$filetype = $_GET['filetype'];
$filename = $_GET['file'];
$wfdocument = new WFDocument();
$wfdocument->filename = $filename;
$wfdocument->filetype = $filetype;
$wfdocument->download();
?>
注意这里的符号。 您正在使用对象名称$wfdocument
, ->
运算符和属性名称。 一旦设置了这些属性,就可以从对象内部使用它们,因此您不必将它们传递给download()
方法。
现在,完成所有这些操作之后,实际上有更好的方式来处理此类事情,因此让我们看一下替代方法。
隐藏属性
尽管可以像上一节中那样直接设置属性的值,但这并不是处理问题的最佳方法。 相反,通常的做法是向公众隐藏实际属性,并使用getter和setter来获取和设置其值,如清单18所示 。
清单18.使用私有属性
<?php
include_once("../scripts.txt");
class WFDocument {
private $filename;
private $filetype;
function setFilename($newFilename){
$this->filename = $newFilename;
}
function getFilename(){
return $this->filename;
}
function setFiletype($newFiletype){
$this->filetype = $newFiletype;
}
function getFiletype(){
return $this->filetype;
}
function download() {
$filepath = UPLOADEDFILES.$this->getFilename();
if($stream = fopen($filepath, "rb")){
$file_contents = stream_get_contents($stream);
header("Content-type: ".$this->getFiletype());
print($file_contents);
}
}
}
?>
首先,将属性定义为private
。 这意味着,如果您像以前那样尝试直接设置它们,将会出现错误。 但是您仍然必须设置这些值,因此可以使用getFilename()
, setFilename()
, getFiletype()
和setFiletype()
方法。 请注意,您可以在download()
方法中使用它们,就像使用原始属性一样。
使用getter和setter很方便,因为它可以让您更好地控制数据的变化。 例如,在允许为属性设置特定值之前,您可能需要执行某些验证检查。
调用隐藏属性
现在,您已经隐藏了属性,您需要返回并修改download_file.php页面,以免出现错误(参见清单19 )。
清单19.使用setter
<?php
include ("../WFDocument.php");
$filetype = $_GET['filetype'];
$filename = $_GET['file'];
$wfdocument = new WFDocument();
$wfdocument->setFilename($filename);
$wfdocument->setFiletype($filetype);
$wfdocument->download();
?>
尽管这种方法很方便,但是有更简单的方法可以设置对象的属性。
创建一个构造函数
如果对象具有构造函数,则每次您创建该特定类的新实例时都会调用该对象。 例如,您可以创建一个简单的构造函数,如清单20所示 。
清单20.一个简单的构造函数
...
function getFiletype(){
return $this->filetype;
}
function __construct(){
echo "Creating new WFDocument";
}
function download() {
$filepath = UPLOADEDFILES.$this->filename;
...
如果尝试按原样运行此脚本,则会看到错误,因为该对象在输出标题之前先输出文本( Creating new WFDocument
), 如图8所示 。
图8.运行脚本后的错误
因此,即使您从未显式调用__construct()
方法,应用程序也会在实例化对象后立即调用它。 您可以通过向构造函数添加信息来利用它,以发挥自己的优势。
用信息创建对象
构造函数最常见的用途之一是提供一种在创建对象时初始化各种值的方法。 例如,您可以设置WFDocument
类,以便在创建对象时设置filename
和filetype
属性(请参见清单21 )。
清单21.一个更复杂的构造函数
...
function getFiletype(){
return $this->filetype;
}
function __construct($filename = "", $filetype = ""){
$this->setFilename($filename);
$this->setFiletype($filetype);
}
function download() {
$filepath = UPLOADEDFILES.$this->filename;
...
创建对象时,PHP会继续执行构造函数中的所有指令。 在这种情况下,该构造函数将查找filename
和filetype
。 如果不提供它们,则仍然不会出错,因为您指定了默认值,如果在调用函数时未给出任何值,则使用默认值。
但是,如何显式调用__construct()
函数?
创建对象:调用构造函数
您实际上并没有显式调用构造函数方法。 而是在每次创建对象时隐式调用它。 这意味着您可以使用该特定时刻为构造函数传递信息(请参见清单22 )。
清单22.使用构造函数
<?php
include ("../WFDocument.php");
$filetype = $_GET['filetype'];
$filename = $_GET['file'];
$wfdocument = new WFDocument($filename, $filetype);
$wfdocument->download();
?>
创建新对象时传递给类的所有信息都传递给构造函数。 这样,您可以简单地创建对象并使用它来下载文件。
处理异常
当程序中发生意外事件时,会发生异常。 发生异常时,通常会设计一个程序来停止或显示错误。 由于异常在应用程序中出现问题时会发挥作用,因此它们常常与错误相混淆。 但是,例外情况要灵活得多。 在本节中,您将看到如何定义不同类型的异常,并使用它们来确定应用程序的状况。
通用例外
让我们从WFDocument
类的定义中的一个简单的通用异常开始(请参见清单23 )。
清单23.引发异常
<?php
include_once("../scripts.txt");
class WFDocument {
...
function download() {
$filepath = UPLOADEDFILES.$this->filename;
try {
if(file_exists($filepath)){
if ($stream = fopen($filepath, "rb")){
$file_contents = stream_get_contents($stream);
header("Content-type: ".$this->filetype);
print($file_contents);
}
} else {
throw new Exception ("File '".$filepath."' does not exist.");
}
} catch (Exception $e) {
echo "<p style='color: red'>".$e->getMessage()."</p>";
}
}
}
?>
异常不仅会发生, 还会引发异常 。 如果您扔东西,则必须抓住它,因此您创建了一条try-catch语句。 在try
部分中,放置了代码。 如果发生了一些不愉快的事情,例如在这种情况下文件不存在,并且引发了异常,PHP会立即移到catch
块以捕获异常。
异常具有许多属性,例如引发异常的行和文件以及一条消息。 通常,应用程序在引发异常时设置消息,如您在此处看到的。 然后,异常本身$e
可以使用getMessage()
方法提供该文本。 例如,如果您尝试下载一个不存在的文件,则会看到消息File 'c:/sw/temp/NoTooMiLogoQWSQ.png' does not exist.
(请参阅图9 )。
图9.基本异常
但是,异常的真正力量来自于创建自己的异常。
创建自定义异常
在上一节中,您检查了对象,但我忽略了它们的一个非常重要的方面:继承。
使用类的一个优点是能够将一个类用作另一个类的基础。 例如,您可以创建一个新的异常类型NoFileExistsException
,它扩展了原始的Exception
类(请参见清单24 )。
清单24.创建一个自定义异常
class NoFileExistsException extends Exception {
public function informativeMessage(){
$message = "The file, '".$this->getMessage()."', called on line ".
$this->getLine()." of ".$this->getFile().", does not exist.";
return $message;
}
}
(为简单起见,我将此代码添加到了WFDocument.php文件中,但是您可以在需要时将其添加到任何可访问的位置。)
在这里,您使用一个方法创建了一个新类NoFileExistsException
: NoFileExistsException
informativeMessage()
。 实际上,此类也是 Exception
,因此Exception
对象的所有公共方法和属性也都可用。
例如,请注意,在getLine()
informativeMessage()
函数中,即使未在此处定义它们,也调用了getLine()
和getFile()
方法。 它们是在基类Exception
定义的,因此您可以使用它们。
现在,让我们来看一下它的作用。
捕获自定义异常
使用新异常类型的最简单方法是像抛出一般Exception
一样简单地抛出它(请参见清单25 )。
清单25.引发和捕获自定义异常
function download() {
$filepath = UPLOADEDFILES.$this->filename;
try {
if(file_exists($filepath)){
if ($stream = fopen($filepath, "rb")){
$file_contents = stream_get_contents($stream);
header("Content-type: ".$this->filetype);
print($file_contents);
}
} else {
throw new NoFileExistsException ($filepath);
}
} catch (NoFileExistsException $e) {
echo "<p style='color: red'>".$e->informativeMessage()."</p>";
}
}
请注意,即使在创建异常时仅传递$filepath
,也将返回完整消息: The file, 'c:/sw/temp/NoTooMiLogoQWSQ.png', called on line 41 of C;\sw\xampp\htdocs\WFDocument.php, does not exist.
(请参见图10 )。
图10.使用自定义异常
处理多个异常
创建自定义异常类的一个原因是,您可以使用PHP的功能来区分它们。 例如,您可以为一次try
创建多个catch
块(请参见清单26 )。
清单26.区分异常
...
function download() {
$filepath = UPLOADEDFILES.$this->filename;
try {
if(file_exists($filepath)){
if ($stream = fopen($filepath, "rb")){
$file_contents = stream_get_contents($stream);
header("Content-type: ".$this->filetype);
print($file_contents);
} else {
throw new Exception ("Cannot open file ".$filepath);
}
} else {
throw new NoFileExistsException ($filepath);
}
} catch (NoFileExistsException $e) {
echo "<p style='color: red'>".$e->informativeMessage()."</p>";
} catch (Exception $e){
echo "<p style='color: red'>".$e->getMessage()."</p>";
}
}
}
在这种情况下,您尝试通过检查文件是否存在并抛出NoFileExistsException
来捕获问题。 如果您克服了这一障碍,并且其他原因使您无法打开文件,则会引发一般异常。 PHP将检测您抛出的异常类型并执行适当的catch
块。
对于仅输出消息而言,所有这些似乎有些过头,但是没有什么可以说明的。 您可以为自己的异常创建自定义方法,例如,为特定事件发送通知。 您还可以创建自定义catch
块,根据情况执行不同的操作。
您还可以使用异常来捕获从技术上讲是错误但实际上不应停止程序的情况。 例如,您可能尝试处理图像,如果图像不成功,则将其保留不变,然后继续前进而不是退出。
仅仅因为您定义了所有这些不同的异常并不意味着您必须逐个捕获每个异常,正如您将在接下来看到的那样。
传播异常
继承的另一个方便特性是能够将对象视为其基类的成员。 例如,您可以抛出NoFileExistsException
并将其捕获为通用Exception
(请参见清单27 )。
清单27.组合异常捕获
...
function download() {
$filepath = UPLOADEDFILES.$this->filename;
try {
if(file_exists($filepath)){
if ($stream = fopen($filepath, "rb")){
$file_contents = stream_get_contents($stream);
header("Content-type: ".$this->filetype);
print($file_contents);
} else {
throw new Exception ("Cannot open file ".$filepath);
}
} else {
throw new NoFileExistsException ($filepath);
}
} catch (Exception $e){
echo "<p style='color: red'>".$e->getMessage()."</p>";
}
}
}
在这种情况下,当您引发异常时,PHP会沿catch
块列表向下移动,寻找适用的第一个异常。 在这里,您只有一个,但是它将捕获任何 Exception
,正如File 'c:/sw/temp/NoTooMiLogoQWSQ.png' does not exist.
图11中的消息。
图11.传播异常
把它放在一起
现在您已经完成了文件下载过程,是时候将所有内容放在一起并完成应用程序了。 在本节中,您将处理一些未完成的杂项任务:
- 检测管理员
- 创建使管理员能够批准文件的表单
- 检查下载以确保未从其他服务器调用下载
首先,您需要设置将批准文件的管理员。
检测管理员
最初在数据库中创建用户表时,您并未考虑需要区分常规用户和管理员的事实,因此您现在必须加以注意。 登录MySQL并执行以下命令:
alter table users add status varchar(10) default 'USER';
update users set status = 'USER';
update users set status = 'ADMIN' where id=3;
第一条命令将新列status
添加到users表。 您没有在注册页面上指定用户类型,因此只需为添加到系统中的所有新用户指定默认值USER
。 第二条命令为现有用户设置此状态。 最后,选择一个要成为管理员的用户。 (确保为数据使用适当的id
值。)
有了数据后,您就可以创建一个返回当前用户状态的函数。 将此功能添加到scripts.txt中,如清单28所示。
清单28.检测用户状态
function getUserStatus()
{
$dbh = new PDO('mysql:host=localhost;dbname=workflow', 'wfuser', 'wfpass');
$stmt = $dbh->prepare("select * from users where username= :username");
$stmt->bindParam("username", $username);
$username = $_SESSION["username"];
$stmt->execute();
$status = "NONE";
if ($row = $stmt->fetch()) {
$status = $row["status"];
}
$dbh = null;
return $status;
}
要查看此过程的工作方式,请创建与相应数据库的连接。 然后使用用户名的参数准备一条SQL语句。 将该参数设置为存储在$_SESSION
变量中的实际用户名。 接下来,执行该语句并尝试获取第一行(可能是唯一的)数据。
首先将$status
定义为NONE
。 如果不存在任何行,则此变量将保持原样。 另一方面,如果存在一行,则将状态设置为等于status
列的值。 最后,关闭连接并返回值。
批准文件:表格
现在,您准备向表单添加批准功能。 如果要查看文件列表的用户是管理员,则要显示一个未决文件复选框。 scripts.txt中的display_files()
函数对此进行了处理(请参见清单29 )。
清单29.添加管理功能
function display_files()
{
$userStatus = getUserStatus($_SESSION["username"]);
if ($userStatus == "ADMIN") {
echo "<form action='/approve_action.php' method='POST'>";
}
$workflow = json_decode(file_get_contents(UPLOADEDFILES . "docinfo.json"), true);
echo "<table width='100%'>";
$files = $workflow["fileInfo"];
echo "<tr><th>File Name</th>";
echo "<th>Submitted By</th><th>Size</th>";
echo "<th>Status</th>";
if ($userStatus == "ADMIN") {
echo "<th>Approve</th>";
}
echo "</tr>";
for ($i = 0; $i < count($workflow["fileInfo"]); $i++) {
$thisFile = $workflow["fileInfo"][$i];
if (
>($userStatus == "ADMIN") ||>
($thisFile["approvedBy"] != null) ||
(
isset($_SESSION["username"]) &&
($thisFile["submittedBy"] == $_SESSION["username"])
)
) {
echo "<tr>";
echo "<td><a href='/loggedin/download_file.php?file=" .
$thisFile["fileName"] . "&filetype=" . $thisFile["fileType"] .
"'>" . $thisFile["fileName"] . "</a></td>";
echo "<td>" . $thisFile["submittedBy"] . "<lt;/td>";
echo "<td>" . $thisFile["size"] . "</td>";
echo "<td>" . $thisFile["status"] . "<td>";
if ($userStatus == "ADMIN") {
if ($thisFile["status"] == "pending") {
echo "<input type='checkbox' name='toapprove[]' ".
"value='" . $i . "' checked='checked' />";
}
}
echo "</tr>";
}
}
echo "</table>";
if ($userStatus == "ADMIN") {
echo "<input type='submit' value='Approve Checked Files' />";
echo "</form>";
}
}
从顶部开始,首先确定用户的状态。 这很重要,原因有两个。 一方面,如果用户是管理员,则需要显示批准表。 另外,如果用户是管理员,则将显示所有文件,无论是谁上传的文件或它们的状态如何。 为此,您只需向if
语句添加另一个条件。
在实际文件显示中,如果用户是管理员, 并且文件仍处于挂起状态,则显示一个预选中的复选框。 复选框的值是文件的编号,因此您以后可以参考它。 给用户一个类似数组的名称(在这种情况下, toapprove []
告诉Web服务器期望同一个字段名称具有多个值。
图12显示了结果,一个具有适当字段(文件名,提交者,文件大小,状态)的表单以及用于选择批准的复选框。
图12.批准表格
批准文件:更新JSON
接受批准复选框的实际表单页面approve_action.php非常简单(参见清单30 )。
清单30.处理批准表格
<?php
session_start();
include "/scripts.txt";
$allApprovals = $_POST["toapprove"];
foreach ($allApprovals as $thisFileNumber) {
approveFile($thisFileNumber);
}
echo "Files approved.";
?>
对于每个要toapprove
复选框,您只需在approveFile()
调用approveFile()
函数(请参见清单31 )。
清单31.批准表单
function approveFile($fileNumber){
$workflow = json_decode(file_get_contents(UPLOADEDFILES . "docinfo.json"), true);
$workflow["fileInfo"][$fileNumber]["approvedBy"] = $_SESSION["username"];
$workflow["fileInfo"][$fileNumber]["status"] = "approved";
$jsonText = json_encode($workflow);
file_put_contents(UPLOADEDFILES . "docinfo.json", $jsonText);
}
首先,使用json_decode()
加载数据,就像显示文件时一样。 在这种情况下,您可以将许多不同的引用串在一起以设置approvedBy
值。 $workflow
变量包含所有数据。 它的fileInfo
属性是一个包含所有文件的数组,因此$fileNumber
指向所需的文件。 有了该属性后,就可以设置approvedBy
属性。 同样,将status
设置为approved
。
最后,保存文件。
请注意,尽管为简便起见以这种方式进行操作,但在生产应用程序中,打开和加载文件一次,进行所有更改然后保存文件的效率更高。
通过批准一些文件并重新显示文件来进行测试。
下载安全检查
最后一步,您需要在下载过程中添加安全检查。 因为您完全通过应用程序控制此过程,所以可以使用所需的任何检查。 对于此示例,您将检查以确保用户单击了本地服务器上页面上文件的链接,以防止某人从外部站点链接到该文件,甚至阻止将该链接添加为书签或向某人发送文件否则是原始链接。
为此,您首先在WFDocument.php文件中创建一个新异常(请参见清单32 )。
清单32.禁用远程下载
<?php
include_once("/scripts.txt");
class NoFileExistsException extends Exception {
public function informativeMessage(){
$message = "The file, '".$this->getMessage()."', called on line ".
$this->getLine()." of ".$this->getFile().", does not exist.";
return $message;
}
}
class ImproperRequestException extends Exception {
public function logDownloadAttempt(){
//Additional code here
echo "Notifying administrator ...";
}
}
class WFDocument {
private $filename;
private $filetype;
function setFilename($newFilename){
$this->filename = $newFilename;
}
function getFilename(){
return $this->filename;
}
function setFiletype($newFiletype){
$this->filetype = $newFiletype;
}
function getFiletype(){
return $this->filetype;
}
function __construct($filename = "", $filetype = ""){
$this->setFilename($filename);
$this->setFiletype($filetype);
}
function download() {
$filepath = UPLOADEDFILES.$this->filename;
try {
$referer = $_SERVER['HTTP_REFERER'];
$noprotocol = substr($referer, 7, strlen($referer));
$host = substr($noprotocol, 0, strpos($noprotocol, "/"));
if ( $host != 'boxersrevenge' &&
$host != 'localhost'){
throw new ImproperRequestException("Remote access not allowed.
Files must be accessed from the intranet.");
}
if(file_exists($filepath)){
if ($stream = fopen($filepath, "rb")){
$file_contents = stream_get_contents($stream);
header("Content-type: ".$this->filetype);
print($file_contents);
} else {
throw new Exception ("Cannot open file ".$filepath);
}
} else {
throw new NoFileExistsException ($filepath);
}
} catch (ImproperRequestException $e){
echo "<p style='color: red'>".$e->getMessage()."</p>";
$e->logDownloadAttempt();
} catch (Exception $e){
echo "<p style='color: red'>".$e->getMessage()."</p>";
}
}
}
?>
在ImproperRequestException
,创建一个新方法logDownloadAttempt()
,该方法可以发送电子邮件或执行其他操作。 您可以在此异常类型的catch
块中使用该方法。
在实际的download()
函数中,您要做的第一件事是获取HTTP_REFERER
。 该可选标头与一个Web请求一起发送,该Web请求标识了发出请求的页面。 例如,如果您从博客链接到developerWorks ,然后单击该链接,则IBM日志将博客的URL显示为该访问的HTTP_REFERER
。
对于您的情况,您要确保请求来自您的应用程序,因此您首先要去除开头的“ http://”字符串,然后将所有文本保存到第一个斜杠(/)。 这是请求中的主机名。
对于外部请求,此主机名可能类似于boxersrevenge.nicholaschase.com,但您只在查找内部请求,因此您接受boxersrevenge
或localhost
。 如果请求来自其他任何地方,则抛出ImproperRequestException
,该异常由相应的块捕获。
请注意,就安全性而言,此方法并非万无一失。 某些浏览器无法正确发送引荐来源信息,因为它们不支持引荐信息或用户更改了发送内容。 但是此示例应该使您对可以用来帮助控制内容的操作类型有所了解。
摘要
本教程总结了关于“学习PHP”的三部分系列,其中您构建了一个简单的工作流应用程序。 较早的部分集中于基础知识,例如语法,表单处理,数据库访问,文件上传,XML和JSON。 在这一部分中,您将所有这些步骤进一步进行了整合,以创建一个表单,管理员可以通过该表单来批准各种文件。 我们讨论了以下主题:
- 使用HTTP身份验证
- 流文件
- 创建类和对象
- 对象属性和方法
- 使用对象构造函数
- 使用对象继承
- 使用异常
- 创建自定义例外
- 对下载执行其他安全检查
翻译自: https://www.ibm.com/developerworks/opensource/tutorials/os-phptut3/os-phptut3.html