身份验证,对象,异常和流

在你开始前

在本教程中,您将学习如何使用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所示,虽然消息显示用户已登录,但实际内容似乎并不相同。 您仍然会看到RegisterLogin链接,并且文件列表仍然仅显示管理员已批准的文件,而不显示当前用户已上传但仍在等待处理的文件, 如图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;'>&nbsp;&nbsp;&nbsp;
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/jpegheader发送到浏览器。

最后,将文件的内容输出到浏览器。 收到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的用法。 到目前为止,您几乎完成的所有操作都是过程性的 ,这意味着您拥有一个从头到尾运行的脚本。 现在您将远离它。

什么是物体?

面向对象编程的中心概念是可以将“事物”表示为自给自足的捆绑包。 例如,电热水壶具有诸如其颜色和最高温度之类的特性,以及诸如加热水并自行关闭之类的功能。

如果要将该水壶表示为一个对象,则它还将具有诸如colormaximumTemperature类的属性以及一些功能或方法(如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类,以便在创建对象时设置filenamefiletype属性(请参见清单21 )。

清单21.一个更复杂的构造函数
...
   function getFiletype(){
      return $this->filetype;
   }

   function __construct($filename = "", $filetype = ""){
      $this->setFilename($filename);
      $this->setFiletype($filetype);
   }

   function download() {

      $filepath = UPLOADEDFILES.$this->filename;

...

创建对象时,PHP会继续执行构造函数中的所有指令。 在这种情况下,该构造函数将查找filenamefiletype 。 如果不提供它们,则仍然不会出错,因为您指定了默认值,如果在调用函数时未给出任何值,则使用默认值。

但是,如何显式调用__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.基本异常
基本异常消息的屏幕截图

但是,异常的真正力量来自于创建自己的异常。

创建自定义异常

在上一节中,您检查了对象,但我忽略了它们的一个非常重要的方面:继承。

使用类的一个优点是能够将一个类用作另一个类的基础。 For example, you can create a new exception type, NoFileExistsException , which extends the original Exception class (see Listing 24 ).

Listing 24. Creating a custom exception
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;
   }

}

(For simplicity's sake, I added this code to the WFDocument.php file, but you can add it wherever it's accessible when you need it.)

Here, you created a new class, NoFileExistsException , with a single method: informativeMessage() . In actuality, this class is also an Exception , so all the public methods and properties for an Exception object are also available.

For example, notice that within the informativeMessage() function, you call the getLine() and getFile() methods, even though they're not defined here. They're defined in the base class, Exception , so you can use them.

Now let's see it in action.

Catching a custom exception

The easiest way to use the new exception type is to simply throw it just as you would throw a generic Exception (see Listing 25 ).

Listing 25. Throwing and catching a custom exception
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>";

      }
   }

Notice that even though you pass only the $filepath when you create the exception, the full message is returned: The file, 'c:/sw/temp/NoTooMiLogoQWSQ.png', called on line 41 of C;\sw\xampp\htdocs\WFDocument.php, does not exist. (see Figure 10 ).

Figure 10. Using a custom exception
Screen capture of using a custom exception

Working with multiple exceptions

One reason to create custom exception classes is so you can use PHP's ability to distinguish between them. For example, you can create multiple catch blocks for a single try (see Listing 26 ).

Listing 26. Distinguishing between exceptions
...
   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>";
      }
   }
}

In this case, you attempt to catch problems before they happen by checking for the existence of the file and throwing a NoFileExistsException . If you get past that hurdle and something else keeps you from opening the file, you throw a generic exception. PHP detects which type of exception you throw and executes the appropriate catch block.

All of this might seem a little overboard for simply outputting messages, but there's nothing that says that's all you can do. You can create custom methods for your exception that, for example, send notifications for particular events. You can also create custom catch blocks that perform different actions depending on the situation.

You can also use exceptions to trap for situations that technically are errors, but shouldn't actually stop your program. For example, you might attempt to process an image, and if it's not successful, leave it as it is and move on rather than exiting.

Just because you defined all these different exceptions doesn't mean you have to catch each one individually, as you'll see next.

Propagating exceptions

Another handy feature of inheritance is the ability to treat an object as though it were a member of its base class. For example, you can throw a NoFileExistsException and catch it as a generic Exception (see Listing 27 ).

Listing 27. Combining exception catching
...
   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>";
      }
   }
}

In this case, when you throw the exception, PHP works its way down the list of catch blocks, looking for the first one that applies. Here you have only one, but it will catch any Exception , as you can see by the File 'c:/sw/temp/NoTooMiLogoQWSQ.png' does not exist. message in Figure 11 .

Figure 11. Propagating exceptions
Screen capture of propagating exceptions

把它放在一起

Now that you have the file download process in place, it's time to put everything together and finish off the application. In this section, you will take care of some uncompleted miscellaneous tasks:

  • Detecting administrators
  • Creating the form that enables an administrator to approve files
  • Checking downloads to make sure they're not being called from another server

First you need to set up the administrators who will approve files.

Detecting administrators

When you originally created the users table in the database, you didn't take into consideration the fact that you need to distinguish between regular users and administrators, so you have to take care of that now. Log into MySQL and execute the following commands:

alter table users add status varchar(10) default 'USER';
update users set status = 'USER';
update users set status = 'ADMIN' where id=3;

The first command adds the new column, status , to the users table. You didn't specify the user type on the registration page, so you simply specify a default value of USER for any new users added to the system. The second command sets this status for the existing users. Finally, you choose a user to make into an administrator. (Make sure to use the appropriate id value for your data.)

Now that you have the data, you can create a function that returns the status of the current user. Add this function to scripts.txt as shown in Listing 28 .

Listing 28. Detecting user status
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;
}

To review how this process works, you create a connection to the appropriate database. Then prepare a SQL statement using a parameter for the username. Set that parameter to the actual username, stored in the $_SESSION variable. Next, execute that statement and attempt to get the first (and presumably only) row of data.

You start by defining the $status as NONE . If no row exists, this variable will simply stay as it is. On the other hand, if a row exists, you set the status equal to the value of the status column. Finally, close the connection and return the value.

Approving the file: The form

Now you're ready to add approval capabilities to the form. What you want is to display a check box for pending files if the user viewing the list of files is an administrator. The display_files() function in scripts.txt handles that (see Listing 29 ).

Listing 29. Adding admin functions
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>";
    }

}

Starting at the top, you first determine the user's status. 这很重要,原因有两个。 For one thing, if a user is an administrator, you need to display the approval form. For another, if a user is an administrator, you'll display all files, no matter who uploaded them or what their status is. To do that, you just add another condition to the if statement.

Within the actual file display, if the user is an administrator, and if the file is still pending, you display a pre-checked checkbox. The value of the checkbox is the number of the file, so you can refer to it later. Giving the user an array-like name (in this case toapprove [] tells the web server to expect multiple values for the same field name.

Figure 12 shows the result, a form with the appropriate fields (filename, submitter, file size, status) and check boxes to select for approval.

Figure 12. The approval form
Screen capture of the approval form

Approving the file: Updating the JSON

The actual form page that accepts the approval check boxes, approve_action.php, is very simple (see Listing 30 ).

Listing 30. Processing the approval form
<?php

  session_start();

  include "/scripts.txt";

  $allApprovals = $_POST["toapprove"];
  foreach ($allApprovals as $thisFileNumber) {
     approveFile($thisFileNumber);
  }
  echo "Files approved.";

?>

For each toapprove check box, you simply call the approveFile() function, in scripts.txt (see Listing 31 ).

Listing 31. Approving a form
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);

}

You start by loading the data using json_decode() , just as you do when displaying the files. In this case, you string together a number of different references to set the approvedBy value. The $workflow variable contains all the data. Its fileInfo property is an array that includes all of your files, so the $fileNumber refers to the one that you want. Once you have that, you can set the approvedBy property. Similarly, set the status to approved .

Finally, save the file.

Note that while you did it this way for simplicity's sake, in a production application, it's more efficient to open and load the file just once, make all the changes, then save the file.

Test this out by approving some of the files and redisplaying the files.

Security checks on download

As the last step, you'll need to add a security check to the download process. Because you control this process entirely through the application, you can use whichever checks you want. For this example, you'll check to make sure that the user clicked the link for a file on a page that is on your local server, preventing someone from linking to it from an external site, or even from bookmarking the link or sending someone else a raw link.

You start by creating a new exception, just for this occasion, in the WFDocument.php file (see Listing 32 ).

Listing 32. Disabling remote downloading
<?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>";

      }
   }
}

?>

In the ImproperRequestException , you create a new method, logDownloadAttempt() , that can send an email or perform some other action. You use that method in this exception type's catch block.

In the actual download() function, the first thing you do is get the HTTP_REFERER . This optional header is sent with a web request identifying the page from which the request was made. For example, if you link to developerWorks from your blog, and you click that link, the IBM logs show the URL of your blog as the HTTP_REFERER for that access.

In your case, you want to make sure the request is coming from your application, so you first strip off the "http://" string at the beginning, then save all the text up to the first slash (/). This is the hostname in the request.

For an external request, this hostname might be something along the lines of boxersrevenge.nicholaschase.com, but you're looking for only internal requests, so you accept boxersrevenge or localhost . If the request comes from anywhere else, you throw the ImproperRequestException , which is caught by the appropriate block.

Note that this method is not foolproof as far as security is concerned. Some browsers don't send referrer information properly because either they don't support it or the user has altered what's being sent. But this example should give you an idea of the types of things you can do to help control your content.

摘要

This tutorial wrapped up the three-part series on "Learning PHP," in which you built a simple workflow application. Earlier parts focused on the basics, such as syntax, form handling, database access, file uploading, XML, and JSON. In this part, you took all of that a step further and put it together to create a form through which an administrator can approve various files. We discussed the following topics:

  • Using HTTP authentication
  • Streaming files
  • Creating classes and objects
  • Object properties and methods
  • Using object constructors
  • Using object inheritance
  • Using exceptions
  • Creating custom exceptions
  • Performing additional security checks for downloads

翻译自: https://www.ibm.com/developerworks/opensource/tutorials/os-phptut3/os-phptut3.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值