PHP 零基础初学者手册(四)
十一、向博客条目添加图像
你的博客已经完成了!当然还有改进的空间,但它功能齐全。管理模块可以用来创建博客条目,但是有一些严重的缺陷。
- 如果数据库中有与博客条目相关的评论,它不能删除该条目。
- 它不能给博客文章添加图片,我相信你会想用图片给你的博客文章增添情趣。
- 为了写博客,管理员必须了解 HTML,并不是所有伟大的作家都同样擅长 HTML。
- 管理员可以创建一个没有标题的条目,有效地使用户无法访问博客条目。
我相信你可以想出其他你想添加到管理模块的功能。本章将实现刚刚列出的特性。在此过程中,您将了解以下内容:
- 删除条目和相关注释
- 为您的条目编辑器使用 WYSIWYG 编辑器
- 将图像上传到服务器
- 从服务器中删除图像文件
问题:无法删除带有注释的条目
我相信你会同意评论系统是一个很大的改进。不幸的是,注释也在管理模块中引入了不必要的系统行为。无法删除带有注释的条目。
我想让你在解决问题之前看到它。转到http://localhost/blog/admin.php?page=editor并创建新的博客条目。现在将你的浏览器指向http://localhost/blog/index.php,点击阅读你刚刚创建的条目。通过意见表为新条目添加一两条意见。现在你有了一个带有评论的博客条目。
在你的浏览器中加载http://localhost/blog/admin.php?page=entries并点击你刚刚写的博客条目的标题。博客条目将被加载到您的博客条目编辑器中。点击删除,尝试删除帖子。单击 Delete 应该会给出一条类似于下面所示的错误消息。
Exception: exception 'PDOException' with message 'SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or update a parent row: a foreign key constraint fails
了解外键约束
遇到错误总是有点烦人,但这是一个友好的错误。这确实可以防止您破坏数据库的完整性。Table 11-1 查看我的表,以查看我通过编辑器表单创建的博客条目。
表 11-1。
One Row from the blog_entry Table
| 条目 id | 标题 | 条目 _ 文本 | 创造 |
|---|---|---|---|
| Seventeen | 删除我 | 测试测试 | 2014-03-23 10:26:18 |
我还创建了一个与该特定博客条目相关的评论。表 11-2 显示了注释表中的相应行。
表 11-2。
One Row from the Comment Table
| comment_id | 条目 id | 作者 | 文本文件(textfile) | 日期 |
|---|---|---|---|---|
| four | Seventeen | 托马斯 | 测试注释 | 2014-03-23 10:26:40 |
假设你用entry_id = 17删除了blog_entry。你可能会有一个与不再存在的博客条目相关的评论。注释只有在正确的上下文中才有意义。与comment_id = 4的评论会失去它的上下文;它会失去它的完整性。你可以想象如果用entry_id = 17插入一个新的博客条目会发生什么。该博客条目将带有完全不相关的评论。
外键约束的目的是维护数据完整性。所以,当你试图删除一个有评论的blog_entry时,MySQL 会阻止你,因为删除操作会在你的系统中留下一个叛逆的评论,没有有意义的上下文。只有没有评论的博客条目可以被删除,而不会丢失数据完整性。
删除博客条目前的评论
一旦你看到只有没有评论的博客条目才能被删除而不丧失数据完整性,那么解决方案就很容易看到了:删除博客条目时,你应该先删除任何与该条目相关的评论。您已经有了一个类,它提供了对注释表的单点访问。您可以添加一个新方法来删除与特定entry_id相关的所有注释,如下所示:
//partial code for models/Comment_Table.class.php
//declare new method
public function deleteByEntryId( $id ) {
$sql = "DELETE FROM comment WHERE entry_id = ?";
$data = array( $id );
$statement = $this->makeStatement( $sql, $data );
}
应该在删除博客条目之前调用此方法。博客条目从Blog_Entry_Table类中删除。一个解决方案是让Blog_Entry_Table类使用Comment_Table类,如下所示:
//partial code for models/Blog_Entry_Table.class.php
//edit existing method
public function deleteEntry ( $id ) {
//new code: delete any comments before deleting entry
$this->deleteCommentsByID( $id );
$sql = "DELETE FROM blog_entry WHERE entry_id = ?";
$data = array( $id );
$statement = $this->makeStatement( $sql, $data );
}
//new code: declare a new private method inside Blog_Entry_Table.class.php
private function deleteCommentsByID( $id ) {
include_once "models/Comment_Table.class.php";
//create a Comment_Table object
$comments = new Comment_Table( $this->db );
//delete any comments before deleting entry
$comments->deleteByEntryId( $id );
}
前面的代码完成后,您就可以开始测试了。您应该能够通过编辑器删除任何博客条目。如果有任何与blog_entry相关的注释,这些注释将首先被删除,以避免违反外键约束。
利用所见即所得提高可用性
所见即所得的缩写。有了 WYSIWYG HTML 编辑器,对 HTML 知之甚少或一无所知的管理员可以编写以编程方式标记为 HTML 的博客条目。所见即所得编辑器看起来很像任何其他文字处理软件,但它可以将文本保存为 HTML,而不是保存为.doc或.odf文件。
集成 TinyMCE
TinyMCE 是一个流行的开源所见即所得编辑器。它集成在许多流行的 CMS 系统中,如 Drupal、Joomla 和 WordPress。很快,它将集成到您的条目编辑器中,这将是一个非常棒的改进。
集成 TinyMCE 相当容易。第一步,从 www.tinymce.com/download/download.php 下载。解压缩下载的文件,并在现有的js文件夹中保存一份tinymce文件夹的副本。查看一下js/tinymce内部,确认您可以找到主 JavaScript 文件tinymce.min.js。这就是你的admin.php应该使用的 JavaScript 文件。
TinyMCE JavaScript 和 CSS 文件可以将普通的<textarea>元素变成所见即所得编辑器。你所要做的就是初始化 TinyMCE 并把它指向你的文本区域。您可以从views/admin/editor-html.php开始,如下所示:
//partial code for views/admin/editor-html.php
//notice the new <script> elements added at the end
return "
<form method='post' action='admin.php?page=editor' id='editor'>
<input type='hidden' name='entry_id'
value='$entryData->entry_id' />
<fieldset>
<legend>New Entry Submission</legend>
<label>Title</label>
<input type='text' name='title' maxlength='150'
value='$entryData->title' />
<label>Entry</label>
<textarea name='entry'>$entryData->entry_text</textarea>
<fieldset id='editor-buttons'>
<input type='submit' name='action' value='save' />
<input type='submit' name='action' value='delete' />
<p id='editor-message'>$entryData->message</p>
</fieldset>
</fieldset>
</form>
<script type='text/javascript' src='js/tinymce/tinymce.min.js'> </script>
<script type='text/javascript'>
tinymce.init({
selector: 'textarea',
plugins: 'image'
});
</script>";
看看这两个<script>元素。第一个嵌入了主 TinyMCE JavaScript 文件。第二个函数初始化 TinyMCE 并用几个参数对它进行配置。
selector参数表示 TinyMCE 应该将页面上的每个<textarea>元素转换成花哨的 WYSIWYG 编辑器。在这个特殊的页面上,只有一个<textarea>元素。
plugins参数指示应该为编辑器激活哪些 TinyMCE 插件。图像插件将允许用户在博客条目中插入图像。
大多数时候,你不应该在你生成的 HTML 中散布小的<script>元素。您在代码中保留的<script>元素越多,您就越难追踪 JavaScript 错误。但是在这种情况下,我会接受这种不适,因为我只希望 TinyMCE 编辑器出现在这个特定的页面上。我特别不希望 TinyMCE 控制我博客系统中的任何其他<textarea>元素。要看 TinyMCE 长什么样,见图 11-1 。您也可以将浏览器导航至http://localhost/blog/admin.php?page=editor。
图 11-1。
The TinyMCE WYSIWYG editor
在图 11-1 中,我稍微修改了一下我的 CSS,使编辑器<form>的宽度为 625 像素。我在css/blog.css做了改动。
创建图像管理器
此时,admin模块有两个不同的页面视图:条目列表和条目编辑器。您可以创建第三个页面来上传和删除您将在博客条目中使用的图像。您可以从创建图像管理器的菜单项开始。更新views/admin/admin-navigation.php中的代码,如下所示:
<?php
//complete code for views/admin/admin-navigation.php
//notice item added for image manager
return "
<nav id='admin-navigation'>
<a href='admin.php?page=entries'>All entries</a>
<a href='admin.php?page=editor'>Editor</a>
<a href='admin.php?page=images'>Image manager</a>
</nav>";
参见href中的图像管理器菜单项。单击该项将对一个名为page的 URL 变量进行编码,并将其值设置为images。由于你在admin.php中编写前端控制器的方式,你可能会猜到下一步:你需要一个新的控制器脚本,名为images.php。另外,在controllers/admin中保存也很重要。这样,每当点击菜单项图像管理器时,控制器将从admin.php自动加载。像往常一样,从一小步开始,在错误仍然容易纠正的时候捕捉它们。在controllers/admin/images.php中创建一个新文件。
<?php
//complete code for controllers/admin/images.php
$imageManagerHTML = "Image manager coming soon!";
return $imageManagerHTML;
在您的浏览器中加载http://localhost/blog/admin.php?page=images,您应该会看到确认您的脚本按预期运行的输出。
Image manager coming soon!
显示用于上传图像的表单
既然已经设置了 image manager 控制器脚本,就可以继续创建和输出 image manager 视图了。让我们从一个基本的 HTML 表单开始,您最终可以用它来上传图像。创建一个新的 php 文件,并将其另存为views/admin/images-html.php:
<?php
//complete code for views/admin/images-html.php
if ( isset( $uploadMessage ) === false ){
$uploadMessage = "Upload a new image";
}
return "
<form method='post' action='admin.php?page=images'
enctype='multipart/form-data'>
<p>$uploadMessage</p>
<input type='file' name='image-data' accept='image/jpeg' />
<input type='submit' name='new-image' value='upload' />
</form>
";
您可以看到,views代码准备了一个占位符,用于向用户显示上传消息。默认上传消息是上传新图像。很快,你的系统就会让用户知道上传是否成功。但是在你开始之前,我想重复一下上传的基本知识。
要允许用户上传图像等文件,您需要一个 HTML 表单。您必须使用 HTTP 方法POST,并且必须显式声明表单的编码类型,以允许文件上传。默认情况下,HTML 表单被设置为application/x-www-form-urlencoded。然而,这在你上传文件的时候是行不通的。相反,您必须将表单的 enctype 设置为multipart/form-data,它将接受文件和普通 URL 编码的表单数据。
带有type=file的<input>元素将创建一个文件选择器,允许用户浏览他们的本地计算机来上传文件。这个特殊的文件选择器有一个accept属性,限制可以选择的文件类型。这个文件选择器将只接受 JPEG 图像。请记住,客户端验证可以提高可用性,但不能提高安全性。可以相信恶意用户能够绕过任何类型的客户端验证。为了保护您的系统免受攻击,您必须实现服务器端验证。回头给你点提示。首先,更新 image manager 控制器,以便显示上传表单。更改controllers/admin/images.php中的代码:
<?php
//complete code for controllers/admin/images.php
$imageManagerHTML = include_once "views/admin/images-html.php";
return $imageManagerHTML;
保存并将浏览器指向http://localhost/blog/admin.php?page=images,以确认表单确实显示在您的浏览器中。
$_FILES 超全局阵列的快速复习
你在第四章中了解到了$_FILES超级地球,但是在继续之前回顾一下它的作用可能会有所帮助。每当一个文件通过 HTML 表单上传时,该文件被存储在临时内存中,并且关于该文件的信息在$_FILES超级全局变量中传递。你可以通过查看传递给你的images控制器的内容来了解这一点。将此代码添加到controllers/admin/images.php的顶部,如下所示:
<?php
//complete code for controllers/admin/images.php
$imageSubmitted = isset( $_POST['new-image'] );
if ( $imageSubmitted ) {
$testOutput = "<pre>";
$testOutput .=print_r($_FILES, true);
$testOutput .= "</pre>";
return $testOutput;
}
$imageManagerHTML = include_once "views/admin/images-html.php";
return $imageManagerHTML;
您可以通过在浏览器中加载http://localhost/blog/admin.php?page=images来测试您的代码。使用该表单选择要上传的图像。您的代码不会将上传的文件保存在您的服务器上,但是您可以从下面的输出中看到 PHP 可以访问该文件:
Array (
[image-data] => Array (
[name] => alberte-lea.jpg
[type] => image/jpeg
[tmp_name] => /Applications/XAMPP/xamppfiles/temp/phprDui5l
[error] => 0
[size] => 119090
)
)
你可以看到$_FILES是一个Array。第一个Array在命名索引image-data下持有另一个Array。默认情况下,$_FILES将提供关于文件名、类型、tmp_name、error和size的信息。但是你必须问自己一个重要的问题是,image-data这个名字是从哪里来的?
答案是你提供的!这里的image-data是因为您为文件选择器元素编写的 name 属性。如果您在views/admin/images-html.php中查看您的代码,您可以找到您设置文件选择器的 name 属性的地方:
//one line of code from views/admin/images-html.php
<input type=``'file' name='image-data'
因为您将image-data设置为文件选择器的 name 属性,PHP 可以在嵌套在$_FILES中的数组中找到相关的文件数据,该数组位于命名索引image-data下。
嵌套数组保存了您试图用这个特定的文件选择器上传的图像的信息。您可以看到原始图像名称及其 mimetype。您可以看到图像数据被临时保存在您的服务器上,临时名称为tmp_name。可以看到错误代码0,表示上传过程中没有出现错误。您还可以看到图像文件的size,以字节为单位。
如果您在浏览器中看到类似的内容,您只需一两行代码就可以上传图片了。你所要做的就是把文件数据保存在你的服务器上。文件数据已经上传并临时保存在tmp_name下image-data数组内$_FILES内。要获取文件数据,您只需按照下面的方式编写一些内容:
//don't write this anywhere...yet
$fileData = $_FILES['image-data']['tmp_name'];
注意你的代码是如何访问$_FILES来找到数组image-data的。在image-data内部,PHP 找到了tmp_name。要保存文件数据,只需将其从临时位置移走,并以一个名称保存在目标文件夹中。
但是我建议你不要在controllers/admin/images.php中这样做,主要是因为你想写代码来处理上传过程中可能出现的一些错误。在未来的某一天,您将需要再次通过表单上传文件。因此,您可以编写一个可重用的类来上传,而不是每次需要时都重新发明一个解决方案。这样,您可以在许多项目中重用您的上传代码,而无需更改它。
想起来了,你已经在第四章的中写了一个Uploader类。你可以照原样使用它。从简单地重用Uploader开始。在这个过程中,我会指出进一步改进Uploader的方法。在XAMPP/htdocs/ch4/classes/Uploader.class.php中复制一个Uploader类,并将副本保存在XAMPP/htdocs/blog/models/Uploader.class.php中。或者,如果你从本书的配套网站 www.apress.com 下载第四章的源代码,你也可以获得Uploader类。
上传图像
您可以尝试使用Uploader类上传图像。首先为图像创建一个新文件夹。我在项目的根目录下创建了一个名为img的文件夹。我会上传图片到img文件夹。
提交图像管理器上传表单时,您的代码应该尝试上传指定的文件。提交表单是一个用户交互,所以代码属于控制器,在controllers/admin/images.php中:
<?php
//complete code for controllers/admin/images.php
//new code: include Uploader class definition
include_once "models/Uploader.class.php";
$imageSubmitted = isset( $_POST['new-image'] );
//if the upload form was submitted
if ( $imageSubmitted ) {
//new code below
//create an Uploader object
$uploader = new Uploader( 'image-data' );
//indicate destination folder on server
//please check that you have an img folder in your project folder
$uploader->saveIn( "img" );
$uploader->save();
$uploadMessage = "file probably uploaded!";
//end of new code
}
$imageManagerHTML = include_once "views/admin/images-html.php";
return $imageManagerHTML;
一切都准备好上传了。您可以通过将浏览器指向http://localhost/blog/admin.php?page=images来测试您的代码。现在,尝试通过表单上传一张图片。您的代码现在应该能够上传文件了,所以您应该能够在img文件夹中找到您上传的图像。
此时遇到的最常见的错误与文件夹权限有关。当您在像 XAMPP 这样的本地 web 服务器上开发时,这种问题尤其常见。如果你的目标文件夹是写保护的,PHP 不能保存上传文件。因此,如果您无法上传文件,请尝试更改img文件夹的文件夹权限。将权限设置更改为read & write。
会出什么问题呢?
当您尝试通过表单上传文件时,可能会出现一些问题。有八个可能的错误代码与$_FILES相关,其中一个错误代码表示没有错误。实际上,在上传过程中潜伏着七个以上的潜在问题。如果当你遇到这些错误时,你的整个博客系统暂时崩溃,那将是一个遗憾。系统可以在不崩溃的情况下处理错误。是时候学习如何编写优雅地失败的代码了。
更新上传程序类
您可以更改Uploader类,这样它会正常地失败,并在失败时提供有意义的错误消息。为此,您需要一个属性来存储错误消息,另一个属性来存储遇到的任何标准 PHP 错误代码。在models/Uploader.class.php中添加几个属性。
//partial code for models/Uploader.class.php
//edit existing Uploader class
class Uploader {
private $filename;
private $fileData;
private $destination;
//new code: add a property for an error message
private $errorMessage;
//new code: add a property for standard PHP error codes
private $errorCode;
查看代码,注意新属性没有赋值。每当您的代码准备尝试上载时,您都必须为这些属性分配实际值。$_FILES数组将立即提供一个错误代码。在Uploader的构造函数方法中获取当前的错误代码是一个显而易见的选择,因为您知道构造函数将只运行一次,就像一个新的Uploader对象被创建一样:
//partial code for models/Uploader.class.php
//edit existing constructor method
public function __construct( $key ) {
$this->filename = $_FILES[$key]['name'];
$this->fileData = $_FILES[$key]['tmp_name'];
//new code: remember the current upload error code
$this->errorCode = ( $_FILES[$key]['error'] );
}
我怀疑你已经在第四章中读到了大部分代码,就像你输入的一样。我也强烈怀疑你没有完全理解每一个小细节。如果你像大多数学习者一样,你将不得不允许你对代码的理解在一小步一小步中增长。每一小步,你都会学到东西,总会有你没学到的东西。通过重复已经学过的课程,你可以逐渐形成更全面的理解。您可以仔细看看在Uploader类中使用的$key参数。请记住,要获取文件数据,您可以编写如下内容:
$fileData = $_FILES['image-data']['tmp_name'];
tmp_name是由$_FILES提供的默认名称,image-data是在设置 HTML 文件选择器输入元素的name属性时提供的名称。您希望您的Uploader类能够上传文件,而不管文件选择器的名称属性。总是依赖image-data将是一个糟糕的设计决策。所以,我为你做的设计决定是,Uploader类应该被赋予 used name属性作为构造函数方法的参数。就是$key的说法。这里有一个例子来告诉你这个想法:
//Example: don't write any of this code anywhere
//this would upload from <input type='file' name='image-data' />
$imgUploader = new Uploader("image-data");
//this would upload from <input type='file' name='video-file' />
$vidUploader = new Uploader("video-file");
您可能会看到,因为相关的 name 属性是作为参数提供的,Uploader类更容易在不同的情况下重用。
错误:限制性文件夹权限
当您在像 XAMPP 这样的本地 web 服务器上进行开发时,经常会遇到文件夹权限过于严格的情况。如果一个文件夹是只读的,PHP 不能将上传图像的文件数据写入该文件夹。它不属于通过$_FILES错误代码报告的标准错误。但是测试一个目标文件夹是否可写是非常简单的。你已经在第四章中做过了,但是我想稍微修改一下代码。您可以通过在Uploader中声明一个新方法来检查问题,如下所示:
//partial code for models/Uploader.class.php
//declare a new private method in the Uploader class
private function readyToUpload(){
$folderIsWriteAble = is_writable( $this->destination );
if( $folderIsWriteAble === false ){
//provide a meaningful error message
$this->errorMessage = "Error: destination folder is ";
$this->errorMessage .= "not writable, change permissions";
//indicate that code is NOT ready to upload file
$canUpload = false;
} else {
//assume no other errors - indicate we're ready to upload
$canUpload = true;
}
return $canUpload ;
}
和往常一样,方法在被显式调用之前不会运行。因此,您应该调用这个新方法,就像代码试图保存上传文件一样。您应该完全重写models/Uploader.class.php中的现有方法save(),如下所示:
//partial code for models/Uploader.class.php
// rewrite existing method save() completely
public function save () {
//call the new method to look for upload errors
//if it returns TRUE, save the uploaded file
if ( $this->readyToUpload() ) {
move_uploaded_file(
$this->fileData,
"$this->destination/$this->filename" );
} else {
//if not create an exception - pass error message as argument
$exc = new Exception( $this->errorMessage );
//throw the exception
throw $exc;
}
}
本地 PHP 函数move_uploaded_file()将上传的文件数据保存在服务器上的一个新的目的地。这是一个敏感的过程,只有在没有潜在错误等待发生的情况下,您的代码才应该尝试这样做。
还记得在用 PDO 建立到数据库的连接时,您是如何使用try-catch语句的吗?我怀疑你发现尝试一些可能出错的东西,然后捕捉任何异常有点抽象。什么是例外呢?一个例外是由 PHP 的原生Exception类生成的 PHP 对象。当可能出错的事情确实出错时,PHP 会抛出一个异常。
打个比喻,你可以把一个异常想象成一封有坏消息的信。抛出异常就像是在途中发送信件一样。要阅读坏消息,你必须收到这封信。异常也是这样,但是它们是被捕获的,而不是被发送到邮箱。
抛出异常与触发错误非常相似,但有一个显著的区别:异常很容易被捕获,因此您的代码可以继续运行,并且您的代码可以优雅地解决问题。当您调用一个可能抛出异常的方法时,您可以将该方法调用包装在一个try-catch语句中,从而编写一个优雅地失败的系统。
Note
也可以拦截普通的 PHP 错误。捕捉异常是面向对象的方法。
你就快到了。您的Uploader类检查上传文件是否太大,如果太大,就会抛出一个异常。要创建一个正常失败的图像管理器,您必须编写代码来捕捉任何可能抛出的异常。您可以从controllers/admin/images.php开始,如下所示:
//partial code for controllers/admin/images.php
//edit existing if-statement
if ( $imageSubmitted ) {
$uploader = new Uploader( 'image-data' );
$uploader->saveIn( "img" );
//try to save the upload file
try {
$uploader->save();
//create an upload message that confirms succesful upload
$uploadMessage = "file uploaded!";
//catch any exception thrown
} catch ( Exception $exception ) {
//use the exception to create an upload message
$uploadMessage = $exception->getMessage();
}
}
该测试了,但首先,你要确定有问题。您可以更改img文件夹的文件夹权限。制作文件夹Read-only。现在,在你的浏览器中加载http://localhost/blog/admin.php?page=images,并尝试通过你的表单上传一个图像文件。
图像不应该被上传,您也不应该看到任何标准的 PHP 错误消息。您应该会看到一条从 PHP 脚本发出的消息。
Error: destination folder is not writable, change permissions
一旦您确认您的代码发现了文件夹权限问题并提供了一条错误消息,您可以将文件夹权限改回Read & Write,这样您就可以再次上传图像。
错误:上传文件太大
您已经成功构建了一个图像管理器,它可以正常失败,但前提是目标文件夹不可写。文件上传过程中还潜伏着许多其他潜在的问题。一个是 PHP 配置了最大文件上传大小。因为上传有最大文件大小限制,一些用户可能会尝试上传太大的文件。凡是能破的,用户一定会破。您的系统应该为此类用户提供有意义的错误消息。
对一个上传错误的测试涉及到几个不同文件中相当多的代码更改。测试多一个上传错误是非常容易的。只需在私有的readyToUpload方法中添加一个额外的else-if块:
//partial code for models/Uploader.class.php
//edit existing method in the Uploader class
private function readyToUpload(){
$folderIsWriteAble = is_writable( $this->destination );
if( $folderIsWriteAble === false ){
$this->errorMessage = "Error: destination folder is ";
$this->errorMessage .= "not writable, change permissions";
$canUpload = false;
//new code: add an else-if code block to test for error code 1
} else if ( $this->errorCode === 1 ) {
$maxSize = ini_get( 'upload_max_filesize' );
$this->errorMessage = "Error: File is too big. ";
$this->errorMessage .= "Max file size is $maxSize";
$canUpload = false;
//end of new code
} else {
//assume there are no other errors
//indicate that we're ready to upload
$canUpload = true;
}
return $canUpload ;
}
现在,您的代码可以进行测试了。你应该有一个系统,如果上传一个太大的文件有问题,它会优雅地失败。为了测试您的工作,您应该尝试上传一个大于最大上传文件大小的文件。嗯,我的最大上传文件大小是多少?我几乎能听到你的问题。我很高兴你问了这个问题,因为这给了我一个写更多关于 PHP 配置的借口。
要查看您的 PHP 是如何配置的,您可以创建一个新的 PHP 文件,例如,在您的 blog 文件夹中。称之为test.php:
<?
//complete source code for test.php
phpinfo();
保存文件并在浏览器中加载http://localhost/blog/test.php。您可以看到很多关于您的 PHP 安装的信息——我的意思是,很多!
我发现我的安装有一个 128MB 的upload_max_filesize。此设置声明了上传文件的上限。
为了测试当用户试图上传太大的文件时,我的Uploader类是否会提供错误消息,我可以尝试上传大于 128MB 的 JPEG 图像!我没有那么大的 JPEG 图片。我改改upload_max_filesize吧。
通过 ini.php 或配置。文件
当试图配置 PHP 时,您有几个选择。如果您拥有自己的服务器,您可以直接在 PHP 配置文件中更改upload_max_filesize。它叫做ini.php,phpinfo()的输出可以告诉你ini.php保存在你电脑的什么地方,如果你在Loaded Configuration File下找的话。您可以使用 Komodo Edit 或任何其他文本编辑器打开ini.php,并手动更改upload_max_filesize。一旦保存了更改,就应该重启 Apache 服务器。
当你在互联网上发布你的 PHP 项目时,你通常会使用一个共享的托管解决方案。你将通过一些虚拟主机服务提供商购买域名。它既便宜又方便,但是您可能无法访问ini.php文件。大多数网络主机都允许你通过一个所谓的.htaccess文件进行一些配置,你不妨马上熟悉一下.htaccess。幸运的是,更换upload_max_filesize是一项非常简单的任务。
用 Komodo Edit 或你使用的任何编辑器创建一个新文件。将新文件另存为.htaccess并保存在blog文件夹中。注意,完整的文件名不应该是.htaccess.txt或.htaccess.php。文件名只能是.htaccess。下面是.htaccess文件的完整内容:
php_value upload_max_filesize 1M
保存后,您可以在浏览器中重新加载http://localhost/blog/test.php。你应该看到local values for upload_max_filesize现在是 1M(意思是 1MB)。Master Values仍然是 128 米,但是对于你的博客文件夹和子文件夹,它现在改为 1 米。
Note
你可以在 http://htaccess-guide.com 了解更多关于.htaccess的事情。使用.htaccess来限制对文件夹的访问是很常见的。另一个常见的任务是动态重写 URL,以提供搜索引擎友好的 URL。
现在,测试您的图像管理器的上传表单将变得更加容易。找一个大于 1MB 的 JPEG 应该不难。试着通过你的表单上传这样一个文件,你会发现文件不会上传。您应该会看到一条错误消息,告诉您上传文件太大。现在你已经测试了你的系统,并确认它正常地失败了,你可以把upload_max_filesize改回你认为适合你的博客的值。
检测其他错误
你知道$_FILES超级全局天生支持七种不同的错误。您只是在测试其中一个问题:Upload文件太大。
Note
您可以在 www.php.net/manual/en/features.file-upload.errors.php 了解更多错误代码及其含义。
我不打算详细讲述你在上传文件时可能遇到的其他类型的错误。但是我将向您展示如何提供一个默认的错误消息,以防其余六个错误中的任何一个发生。这和你想象的一样简单。您只需在您的readyToUpload()方法中添加一个额外的else-if代码块,如下所示:
//partial code for models/Uploader.class.php
//edit existing function
private function readyToUpload(){
$folderIsWriteAble = is_writable( $this->destination );
if( $folderIsWriteAble === false ){
$this->errorMessage = "Error: destination folder is ";
$this->errorMessage .= "not writable, change permissions";
$canUpload = false;
} else if ( $this->errorCode === 1 ) {
$maxSize = ini_get( 'upload_max_filesize' );
$this->errorMessage = "Error: File is too big. ";
$this->errorMessage .= "Max file size is $maxSize";
$canUpload = false;
//new code: add code block for other errors
//the error codes have values 1 to 8
//we already check for code 1
//if error code is greater than one, some other error occurred
} else if ( $this->errorCode > 1 ) {
$this->errorMessage = "Something went wrong! ";
$this->errorMessage .= "Error code: $this->errorCode";
$canUpload = false;
//end of new code
//if error code is none of the above
//we can safely assume no error occurred
} else {
//indicate that we're ready to upload
$canUpload = true;
}
return $canUpload;
}
太棒了!您的代码现在检查两个常见问题,并为这些问题提供自定义错误信息。此时,您的系统甚至应该提供一个默认的错误消息,以防发生任何其他错误。您可以通过强制一个错误来测试是否捕捉到这样的错误。浏览浏览器至http://localhost/blog/admin.php?page=images,点击上传按钮,无需选择要上传的文件。您的代码将尝试不上传任何内容,这将导致错误 4:没有文件上传。
如果您看到该错误消息,则表明您已经确认,当发生一个典型的文件上传错误时,您的系统会提供默认的错误消息。
进一步的改进
您应该能够进一步改进错误消息。您可以简单地检查剩余的错误代码,并为它们提供定制的错误消息。从几个方面来说,这对你来说都是一次极好的学习锻炼。
- 您必须了解其余错误代码的含义。
- 你必须学会如何触发这些错误。
- 您应该考虑如何向用户传达有意义的错误消息。
我希望你能花时间做这件事。您还可以考虑自己实现其他一些改进。你可以看看图片上传的一个不幸的特征。假设你上传了一张图片test.jpg。现在试着上传一个不同的图片,名字也是test.jpg。结果将是第一幅图像被下一幅图像覆盖。
如果您能在上传前检查名称冲突,这将是一个显著的改进。也许你的系统应该抛出一个异常,提示用户在上传之前重命名图像?或者您甚至可以更改上传表单,允许用户通过表单重命名图像?
另一个显著的改进是,如果用户试图上传一个不属于类型image/jpeg的文件,就会抛出一个异常。您已经有了客户端验证,但是恶意用户可以很容易地绕过它。实现文件类型的服务器端验证怎么样?您需要您的代码将上传文件的 mimetype 与一个或多个可接受的 mime type 进行比较。PHP 可以在$_FILES超全局中找到上传文件的 mimetype。
显示图像
您有一个允许上传图像的图像管理器,并且在条目编辑器中集成了 TinyMCE 富文本编辑器。博客管理员应该可以看到所有可用的图像,并在博客条目中使用其中的任何一个。
要使用 TinyMCE 在博客条目中嵌入图像,您必须知道图像文件的路径。是时候更新图像管理器了,所以它显示了所有上传图像的缩略图以及每个图像的路径。这样,博客管理员可以相对容易地将路径复制到给定的图像,并将其粘贴到 TinyMCE 中,以便在博客条目中使用图像。列出所有的图片将会非常非常类似于你在第四章中为图片库编写的代码。
您可以创建一个<dl>元素来列出所有图像。你可以使用一个DirectoryIterator对象来遍历img文件夹并找到所有的 JPEG 图片。每个图像应该显示为一个<img>元素。在图片的正下方,你应该显示图片的路径。如果图片能再删除就好了。所以,你最好提供一个删除每张图片的链接。更新views/admin/images-html.php中的代码,如下所示:
<?php
//complete source code for views/admin/images-html.php
if ( isset( $uploadMessage ) === false ){
$uploadMessage = "Upload a new image";
}
//new code starts here
//declare a variable to hold HTML for all your images
$imagesAsHTML = "<h1>Images</h1>";
$imagesAsHTML .= "<dl id='images'>";
$folder = "img";
$filesInFolder = new DirectoryIterator( $folder);
//loop through all files in img folder
while ( $filesInFolder->valid() ) {
$file = $filesInFolder->current();
$filename = $file->getFilename();
$src = "$folder/$filename";
$fileInfo = new Finfo( FILEINFO_MIME_TYPE );
$mimeType = $fileInfo->file( $src );
//if file is a jpg...
if ( $mimeType === 'image/jpeg' ) {
//display image and its src
$href = "admin.php?page=images&delete-image=$src";
$imagesAsHTML .= "<dt><img src='$src' /></dt>
</dd>Source: $src <a href='$href'>delete</a></dd>";
}
$filesInFolder->next();
}
$imagesAsHTML .= "</dl>";
//notice that $imagesAsHTML is added at the end of the returned HTML
return "
<form method='post' action='admin.php?page=images' enctype='multipart/form-data'>
<p>$uploadMessage</p>
<input type='file' name='image-data' accept='image/jpeg' />
<input type='submit' name='new-image' value='upload' />
</form>
<div>
$imagesAsHTML
</div>";
//end of changes
查看一下http://localhost/blog/admin.php?page=images,看看img文件夹中的所有图像是如何显示的,每个图像都有一个路径和一个删除链接。
删除图像
此时,图像管理器会以原始大小列出所有图像。它不是很漂亮,但它足以让你使用图像管理器浏览图像。你可以很容易地写一些 CSS 来提高美感。你甚至可以在第五章中重用图片库中的一些 JavaScript 想法。当你读完这一章时,请考虑这样做。在你开始研究美学之前,我还想让你做一些功能上的增强。首先:删除图像。
单击链接删除文件是一种用户交互。因此,响应点击的代码属于控制器。有人可能会说,实际删除文件的代码属于模型脚本。但这只是一行代码,所以我将全部保存在控制器中,在controllers/admin/images.php中:
<?php
//complete code for controllers/admin/images.php
include_once "models/Uploader.class.php";
$imageSubmitted = isset( $_POST['new-image'] );
if ( $imageSubmitted ) {
$uploader = new Uploader( 'image-data' );
$uploader->saveIn( "img" );
try{
$uploader->save();
$uploadMessage = "file uploaded!";
} catch ( Exception $exception ) {
$uploadMessage = $exception->getMessage();
}
}
//new code starts here: if a delete link was clicked...
$deleteImage = isset( $_GET['delete-image'] );
if ( $deleteImage ) {
//grab the src of the image to delete
$whichImage = $_GET['delete-image'];
unlink($whichImage);
}
//end of new code
$imageManagerHTML = include_once "views/admin/images-html.php";
return $imageManagerHTML;
现在,您应该能够使用图像管理器删除图像了。先尝试一下,然后你可以继续读下去,了解它是如何发生的。
删除文件的原生 PHP 函数是unlink。要删除一个文件,需要将文件的路径和名称作为参数传递给unlink()。理解 PHP 如何找到图像的路径和名称是很重要的。如果查看代码,可以看到 PHP 从一个 URL 变量delete-image中获取了要删除的图像源,这个变量是用$_GET方法检索的。下一个要思考的问题是,图像的来源是如何被编码成 URL 变量的?
您可以在浏览器中查看图像管理器的 HTML 源代码。您会看到每个删除链接都编码了一个保存图像源的 URL 变量。如果您仔细观察一下href属性,就可以看到它。它们大概如下所示:
<a href='``admin.php?page=images&delete-image=img/coffee.jpg
delete
</a>
看看一个 URL 变量page如何被设置为images,另一个 URL 变量delete-image如何被设置为img/coffee.jpg。因此,点击这个删除链接将加载images控制器并删除在img/coffee.jpg中找到的图像。
要考虑的最后一个问题是,在 PHP 代码的什么地方创建了删除链接?你有一个选择:要么你思考这个问题并浏览代码来找到你自己的答案,要么你继续阅读来看我的答案。
删除链接在views/admin/images-html.php中创建。在while循环中,您可以找到创建 HTML 的 PHP,用于显示一个图像、它的源和一个删除链接:
//partial code for views/admin/images-html.php
//make no changes to any code
$file = $filesInFolder->current();
$filename = $file->getFilename();
//$src holds the relative path to the file
$src = "$folder/$filename";
$fileInfo = new Finfo( FILEINFO_MIME_TYPE );
$mimeType = $fileInfo->file( $src );
if ( $mimeType === 'image/jpeg' ) {
//href for delete link created below
$href = "admin.php?page=images``&
$imagesAsHTML .= "<dt><img src='$src' /></dt>
</dd>Source: $src
<a href='``$href
</dd>";
给你。变量$src保存了一个文件 PHP 的相对路径,该路径是通过用一个DirectoryIterator对象遍历一个文件夹找到的。如果找到的文件是 JPEG 图像,它将显示为 HTML。注意如何使用$src为删除链接创建一个href属性。
在博客条目中使用图像
列出了所有的图片后,在你的博客条目中嵌入图片应该是一件简单的事情。在http://localhost/blog/admin.php?page=images查看您的所有图像,找到您想要使用的图像。请注意每个图像的源是如何在浏览器中显示的。选择并复制一些你想在博客文章中使用的图片的来源。现在,将现有的博客条目加载到您的博客编辑器中。请注意 TinyMCE 是如何提供插入图像的按钮的。点击图像按钮,弹出 TinyMCE 的图像对话框,如图 11-2 所示。通过将图像src粘贴到源字段来嵌入图像。
图 11-2。
The image dialog pop-up in TinyMCE
一旦你在博客条目中嵌入了图像,你应该保存它,然后在你的浏览器中加载http://localhost/blog/index.php,以检查图像是否确实显示在博客条目中。
提高编辑器的可用性
现在你的博客有了一个了不起的管理模块。通过管理模块,您可以创建、更新和删除博客条目。在我的指导下不会添加更多的功能。
条目编辑器有一个可用性缺陷,我想指出并改进它。将一个现有的条目加载到条目编辑器中,稍微更改条目,然后保存更改。这将在您的条目编辑器中重新加载已保存的更改,您应该会看到一条提示条目已保存的消息,清楚地告诉您在编辑器中看到的内容已保存在数据库中。
但是,如果您将条目更改得更多一点,消息仍然会坚持说条目已保存,即使您我都知道这是误导。在您单击“保存”按钮之前,不会保存更改。这是糟糕的可用性!来自系统的误导性反馈可能比没有反馈更糟糕。你如何补救这个问题?这是一个调用 PHP 或 JavaScript 的任务吗?在你继续读下去之前,花一分钟思考一下。
这是一个绝对需要 JavaScript 的任务!PHP 是一种服务器端脚本语言。每次联系服务器上的 PHP 脚本时,它都会运行一次。如果请求寻找 PHP 资源,PHP 将为每个 HTTP 请求运行一次。您的条目编辑器只是一个 HTML 表单,带有一个用 JavaScript 和 CSS 增强的<textarea>元素。PHP 只会在你提交表单时运行。在<textarea>中输入几个字符不会提交表单。
JavaScript 是一种客户端脚本语言。它在浏览器中运行得非常好。您已经看到了当某些浏览器事件发生时如何调用 JavaScript 函数。到目前为止,您已经了解了当 HTML DOM 内容被加载到浏览器中、当用户单击以及当表单被提交时,如何调用 JavaScript 函数。每当用户按下键盘上的某个键时,JavaScript 也会做出响应。您可以在这里使用该事件。如果用户在<input name='title'>处于焦点时按下一个键,你就知道标题被更改了。如果标题已更改,但表单尚未提交,您知道更改尚未保存在数据库中。打开您的editor.js JavaScript 文件,并声明一个用于更新更新消息的新函数。在这个过程中,添加一个事件侦听器,以便在用户每次更改标题时调用这个新函数,如下所示:
//partial code for js/editor.js
//new code: declare a new function
function updateEditorMessage () {
console.log( "editor changes not saved yet!" );
}
//edit existing function
function init(){
var editorForm = document.querySelector("form#editor");
var title = document.querySelector("input[name='title']");
title.required = false;
//code changes start here
title.addEventListener("keyup", updateEditorMessage, false);
//end of changes
editorForm.addEventListener("submit", checkTitle, false);
}
updateEditorMessage()函数还没有做任何有意义的事情。它只是向控制台输出一条消息。但是你可以用它来测试你的进步。打开您的浏览器及其 JavaScript 控制台。如果你用的是 Chrome,你可以用 Cmd+Alt+J 打开控制台,如果你用的是 Mac,或者 Ctrl+Alt+J,如果你用的是 Windows。
一旦浏览器的 JavaScript 控制台打开,您就可以将浏览器导航到http://localhost/blog/admin.php?page=entries。单击一个条目,将其加载到您的博客条目编辑器中。编辑条目并保存更改。注意,通常的条目是由 PHP 创建的保存消息。现在,对条目标题做一点修改,注意控制台中的输出:JavaScript 注意到发生了一些事情。还要注意,条目已保存的消息现在变得具有误导性(见图 11-3 ):条目中最近的更改没有保存!
Note
如果你在控制台中看不到预期的输出,可能是因为 Google Chrome 保存了你之前 JavaScript 文件的缓存版本。换句话说,谷歌 Chrome 还没有注意到你已经更新了你的 JavaScript。你只需打开“清除浏览数据”对话框(Mac: Cmd+Shift+Delete,Windows: Ctrl+Shift+Delete),清除“缓存的图像和文件”
图 11-3。
A message in Chrome’s console, and a misleading editor message
很高兴看到 JavaScript 注意到编辑器中发生了一些事情。JavaScript 知道更改还没有保存在数据库中。JavaScript 知道编辑器消息条目已保存不再有效,但实际上是误导。现在 JavaScript 已经检测到条目编辑器的标题字段发生了变化,是时候为用户改变反馈了。在js/editor.js中又用了几行 JavaScript 代码:
//partial code for js/editor.js
//edit existing function
function updateEditorMessage() {
var p = document.querySelector("#editor-message");
p.innerHTML = "Changes not saved!";
}
就是这样!每当<title>中有未保存的更改时,用户都会收到通知。每当<textarea>发生变化时,添加一个事件监听器来调用updateEditorMessage()是非常简单的。您可能认为可以在init函数中添加一个额外的事件监听器——如下所示:
//partial code for js/editor.js
//edit existing function
function init(){
var editorForm = document.querySelector("form#editor");
var title = document.querySelector("input[name='title']");
title.required = false;
//changes start here
//sadly, this code will not solve the problem...
var textarea = document.querySelector("form textarea");
textarea.addEventListener("keyup", updateEditorMessage, false);
//end of changes
title.addEventListener("keyup", updateEditorMessage, false);
editorForm.addEventListener("submit", checkTitle, false);
}
您可能会惊讶地发现,每当<textarea>发生变化时,前面的代码并不更新编辑器消息。问题是您的 JavaScript 代码与 TinyMCE JavaScript 冲突。看起来你运气不好。幸运的是,TinyMCE 是可以配置的。TinyMCE 开发人员甚至努力记录配置选项。我在 TinyMCE 文档中搜索了一下,发现只要 TinyMCE 编辑器发生变化,TinyMCE JavaScript 就可以调用您的updateEditorMessage()。在 Komodo Edit 中打开views/admin/editor-html.php,稍微修改一下代码,如下:
//partial code for views/admin/editor-html.php
<script type='text/javascript'>
//change the existing call to tinymce.init
//add the code to call your updateEditorMessage function...
//...whenever the tinymce editor changes
tinymce.init ({
selector: 'textarea',
plugins: 'image',
setup: function (editor) {
editor.on ('change', function (e) {
updateEditorMessage();
});
}
});
</script>
每当 TinyMCE 编辑器以任何方式改变时,该代码将调用函数updateEditorMessage()。那正是你想要的。
Note
了解如何在 www.tinymce.com/wiki.php/Configuration 配置 TinyMCE。
期末考试的时间到了。将一个现有条目加载到条目编辑器中,稍加修改,然后保存。现在,您应该看到一条编辑器消息,说明条目已保存。对条目再做一点修改,看看编辑器消息如何立即变成 Changes not saved!这是您的 JavaScript 函数updateEditorMessage()的工作,该函数通过 TinyMCE change事件调用,由 TinyMCE 编辑器中的任何更改触发。
需要记住的一点是,TinyMCE 依赖于 JavaScript。如果不启用 JavaScript,条目编辑器不会给人留下深刻印象。任何博客管理员都必须承认,他们需要一个支持 JavaScript 的现代浏览器。
下一章将向您展示如何限制对管理模块的访问。您将创建一个登录表单,以便只允许拥有正确用户名和密码的用户使用管理模块。
摘要
本章对管理模块进行了一些重大改进。在这个过程中,您还有机会了解 PHP 文件上传以及如何用 PHP 删除文件。
您已经仔细研究了与定制 PHP 异常一起使用的try-catch语句。您已经看到了如何使用try-catch,以便您的 PHP 代码可以优雅地失败。您已经使用异常向用户提供了有意义的错误消息。您应该知道一些开发人员会反对这种异常的使用。一些开发人员会说,你能预测的任何错误都不是例外。这些情况不应使用例外。最终决定权在你:至少现在,你有选择权。
我希望你和 TinyMCE 的合作给你带来了开胃菜。在网上可以找到许多可靠的、记录良好的 JavaScript,将这样的 JavaScript 集成到您的项目中可以极大地提高您的工作质量。另一方面,你应该小心处理你不完全理解的库和框架。如果你向客户交付一个项目,然后发现系统中有一个 bug,如果你不理解使用的代码,你可能很难修复这个 bug。和例外的情况一样,最终都要做出明智的决定。
十二、密码保护
在你可以称你的博客为“网络就绪”之前,你需要做的最后一件事是对没有被授权的用户隐藏管理模块。在本章中,您将学习如何构建一个系统,让您创建新的管理员,并要求管理员在创建、编辑和删除博客条目之前使用密码登录。创建此系统需要您执行以下任务:
- 在
simple_blog数据库中创建一个管理表 - 使用单向密码加密
- 创建用于创建新管理员的 HTML 表单
- 在管理表中插入一个或多个管理员
- 为管理员创建登录表单
- 对未授权用户隐藏管理模块
- 使用会话跨多个 HTTP 请求保持登录状态
在数据库中创建 admin_table
为您的站点启用授权管理员要求您创建一个表来存储他们的数据。我创建了一个管理表来存储关于管理员的以下信息:
admin_id:识别一个管理员的唯一编号email:管理员的电子邮件地址password:管理员的密码
您将使用所谓的 MD5 算法将密码加密成 32 个字符的字符串。因此,密码列可以使用VARCHAR数据类型,并将输入限制为 32 个字符。
要创建管理表,在浏览器中导航到http://localhost/phpmyadmin,选择您的simple_blog数据库并打开 SQL 选项卡。输入以下命令创建您的表:
create table admin(
admin_id INT NOT NULL AUTO_INCREMENT,
email TEXT,
password VARCHAR(32),
primary key (admin_id)
)
用 MD5 加密密码
一旦您开始向 admin 表中插入新用户,您将看到如何用 MD5 算法加密字符串。MD5 算法提供所谓的单向加密。
加密密码是一种很好的做法,因为加密的密码更难被窃取。让我们假设你的密码是 test。通过 MD5 运行,test 变成 098 f 6 BCD 4621d 373 cade 4 e 832627 B4 f 6。无论你的密码有多长或多短,一旦通过 MD5,它就会变成 32 个字符的字符串。
即使您的数据库遭受严重攻击,并且所有用户名和密码都暴露了,攻击者仍然无法滥用用户名和密码。
单向加密
密码通常通过所谓的单向加密来保护。如果用单向加密算法加密密码,实际上应该是无法解密的。因此,如果您有一个加密的密码,如 098 F6 BCD 4621d 373 cade 4 e 832627 B4 f 6,那么实际上不可能发现未加密的密码是 test。这意味着即使您的数据库遭到破坏,密码仍然受到保护。
任何可以恢复丢失密码的系统本质上都是不安全的。如果您的原始密码可以恢复,系统必须以某种方式记住您的密码。如果您的密码被系统记住,系统管理员很可能会看到您的密码。如果你的密码可以被看到,那它就是易受攻击的。一个安全的系统只会存储加密的密码。如果您忘记了密码,安全系统会为您提供重新设置密码的机会。使用单向加密,可以对系统管理员隐藏密码等敏感数据。
在您的博客系统中,这意味着管理员密码受到保护,不会被您或其他博客管理员知道。如果用户丢失了密码,您就不能再将密码发送给她。您可以给她一个重置密码的机会,但密码是完全安全的——就像它应该的那样。
足够的安全性
假设即使是最安全的系统也可能被黑客攻击。问题是攻击者需要花费多少时间和精力。您的任务是提供足够的安全性来阻止攻击者访问您的内容。如果你的内容非常有价值,你需要非常严格的安全措施。大多数个人博客对 IT 罪犯来说可能不是特别有吸引力的目标。
MD5 是一种单向加密算法。它经常在 PHP 中使用,因为它在 PHP 应用中非常容易使用。MD5 本身不足以创建最佳的密码安全性。尽管 MD5 有很多优点,但它已经被攻破了。但对我们来说,这就足够了。你们已经对 SQL 注入的袭击采取了措施。很快,你也将拥有加密的密码,这样你就有可能阻止任何来自你博客系统的攻击者。
Note
还有另一种常见的攻击类型,称为跨站脚本(XSS)攻击。你可以考虑做一些互联网研究,防止 XSS 对 PHP 站点的攻击。
您应该知道,关于构建安全的 PHP 应用,还有很多东西需要学习。如果您开发一个保存有价值数据的系统,您就不能指望 MD5 能阻止攻击者。您可以考虑参考 Chris Snyder 的 Pro PHP Security (Apress,2010)来了解更多关于这个主题的信息。
在数据库中添加管理员
您有一个数据库表来保存管理员凭据;您已经准备好开始创建博客管理员了。第一步是创建一个允许您输入电子邮件地址和相应密码的表单。完成后,您必须将信息存储在数据库中以备后用。
构建 HTML 表单
正如您已经了解到的,最好创建向用户提供反馈的表单。您也可以马上为用户反馈准备好表单。在views/admin/new-admin-form-html.php中创建新文件。我的表单看起来是这样的:
<?php
//complete code for views/admin/new-admin-form-html.php
if( isset($adminFormMessage) === false ) {
$adminFormMessage = "";
}
return "<form method='post' action='admin.php?page=users'>
<fieldset>
<legend>Create new admin user</legend>
<label>e-mail</label>
<input type='text' name='email' required/>
<label>password</label>
<input type='password' name='password' required/>
<input type='submit' value='create user' name='new-admin'/>
</fieldset>
<p id='admin-form-message'>$adminFormMessage</p>
</form>
";
要显示表单,您需要一个控制器来加载它并将其返回给admin.php。您可以创建一个新的导航项目和一个相应的控制器。首先使用附加链接更新您的导航,如下所示:
<?php
//complete code for views/admin/admin-navigation.php
//new code: notice item added for a 'Create admin user' page
return "
<nav id='admin-navigation'>
<a href='admin.php?page=entries'>All entries</a>
<a href='admin.php?page=editor'>Editor</a>
<a href='admin.php?page=images'>Image manager</a>
<a href='admin.php?page=users'>Create admin user</a>
</nav>
";
创建了表单的视图和新的导航项后,下一步可以创建一个控制器来加载视图并显示它。在前面的例子中,我为请求名为 users 的新页面的新链接创建了一个href。所以,我的系统期望一个名为controllers/admin/users.php的控制器。创建这样一个文件,并编写以下代码:
<?php
//complete code for controllers/admin/users.php
$newAdminForm = include_once "views/admin/new-admin-form-html.php";
return $newAdminForm;
保存您的工作并在浏览器中加载http://localhost/blog/admin.php?page=users。您应该会看到一个无样式但有效的 HTML 表单。您还不能真正使用它:它不能向数据库表中插入新管理员。
在数据库中保存新管理员
向数据库表中插入新行是模型脚本的工作。您已经多次很好地利用了表数据网关设计模式。您甚至准备了一个基本的Table类,您可以扩展它来为 admin 表创建一个表数据网关。
此时,您不需要做太多事情:您希望避免名称冲突,因此在创建新的 admin 用户之前,代码应该检查该电子邮件是否已经在表中使用。如果电子邮件没有被使用,模型脚本应该在 admin 表中插入一个新条目。创建新文件models/Admin_Table.class.php,并编写新的类定义,如下所示:
<?php
//complete code for models/Admin_Table.class.php
//include parent class' definition
include_once "models/Table.class.php";
class Admin_Table extends Table {
public function create ( $email, $password ) {
//check if e-mail is available
$this->checkEmail( $email );
//encrypt password with MD5
$sql = "INSERT INTO admin ( email, password )
VALUES( ?, MD5(?) )";
$data= array( $email, $password );
$this->makeStatement( $sql, $data );
}
private function checkEmail ($email) {
$sql = "SELECT email FROM admin WHERE email = ?";
$data = array( $email );
$this->makeStatement( $sql, $data );
$statement = $this->makeStatement( $sql, $data );
//if a user with that e-mail is found in database
if ( $statement->rowCount() === 1 ) {
//throw an exception > do NOT create new admin user
$e = new Exception("Error: '$email' already used!");
throw $e;
}
}
}
前面的代码正在等待控制器调用。create()方法为新电子邮件和相关密码提供两个参数。私有方法checkEmail()用于检查所提供的电子邮件是否已经存在于数据库中。如果是的话,一个Exception对象被创建,并带有一条相关的消息,创建的异常被抛出。这意味着您可以在控制器中使用try-catch语句。您将很快编写代码,尝试创建一个新的管理员用户。如果操作失败,您的代码将通过捕捉异常并向用户提供相关反馈来正常失败。
看看create()方法中的$sql变量。看看接收到的密码在插入数据库时是如何用 MD5 加密的?这是可能的,因为 SQL 语言有一个内置的 MD5 函数。让我们回到controllers/admin/users.php中的控制器脚本,写一些代码,这样来自表单的输入将用于在数据库中插入新的管理员用户:
<?php
//complete code for controllers/admin/users.php
//new code starts here
include_once "models/Admin_Table.class.php";
//is form submitted?
$createNewAdmin = isset( $_POST['new-admin'] );
//if it is...
if( $createNewAdmin ) {
//grab form input
$newEmail = $_POST['email'];
$newPassword = $_POST['password'];
$adminTable = new Admin_Table($db);
try {
//try to create a new admin user
$adminTable->create( $newEmail, $newPassword );
//tell user how it went
$adminFormMessage = "New user created for $newEmail!";
} catch ( Exception $e ) {
//if operation failed, tell user what went wrong
$adminFormMessage = $e->getMessage();
}
}
//end of new code
$newAdminForm = include_once "views/admin/new-admin-form-html.php";
return $newAdminForm;
保存您的代码,并在浏览器中运行http://localhost/blog/admin.php?page=users进行测试。尝试创建一个管理员用户。您应该会在表单中看到一条确认消息。检查http://localhost/phpmyadmin以查看 admin 用户是否确实被插入到 admin 数据库表中。
创建新的管理员用户后,您可以尝试使用相同的电子邮件地址再创建一个管理员用户。这是不允许的,您应该会在表单中收到一条错误消息。更具体地说,您应该看到从Admin_Table类的checkEmail()方法抛出的Exception的消息。
规划登录
本章的其余部分集中在限制对管理模块的访问。应该只允许经过身份验证的用户创建、编辑和删除博客条目。
您可以继续使用 MVC 思想来组织代码。您需要两个视图:一个登录表单和一个注销表单。您需要一个控制器来处理从这两个视图接收到的用户交互,还需要一个模型来实际执行登录和注销。该模型还应该记住一个状态:它应该记住用户是否登录。
创建登录表单
从登录表单开始。您最终应该得到一个系统,在这个系统中,用户必须提供有效的电子邮件和匹配的密码,才能被允许访问博客管理模块。因此,您需要一个包含电子邮件和密码字段的表单。在views/admin/login-form-html.php中创建一个新文件:
<?php
//complete code for views/admin/login-form-html.php
return " <form method='post' action='admin.php'>
<p>Login to access restricted area</p>
<label>e-mail</label><input type='email' name='email' required />
<label>password</label>
<input type='password' name='password' required />
<input type='submit' value='login' name='log-in' />
</form>";
您还应该创建一个简单的控制器。此时,它应该只加载视图并输出它。在controllers/admin/login.php中创建一个新文件:
<?php
//complete code for controllers/admin/login.php
$view = include_once "views/admin/login-form-html.php";
return $view;
管理导航没有为登录提供菜单项,也不应该提供。您希望登录表单显示为admin.php的默认视图。只有当用户通过身份验证并登录后,才允许用户查看博客管理模块。
您可以创建一个类来表示管理员用户。这个类应该记住当前的访问者是否已经登录。最初,您可以安全地假设用户没有登录。用户应该可以登录和注销。您可以用属性表示状态(即用户是否登录)。您将需要登录和注销的方法。每个方法都应该操作由属性表示的登录状态。在models/Admin_User.class.php中创建一个新文件,如下:
<?php
//complete code for models/Admin_User.class.php
class Admin_User {
private $loggedIn = false;
public function isLoggedIn(){
return $this->loggedIn;
}
public function login () {
$this->loggedIn = true;
}
public function logout () {
$this->loggedIn = false;
}
}
对未授权用户隐藏控件
非常重要的是,登录实际上可以对未授权用户隐藏系统的一部分。你可能会说这是登录的全部意义。到目前为止,访问admin.php的任何人都可以很容易地获得管理模块。你需要在这里做一些改变。
Create an Admin_User object to remember login state. If a visitor is not logged in, show only the login form. If a user is logged in, show the admin navigation and the administration module.
为此,您必须对admin.php进行一些修改。一些变化将涉及注释掉或删除现有代码。但是您还必须添加一些新的代码行,如下所示:
<?php
//complete code for blog/admin.php
error_reporting( E_ALL );
ini_set( "display_errors", 1 );
include_once "models/Page_Data.class.php";
$pageData = new Page_Data();
$pageData->title = "PHP/MySQL blog demo";
$pageData->addCSS("css/blog.css");
$pageData->addScript("js/editor.js");
//code changes start here: comment out navigation
//$pageData->content = include_once "views/admin/admin-navigation.php";
$dbInfo = "mysql:host=localhost;dbname=techreview_blog";
$dbUser = "root";
$dbPassword = "";
$db = new PDO( $dbInfo, $dbUser, $dbPassword );
$db->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
//code changes here: comment out some of the existing code in admin.php
//$navigationIsClicked = isset( $_GET['page'] );
//if ($navigationIsClicked ) {
// $contrl = $_GET['page'];
//} else {
// $contrl = "entries";
//}
//new code added below
include_once "models/Admin_User.class.php";
$admin = new Admin_User();
//load the login controller, which will show the login form
$pageData->content = include_once "controllers/admin/login.php";
//add a new if statement
//show admin module only if user is logged in
if( $admin->isLoggedIn() ) {
$pageData->content .=include "views/admin/admin-navigation.php";
$navigationIsClicked = isset( $_GET['page'] );
if ($navigationIsClicked ) {
$controller = $_GET['page'];
} else {
$controller = "entries";
}
$pathToController = "controllers/admin/$controller.php";
$pageData->content .=include_once $pathToController;
} //end if-statement
//end of new code
$page = include_once "views/page.php";
echo $page;
有了这段代码,您就可以测试您的登录了。如果您将浏览器导航到http://localhost/blog/admin.php,除了登录表单,您应该什么也看不到。这是理所应当的,因为$admin对象的loggedIn属性默认为false,当用户未登录时,不允许用户进入管理区域。
登录用户
是时候允许用户登录了。此时,我不会真正对用户进行身份验证。我不会编写代码来检查所提供的电子邮件和密码是否有效。在本章的后面,我将向你展示一种方法。现在,您只需登录任何提交登录表单的人。处理这样的用户交互是登录控制器的工作。在controllers/admin/login.php中更新您的代码:
<?php
//complete code for controllers/admin/login.php
//new code here
$loginFormSubmitted = isset( $_POST['log-in'] );
if( $loginFormSubmitted ) {
$admin->login();
}
//end of new code
$view = include_once "views/admin/login-form-html.php";
return $view;
变化很小,现在对你来说应该很容易理解。尝试将浏览器指向http://localhost/blog/admin.php并登录。你可以使用任何凭证!
它应该像魔咒一样工作,你应该看到管理导航和所有博客条目的可点击列表。但是等等!单击其中一个条目,将其加载到条目编辑器中。会发生什么?哦不!您看不到条目编辑器。相反,您发现自己再次查看登录表单。很明显,你立刻被注销了?为什么呢?!?
HTTP 是无状态的
超文本传输协议是互联网上许多数据通信的基础。它是无状态的,这意味着它将每个新请求视为一个单独的事务。实际上,这意味着所有 PHP 变量和对象都是随着每个新请求从头开始创建的。
这对你有一定的影响。当您提交登录表单时,您实际上发出了一个 HTTP 请求。PHP 将会运行,$admin对象将会记住您已经登录。接下来,单击一个条目。这将产生一个新的 HTTP 请求,从而创建一个新的$admin对象。PHP 不会记得您刚刚登录,因为新的 HTTP 请求被视为一个独立的事务。之前的 HTTP 请求发生了什么完全被遗忘了。
重新访问超级全局变量:$_SESSION
这种无状态 HTTP 对您的登录来说是个坏消息。无论你登录多少次,PHP 都会随着每个新的请求而忘记它。这是非常不切实际的,当然,有一个解决方案:跨请求保持状态。您需要一种方法来强制 PHP 记住一个给定的用户已经登录。你需要一个 PHP 会话。
当 PHP 会话启动时,访问用户的浏览器将被分配一个唯一的标识号:会话 id。服务器将在服务器端创建一个小的临时文件。您需要应用跨请求记住的任何信息都应该存储在这个文件中。PHP 将为您处理这个临时文件。PHP 会话的默认生存期为 20 分钟,持续时间可以在php.ini文件中配置。
Note
www.php.net/manual/en/intro.session.php 阅读更多。
PHP 提供了一个超级全局变量来简化会话处理。你已经见过一些超级大人物:$_GET、$_POST和$_FILES。现在,你该见见$_SESSION了。
通过会话保持状态
若要使用会话,您的代码必须启动一个会话。这样,您就可以创建一个会话变量,它的值是有状态的,这意味着它的值可以跨 HTTP 请求保持不变。
您已经有了一个Admin_User类,它使用一个普通属性来记住登录状态。您可以使用会话变量来代替。这是如何做到的:
<?php
//complete code for models/Admin_User.class.php
class Admin_User {
//declare a new method, a constructor
public function __construct(){
//start a session
session_start();
}
//edit existing method
public function isLoggedIn(){
$sessionIsSet = isset( $_SESSION['logged_in'] );
if ( $sessionIsSet ) {
$out = $_SESSION['logged_in'];
} else {
$out = false;
}
return $out;
}
//edit existing method
public function login () {
//set session variable ['logged_in'] to true
$_SESSION['logged_in'] = true;
}
//edit existing method
public function logout () {
//set session variable ['logged_in'] to false
$_SESSION['logged_in'] = false;
}
}
保存代码更改并在浏览器中加载http://localhost/blog/admin.php。您仍然可以使用任何凭证登录,但是这一次,PHP 将记住您已经登录。因此,点击一个博客条目实际上会将点击的条目加载到条目编辑器中。这是一个好消息:您可以作为管理员登录,一旦登录,您就可以使用管理模块。但是一切都还不完美。任何人都可以登录,因为电子邮件和密码不会真正与合法管理员的数据库记录进行比较。此外,已登录的用户无法注销。让我们先处理最简单的任务。
注销用户
通常为登录用户提供注销选项。这也是一个好主意:您不希望管理员在没有注销选项的情况下保持登录状态。如果管理员不得不离开电脑去冲一杯新咖啡,该怎么办?您不会想让管理模块暴露在外。让我们为注销创建一个新视图。在views/admin/logout-form-html.php中创建一个新文件,如下:
<?php
//complete code for views/admin/logout-form-html.php
return "
<form method='post' action='admin.php'>
<label>logged in as administrator</label>
<input type='submit' value='log out' name='logout' />
</form>";
每当用户登录时,都应该显示该视图。但是简单地显示一个注销表单实际上不会注销用户。如果用户单击 Logout 按钮,应该会运行一个脚本来注销用户。这些是管制员的任务。您必须为controllers/admin/login.php中的登录控制器编写一些代码:
<?php
//complete code for controllers/admin/login.php
$loginFormSubmitted = isset( $_POST['log-in'] );
if( $loginFormSubmitted ) {
$admin->login();
}
//new code below
$loggingOut = isset ( $_POST['logout'] );
if ( $loggingOut ){
$admin->logout();
}
if ( $admin->isLoggedIn() ) {
$view = include_once "views/admin/logout-form-html.php";
} else {
$view = include_once "views/admin/login-form-html.php";
}
//comment out the former include statement
//$view = include_once "views/admin/login-form-html.php";
//end of code changes
return $view;
你进步很大!你现在应该可以登录和注销了,PHP 会通过一个会话变量记住你的当前状态。任何人都可以使用任何凭证登录,这有点问题。该代码不检查提供的用户名和密码是否正确。
仅允许授权用户
登录即将完成。您只需检查提供的电子邮件和密码是否与数据库中的一条记录完全匹配。这些信息可以在 admin 表中找到,并且您已经有了一个名为Admin_Table的表数据网关。您可以创建一个新方法来检查提交的凭据是否有效。
//partial code for models/admin/Admin_Table.class.php
//declare new method in Admin_Table class
public function checkCredentials ( $email, $password ){
$sql = "SELECT email FROM admin
WHERE email = ? AND password = MD5(?)";
$data = array($email, $password);
$statement = $this->makeStatement( $sql, $data );
if ( $statement->rowCount() === 1 ) {
$out = true;
} else {
$loginProblem = new Exception( "login failed!" );
throw $loginProblem;
}
return $out;
}
看看收到的密码是怎么用 MD5 加密的?在数据库中,您有 MD5 加密的密码。要将数据库中的密码与从登录表单中收到的密码进行比较,您的代码必须对收到的密码进行加密。如果将加密的密码与未加密的密码进行比较,用户将无法登录,因为 test 与 098 F6 BCD 4621d 373 cade 4 e 832627 B4 f 6 不同。
请注意,如果提交的电子邮件和密码与 admin 表中的一行数据不完全匹配,该方法将如何创建一个新的Exception对象并抛出它。声明了这个方法后,您就可以在用户试图登录时从登录控制器调用它了。
<?php
//complete code for controllers/admin/login.php
//new code: include the new class definition
include_once "models/Admin_Table.class.php";
$loginFormSubmitted = isset( $_POST['log-in'] );
if( $loginFormSubmitted ) {
//code changes start here: comment out the existing login call
//$admin->login();
//grab submitted credentials
$email = $_POST['email'];
$password = $_POST['password'];
//create an object for communicating with the database table
$adminTable = new Admin_Table( $db );
try {
//try to login user
$adminTable->checkCredentials( $email, $password );
$admin->login();
} catch ( Exception $e ) {
//login failed
}
//end of code changes
}
$loggingOut = isset ( $_POST['logout'] );
if ( $loggingOut ){
$admin->logout();
}
if ( $admin->isLoggedIn() ) {
$view = include_once "views/admin/logout-form-html.php";
} else {
$view = include_once "views/admin/login-form-html.php";
}
return $view;
这在安全性方面是一个巨大的进步。保存前面的代码后,您应该只能使用有效的用户凭据登录。但是不要相信我的话。尝试使用错误的凭据登录;应该是不可能的。
练习
您应该考虑一些与登录相关的附加内容。你已经在前面的章节中尝试过了,所以这些补充对你来说应该是很好的学习练习。
您的登录表单使用了必需的属性。正如你在第十章中了解到的,不同的浏览器对所需属性的处理是不同的。您可以使用 JavaScript 来提供跨现代浏览器的更一致的行为。这将提高可用性,也给你一个机会练习一点 JavaScript。
登录表单此时会自动失败。当用户登录失败时,抛出一个Exception对象。异常甚至会被捕获,但是异常消息不会作为反馈显示给用户。也许您可以获取异常消息并将其显示在登录表单中。这很像在本章开始时创建一个$adminFormMessage。
一旦实现了基本的用户反馈,就可以为登录表单尝试一些更高级的东西。当用户登录失败时,检查数据库中是否存在该电子邮件。如果有,让用户知道。密码错误,请重试。如果电子邮件不存在,告诉用户提供的电子邮件与系统中的任何记录都不匹配,请重试。
摘要
这是一个简短的章节。有了用于隐私的单向加密和用于有状态内存的会话,您已经设法有效地限制了对您的博客的管理模块的访问。我希望你会同意实现一个登录系统是非常有益的。我特别喜欢为您提供额外的机会来创建与用户交流的表单。
你的博客已经可以上网了。下一章将带你完成将你的项目上传到在线主机的过程,这样你的博客就可以在互联网上发布了。
十三、公开你的博客
你已经开发了一个完整的、数据库驱动的博客,甚至还有一个管理模块。但是它只能在你的本地计算机上运行。博客实际上是为读者写作。如果你的博客只在你的本地电脑上运行,那么它的受众肯定是有限的。本章将向您展示将您的博客从本地开发环境迁移到在线 web 主机所需的所有知识。
将你的博客放在互联网上的过程包括几个步骤。
You’ll need access to a web hosting service. It should offer an A-pache web server, MySQL, and PHP. You’ll need an FTP program, so that you can upload your files from your local computer to your web host. You’ll need to export your local database, so that you can import it into your web host’s MySQL database.
Web 主机要求
你需要一个虚拟主机来存放你的在线博客。它应该支持你用过的技术:PHP 5.4 和 MySQL 5.x .你的 web 主机也应该支持 FTP,这将允许你把你的 PHP 文件上传到 web 主机。
您已经使用 phpMyAdmin 访问了 MySQL 数据库。还有其他程序可以提供对 MySQL 的访问。如果这是您的第一个 PHP/MySQL 项目,您可能应该寻找一个提供 phpMyAdmin 的 web 主机。但是要明白 phpMyAdmin 并不是一个固定的需求。我推荐它,因为它是你已经用过的程序。
搜索“虚拟主机”,寻找适合你的解决方案。有免费的解决方案、便宜的解决方案和昂贵的解决方案。我假设这是你的第一个 PHP/MySQL 项目,所以我建议你寻找一个提供实时聊天支持的网络主机。一路上你可能会遇到问题,和一个有能力的支持人员实时聊天可以解决很多小问题。
导出和导入数据库
一旦确定了托管解决方案,就可以开始将数据库从本地开发环境迁移到新的 web 主机上。移动数据库包括创建一个 SQL 文件,该文件包含所有用于创建和填充表的 SQL 语句。幸运的是,这很容易做到。
首先打开 XAMPP 控制面板,确保 Apache 和 MySQL 都在运行。将浏览器指向http://localhost/phpmyadmin并选择您的simple_blog数据库。现在,点击顶部导航中的导出选项卡(参见图 13-1 )。
图 13-1。
Exporting the simple_blog database with phpMyAdmin
要导出您的simple_blog数据库及其所有的表和内容,您只需接受 SQL 作为导出格式,然后单击 Go。phpMyAdmin 将生成一个名为simple_blog.sql的 SQL 文件。你可以在你的downloads文件夹中找到生成的文件。这个simple_blog.sql文件保存了你的simple_blog数据库中所有东西的备份。
下一步是访问 web 主机的 phpMyAdmin。您需要查阅来自您的 web 主机的信息,以找到在您的 web 主机上访问 phpMyAdmin 的正确 URL。您需要一个有效的数据库用户名和密码来登录 phpMyAdmin。
一旦您在 web 主机上登录到 phpMyAdmin,您就可以准备导入您的simple_blog数据库了。在某些 web 主机上,允许用户创建新的数据库。如果您可以创建一个新的数据库,我建议您为您的项目这样做。您可以将新数据库称为simple_blog。
如果您的 web 主机不允许您创建新的数据库,您可以简单地为您的项目使用任何预制的数据库。这对你来说应该不成问题。
选择将用于项目的数据库,并单击 phpMyAdmin 导航栏中的 Import 选项。要导入您的simple_blog,您需要做的就是将simple_blog.sql文件从您的本地计算机上传到您的 web 主机。一旦你点击 Go,你的simple_blog数据库中的所有表格和数据都应该被导入到你的新数据库中,运行在你的 web 主机上。通过使用 phpMyAdmin 浏览数据库和表,您可以自己看到。
您的数据库已经准备好了,您可以安全地进入下一步了。
为上传准备 PHP 文件
一旦您的数据库被导入到您的 web 主机,您就可以准备您的 PHP 文件进行上传。不需要太多努力。您只需更改在index.php中使用的数据库凭证,如下所示:
//Partial code for index.php
//comment out or delete localhost credentials
//$dbInfo = "mysql:host=localhost;dbname=simple_blog";
//$dbUser = "root";
//$dbPassword = "";
//declare valid web host credentials
$dbInfo = "mysql:host=[????];dbname= [????]";
$dbUser = "[????]";
$dbPassword = "[????]";
如果您使用前面的代码示例,您必须用 web 主机所需的有效值替换所有的[????]实例。您可能需要查阅您的 web 主机文档,看看使用哪个mysql:host。dbname应该是您在上一步导入simple_blog数据库时使用的数据库。Username和password应该是您在 web 主机上登录 phpMyAdmin 时使用的用户名和密码。
一旦用有效凭证更新了index.php中的这三行,就可以将 PHP 文件上传到 web 主机了。
用 FileZilla FTP 上传文件
FTP(文件传输协议)是用于将文件从本地计算机上传到 web 主机的标准协议。许多不同的程序可以用来处理 FTP。你可以使用浏览器,甚至是 Komodo Edit。
也许你已经使用 FTP 将一个 HTML 项目上传到一个网站上。如果你已经熟悉了 FTP 程序,我建议你继续使用它。
如果这是你第一次使用 FTP,建议你从 https://filezilla-project.org/download.php?type=client 下载安装 FileZilla FTP 客户端。我喜欢 FileZilla FTP,因为它简单、功能多样、易于使用。
要将您的博客文件上传到您的 web 主机,您必须建立 FTP 连接。您必须查阅您的 web 主机文档,以了解要连接到哪个 URL 或主机。在 FileZilla FTP 中,你可以在 host 字段中输入你的 FTP 主机的 URL,如图 13-2 所示。您还必须输入用户名和密码。再一次,你将不得不查阅你的 web 主机文档来找到你的用户名和密码。输入凭据后,您可以点按“快速连接”来建立 FTP 连接。
图 13-2。
FileZilla FTP connected to a web host
图 13-2 显示 FileZilla 连接到 web 主机。右侧的窗口显示 web 主机上的文件和文件夹。左边的窗口显示我的本地计算机上的文件和文件夹。
还记得你不得不把你的 PHP 文件放在 XAMPP 的htdocs文件夹里吗?一些网络主机也有这样的文件夹。互联网上的任何人都可以看到你上传到那个文件夹的任何东西。在你的网络主机上,这个文件夹可能被叫做htdocs、public_html或www。或者你的网络主机会让你用 FTP 上传的任何东西都可以在网上看到。再次,你将不得不查阅你的网站主机文档,看看你的网站主机是如何设置的。
你可以简单地拖放文件和文件夹来上传你的博客。您可能希望将所有来自XAMPP/htdocs/blog的文件上传到您的本地计算机上,并将这些文件和文件夹传输到您的 web 主机上。
一旦你的 PHP 文件被上传,你的博客就在互联网上直播了!要查看您的作品,您应该将浏览器导航到您的域名的 URL。您应该会看到您的博客,如果您导航到admin.php,您还应该能够使用您的博客管理员凭据登录。你的博客是实时的,你可以开始为你的读者写吸引人的博文:互联网上的每个人。
摘要
这是一个简短但重要的章节。只需几个步骤,你就完成了博客项目,并在互联网上发布给全世界。
你也到了这本书的结尾,但这并不意味着没有更多的东西要学。实际上,在你从本书中学到的所有知识完全整合之前,你可能还需要创建一些 PHP/MySQL 驱动的网站。进一步发展你的能力的过程可能会很有趣、迷人和令人沮丧。你可以考虑开始学习 PHP 和 MySQL,从新手到专家,作者是 W. Jason Gilmore (Apress,2010)。它更详细地介绍了 PHP 和 MySQL 的概念,并且将带您超越本书所涉及的基础知识。