PHP-和-jQuery-高级教程-三-

32 阅读17分钟

PHP 和 jQuery 高级教程(三)

原文:Pro PHP and jQuery

协议:CC BY-NC-SA 4.0

五、添加控件来创建、编辑和删除事件

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-1230-1_​5) contains supplementary material, which is available to authorized users.

现在可以查看日历了,您需要添加允许管理员创建、编辑和删除事件的控件。

生成表单以创建或编辑事件

要编辑事件或向日历中添加新事件,您需要使用一个表单。通过向Calendar类添加一个名为displayForm()的方法来实现这一点,该方法生成一个用于编辑和创建事件的表单。

这个简单的方法可以完成以下任务:

  • 检查作为事件 ID 传递的整数。
  • 为用于描述事件的不同字段实例化空变量。
  • 如果传递了事件 ID,则加载事件数据。
  • 将事件数据存储在先前实例化的变量中(如果存在)。
  • 输出表单。

Note

通过显式清理在$_POST超全局中传递的事件 ID,您可以确保该 ID 可以安全使用,因为任何非整数值都将被转换为0

通过向Calendar类添加以下粗体代码来构建displayForm()方法:

<?php

declare(strict_types=1);

class Calendar extends DB_Connect

{

private $_useDate;

private $_m;

private $_y;

private $_daysInMonth;

private $_startDay;

public function __construct(``$db=NULL

public function buildCalendar() {...}

public function displayEvent($id) {...}

/**

* Generates a form to edit or create events

*

* @return string the HTML markup for the editing form

*/

public function displayForm()

{

/*

* Check if an ID was passed

*/

if ( isset($_POST['event_id']) )

{

$id = (int) $_POST['event_id'];

// Force integer type to sanitize data

}

else

{

$id = NULL;

}

/*

* Instantiate the headline/submit button text

*/

$submit = "Create a New Event";

/*

* If no ID is passed, start with an empty event object.

*/

$event = new Event();

/*

* Otherwise load the associated event

*/

if ( !empty($id) )

{

$event = $this->_loadEventById($id);

/*

* If no object is returned, return NULL

*/

if ( !is_object($event) ) { return NULL; }

$submit = "Edit This Event";

}

/*

* Build the markup

*/

return <<<FORM_MARKUP

<form action="assets/inc/process.inc.php" method="post">

<fieldset>

<legend>$submit</legend>

<label for="event_title">Event Title</label>

<input type="text" name="event_title"

id="event_title" value="$event->title" />

<label for="event_start">Start Time</label>

<input type="text" name="event_start"

id="event_start" value="$event->start" />

<label for="event_end">End Time</label>

<input type="text" name="event_end"

id="event_end" value="$event->end" />

<label for="event_description">Event Description</label>

<textarea name="event_description"

id="event_description">$event->description</textarea>

<input type="hidden" name="event_id" value="$event->id" />

<input type="hidden" name="token" value="$_SESSION[token]" />

<input type="hidden" name="action" value="event_edit" />

<input type="submit" name="event_submit" value="$submit" />

or <a href="./">cancel</a>

</fieldset>

</form>

FORM_MARKUP;

}

private function _loadEventData($id=NULL) {...}

private function _``createEventObj

private function _loadEventById($id) {...}

}

?>

向表单添加令牌

如果您查看前面的表单,有一个名为token的隐藏输入,它保存一个会话值,也称为token。这是一种防止跨站点请求伪造(CSRF)的安全措施,跨站点请求伪造是通过从表单本身以外的其他地方将表单提交到应用的处理文件来伪造的表单提交。这是垃圾邮件发送者发送多个伪造的条目提交的常用策略,这是令人讨厌的、潜在有害的,并且肯定是不受欢迎的。

这个令牌是通过生成一个随机散列并将其存储在会话中,然后将令牌与表单数据一起提交来创建的。如果$_POST超全局中的令牌与$_SESSION超全局中的令牌匹配,那么可以相当肯定地断定提交是合法的。

通过用粗体显示的代码修改初始化文件,可以将反 CSRF 令牌添加到应用中:

<?php

declare(strict_types=1);

/*

* Enable sessions if needed.

* Avoid pesky warning if session already active.

*/

$status = session_status();

if ($status == PHP_SESSION_NONE){

//There is no active session

session_start();

}

/*

* Generate an anti-CSRF token if one doesn’t exist

*/

if ( !isset($_SESSION['token']) )

{

$_SESSION['token'] = sha1(uniqid((string)mt_rand(), TRUE));

}

/*

* Include the necessary configuration info

*/

include_once '../sys/config/db-cred.inc.php'; // DB info

/*

* Define constants for configuration info

*/

foreach ( $C as $name => $val )

{

define($name, $val);

}

/*

* Create a PDO object

*/

$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME;

$dbo = new PDO($dsn, DB_USER, DB_PASS);

/*

* Define the auto-load function for classes

*/

function __autoload($class)

{

$filename = "../sys/class/class." . $class . ".inc.php";

if ( file_exists($filename) )

{

include_once $filename;

}

}

?>

Caution

您可能希望包括令牌的时间限制,以进一步提高安全性。例如,确保令牌不超过 20 分钟,有助于防止用户离开无人看管的计算机,并防止恶意用户稍后开始四处窥探。欲了解更多关于代币和预防 CSRF 的信息,请访问 Chris Shiflett 的博客,并在 http://shiflett.org/csrf 阅读他关于该主题的文章。

创建显示表单的文件

现在已经有了显示表单的方法,您需要创建一个调用该方法的文件。这个文件将被命名为admin.php,它将驻留在public文件夹(/public/admin.php)的根级别中。

view.php类似,该文件完成以下任务:

  • 加载初始化文件。
  • 设置页面标题和 CSS 文件数组。
  • 包括标题。
  • 创建Calendar类的新实例。
  • 调用displayForm()方法。
  • 包括页脚。

接下来,在新的admin.php文件中添加以下内容:

<?php

declare(strict_types=1);

/*

* Include necessary files

*/

include_once '../sys/core/init.inc.php';

/*

* Output the header

*/

$page_title = "Add/Edit Event";

$css_files = array("style.css");

include_once 'assets/common/header.inc.php';

/*

* Load the calendar

*/

$cal = new Calendar($dbo);

?>

<div id="content">

<?php echo $cal->displayForm(); ?>

</div>``<!--``end #content

<?php

/*

* Output the footer

*/

include_once 'assets/common/footer.inc.php';

?>

保存该代码后,导航到http://localhost/admin.php查看结果表单,如图 5-1 所示。

A978-1-4842-1230-1_5_Fig1_HTML.jpg

图 5-1。

The form before adding any CSS styles

为管理功能添加新样式表

显然,前面的表单需要一些视觉上的增强,以使它更有用。然而,这个表单最终只能由管理员访问(因为您不希望任何人对您的日历进行更改),所以 CSS 规则将被分离到一个名为admin.css的单独样式表中。你可以在css文件夹(/public/assets/css/)中找到这个文件。

还是那句话,既然这本书不是讲 CSS 的,规则就不解释了。本质上,下面的 CSS 使表单元素看起来更像用户期望的表单;它还为即将创建的元素添加了一些规则。

现在将以下代码添加到admin.css中:

fieldset {

border: 0;

}

legend {

font-size: 24px;

font-weight: bold;

}

input[type=text],input[type=password],label {

display: block;

width: 70%;

font-weight: bold;

}

textarea {

width: 99%;

height: 200px;

}

input[type=text],input[type=password],textarea {

border: 1px solid #123;

-moz-border-radius: 6px;

-webkit-border-radius: 6px;

border-radius: 6px;

-moz-box-shadow: inset 1px 2px 4px #789;

-webkit-box-shadow: inset 1px 2px 4px #789;

box-shadow: inset 1px 2px 4px #789;

padding: 4px;

margin: 0 0 4px;

font-size: 16px;

font-family: georgia, serif;

}

input[type=submit] {

margin: 4px 0;

padding: 4px;

border: 1px solid #123;

-moz-border-radius: 6px;

-webkit-border-radius: 6px;

border-radius: 6px;

-moz-box-shadow: inset -2px -1px 3px #345,

inset 1px 1px 3px #BCF,

1px 2px 6px #789;

-webkit-box-shadow: inset -2px -1px 3px #345,

inset 1px 1px 3px #BCF,

1px 2px 6px #789;

box-shadow: inset -2px -1px 3px #345,

inset 1px 1px 3px #BCF,

1px 2px 6px #789;

background-color: #789;

font-family: georgia, serif;

text-transform: uppercase;

font-weight: bold;

font-size: 14px;

text-shadow: 0px 0px 1px #fff;

}

.admin-options {

text-align: center;

}

.admin-options form,.admin-options p {

display: inline;

}

a.admin {

display: inline-block;

margin: 4px 0;

padding: 4px;

border: 1px solid #123;

-moz-border-radius: 6px;

-webkit-border-radius: 6px;

border-radius: 6px;

-moz-box-shadow: inset -2px -1px 3px #345,

inset 1px 1px 3px #BCF,

1px 2px 6px #789;

-webkit-box-shadow: inset -2px -1px 3px #345,

inset 1px 1px 3px #BCF,

1px 2px 6px #789;

box-shadow: inset -2px -1px 3px #345,

inset 1px 1px 3px #BCF,

1px 2px 6px #789;

background-color: #789;

color: black;

text-decoration: none;

font-family: georgia, serif;

text-transform: uppercase;

font-weight: bold;

font-size: 14px;

text-shadow: 0px 0px 1px #fff;

}

保存该文件,然后通过进行粗体显示的更改将admin.css添加到admin.php中的$css_files数组:

<?php

declare(strict_types=1);

/*

* Include necessary files

*/

include_once '../sys/core/init.inc.php';

/*

* Output the header

*/

$page_title = "Add/Edit Event";

$css_files = array("style.css", "admin.css");

include_once 'assets/common/header.inc.php';

/*

* Load the calendar

*/

$cal = new Calendar($dbo);

?>

<div id="content">

<?php echo $cal->displayForm(); ?>

</div><!-- end #content -->

<?php

/*

* Output the footer

*/

include_once 'assets/common/footer.inc.php';

?>

保存前面的代码后,重新加载http://localhost/admin.php以查看样式化的表单(见图 5-2 )。

A978-1-4842-1230-1_5_Fig2_HTML.jpg

图 5-2。

The form to add or edit events after applying CSS styles

在数据库中保存新事件

为了保存表单中输入的事件,您在Calendar类中创建了一个名为processForm()的新方法,它完成了以下任务:

  • 清理通过POST从表单传递的数据。
  • 确定事件是正在编辑还是正在创建。
  • 如果没有正在编辑的事件,则生成一条INSERT语句;如果发布了事件 ID,则生成一条UPDATE语句。
  • 创建预准备语句并绑定参数。
  • 执行查询并在失败时返回TRUE或错误消息。

以下代码在Calendar类中创建了processForm()方法:

<?php

declare(strict_types=1);

class Calendar extends DB_Connect

{

private $_useDate;

private $_m;

private $_y;

private $_daysInMonth;

private $_startDay;

public function __construct($dbo=NULL, $useDate=NULL) {...}

public function buildCalendar() {...}

public function displayEvent($id) {...}

public function displayForm() {...}

/**

* Validates the form and saves/edits the event

*

* @return mixed TRUE on success, an error message on failure

*/

public function processForm()

{

/*

* Exit if the action isn’t set properly

*/

if ( $_POST['action']!='event_edit' )

{

return "The method processForm was accessed incorrectly";

}

/*

* Escape data from the form

*/

$title = htmlentities($_POST['event_title'], ENT_QUOTES);

$desc = htmlentities($_POST['event_description'], ENT_QUOTES);

$start = htmlentities($_POST['event_start'], ENT_QUOTES);

$end = htmlentities($_POST['event_end'], ENT_QUOTES);

/*

* If no event ID passed, create a new event

*/

if ( empty($_POST['event_id']) )

{

$sql = "INSERT INTO events``

(event_title, event_desc, event_start,

``event_end)

VALUES

(:title, :description, :start, :end)";

}

/*

* Update the event if it’s being edited

*/

else

{

/*

* Cast the event ID as an integer for security

*/

$id = (int) $_POST['event_id'];

$sql = "UPDATE events``

SET

``event_title=:title,

``event_desc=:description,

``event_start=:start,

``event_end=:end

WHERE event_id=$id";

}

/*

* Execute the create or edit query after binding the data

*/

try

{

$stmt = $this->db->prepare($sql);

$stmt->bindParam(":title", $title, PDO::PARAM_STR);

$stmt->bindParam(":description", $desc, PDO::PARAM_STR);

$stmt->bindParam(":start", $start, PDO::PARAM_STR);

$stmt->bindParam(":end", $end, PDO::PARAM_STR);

$stmt->execute();

$stmt->closeCursor();

return TRUE;

}

catch ( Exception $e )

{

return $e->getMessage();

}

}

private function _loadEventData($id=NULL) {...}

private function _createEventObj() {...}

private function _loadEventById($id) {...}

}

?>

添加处理文件以调用处理方法

添加和编辑事件的表单被提交到一个名为process.inc.php的文件中,该文件位于inc文件夹(/public/assets/inc/process.inc.php)中。该文件检查提交的表单数据,并通过执行以下步骤保存或更新条目:

Enables the session.   Includes the database credentials and the Calendar class.   Defines constants (as occurs in the initialization file).   Creates an array that stores information about each action.   Verifies that the token was submitted and is correct, and that the submitted action exists in the lookup array. If so, go to Step 6. If not, go to Step 7.   Creates a new instance of the Calendar class.

  • 调用processForm()方法。
  • 将用户返回到主视图,或者在失败时输出错误。

  Sends the user back out to the main view with no action if the token doesn’t match.  

在步骤 4 中创建的数组允许您避免一长串重复的if...elseif块来测试每个单独的动作。使用 action 作为数组键,并将对象、方法名和用户应该重定向到的页面存储为数组值,这意味着您可以使用数组中的变量编写单个逻辑块。

将以下代码插入process.inc.php以完成刚刚描述的步骤:

<?php

declare(strict_types=1);

/*

* Enable sessions if needed.

* Avoid pesky warning if session already active.

*/

$status = session_status();

if ($status == PHP_SESSION_NONE){

//There is no active session

session_start();

}

/*

* Include necessary files

*/

include_once '../../../sys/config/db-cred.inc.php';

/*

* Define constants for config info

*/

foreach ( $C as $name => $val )

{

define($name, $val);

}

/*

* Create a lookup array for form actions

*/

define('ACTIONS', array(

'event_edit' => array(

'object' => 'Calendar',

'method' => 'processForm',

'header' => 'Location: ../../'

)

)

);

/*

* Need a PDO object.

*/

$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME;

$dbo = new PDO($dsn, DB_USER, DB_PASS);

/*

* Make sure the anti-CSRF token was passed and that the

* requested action exists in the lookup array

*/

if ( $_POST['token']==$_SESSION['token']

&& isset(ACTIONS[$_POST['action']]) )

{

$use_array = ACTIONS[$_POST['action']];

$obj = new $use_array'object';

$method = $use_array['method'];

if ( TRUE === $msg=$obj->$method() )

{

header($use_array['header']);

exit;

}

else

{

// If an error occured, output it and end execution

die ( $msg );

}

}

else

{

// Redirect to the main index if the token/action is invalid

header("Location: ../../");

exit;

}

function __autoload($class_name)

{

$filename = '../../../sys/class/class.'

. strtolower($class_name) . '.inc.php';

if ( file_exists($filename) )

{

include_once $filename;

}

}

?>

保存该文件,然后导航至http://localhost/admin.php并用以下信息创建一个新事件:

  • 活动名称:晚宴
  • 开始时间:2016-01-22 17:00:00
  • 结束时间:2016-01-22 19:00:00
  • 描述:在约翰家吃五道菜并搭配葡萄酒

点击“创建新事件”按钮后,日历会更新为新事件,如图 5-3 所示。

A978-1-4842-1230-1_5_Fig3_HTML.jpg

图 5-3。

The new event as it appears when hovered over

向主视图添加按钮以创建新事件

为了让授权用户更容易创建新事件,在日历中添加一个按钮,将用户带到admin.php中的表单。通过在Calendar类中创建一个名为_adminGeneralOptions()的新私有方法来实现这一点:

<?php

declare(strict_types=1);

class Calendar extends DB_Connect

{

private $_useDate;

private $_m;

private $_y;

private $_daysInMonth;

private $_startDay;

public function __construct($dbo=NULL, $useDate=NULL) {...}

public function buildCalendar() {...}

public function displayEvent($id) {...}

public function displayForm() {...}

public function processForm() {...}

private function _loadEventData($id=NULL) {...}

private function _createEventObj() {...}

private function _loadEventById($id) {...}

/**

* Generates markup to display administrative links

*

* @return string markup to display the administrative links

*/

private function _adminGeneralOptions()

{

/*

* Display admin controls

*/

return <<<ADMIN_OPTIONS

<a href="admin.php" class="admin">+ Add a New Event</a>

ADMIN_OPTIONS;

}

}

?>

Note

确保该按钮仅显示给授权用户的检查将添加到第六章。

接下来,通过插入以下粗体代码,修改buildCalendar()方法以调用新的_adminGeneralOptions()方法:

public function buildCalendar()

{

// To save space, the bulk of this method has been omitted

/*

* Close the final unordered list

*/

$html .= "\n\t</ul>\n\n";

/*

* If logged in, display the admin options

*/

$admin = $this->_adminGeneralOptions();

/*

* Return the markup for output

*/

return $html . $admin;

}

最后,使用以下粗体代码将管理样式表(admin.css)添加到index.php,以确保链接正确显示:

<?php

declare(strict_types=1);

/*

* Include necessary files

*/

include_once '../sys/core/init.inc.php';

/*

* Load the calendar

*/

$cal = new Calendar($dbo, "2016-01-01 12:00:00");

/*

* Set up the page title and CSS files

*/

$page_title = "Events Calendar";

$css_files = array('style.css', 'admin.css');

/*

* Include the header

*/

include_once 'assets/common/header.inc.php';

?>

<div id="content">

<?php

/*

* Display the calendar HTML

*/

echo $cal->buildCalendar();

?>

</div><!-- end #content -->

<?php

/*

* Include the footer

*/

include_once 'assets/common/footer.inc.php';

?>

保存文件并重新加载http://localhost/以查看按钮(见图 5-4 )。

A978-1-4842-1230-1_5_Fig4_HTML.jpg

图 5-4。

The Admin button appears in the bottom left of the calendar

将编辑控制添加到完整事件视图

接下来,您需要让授权用户能够编辑事件。您将通过在view.php中向事件的完整视图添加一个按钮来实现这一点。

然而,与用于创建新选项的简单链接不同,编辑按钮需要实际的表单提交。为了保持这段代码的可管理性,您将在Calendar类中创建一个名为_adminEntryOptions()的新私有方法,该方法将为表单生成标记。

现在,这个表单将简单地返回表单标记来显示编辑按钮。随着您继续完成本书中的练习,更多内容将添加到表格中。

您可以通过向Calendar类添加以下粗体代码来创建该方法:

<?php

declare(strict_types=1);

class Calendar extends DB_Connect

{

private $_useDate;

private $_m;

private $_y;

private $_daysInMonth;

private $_startDay;

public function __construct($dbo=NULL, $useDate=NULL) {...}

public function buildCalendar() {...}

public function displayEvent($id) {...}

public function displayForm() {...}

public function processForm() {...}

private function _loadEventData($id=NULL) {...}

private function _createEventObj() {...}

private function _loadEventById($id) {...}

private function _adminGeneralOptions() {...}

/**

* Generates edit and delete options for a given event ID

*

* @param int $id the event ID to generate options for

* @return string the markup for the edit/delete options

*/

private function _adminEntryOptions($id)

{

return <<<ADMIN_OPTIONS

<div class="admin-options">

<form action="admin.php" method="post">

<p>

<input type="submit" name="edit_event"

value="Edit This Event" />

<input type="hidden" name="event_id"

value="$id" />

</p>

</form>

</div>``<!--``end .admin-options

ADMIN_OPTIONS;

}

}

?>

修改完整事件显示方法以显示管理控件

在显示编辑按钮之前,需要从displayEvent()方法中调用_adminEntryOptions()方法。这很简单,只需将_adminEntryOptions()的返回值存储在变量$admin中,然后将该变量与其余的条目标记一起输出。

将以下粗体修改添加到Calendar类中的displayEvent():

/**

* Displays a given event’s information

*

* @param int $id the event ID

* @return string basic markup to display the event info

*/

public function displayEvent($id)

{

/*

* Make sure an ID was passed

*/

if ( empty($id) ) { return NULL; }

/*

* Make sure the ID is an integer

*/

$id = preg_replace('/[⁰-9]/', '', $id);

/*

* Load the event data from the DB

*/

$event = $this->_loadEventById($id);

/*

* Generate strings for the date, start, and end time

*/

$ts = strtotime($event->start);

$date = date('F d, Y', $ts);

$start = date('g:ia', $ts);

$end = date('g:ia', strtotime($event->end));

/*

* Load admin options if the user is logged in

*/

$admin = $this->_adminEntryOptions($id);

/*

* Generate and return the markup

*/

return "<h2>$event->title</h2>"

. "\n\t<p class=\"dates\">$date, $start—$end</p>"

. "\n\t<p>$event->description</p>$admin";

}

Note

确保在return字符串的末尾包含了$admin变量。

与“创建新条目”按钮一样,稍后将添加检查,以确保只有授权用户才能看到编辑控件。

将管理样式表添加到完整事件视图页面

编辑按钮准备就绪之前的最后一步是将admin.css样式表包含在view.php$css_files变量中:

<?php

declare(strict_types=1);

/*

* Make sure the event ID was passed

*/

if ( isset($_GET['event_id']) )

{

/*

* Collect the event ID from the URL string

*/

$id = htmlentities($_GET['event_id'], ENT_QUOTES);

}

else

{

/*

* Send the user to the main page if no ID is supplied

*/

header("Location: ./");

exit;

}

/*

* Include necessary files

*/

include_once '../sys/core/init.inc.php';

/*

* Output the header

*/

$page_title = "View Event";

$css_files = array("style.css", "admin.css");

include_once 'assets/common/header.inc.php';

/*

* Load the calendar

*/

$cal = new Calendar($dbo);

?>

<div id="content">

<?php echo $cal->displayEvent($id) ?>

<a href="./">« Back to the calendar</a>

</div><!-- end #content -->

<?php

/*

* Output the footer

*/

include_once 'assets/common/footer.inc.php';

?>

保存该文件,然后点击一个事件以查看编辑按钮(参见图 5-5 )。

A978-1-4842-1230-1_5_Fig5_HTML.jpg

图 5-5。

The Edit button as it appears when viewing a full event description

点击编辑按钮将调出admin.php上的表格,表格中加载了所有事件的数据(见图 5-6 )。

A978-1-4842-1230-1_5_Fig6_HTML.jpg

图 5-6。

The admin form when an event is being edited

删除事件

创建Calendar类的最后一步是允许授权用户删除事件。事件删除不同于创建或编辑事件,因为您希望在删除事件之前确认用户的意图。否则,意外的点击会给用户带来挫败感和不便。

这意味着您必须分两个阶段实现删除按钮。

The Delete button is clicked, and the user is taken to a confirmation page.   The confirmation button is clicked, and the event is removed from the database.  

生成删除按钮

首先,通过用粗体显示的代码修改Calendar类中的_adminEntryOptions(),向全视图编辑控件添加一个删除按钮:

/**

* Generates edit and delete options for a given event ID

*

* @param int $id the event ID to generate options for

* @return string the markup for the edit/delete options

*/

private function _adminEntryOptions($id)

{

return <<<ADMIN_OPTIONS

<div class="admin-options">

<form action="admin.php" method="post">

<p>

<input type="submit" name="edit_event"

value="Edit This Event" />

<input type="hidden" name="event_id"

value="$id" />

</p>

</form>

<form action="confirmdelete.php" method="post">

<p>

<input type="submit" name="delete_event"

value="Delete This Event" />

<input type="hidden" name="event_id"

value="$id" />

</p>

</form>

</div><!-- end .admin-options -->

ADMIN_OPTIONS;

}

这将添加一个按钮,将用户发送到一个尚未创建的名为confirmdelete.php的确认页面,您将在本节稍后构建该页面。保存前面的更改后,在查看完整的事件描述时,您将看到编辑和删除选项(参见图 5-7 )。

A978-1-4842-1230-1_5_Fig7_HTML.jpg

图 5-7。

The Delete button as it appears on the full event view

创建需要确认的方法

当用户单击 Delete 按钮时,她会被发送到一个确认页面,该页面包含一个确认她确实想要删除该事件的表单。该表单将由名为confirmDelete()Calendar类中的新公共方法生成。

此方法通过执行以下操作来确认应删除事件:

Checks if the confirmation form was submitted and a valid token was passed. If so, go to Step 2. If not, go to Step 3.   Checks whether the button clicked was the Confirmation button.

  • 如果是,它将删除该事件。
  • 如果没有,它会将用户送回主日历视图。

  It loads the event data and displays the confirmation form.  

通过将粗体显示的新方法添加到Calendar类中,可以完成前面的步骤:

<?php

declare(strict_types=1);

class Calendar extends DB_Connect

{

private $_useDate;

private $_m;

private $_y;

private $_daysInMonth;

private $_startDay;

public function __construct($dbo=NULL, $useDate=NULL) {...}

public function buildCalendar() {...}

public function displayEvent($id) {...}

public function displayForm() {...}

public function processForm() {...}

/**

* Confirms that an event should be deleted and does so

*

* Upon clicking the button to delete an event, this

* generates a confirmation box. If the user confirms,

* this deletes the event from the database and sends the

* user back out to the main calendar view. If the user

* decides not to delete the event, they’re sent back to

* the main calendar view without deleting anything.

*

* @param int $id the event ID

* @return mixed the form if confirming, void or error if deleting

*/

public function confirmDelete($id)

{

/*

* Make sure an ID was passed

*/

if ( empty($id) ) { return NULL; }

/*

* Make sure the ID is an integer

*/

$id = preg_replace('/[⁰-9]/', '', $id);

/*

* If the confirmation form was submitted and the form

* has a valid token, check the form submission

*/

if ( isset($_POST['confirm_delete'])

&& $_POST['token']==$_SESSION['token'] )

{

/*

* If the deletion is confirmed, remove the event

* from the database

*/

if ( $_POST['confirm_delete']=="Yes, Delete It" )

{

$sql = "DELETE FROM events``

WHERE event_id=:id

LIMIT 1";

try

{

$stmt = $this->db->prepare($sql);

$stmt->bindParam(

":id",

$id,

PDO::PARAM_INT

);

$stmt->execute();

$stmt->closeCursor();

header("Location: ./");

return;

}

catch ( Exception $e )

{

return $e->getMessage();

}

}

/*

* If not confirmed, sends the user to the main view

*/

else

{

header("Location: ./");

return;

}

}

/*

* If the confirmation form hasn’t been submitted, display it

*/

$event = $this->_loadEventById($id);

/*

* If no object is returned, return to the main view

*/

if ( !is_object($event) ) { header("Location: ./"); }

return <<<CONFIRM_DELETE

<form action="confirmdelete.php" method="post">

<h2>

Are you sure you want to delete "$event->title"?

</h2>

<p>There is <strong>no undo</strong> if you continue.</p>

<p>

<input type="submit" name="confirm_delete"

value="Yes, Delete It" />

<input type="submit" name="confirm_delete"

value="Nope! Just Kidding!" />

<input type="hidden" name="event_id"

value="$event->id" />

<input type="hidden" name="token"

value="$_SESSION[token]" />

</p>

</form>

CONFIRM_DELETE;

}

private function _loadEventData($id=NULL) {...}

private function _createEventObj() {...}

private function _loadEventById($id) {...}

private function _adminGeneralOptions() {...}

private function _adminEntryOptions($id) {...}

}

?>

创建文件以显示确认表单

为了调用confirmDelete()方法,需要创建文件confirmdelete.php。这个文件将驻留在public文件夹(/public/confirmdelete.php)的根目录下,它将非常类似于index.php。该文件完成以下任务:

  • 确保事件 ID 被传递并存储在$id变量中;否则将用户发送到主视图。
  • 加载初始化文件。
  • 创建Calendar对象的新实例。
  • confirmDelete()的返回值加载到变量$markup中。
  • 定义$page_title$css_files变量,包括标题。
  • 输出存储在$markup中的数据。
  • 输出页脚。

Note

在包含头部之前将confirmDelete()的输出加载到一个变量中的原因是,该方法有时会使用header()将用户发送到应用中的其他地方;如果在调用confirmDelete()之前包含了头文件,脚本在某些情况下会失败,因为在调用header()之前没有数据可以输出到浏览器,或者发生致命错误。关于header()功能的更多信息,请访问 http://php.net/header

现在在confirmdelete.php中添加以下代码:

<?php

declare(strict_types=1);

/*

* Make sure the event ID was passed

*/

if ( isset($_POST['event_id']) )

{

/*

* Collect the event ID from the URL string

*/

$id = (int) $_POST['event_id'];

}

else

{

/*

* Send the user to the main page if no ID is supplied

*/

header("Location: ./");

exit;

}

/*

* Include necessary files

*/

include_once '../sys/core/init.inc.php';

/*

* Load the calendar

*/

$cal = new Calendar($dbo);

$markup = $cal->confirmDelete($id);

/*

* Output the header

*/

$page_title = "View Event";

$css_files = array("style.css", "admin.css");

include_once 'assets/common/header.inc.php';

?>

<div id="content">

<?php echo $markup; ?>

</div>``<!--``end #content

<?php

/*

* Output the footer

*/

include_once 'assets/common/footer.inc.php';

?>

保存这个文件,然后通过删除“晚宴”条目来测试系统。在向您展示完整的事件描述后,日历会带您进入确认表单(参见图 5-8 )。

A978-1-4842-1230-1_5_Fig8_HTML.jpg

图 5-8。

The confirmation form a user sees after clicking the Delete button

点击“是,删除”按钮后,该事件将从日历中删除(参见图 5-9 )。

A978-1-4842-1230-1_5_Fig9_HTML.jpg

图 5-9。

After the user confirms the deletion, the event is removed from the calendar

摘要

至此,您已经有了一个功能齐全的活动日历。您已经学习了如何创建表单来创建、编辑、保存和删除事件,包括如何确认事件删除。但是,管理控件目前对访问该站点的任何人都可用。

在下一章中,您将构建一个类来授予授权用户对站点管理控件的访问权限。

六、通过密码保护敏感动作和区域

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-1230-1_​6) contains supplementary material, which is available to authorized users.

现在您的应用可以添加、编辑和移除事件,您需要通过要求用户登录才能进行任何更改来保护这些操作。要做到这一点,你需要在数据库中创建一个新表,在应用中创建一个新类;您还需要对现有文件进行一些修改。

在数据库中构建管理表

要存储被授权修改事件的用户的信息,您需要创建一个新的数据库表。这个表将被称为users,它将存储每个用户的四条信息:ID、姓名、密码散列和电子邮件地址。

要创建该表,导航到http://localhost/phpmyadmin并选择 SQL 选项卡执行以下命令:

CREATE TABLE IF NOT EXISTS php-jquery_example.users (

``user_id INT(11) NOT NULL AUTO_INCREMENT,

``user_name VARCHAR(80) DEFAULT NULL,

``user_pass VARCHAR(47) DEFAULT NULL,

``user_email VARCHAR(80) DEFAULT NULL,

PRIMARY KEY (user_id),

UNIQUE (user_name)

) ENGINE=MyISAM CHARACTER SET utf8 COLLATE utf8_unicode_ci;

这段代码执行后,从左栏选择php-jquery_example数据库,点击users表查看新表(见图 6-1 )。

A978-1-4842-1230-1_6_Fig1_HTML.jpg

图 6-1。

The users table as it appears in phpMyAdmin

构建文件以显示登录表单

为了登录,用户需要访问登录表单。这将显示在名为login.php的页面上,该页面存储在公共文件夹(/public/login.php)中。这个文件将类似于admin.php,除了它将简单地输出表单,因为它的信息没有一个是可变的。

该表单将接受用户名和密码,还将传递会话令牌和一个动作user_login。将以下代码插入到login.php中以创建该表单:

<?php

declare(strict_types=1);

/*

* Include necessary files

*/

include_once '../sys/core/init.inc.php';

/*

* Output the header

*/

$page_title = "Please Log In";

$css_files = array("style.css", "admin.css");

include_once 'assets/common/header.inc.php';

?>

<div id="content">

<form action="assets/inc/process.inc.php" method="post">

<fieldset>

<legend>Please Log In</legend>

<label for="uname">Username</label>

<input type="text" name="uname"

id="uname" value="" />

<label for="pword">Password</label>

<input type="password" name="pword"

id="pword" value="" />

<input type="hidden" name="token"

value="<?php echo $_SESSION['token']; ?>" />

<input type="hidden" name="action"

value="user_login" />

<input type="submit" name="login_submit"

value="Log In" />

or <a href="./">cancel</a>

</fieldset>

</form>

</div>``<!--``end #content

<?php

/*

* Output the footer

*/

include_once 'assets/common/footer.inc.php';

?>

保存此代码,并在浏览器中导航至http://localhost/login.php以查看生成的登录表单(参见图 6-2 )。

A978-1-4842-1230-1_6_Fig2_HTML.jpg

图 6-2。

The login form

创建管理类

有了您的表,现在您可以开始构建将与之交互的类了。在class文件夹中,创建一个名为class.admin.inc.php ( /sys/class/class.admin.inc.php)的新文件。这个类将包含允许用户登录和注销的方法。

定义类别

首先,定义一个类,它将扩展DB_Connect来访问数据库。这个类将有一个私有属性,$_saltLength,您将在本节稍后了解到。

构造函数将调用父构造函数以确保数据库对象存在,然后它将检查是否有一个整数作为构造函数的第二个参数传递。如果是,则该整数被用作$_saltLength的值。

现在将以下代码插入到class.admin.inc.php中,以定义类、属性和构造函数:

<?php

declare(strict_types=1);

/**

* Manages administrative actions

*

* PHP version 7

*

* LICENSE: This source file is subject to the MIT License, available

* athttp://www.opensource.org/licenses/mit-license.html

*

* @author     Jason Lengstorf <jason.lengstorf@ennuidesign.com>

* @copyright  2010 Ennui Design

* @licensehttp://www.opensource.org/licenses/mit-license.html

*/

class Admin extends DB_Connect

{

/**

* Determines the length of the salt to use in hashed passwords

*

* @var int the length of the password salt to use

*/

private $_saltLength = 7;

/**

* Stores or creates a DB object and sets the salt length

*

* @param object $db a database object

* @param int $saltLength length for the password hash

*/

public function __construct($db=NULL, $saltLength=NULL)

{

parent::__construct($db);

/*

* If an int was passed, set the length of the salt

*/

if ( is_int($saltLength) )

{

$this->_saltLength = $saltLength;

}

}

}

?>

构建检查登录凭证的方法

需要对来自login.php的数据进行验证,以确认用户有权对events表进行更改。您可以按照这些步骤来完成。

Verify that the form was submitted using the proper action.   Sanitize the user input with htmlentities().   Retrieve user data that has a matching username from the database.   Store the user information in a variable, $user, and make sure it isn’t empty.   Generate a salted hash from the user-supplied password and the password stored in the database.   Make sure the hashes match.   Store user data in the current session using an array and return TRUE.   Note

加盐散列将在下一节“构建创建加盐散列的方法”中讨论

首先在Admin类中定义方法,并使用下面的粗体代码完成前面的步骤 1 和 2:

<?php

declare(strict_types=1);

class Admin extends DB_Connect

{

private $_saltLength = 7;

public function __construct($db=NULL, $saltLength=NULL) {...}

/**

* Checks login credentials for a valid user

*

* @return mixed TRUE on success, message on error

*/

public function processLoginForm()

{

/*

* Fails if the proper action was not submitted

*/

if ( $_POST['action']!='user_login' )

{

return "Invalid action supplied for processLoginForm.";

}

/*

* Escapes the user input for security

*/

$uname = htmlentities($_POST['uname'], ENT_QUOTES);

$pword = htmlentities($_POST['pword'], ENT_QUOTES);

// finish processing...

}

}

?>

接下来,通过添加以粗体显示的以下代码来完成步骤 3 和 4:

public function processLoginForm()

{

/*

* Fails if the proper action was not submitted

*/

if ( $_POST['action']!='user_login' )

{

return "Invalid action supplied for processLoginForm.";

}

/*

* Escapes the user input for security

*/

$uname = htmlentities($_POST['uname'], ENT_QUOTES);

$pword = htmlentities($_POST['pword'], ENT_QUOTES);

/*

* Retrieves the matching info from the DB if it exists

*/

$sql = "SELECT

user_id`, `user_name`, `user_email`, `user_pass

FROM users``

WHERE

``user_name = :uname

LIMIT 1";

try

{

$stmt = $this->db->prepare($sql);

$stmt->bindParam(':uname', $uname, PDO::PARAM_STR);

$stmt->execute();

$user = array_shift($stmt->fetchAll());

$stmt->closeCursor();

}

catch ( Exception $e )

{

die ( $e->getMessage() );

}

/*

* Fails if username doesn’t match a DB entry

*/

if ( !isset($user) )

{

return "Your username or password is invalid.";

}

// finish processing.. .

}

现在,用户的数据存储在变量$user中(或者该方法失败,因为在users表中没有找到与提供的用户名匹配的内容)。

完成步骤 5-7 完成该方法;通过添加以下粗体代码来实现这一点:

public function processLoginForm()

{

/*

* Fails if the proper action was not submitted

*/

if ( $_POST['action']!='user_login' )

{

return "Invalid action supplied for processLoginForm.";

}

/*

* Escapes the user input for security

*/

$uname = htmlentities($_POST['uname'], ENT_QUOTES);

$pword = htmlentities($_POST['pword'], ENT_QUOTES);

/*

* Retrieves the matching info from the DB if it exists

*/

$sql = "SELECT

user_id`, `user_name`, `user_email`, `user_pass

FROM users``

WHERE

``user_name = :uname..

LIMIT 1";

try

{

$stmt = $this->db->prepare($sql);

$stmt->bindParam(':uname', $uname, PDO::PARAM_STR);

$stmt->execute();

$user = array_shift($stmt->fetchAll());

$stmt->closeCursor();

}

catch ( Exception $e )

{

die ( $e->getMessage() );

}

/*

* Fails if username doesn’t match a DB entry

*/

if ( !isset($user) )

{

return "No user found with that ID.";

}

/*

* Get the hash of the user-supplied password

*/

$hash = $this->_getSaltedHash($pword, $user['user_pass']);

/*

* Checks if the hashed password matches the stored hash

*/

if ( $user['user_pass']==$hash ) ..

{

/*

* Stores user info in the session as an array

*/

$_SESSION['user'] = array(

'id' => $user['user_id'],

'name' => $user['user_name'],

'email' => $user['user_email']

);

return TRUE;

}

/*

* Fails if the passwords don’t match

*/

else

{

return "Your username or password is invalid.";

}

}

该方法现在将验证登录表单提交。然而,它还没有完全发挥作用;首先,您需要构建_getSaltedHash()方法。

构建创建加盐散列的方法

为了验证存储在数据库中的用户密码哈希,您需要一个函数来从用户提供的密码生成加盐哈希(哈希是由 MD5 或 SHA1 等安全算法生成的加密字符串)。

Note

有关密码哈希和安全算法的更多信息,请访问 http://en.wikipedia.org/wiki/Cryptographic_hash_function

Increasing Security with Salted Passwords

即使 PHP 提供了散列或加密字符串的功能,您也应该使用额外的安全措施来确保您的信息完全安全。提高安全性的最简单、最有效的方法之一是使用 saltss,salt 是散列密码时使用的附加字符串。

使用彩虹表和普通加密算法

常见的加密算法,如 SHA1 和 MD5,已经使用彩虹表、 1 进行了完全映射,彩虹表是密码哈希的反向查找表。简而言之,彩虹表允许攻击者在一个大表中搜索由给定加密算法产生的散列,该大表包含每个可能的散列和将产生该散列的值。

彩虹表是为 MD5 和 SHA1 生成的,所以如果不采取额外的安全措施,攻击者可以相对容易地破解用户的密码。

使用加盐散列提高安全性

虽然不是刀枪不入,但在哈希算法中加入盐会让攻击者破解用户密码变得更加麻烦。salt 是一个预定义的或随机的字符串,在散列时除了用户输入之外还使用它。

如果不使用 salt,密码可能会被哈希如下:

$hash = sha1($password);

要将随机 salt 添加到前面的散列中,您可以对其应用以下代码:

$salt = substr(md5(time()), 0, 7); // create a random salt

$hash = $salt . sha1($salt . $password);

前面的代码生成一个随机的七位数 salt。在散列之前,salt 被添加到密码字符串的前面;这意味着,即使两个用户拥有相同的密码,他们各自的密码哈希也会不同。

然而,为了再现该散列,盐需要可用。由于这个原因,salt 也以未加密的形式添加到哈希中。这样,当用户登录时,您就能够从数据库检索到的散列中提取 salt,并使用它来重新创建用户密码的 salt 散列:

$salt = substr($dbhash, 0, 7); // extract salt from stored hash

$hash = $salt . sha1($salt . $_POST['password']);

if ( $dbhash==$hash )

{

echo "Match!";

}

else

{

echo "No match.";

}

结合咸哈希和彩虹表

加入盐后,彩虹桌就没用了。需要生成一个新的表,将 salt 考虑在内,以便破解用户密码;虽然这并非不可能,但对攻击者来说非常耗时,而且会给你的应用增加一层额外的安全性。

在大多数应用中(尤其是那些不存储太多敏感个人信息的应用,如信用卡信息),加盐密码足以阻止潜在的攻击者。

作为一种额外的对策,还建议添加对重复失败登录尝试的检查。这样,攻击者在被锁定在系统之外之前有有限次数的尝试来破解密码。这还可以防止拒绝服务攻击(发送大量请求试图使网站过载并使其离线的攻击)。

创建这个函数相对简单,只需要几个步骤。

Check whether a salt was supplied; if not, generate a new salt by hashing the current UNIX timestamp, and then take a substring of the returned value at the length specified in $_saltLength and store it as $salt.   Otherwise, take a substring of the supplied salted hash from the database at the length specified in $_saltLength and store it as $salt.   Prepend the salt to the hash of the salt and the password, and return the new string.  

通过将以下方法插入到Admin类中来完成所有三个步骤:

<?php

declare(strict_types=1);

class Admin extends DB_Connect

{

private $_saltLength = 7;

public function __construct($db=NULL, $saltLength=NULL) {...}

public function processLoginForm() {...}

/**

* Generates a salted hash of a supplied string

*

* @param string $string to be hashed

* @param string $salt extract the hash from here

* @return string the salted hash

*/

private function _getSaltedHash($string, $salt=NULL)

{

/*

* Generate a salt if no salt is passed

*/

if ( $salt==NULL )

{

$salt = substr(md5((string)time()), 0, $this->_saltLength);

}

/*

* Extract the salt from the string if one is passed

*/

else

{

$salt = substr($salt, 0, $this->_saltLength);

}

/*

* Add the salt to the hash and return it

*/

return $salt . sha1($salt . $string);

}

}

?>

为加盐哈希创建测试方法

为了了解加盐散列是如何工作的,为_getSaltedHash()创建一个名为testSaltedHash()的快速测试方法。这将是一个调用并输出值的公共函数,使您能够看到脚本是如何运行的。

Admin类中,定义testSaltedHash()方法:

<?php

declare(strict_types=1);

class Admin extends DB_Connect

{

private $_saltLength = 7;

public function __construct($db=NULL, $saltLength=NULL) {...}

public function processLoginForm() {...}

private function _getSaltedHash($string, $salt=NULL) {...}

public function testSaltedHash($string, $salt=NULL)

{

return $this->_getSaltedHash($string, $salt);

}

}

?>

接下来,添加一个名为test.php的新文件来使用这个函数,并将它放在public文件夹(/public/test.php)中。在这个函数内部,调用初始化文件,创建一个新的Admin类,输出这个单词的三个哈希:test。创建第一个不加盐的 hash,然后休眠一秒钟获得新的时间戳。创建第二个不加盐的 hash,然后再睡一秒。最后,使用第二个散列中的 salt 创建第三个散列。插入以下代码来完成该测试:

<?php

declare(strict_types=1);

// Include necessary files

include_once '../sys/core/init.inc.php';

// Load the admin object

$obj = new Admin($dbo);

// Load a hash of the word test and output it

$hash1 = $obj->testSaltedHash("test");

echo "Hash 1 without a salt:<br />", $hash1, "<br /><br />";

// Pause execution for a second to get a different timestamp

sleep(1);

// Load a second hash of the word test

$hash2 = $obj->testSaltedHash("test");

echo "Hash 2 without a salt:<br />", $hash2, "<br /><br />";

// Pause execution for a second to get a different timestamp

sleep(1);

// Rehash the word test with the existing salt

$hash3 = $obj->testSaltedHash("test", $hash2);

echo "Hash 3 with the salt from hash 2:<br />", $hash3;

?>

Note

sleep()函数将脚本的执行延迟给定的秒数,作为唯一的参数传递。您可以在 http://php.net/sleep 了解该功能的更多信息。

您的结果不会完全相同,因为用于 salt 的时间戳哈希会有所不同;但是,您的结果应该看起来像这样:

Hash 1 without a salt:

fa260349138b9541c4b2895aeb0f0effe490194f4ef6c30

Hash 2 without a salt:

8d130612280d9e54c5fa4558cf80fac18a0514456dc11dc

Hash 3 with the salt from hash 2:

8d130612280d9e54c5fa4558cf80fac18a0514456dc11dc

如您所见,单词 test 的散列在单独通过时并不匹配;但是,如果您提供 test 的现有 salted 散列,则会生成相同的散列。这样,即使两个用户有相同的密码,他们存储的哈希值也会不同,这使得潜在的攻击者更难破解密码。

Note

请记住,没有任何算法是 100%有效的。然而,使用像加盐散列这样的技术可以显著降低攻击的可能性。

创建用户以测试管理访问权限

为了测试管理功能,您需要在您的users表中存在一个用户名/密码对。为简单起见,用户名为testuser,密码为admin,电子邮件地址为admin@example.com。请记住,这不是一个安全密码;它仅用于说明目的,在您将其用于任何生产脚本之前,应该对其进行更改。

首先生成密码的散列值admin,使用测试方法testSaltedHash()test.php很容易做到这一点。将以下粗体代码添加到test.php中,以生成测试用户密码的加盐散列:

<?php

declare(strict_types=1);

// Include necessary files

include_once '../sys/core/init.inc.php';

// Load the admin object

$obj = new Admin($dbo);

// Generate a salted hash of "admin"

$pass = $obj->testSaltedHash("admin");

echo 'Hash of "admin":<br />', $pass, "<br /><br />";

?>

导航到http://localhost/test.php,您将看到类似如下的输出:

Hash of "admin":

0c6c835a48f6c5577e322f49c96ce0c719a8c272d4d8609

复制散列,导航到http://localhost/phpmyadmin,然后单击 SQL 选项卡。执行以下查询,将测试用户插入表中:

INSERT INTO php-jquery_example.users``

(user_name, user_pass, user_email)

VALUES

(

'testuser',

'a1645e41f29c45c46539192fe29627751e1838f7311eeb4',

'admin@example.com'

);

执行上述代码后,单击php-jquery_example数据库,然后单击users表。选择浏览选项卡查看表格中的用户信息(参见图 6-3 )。

A978-1-4842-1230-1_6_Fig3_HTML.jpg

图 6-3。

The test user data after inserting it into the database

现在用户已经存在于用户数据库中,并且存储了一个 salted hash,您可以从Admin类中删除testSaltedHash()方法和整个test.php文件。

修改应用以处理登录表单提交

至此,您差不多已经准备好测试用户登录了。然而,在它工作之前,您需要修改process.inc.php来处理来自登录表单的表单提交。

由于文件设置的方式,这种改变就像向ACTIONS数组添加一个新元素一样简单。打开process.inc.php并插入以下粗体代码:

<?php

declare(strict_types=1);

/*

* Enable sessions if needed.

* Avoid pesky warning if session already active.

*/

$status = session_status();

if ($status == PHP_SESSION_NONE){

//There is no active session

session_start();

}

/*

* Include necessary files

*/

include_once '../../../sys/config/db-cred.inc.php';

/*

* Define constants for config info

*/

foreach ( $C as $name => $val )

{

define($name, $val);

}

/*

* Create a lookup array for form actions

*/

define(ACTIONS, array(

'event_edit' => array(

'object' => 'Calendar',

'method' => 'processForm',

'header' => 'Location: ../../'

),

'user_login' => array(

'object' => 'Admin',

'method' => 'processLoginForm',

'header' => 'Location: ../../'

)

)

);

/*

* Need a PDO object.

*/

$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME;

$dbo = new PDO($dsn, DB_USER, DB_PASS);

/*

* Make sure the anti-CSRF token was passed and that the

* requested action exists in the lookup array

*/

if ( $_POST['token']==$_SESSION['token']

&& isset(ACTIONS[$_POST['action']]) )

{

$use_array = ACTIONS[$_POST['action']];

$obj = new $use_array'object';

$method = $use_array['method'];

if ( TRUE === $msg=$obj->$method() )

{

header($use_array['header']);

exit;

}

else

{

// If an error occured, output it and end execution

die ( $msg );

}

}

else

{

// Redirect to the main index if the token/action is invalid

header("Location: ../../");

exit;

}

function __autoload($class_name)

{

$filename = '../../../sys/class/class.'

. strtolower($class_name) . '.inc.php';

if ( file_exists($filename) )

{

include_once $filename;

}

}

?>

现在您可以正式测试登录了。因为还没有对登录进行检查,所以只需在文件中插入以下粗体行,在index.php中添加一个条件注释来显示登录或注销状态:

<?php

declare(strict_types=1);

/*

* Include necessary files

*/

include_once '../sys/core/init.inc.php';

/*

* Load the calendar

*/

$cal = new Calendar($dbo, "2016-01-01 12:00:00");

/*

* Set up the page title and CSS files

*/

$page_title = "Events Calendar";

$css_files = array('style.css', 'admin.css');

/*

* Include the header

*/

include_once 'assets/common/header.inc.php';

?>

<div id="content">

<?php

/*

* Display the calendar HTML

*/

echo $cal->buildCalendar();

?>

</div><!-- end #content -->

<p>

<?php

echo isset($_SESSION['user']) ? "Logged In!" : "Logged Out!";

?>

</p>

<?php

/*

* Include the footer

*/

include_once 'assets/common/footer.inc.php';

?>

现在保存这个文件并导航到http://localhost/以查看“注销!”日历下方的消息(见图 6-4 )。

A978-1-4842-1230-1_6_Fig4_HTML.jpg

图 6-4。

Before the user logs in, the “Logged Out!” message appears below the calendar

接下来,导航到http://localhost/login.php,输入用户名testuser和密码admin(见图 6-5 )。

A978-1-4842-1230-1_6_Fig5_HTML.jpg

图 6-5。

The login form with the username and password information entered

单击登录按钮后,您将被重定向回日历;然而,现在日历下面的消息将显示“已登录!”(参见图 6-6 )。

A978-1-4842-1230-1_6_Fig6_HTML.jpg

图 6-6。

After the user logs in, the “Logged In!” message appears below the calendar

允许用户注销

接下来,您需要添加一个允许用户注销的方法。您将使用一个表单向process.inc.php提交信息。Calendar类中的方法_adminGeneralOptions()生成表单。

将注销按钮添加到日历

要添加允许用户注销的按钮,请修改Calendar类中的_adminGeneralOptions()。除了提供添加新事件的按钮之外,这个方法现在还将输出一个表单,该表单将站点令牌和一个动作值user_logout提交给process.inc.php。打开Calendar类,用下面的粗体代码修改_adminGeneralOptions():

private function _adminGeneralOptions()

{

/*

* Display admin controls

*/

return <<<ADMIN_OPTIONS

<a href="admin.php" class="admin">+ Add a New Event</a>

<form action="assets/inc/process.inc.php" method="post">

<div>

<input type="submit" value="Log Out" class="logout" />

<input type="hidden" name="token"

value="$_SESSION[token]" />

<input type="hidden" name="action"

value="user_logout" />

</div>

</form>

ADMIN_OPTIONS;

}

现在保存更改并刷新浏览器中的http://localhost/以查看新按钮(参见图 6-7 )。

A978-1-4842-1230-1_6_Fig7_HTML.jpg

图 6-7。

The Log Out button as it appears after you modify the Calendar class

创建处理注销的方法

为了处理注销,需要将一个名为processLogout()的新公共方法添加到Admin类中。该方法进行快速检查以确保提供了正确的操作user_logout,然后使用session_destroy()通过完全销毁当前会话来删除用户数据数组。

通过插入以下粗体代码,将该方法添加到Admin类中:

<?php

declare(strict_types=1);

class Admin extends DB_Connect

{

private $_saltLength = 7;

public function __construct($db=NULL, $saltLength=NULL) {...}

public function processLoginForm() {...}

/**

* Logs out the user

*

* @return mixed TRUE on success or messsage on failure

*/

public function processLogout()

{

/*

* Fails if the proper action was not submitted

*/

if ( $_POST['action']!='user_logout' )

{

return "Invalid action supplied for processLogout.";

}

/*

* Removes the user array from the current session

*/

session_destroy();

return TRUE;

}

private function _getSaltedHash($string, $salt=NULL) {...}

}

?>

修改应用以处理用户注销

在用户可以成功注销之前,您需要做的最后一步是向process.inc.php中的ACTIONS数组添加另一个数组元素。将以下粗体代码插入process.inc.php以完成注销过程:

<?php

declare(strict_types=1);

/*

* Enable sessions if needed.

* Avoid pesky warning if session already active.

*/

$status = session_status();

if ($status == PHP_SESSION_NONE){

//There is no active session

session_start();

}

/*

* Include necessary files

*/

include_once '../../../sys/config/db-cred.inc.php';

/*

* Define constants for config info

*/

foreach ( $C as $name => $val )

{

define($name, $val);

}

/*

* Create a lookup array for form actions

*/

define(ACTIONS, array(

'event_edit' => array(

'object' => 'Calendar',

'method' => 'processForm',

'header' => 'Location: ../../'

),

'user_login' => array(

'object' => 'Admin',

'method' => 'processLoginForm',

'header' => 'Location: ../../'

),

'user_logout' => array(

'object' => 'Admin',

'method' => 'processLogout',

'header' => 'Location: ../../'

)

)

);

/*

* Make sure the anti-CSRF token was passed and that the

* requested action exists in the lookup array

*/

if ( $_POST['token']==$_SESSION['token']

&& isset(ACTIONS[$_POST['action']]) )

{

$use_array = ACTIONS[$_POST['action']];

$obj = new $use_array'object';

$method = $use_array['method'];

if ( TRUE === $msg=$obj->$method() )

{

header($use_array['header']);

exit;

}

else

{

// If an error occured, output it and end execution

die ( $msg );

}

}

else

{

// Redirect to the main index if the token/action is invalid

header("Location: ../../");

exit;

}

function __autoload($class_name)

{

$filename = '../../../sys/class/class.'

. strtolower($class_name) . '.inc.php';

if ( file_exists($filename) )

{

include_once $filename;

}

}

?>

保存该文件,然后导航至http://localhost/,并点击日历底部的注销按钮。点按此按钮会导致日历下方的消息显示为“已注销!”(参见图 6-8 )。

A978-1-4842-1230-1_6_Fig8_HTML.jpg

图 6-8。

Clicking the Log Out button removes the user data from the session Note

即使我们已经确定登录正在运行,我们仍然会继续显示登录!/已注销!在我们工作的过程中。但是如果您愿意,您可以从index.php中删除消息逻辑和包含它的段落标记。

仅向管理员显示管理工具

您的用户可以登录和注销;您需要采取的最后一个步骤是确保所有需要管理访问权限的操作和选项只显示给已登录的用户。

向管理员显示管理选项

除非用户登录,否则不应显示用于添加和编辑事件的按钮。为了执行这个检查,您需要修改Calendar类中的_adminGeneralOptions()_adminEntryOptions()方法。

修改常规管理选项方法

现在让我们看看日历的常规选项。如果用户已经登录,您希望向她显示创建新条目和注销的选项。

但是,如果用户被注销,她应该会看到一个登录链接。通过对Calendar类中的_adminGeneralOptions()方法进行粗体显示的修改来执行该检查:

<?php

declare(strict_types=1);

class Calendar extends DB_Connect

{

private $_useDate;

private $_m;

private $_y;

private $_daysInMonth;

private $_startDay;

public function __construct($db=NULL, $useDate=NULL) {...}

public function buildCalendar() {...}

public function displayForm() {...}

public function processForm() {...}

public function confirmDelete($id) {...}

private function _loadEventData($id=NULL) {...}

private function _createEventObj() {...}

private function _loadEventById($id) {...}

private function _adminGeneralOptions()

{

/*

* If the user is logged in, display admin controls

*/

if ( isset($_SESSION['user']) )

{

return <<<ADMIN_OPTIONS

<a href="admin.php" class="admin">+ Add a New Event</a>

<form action="assets/inc/process.inc.php" method="post">

<div>

<input type="submit" value="Log Out" class="logout" />

<input type="hidden" name="token"

value="$_SESSION[token]" />

<input type="hidden" name="action"

value="user_logout" />

</div>

</form>

ADMIN_OPTIONS;

}

else

{

return <<<ADMIN_OPTIONS

<a href="login.php">Log In</a>

ADMIN_OPTIONS;

}

}

private function _adminEntryOptions($id) {...}

}

?>

保存更改后,注销时重新加载http://localhost/以查看管理选项被一个简单的Log In链接取代(见图 6-9 )。

A978-1-4842-1230-1_6_Fig9_HTML.jpg

图 6-9。

While a user is logged out, a Log In link is displayed in the lower left corner

修改事件选项方法

接下来,您希望添加代码来防止未经授权的用户编辑和删除事件;您可以通过用以下粗体代码修改Calendar类中的_adminEventOptions()来实现这一点:

<?php

declare(strict_types=1);

class Calendar extends DB_Connect

{

private $_useDate;

private $_m;

private $_y;

private $_daysInMonth;

private $_startDay;

public function __construct($db=NULL, $useDate=NULL) {...}

public function buildCalendar() {...}

public function displayForm() {...}

public function processForm() {...}

public function confirmDelete($id) {...}

private function _loadEventData($id=NULL) {...}

private function _createEventObj() {...}

private function _loadEventById($id) {...}

private function _adminGeneralOptions() {...}

private function _adminEntryOptions($id)

{

if ( isset($_SESSION['user']) )

{

return <<<ADMIN_OPTIONS

<div class="admin-options">

<form action="admin.php" method="post">

<p>

<input type="submit" name="edit_event"

value="Edit This Event" />

<input type="hidden" name="event_id"

value="$id" />

</p>

</form>

<form action="confirmdelete.php" method="post">

<p>

<input type="submit" name="delete_event"

value="Delete This Event" />

<input type="hidden" name="event_id"

value="$id" />

</p>

</form>

</div><!-- end .admin-options -->

ADMIN_OPTIONS;

}

else

{

return NULL;

}

}

}

?>

插入这些更改后,在注销时导航至http://localhost/,并点击一个事件以调出其完整视图;将不显示管理选项(参见图 6-10 )。

A978-1-4842-1230-1_6_Fig10_HTML.jpg

图 6-10。

The full event view while logged out

限制对管理页面的访问

作为额外的安全预防措施,您应该确保只有授权用户才能访问的任何页面,如事件创建/编辑表单,在执行之前检查是否有适当的授权。

不允许在未登录的情况下访问事件创建表单

通过执行添加到文件中的简单检查,可以防止恶意用户在注销时找到事件创建表单。如果用户没有登录,在脚本有机会执行之前,他将被发送到主日历视图。

要实现这一更改,打开admin.php并插入粗体显示的代码:

<?php

declare(strict_types=1);

/*

* Include necessary files

*/

include_once '../sys/core/init.inc.php';

/*

* If the user is not logged in, send them to the main file

*/

if ( !isset($_SESSION['user']) )

{

header("Location: ./");

exit;

}

/*

* Output the header

*/

$page_title = "Add/Edit Event";

$css_files = array("style.css", "admin.css");

include_once 'assets/common/header.inc.php';

/*

* Load the calendar

*/

$cal = new Calendar($dbo);

?>

<div id="content">

<?php echo $cal->displayForm(); ?>

</div><!-- end #content -->

<?php

/*

* Output the footer

*/

include_once 'assets/common/footer.inc.php';

?>

保存该文件后,尝试在注销时导航至http://localhost/admin.php。你会被自动发送到http://localhost/

确保只有登录的用户才能删除事件

此外,为了防止未经授权的用户删除事件,在confirmdelete.php文件中插入一个有效用户会话检查:

<?php

declare(strict_types=1);

/*

* Enable sessions if needed.

* Avoid pesky warning if session already active.

*/

$status = session_status();

if ($status == PHP_SESSION_NONE){

//There is no active session

session_start();

}

/*

* Make sure an event ID was passed and the user is logged in

*/

if ( isset($_POST['event_id'])``&&

{

/*

* Collect the event ID from the URL string

*/

$id = (int) $_POST['event_id'];

}

else

{

/*

* Send the user to the main page if no ID is supplied

* or the user is not logged in

*/

header("Location: ./");

exit;

}

/*

* Include necessary files

*/

include_once '../sys/core/init.inc.php';

/*

* Load the calendar

*/

$cal = new Calendar($dbo);

$markup = $cal->confirmDelete($id);

/*

* Output the header

*/

$page_title = "View Event";

$css_files = array("style.css", "admin.css");

include_once 'assets/common/header.inc.php';

?>

<div id="content">

<?php echo $markup; ?>

</div><!-- end #content -->

<?php

/*

* Output the footer

*/

include_once 'assets/common/footer.inc.php';

?>

现在保存这段代码,并尝试在注销时直接访问http://localhost/confirmdelete.php。不出所料,您将被重定向到http://localhost/

摘要

在本章中,您学习了如何将用户授权添加到您的日历应用,这意味着现在只有授权用户才能修改日历。您了解了如何创建Admin类、检查登录凭证、仅向管理员显示管理工具以及限制对管理页面的访问。

在下一章中,您将开始将 jQuery 集成到应用中,以逐步增强用户体验。

Footnotes 1

http://en.wikipedia.org/wiki/Rainbow_table