一、 Introduction
主要关于网络应用中使用php实现会话控制、编写验证码、文件上传下载等。
二、 会话控制技术
1. 会话控制的需求
当用户通过正确的用户名和密码等登录信息,登录到网站后,那么需要有一个标识能够标明这个用户是处于登录状态。特别是在商城网站中,某用户的购物流程只能是在这个用户的帐号登录状态下进行,否则就会混乱和出错。这个标识也是一个数据。重点是存储这个数据的方式,能够帮助标识这个用户在和网站交互期间的登录状态。一般的,存储数据的方式有变量、常量、数据库、文件等。
变量、常量、static 变量的脚本周期,在跳转时周期结束,变量就销毁了,变量不能传到下个脚本周期。所以,不适合作为存储标识的方式。
使用文件或数据库存储,不便于辨识不同的浏览器下登录到网站,会造成登录漏洞。
结合各种情况,登录标识的存储需要在同一个浏览器器的多次请求周期内,持久存在的数据。这就是会话技术。
2. 会话技术
B/S框架基于http协议进行数据交互,http的请求/响应,是独立的,每次请求响应周期都是完全独立的。会话技术,就是将数据在多次请求周期内,产生的一些需求在一定周期内持续使用的数据,传递存储下来的技术。会话控制技术,主要有两种实现方式,cookie和session。
3. cookie技术
允许服务器端程序在浏览器存储数据的技术,由服务器脚本决定存储的内容,数据存储后,在浏览器向该服务器发送请求时,会携带该服务器所存储的数据。
php对cookie的操作。
设置,setcookie(键, 值);
获取,当php核心程序,接受到浏览器携带的cookie数据时,整理到超全局变量:$_COOKIE中。每个元素,就是一个cookie数据,元素键是cookie名,元素值是cookie值。
Cookie的有效期属性,默认下,浏览器关闭(即会话周期结束)时,cookie失效,也称为会话cookie,临时cookie。可以设置cookie的有效期,语法上,使用setcookie的第三个参数进行设置,通过unix时间戳(从1970年到现在的秒数)来设置cookie的有效期。Php获取时间戳的函数是time();
设置有效期的例子:setcookie(‘long_time’, ‘after-3600s’, time()+600);。
setcookie()函数中时间戳参数的特殊值,0,默认值,表示临时cookie,比如,setcookie(‘login’, ‘yes’, 0);;PHP_INT_MAX常量值,php所能表示的最大整型,时间戳是个整型,也是php能表示的最大时间戳,一般用于表示最大值,比如,setcookie(‘login’, ‘yes’, PHP_INT_MAX);;time()-1,用于删除cookie,强制设为过期,比如,比如,setcookie(‘login’, ‘yes’, time()-1);;。
PHP中,将setcookie的第二个参数设置为空字符串,也可以删除cookie。比如,setcookie(‘login’, ‘’);
Cookie的有效路径,默认,在当前目录及其后代目录有效。可以设置cookie的有效路径,在setcookie的第四个参数设置,比如,setcookie(‘sub_cookie’, ‘sub’, 0, ‘/’);
Cookie的有效域,某个域名下设置的cookie,仅仅可以在当前域名下使用。Cookie,支持在一级域名内,所有的二级域名之间进行cookie数据共享。使用setcookie的第5个参数进行设置,比如,setcookie(‘login’, ‘yes’, 0, ‘’, ‘.php.com’);表示所有php.com下的子域名都有效。
设置是否安全连接(https)传输,告诉浏览器,设置的cookie是否仅仅在https这个协议下,才会被发送到浏览器端。使用setcookie的第六个参数,默认值是false,表示不是仅仅在https协议下。
通过HTTPONLY,设置的cookie是否仅仅在http请求时所使用。如果需要禁止,浏览器端其他脚本使用该cookie,则使用第7个参数。比如,
<?php
setcookie('secure_no', 'n', 0,'', '.php2.com', false, true);
var_dump($_COOKIE);
?>
<hr />
<button onclick='a();'>get cookie</button>
<script>
function a(){
alert(document.cookie);
}
</script>
setcookie()的参数,setcookie(键,值,有效期=0,有效路径=’’,有效域名=’’,是否安全连接传输=false,是否HTTPONLY=false);
注意:
Cookie仅仅支持字符串类型数据,可以转换成字符串的数据会执行自动类型转换,比如,true,数字,但是false,数组等是不可以作为值的,可以使用serialize()将非字符串转成字符串。比如:
<?php
setcookie('secure_no', 'n', 0,'', '.php2.com', false, true);
setcookie('int', serialize(32));
setcookie('boolt', serialize(false));
setcookie('boolf', serialize(true));
setcookie('array', serialize(array('php', 'cookie')));
var_dump(unserialize($_COOKIE['int']));echo "<br />";
var_dump(unserialize($_COOKIE['boolt']));echo "<br />";
var_dump(unserialize($_COOKIE['boolf']));echo "<br />";
var_dump(unserialize($_COOKIE['array']));echo "<br />";
?>
$COOKIE仅仅用来获得浏览器存储的cookie,就是已经存储到浏览器的cookie。
4. session技术
cookie技术有它的劣势,因为会话数据存储在浏览器端,这就降低了会话数据的安全性,浏览器会限制cookie的大小和数据,不适合量大的会话数据的传输。
Session中,会话数据存储在服务器端,为了区分不同的浏览器来存储会话数据,在服务器端,建立很多的会话数据区,为每个session会话数据区分配唯一标识,将该唯一标识,分配给对应的会话浏览器。
1) 基本操作:
开启session机制,session_start();
操作session数据,操作$_SESSION,类似操作一个数组,比如:
<?php
session_start();
//添加session数据
$_SESSION['user'] = 'haha';
$_SESSION['gender'] = 'nan';
//修改
$_SESSION['user'] = 'heihei';
//删除
unset($_SESSION['user']);
//获取
var_dump($_SESSION['gender']);
var_dump(isset($_SESSION['gender']));
?>
注意:使用session前需要先开启session。
Session技术基于cookie技术,因为生成的session都有以cookie存储的sessionid,以保证session的唯一性。默认,每个session会话数据区,就是一个独立的文件。存储于服务器所在操作系统的临时目录中,C:\Windows\Temp。如果有多个会话,则会出现多个不同id为文件名的文件。
使用session实现登录标识:
首先,在登录成功后,在控制器的验证登陆的方法中,分配登陆标识
function CheckLoginAction(){
$u =$_POST['username'];
$p =$_POST['password'];
$model =ModelFactory::MF('AdminModel');
$result = $model->CheckAdmin($u, $p);
if($result ===true){
//echo "登录成功";
session_start();
$_SESSION['login']= 'yes';
header('Location:index.php?p=back&c=Admin&a=Index');
}else{
//失败后,提示并跳转到登录界面
$this->GotoUrl("登录失败","?p=back&c=Admin&a=Login", 2);
}
}
然后,在控制器的跳转到首页的方法中,验证session
function IndexAction(){
session_start();
if(!isset($_SESSION['login'])){
//判断登陆标识,没有标识,就跳转到登陆页面
header('Location:index.php?p=back&c=Admin&a=Login');
die();
}else{
echo "后台登录成功,请使用";
}
}
在实际开发中是使用管理员信息作为登录标识,因为后台管理人员的权限不同的。为了区分不同的管理员的登录,需要使用管理员信息。Session可以存储任何类型的数据,包括数组。
首先,分配标识
在控制器层
function CheckLoginAction(){
$u =$_POST['username'];
$p =$_POST['password'];
$model =ModelFactory::MF('AdminModel');
//$result = $model->CheckAdmin($u, $p);
//当管理员信息合法时,返回管理员信息,一个数组,当管理员信息非法时,返回false
$admin_info = $model->CheckAdminInfo($u, $p);
//非空数组可以自动转换为bool型的true
if($admin_info){
//echo "登录成功";
session_start();
$_SESSION['admin_info']= $admin_info;
//通过$_SESSION['admin_info']['admin_name']可以得到管理员姓名;
header('Location:index.php?p=back&c=Admin&a=Index');
}else{
//失败后,提示并跳转到登录界面
$this->GotoUrl("登录失败","?p=back&c=Admin&a=Login", 2);
}
}
在模型层
/**
*校验管理员是否合法
*@param $u 管理员名
*@param $p 管理员密码
*@return mixed array,合法,管理员信息数组;false,非法
*/
public functionCheckAdminInfo($u, $p){
$sql = "select* from admin_user where admin_name='$u' and admin_pass=md5('$p');";
//echo $sql;
return $this->dao ->GetOneRow($sql);
}
判断标识,并且带有展示管理员信息的输出
function IndexAction(){
session_start();
//var_dump($_SESSION['admin_info']);
if(!isset($_SESSION['admin_info'])){
//判断登陆标识,没有标识,就跳转到登陆页面
header('Location:index.php?p=back&c=Admin&a=Login');
die();
}else{
echo "后台登录成功,请使用";
echo"<hr />";
echo "欢迎". $_SESSION['admin_info']['admin_name'] . "使用后台";
}
}
2) 关于session技术的注意事项:
Session是多数据类型支持的,比如:
$_SESSION['int'] = 32;
$_SESSION['float'] = 4.2;
$_SESSION['string'] = 'abc';
$_SESSION['bool'] = false;
$_SESSION['array'] = array('php' =>'haha');
class A{
private $p;
}
$_SESSION['object'] = new A();
$_SESSION['null'] = NULL;
var_dump($_SESSION);
注意:在session中存储对象时,获取该对象时,需要找到对象对应的类。
支持多种类型的原因是,数据在session会话数据区,采用序列化的方式进行存储,所以在获取session的时候可以采用反序列化获取对应的类型数据。
每次使用session,都需要先开启,php支持自动开启session机制,配置php.ini中session.auto_start = 0,为不自动,设置为1,就是自动。一般,不自动开启。
重复开启session,会报错。一旦重复开启,后边的开启会被忽略,但是,会触发一个notice级的错误。通常在开启session时,增加@可以屏蔽报错,比如@session_start();
Session的一些属性:
Session的有效期,默认是关闭浏览器。
有效域名,默认仅在当前域名下有效。
是否仅安全连接传输,默认为非。
是否HTTPONLY,默认为非。
Cookie中存储的sessionid决定了session的属性。设置cookie中的sessionid的变量属性就可以设置session的属性,设置方法为:
方法1,配置变量php.ini。
session.cookie_lifetime= 0
session.cookie_path= /
session.cookie_domain=
session.cookie_httponly=
方法2,在php脚本中,开启session之前使用函数进行配置,使用ini_set(配置项名,值);,比如:
ini_set('session.cookie_lifetime', '60');
ini_set('session.cookie_domain', 'php2.com');
session_start();
也可以使用session_set_cookie_params(有效期,有效路径,有效域,是否安全连接传输,是否HTTPONLY);比如,session_set_cookie_params(50, ‘/’, ‘php.com’);使用该方法,仅仅影响当前脚本周期,是常用的方法。
实际生产中,很少修改session的有效期,经常改的是有效域名。
3) 重写session的存储机制
默认下,session数据区是以文件的形式存储在服务器操作系统的临时目录中的。当session数据区数据过多时,文件形式的存储,操作速度变慢,因为磁盘的读写开销比较大。实际开发中,都会使用其他方法更快地存储session数据,典型的方法是数据库和内存。
比如,以数据库存储为例,session数据的入库。
需要重写与session数据区直接的相关操作,最基本的操作是读和写。需要定义读和写的函数。然后告知session机制,在需要读写时,使用用户自定义的读写函数完成。
首先,设计一个session表,这个表中,每条记录就是一个session数据区,相当于原来的一个session文件。
create table session(
sess_id varchar(40) primary key not null,
sess_content text
);
然后,自定义函数
关于垃圾回收,如果一个session数据区已经超过一定时间没有使用了,就是被视为垃圾数据,这个时间临界点默认是1440s,也可以配置。在php.ini中session.gc_maxlifetime = 1440。配合最后写入的时间,就可以断定为是否为垃圾。那么,在session表中需要增加一个字段,记录最后写入时间。alter table session add column last_write int not null default 0;
在session_start()过程中,开启session机制过程中,有几率地执行垃圾回收操作,一旦执行,就会删除所有的过期的垃圾数据区。默认的概率为:1/1000,可以在php.ini中配置,session.gc_probability = 1 session.gc_divisor = 1000。也可以使用ini_set(‘session.gc_probability’, ‘1’); ini_set(‘session.gc_divisor’, ‘3’);设置。实现sessionGC()函数,php的session机制会将最大有效期作为参数,传递过来,比如1440。
然后,告知session机制,在需要读写时,使用用户自定义的读写函数完成。
session_set_save_handler(
开始函数,结束函数,读函数,写函数,删除函数,GC函数
);//用来将用户自定义的函数,设置成session存储相关的函数
这个函数,仅仅是设置告知,而不是调用,在session机制运行到某个时间点时,才会被调用。
常规使用session,开启session机制,当运行到某个时间点时,就会调用相应的函数。
比如:
<?php
/**
* 读操作需要的函数
* @param string $sess_id 当前对话的sessionid
* @return string 当前session数据区内容
*/
function sessionRead($sess_id){
echo '<br />read';
$sql = "selectsess_content from session where sess_id = '$sess_id';";
$result =mysql_query($sql);
$row =mysql_fetch_assoc($result);
if($row){
return$row['sess_content'];
}else{
return '';
}
}
/**
* 写操作需要的函数
* @param string sess_id
* @param string sess_content,这是session序列化处理好的session数据
* @return
*/
function sessionWrite($sess_id, $sess_content){
$sql = "replace intosession values ('$sess_id', '$sess_content', unix_timestamp())";
mysql_query($sql);
echo '<br />write';
}
/**
* 删除对应的session数据,在用户强制执行了session_destroy()时调用
* @param string sess_id
* @return bool
*/
function sessionDelete($sess_id){
echo '<br />del';
$sql = "delete fromsession where sess_id = '$sess_id';";
return mysql_query($sql);
}
/**
* 垃圾回收,有几率执行
* @return
*/
function sessionGC($maxlifetime){
echo '<br />gc';
$sql = "delete fromsession where last_write < unix_timestamp() - $maxlifetime;";
return mysql_query($sql);
}
/**
* 初始化工作,比如其他函数需要连接数据库的,可以在begin函数中实现
* @return
*/
function sessionBegin(){
echo '<br />begin';
mysql_connect('127.0.0.1:3306','root', '123456');
mysql_query('set namesutf8');
mysql_query('use php1');
}
/**
* 结尾性工作,一般return true
* @return
*/
function sessionEnd(){
echo '<br />end';
return true;
}
//设置session存储处理器函数
session_set_save_handler(
'sessionBegin','sessionEnd', 'sessionRead', 'sessionWrite', 'sessionDelete', 'sessionGC'
)
?>
测试的例子:
<?php
require './session.db.php';
ini_set('session.gc_probability', '1');
ini_set('session.gc_divisor', '3');
ini_set('session.gc_maxlifetime', 30);
//ini_set('session.cookie_lifetime', '60');
//ini_set('session.cookie_domain', 'php2.com');
session_start();
echo '<br />';
var_dump($_SESSION);
session_destroy();
?>
注意:session_set_save_handler()先于session_start()被调用。不要自动开启session。在php.ini中session.auto_start = 0。关于php.ini中的配置项session.save_handler,默认值为files,一般配置为user,表示为用户自定义。
4) 关于session的相关配置的表格
Session.save_handler |
存储处理器 |
Files|user |
Session.save_path |
存储地址 |
|
Session.cookie_xxx |
关于sessionid的cookie的设置 |
Lifetime,path,domain,secure,httponly |
Session.gc_maxlifetime |
|
|
Session.gc_probablity |
|
|
Session.gc_divisor |
|
|
5) session和cookie的联系和区别
联系,都是会话技术,session基于cookie,sessionid存储于cookie中。
区别,如表:
|
Cookie |
Session |
存储位置 |
浏览器端 |
服务器端 |
安全性 |
低 |
高 |
大小限制 |
低 |
高 |
大小限制 |
有 |
没有 |
数据类型 |
字符串 |
全部 |
有效期使用 |
长时间存储 |
几乎不做持久化 |
Session的持久化,需要设置sessionid的时间,session_set_cookie_params(6000);,同时还有需要设置垃圾回收时间,ini_set(‘session.gc_maxlifetime’, 6000);
理论上,如果cookie被禁用,sessionid不能存储和传输,session也不可用。实际上,通过url或者post数据向服务器端,每次传输sessionid。需要在php.ini中配置,session.use_only_cookies = 0 session.use_trans_sid = 1。比如:
<?php
ini_set('session.use_only_cookies', '0');
ini_set('session.use_trans_sid', '1');
session_start();
?>
<form action = 'session_no_cookies' method = 'post'>
<input type = 'submit'value = 'ok' />
</form>
<a href = 'session_no_cookies' >ok</a>
在浏览器中设置不接收cookie,进行测试。
三、 项目中实现集中登陆校验
因为有时候后台视图是由frame框架完成的,或者后台中几乎所有的操作都需要校验登陆与否,如果在每个操作中都进行验证,将是大的工作量,而且给维护带来困难,所以采用平台控制器,将平台中共同的操作或者业务逻辑抽离出来到平台控制器,以便统一管理这个共同的业务逻辑,降耦合增内聚。
然后在实例化后台控制器对象时,完成平台控制器中校验方法的调用。这个调用需要在平台控制器类的构造方法中完成。
比如,
<?php
/**
* 后台管理首页相关控制器
*/
class PlatformController extends BaseController{
public function__construct(){
//强制调用父类被重写的构造方法
parent::__construct();
//校验是否登陆
$this ->_check();
}
/**
* 校验是否登陆
*/
protected function_check(){
session_start();
//判断当前请求是否是特例,是否需要校验
//获取当前的控制器,和当前的动作
$curr_controller =strtolower(CONTROLLER);
$curr_action =strtolower(ACTION);
if($curr_controller== 'admin' && ($curr_action == 'login' || $curr_action == 'checklogin')){
//echo$curr_action . $curr_controller;
return ;
}
if(!isset($_SESSION['admin_info'])){
//判断登陆标识,没有标识,就跳转到登陆页面
header('Location:index.php?p=back&c=Admin&a=Login');
die();
}
}
}
?>
其中使用的常量CONTROLLER和ACTION是在前端控制器中定义好的。然后,后台控制器,直接继承平台控制器,就可以实现集中校验登陆。
class AdminController extends PlatformController{
……
}
四、 使用cookie实现记录登录状态功能
一般,在登陆界面会有是否自动登陆或者是否保存这次登陆信息类似的单选项,勾选后,在一定时间内可以实现免登陆的功能。这个功能的实现,一般是使用cookie记录登陆状态,然后加密id和密码保存在cookie中,
首先,在控制器的验证登陆方法中,判断登陆状态,是否记录登陆信息。
function CheckLoginAction(){
$u =$_POST['username'];
$p =$_POST['password'];
$model =ModelFactory::MF('AdminModel');
//$result = $model->CheckAdmin($u, $p);
//当管理员信息合法时,返回管理员信息,一个数组,当管理员信息非法时,返回false
$admin_info = $model->CheckAdminInfo($u, $p);
//非空数组可以自动转换为bool型的true
if($admin_info){
//echo "登录成功";
session_start();
$_SESSION['admin_info']= $admin_info;
//通过$_SESSION['admin_info']['admin_name']可以得到管理员姓名;
//判断是否登陆状态,记录与否登陆信息
if(isset($_POST['remember'])){
//记录登陆信息,通常在原始数据上,添加混淆字符串后,再加密
setcookie('admin_id',md5($admin_info['id']. 'SALT', PHP_INT_MAX));
setcookie('admin_pass',md5($admin_info['admin_pass']. 'SALT'), PHP_INT_MAX);
}
header('Location:index.php?p=back&c=Admin&a=Index');
}else{
//失败后,提示并跳转到登录界面
$this->GotoUrl("登录失败","?p=back&c=Admin&a=Login", 2);
}
}
然后在平台控制器中,修改验证登陆的方法:
protected function_check(){
session_start();
//判断当前请求是否是特例,是否需要校验
//获取当前的控制器,和当前的动作
$curr_controller =strtolower(CONTROLLER);
$curr_action =strtolower(ACTION);
if($curr_controller == 'admin' && ($curr_action =='login' || $curr_action == 'checklogin')){
$m_admin =ModelFactory::MF('AdminModel');
//echo$_COOKIE['admin_id'];echo $_COOKIE['admin_pass'];
//$admin_info= $m_admin ->CheckCookieInfo($_COOKIE['admin_id'], $_COOKIE['admin_pass']);
//var_dump($admin_info);die;
//判断cookie是否保存了登陆信息
if(isset($_COOKIE['admin_id'])&& isset($_COOKIE['admin_pass']) &&
$admin_info =$m_admin ->CheckCookieInfo($_COOKIE['admin_id'], $_COOKIE['admin_pass'])){
//同时满足,具有登陆状态,直接跳转到主页,添加登陆标识
$_SESSION['admin_info']= $admin_info;
//echo1;die;
header('Location:index.php?p=back&c=Admin&a=Index');
die();
}
//echo$curr_action . $curr_controller;
//return ;
}
if(!isset($_SESSION['admin_info'])){
$m_admin =ModelFactory::MF('AdminModel');
//echo $_COOKIE['admin_id'];echo$_COOKIE['admin_pass'];
//$admin_info= $m_admin ->CheckCookieInfo($_COOKIE['admin_id'], $_COOKIE['admin_pass']);
//var_dump($admin_info);die;
//判断cookie是否保存了登陆信息
if(isset($_COOKIE['admin_id'])&& isset($_COOKIE['admin_pass']) &&
$admin_info =$m_admin ->CheckCookieInfo($_COOKIE['admin_id'], $_COOKIE['admin_pass'])){
//同时满足,具有登陆状态,直接跳转到主页,添加登陆标识
$_SESSION['admin_info']= $admin_info;
//echo1;die;
}else{
//没有记录登陆状态
header('Location:index.php?p=back&c=Admin&a=Login');
die();
}
}
}
在模型类中,添加验证cookie的方法:
/**
*校验加密过的id和密码是否合法
*@param string $id 管理员id 加密加盐
*@param string $pass 管理员密码 加密加盐
*@return mixed array,合法,管理员信息数组;false,非法
*/
public functionCheckCookieInfo($id, $pass){
$sql = "select* from admin_user where md5(concat(id, 'SALT')) ='$id' andmd5(concat(admin_pass, 'SALT'))= '$pass';";
//echo $sql;
return $this->dao ->GetOneRow($sql);
}
五、 验证码
验证码一般用于防御暴力解密登陆,灌水、刷帖等计算机高频次向服务器发出请求的恶意行为。验证码一般采用计算机不能低成本识别的图片信息。验证码涉及到图片生成技术,验证时,将码值存储在session中。
1. php中的绘图技术
使用php中的GD扩展完成,请事先在php.ini中打开gd2模块。
处理图像的基本步骤:
创建画布资源
创建新的画布,imagecreate(宽,高);//创建基于调色板的画布,支持的颜色少;imagecreatetruecolor(宽,高);//创建正彩色,支持的颜色多。
基于已有图像创建画布,imagecreatefromjpeg(图片地址); imagecreatefrompng(图片地址); imagecreatefromgif(图片地址);
操作画布
分配颜色,为某张画布分配某种颜色。
颜色的表示方式,RGB颜色。Imagecolorallocate(画布,颜色R,颜色G,颜色B);
填充画布,使用某个颜色,在画布的某个位置进行填充。Imagefill(画布,位置X,位置Y,颜色);//位置,采用坐标表示,原点,左上角,0,0;向右,X增加,向下,Y增加。右下角坐标:(width-1,height-1)
导出(输出)画布
Imagepng(画布,图片地址);//输出为png
Imagegif(画布,图片地址);//输出为gif
Imagejpeg(画布,图片地址);//输出为jpeg
如果不使用第二个参数,表示直接输出到浏览器。需要告诉浏览器,作为图片输出,否则浏览器会以为要输出的字符。使用header(‘Content-Type: image/png;’);
销毁资源,imagedestroy(画布);//销毁资源
比如:
<?php
$huabu = imagecreatetruecolor(530, 400);
$green = imagecolorallocate($huabu, 00, 88, 00);
imagefill($huabu, 0, 0, $green);
//imagepgn($huabu, './green.gng');
header('Content-Type: image/png;');
imagepng($huabu);
imagedestroy($huabu);
var_dump($huabu);
?>
2. 一般验证码的制作
一般的验证码是点击更换,图片的背景在有限的几张背景图片中选择背景,码值是大写字母+数字的随机组合。文字颜色,白色或者黑色替换。文字居中。
处理码值,然后存储到session,然后处理背景。
然后操作画布
分配字符串颜色,imagecolorallocate(画布,
将码值字符串写到画布上,imagestring(画布,字体大小,位置X,位置Y,字符串内容,颜色);//字体大小,是函数内置字体,通过1-5表示字号,1小。
然后输出验证码图片,在浏览器端输出,需要告知浏览器格式。
<?php
//处理码值,整理可能的字符
//*
$char_list = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789';
$char_list_len = strlen($char_list);
$code_len = 4;
$code = '';
for($i = 1; $i <=$code_len; ++$i){
$rand_index = mt_rand(0,$char_list_len - 1);
$code .=$char_list[$rand_index];
}
session_start();
$_SESSION['code'] = $code;
//处理背景图片
$bg_file = './captcha/captcha_bg' . mt_rand(1,5) . '.jpg';
//创建画布
//*/
$image = imagecreatefromjpeg($bg_file);
//操作画布
//随机分配白或黑
if(mt_rand(1, 2) == 1){
$str_color =imagecolorallocate($image, 0xff, 0xff, 0xff);
}else{
$str_color =imagecolorallocate($image, 0, 0, 0);
}
//计算图片宽高,设置文字居中
$image_w = imagesx($image);
$image_h = imagesy($image);
//设置字符串位置
$font = 5;
//计算字体的宽高
$font_w = imagefontwidth($font);
$font_h = imagefontheight($font);
//字符串的宽高
$str_w = $font_w * $code_len;
$str_h = $font_h;
//位置
$x = ($image_w - $str_w) / 2;
$y = ($image_h - $str_h) / 2;
imagestring($image, $font, $x, $y, $code, $str_color);
//输出
header('Content-Type: image/jpeg;');
imagepng($image);
//销毁
imagedestroy($image);
?>
3. 在项目中使用验证码
验证码会被应用于很多项目中,所以,验证码需要作为一个类放在框架目录中。需要定义一个验证码相关功能的工具类。
处理该类的自动加载,将所有的框架基础类,与类文件地址,做一个映射。
function __autoload($class){
//$base_class =array("MysqlUtil", "BaseModel", "ModelFactory","BaseController");
$base_class['MysqlUtil'] =FRAMEWORK . 'MysqlUtil.class.php';
$base_class['BaseModel'] =FRAMEWORK . 'BaseModel.class.php';
$base_class['ModelFactory']= FRAMEWORK . 'ModelFactory.class.php';
$base_class['BaseController']= FRAMEWORK . 'BaseController.class.php';
$base_class['Captch'] =FRAMEWORK . Captcha.class.php';
if(isset($base_class[$class])){
require$base_class[$class];
//require FRAMEWORK. $class . '.class.php'; //加载基础模型类
}else if(substr($class, -5)=== "Model"){ //如果后5个字母是Model,则加载Model类
require MODEL_PATH .$class . '.class.php';
}else if(substr($class,-10) === "Controller"){ //如果后10个字母是Controller,则加载Controller类
require CTRL_PATH .$class . '.class.php';
}
}
创建验证码工具类,其中需要生成验证码的方法
<?php
/**
*验证码工具类
*/
class Captcha{
/**
* 生成验证码图片
* @param int $code_len 码值长度,默认为4
*
*/
public function makeImage($code_len= 4){
//处理码值,整理可能的字符
//*
$char_list ='ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789';
$char_list_len =strlen($char_list);
$code = '';
for($i = 1; $i<=$code_len; ++$i){
$rand_index =mt_rand(0, $char_list_len - 1);
$code .= $char_list[$rand_index];
}
@session_start();
$_SESSION['code'] =$code;
//处理背景图片
$bg_file = FRAMEWORK. './captcha/captcha_bg' . mt_rand(1,5) . '.jpg';
//创建画布
//*/
$image =imagecreatefromjpeg($bg_file);
//操作画布
//随机分配白或黑
if(mt_rand(1, 2) ==1){
$str_color =imagecolorallocate($image, 0xff, 0xff, 0xff);
}else{
$str_color =imagecolorallocate($image, 0, 0, 0);
}
//计算图片宽高,设置文字居中
$image_w =imagesx($image);
$image_h =imagesy($image);
//设置字符串位置
$font = 5;
//计算字体的宽高
$font_w = imagefontwidth($font);
$font_h =imagefontheight($font);
//字符串的宽高
$str_w = $font_w *$code_len;
$str_h = $font_h;
//位置
$x = ($image_w -$str_w) / 2;
$y = ($image_h -$str_h) / 2;
imagestring($image,$font, $x, $y, $code, $str_color);
//输出
header('Content-Type:image/jpeg;');
imagepng($image);
//销毁
imagedestroy($image);
}
}
?>
由于需要使用后台控制器类的方法,需要在平台控制器中,为此方法放行
if($curr_controller== 'admin' && $curr_action == 'captcha'){
return ;
}
使用工具类生成登陆页面验证码
在登陆表单模版中,使用img标签,请求可以生成验证码图片的url地址,生成验证码。
<imgsrc="index.php?p=back&c=Admin&a=Captcha"width="145" height="20" alt="CAPTCHA"border="1" onclick=this.src="index.php?act=captcha&"+Math.random()style="cursor: pointer;" title="看不清?点击更换另一个验证码。" />
在后台控制器类中添加获取验证码的方法
public functionCaptchaAction(){
$t_captcha = newCaptcha();
$t_captcha->makeImage();
header('Content-Type:image/jpeg');
}
如果在图片程序中出现错误,是不会报错的。解决方法是,直接请求生成的图片的url。
为了避免更多的错误,写php程序时,所有的程序文件,保证php标记要顶头顶格,而且不要写结束标记。保存php文件时编码格式选择为utf8,不要bom,或者有的编辑器叫做签名。
校验验证码是否正确
通过验证码工具类的校验方法,得到校验结果。
/**
* 校验验证码结果
*
*/
public functioncheckCode($value){
@session_start();
$result =isset($value) && isset($_SESSION['code']) && strtoupper($value)== strtoupper($_SESSION['code']);
//销毁已经使用的验证码
unset($_SESSION['code']);
return $result;
}
在登陆验证方法中添加验证码校验的代码
functionCheckLoginAction(){
$u =$_POST['username'];
$p =$_POST['password'];
$code =$_POST['captcha'];
$t_captcha = newCaptcha();
$checkcode =$t_captcha ->checkCode($code);
if(!$checkcode){
//失败后,提示并跳转到登录界面
$this->GotoUrl("验证码不正确","?p=back&c=Admin&a=Login", 2);
die;
}
$model =ModelFactory::MF('AdminModel');
//$result = $model->CheckAdmin($u, $p);
//当管理员信息合法时,返回管理员信息,一个数组,当管理员信息非法时,返回false
//非空数组可以自动转换为bool型的true
$admin_info = $model->CheckAdminInfo($u, $p);
if($admin_info){
//echo "登录成功";
session_start();
$_SESSION['admin_info']= $admin_info;
//通过$_SESSION['admin_info']['admin_name']可以得到管理员姓名;
//判断是否登陆状态,记录与否登陆信息
if(isset($_POST['remember'])){
//记录登陆信息,通常在原始数据上,添加混淆字符串后,再加密
setcookie('admin_id',md5($admin_info['id']. 'SALT'), PHP_INT_MAX);
setcookie('admin_pass',md5($admin_info['admin_pass']. 'SALT'), PHP_INT_MAX);
}
header('Location:index.php?p=back&c=Admin&a=Index');
}else{
//失败后,提示并跳转到登录界面
$this->GotoUrl("登录失败","?p=back&c=Admin&a=Login", 2);
}
}
点击更换验证码
在点击图片时,重新请求生成验证码的动作。οnclick=this.src="index.php?p=back&c=Admin&a=Captcha&x="+Math.random()
<img src="index.php?p=back&c=Admin&a=Captcha"width="145" height="20" alt="CAPTCHA"border="1" onclick=this.src="index.php?p=back&c=Admin&a=Captcha&x="+Math.random() style="cursor: pointer;"title="看不清?点击更换另一个验证码。" />
六、 上传
文件从浏览器传输到服务器发生在请求阶段。当浏览器需要上传文件时,表单中的数据类型有两种,字符串型和文件型。默认情况下,浏览器会将所有的表单元素,视为字符串型。如果需要上传文件,需要告知浏览器,表单中的数据存在多种类型,通过在form元素中增加属性:Enctype = ‘multipart/form-data’,含义是:编码类型=多部分/表单-数据。
当服务器脚本php,接收到文件类的post类型时,会存储在上传临时目录中。默认为在服务器所在的操作系统的临时目录。
比如:
<form action='file.php', method='post',Enctype='multipart/form-data'>
文件名:<input type='text' name='filename'/><br />
<input type='file'name='filecontent' /><br />
<input type='submit'/>
</form>
在php页面中
<?php
var_dump($_POST);
sleep(20);
在php中需要在临时文件消失前,将其持久化存储。使用函数move_uploaded_file(临时文件地址,目标文件地址);
<?php
//var_dump($_POST);
//sleep(20);
echo '<pre>';
var_dump($_FILES);
$tmp_file = $_FILES['filecontent']['tmp_name'];
$dst_file = './upload.jgp';
$result = move_uploaded_file($tmp_file, $dst_file);
var_dump($result);
1. 典型的文件上传的做法
首先判断临时上传文件,是否满足我们的业务逻辑需求,满足,再移动。
要判断,是否存在错误,类型是否符合要求,大小是否符合要求。
关于文件类型,有两种表示方式,后缀名(如,.jps)和MIME(image/jpeg)。后缀名,在文件系统中,表示文件类型的方式。MIME,text/html,在互联网上传输文件内容时,表示该内容的方法。判断类型时,通常两种方式都需要判断。后缀名在原始文件名中截取,mime使用type元素。
然后,目标文件生成合理的名字。
比如:
<?php
header('Content-type:text/html; charset=utf-8');
$result = uploadFile($_FILES['filecontent']);
/**
* 文件上传函数,业务逻辑判断
* @param array $file_info 临时上传文件的5个信息,由$_FILES中获得
* @return string,目标文件名,成功;false,失败
*/
function uploadFile($file_info){
//判断是否有错误
if($file_info['error'] =0){
echo '文件上传错误';
return false;
}
//判断文件类型,后缀名
$ext_list = array('.jpg','.png', '.gif', 'jpeg');//允许的后缀名列表
$ext = strrchr($file_info['name'],'.');
if(! in_array($ext,$ext_list)){
echo '类型、后缀不符合要求';
return false;
}
//MIEM
$mime_list =array('image/jpeg', 'image/png', 'image/gif');
if(!in_array($file_info['type'], $mime_list)){
echo 'mime不符合要求';
return false;
}
//判断大小
$max_size = 500*1024;
if($file_info['size'] >$max_size){
echo '大小不符合';
return false;
}
//设置上传文件目录
$upload_path = './';
//目标文件名
$prefix = '';
$dst_name = uniqid($prefix,true) . $ext;
//移动
if(move_uploaded_file($file_info['tmp_name'],$upload_path . $dst_name)){
echo '成功';
return $dst_name;
}else{
echo '移动失败';
return false;
}
}
2. 划分子目录存储上传文件
为了减少一个目录中文件过多,需要划分成不同的子目录,划分方案,按照业务逻辑,比如商品图片,用户头像等;按照文件数量,每个目录存储2000个文件,数量达到,创建新子目录;时间划分,每个月或星期使用一个上传子目录。
比如,按照时间划分,需要获取目前应该使用的子目录名,date();,然后判断需要的目录是否已经存在,is_dir();,如果不存在,需要创建目录,mkdir();。
$upload_path = './';
//采用子目录存储
//获取当前需要的子目录名(目录/小时)
$sub_dir = date('YmdH') .'/';
//是否存在
if(is_dir($upload_path .$sub_dir)){
mkdir($upload_path .$sub_dir);
}
返回文件中需要包含子目录信息
if(move_uploaded_file($file_info['tmp_name'],$upload_path . $sub_dir . $dst_name)){
echo '成功';
return $sub_dir .$dst_name;
}else{
echo '移动失败';
return false;
}
3. 安全性问题的补充
注意,$_FILES中的type信息,不是php检测出来的,而是浏览器提供的。因此,php需要检测文件类型,才能进一步保证文件类型的合法。
Php检测文件的mime类型,需要开启fileinfo的扩展模块。php.ini中extension=php_fileinfo.dll。重启apache。
在上传文件的方法中,添加使用php检测mime类型的代码:
//php检测mime
$finfo = newFinfo(FILEINFO_MIME_TYPE);
$mime_type = $finfo->file($file_info['tmp_name']);
if(! in_array($mime_type,$mime_list)){
echo 'php检测mime不合法';
return false;
}
如果在判断临时文件期间,正好有一个同目录下的其他同名文件,那么将导致上传的文件出错,所以,要判断临时文件,是否为真实的上传文件。使用函数is_uploaded_file();可以判断是否为php上传的临时文件。
//是否为php上传文件的检测
if(!is_uploaded_file($file_info['tmp_name'])){
echo '不是上传的临时文件';
return false;
}
//移动
if(move_uploaded_file($file_info['tmp_name'],$upload_path . $sub_dir . $dst_name)){
echo '成功';
return $sub_dir .$dst_name;
}else{
echo '移动失败';
return false;
}
4. 多文件上传
第一种情况是表单文件域元素的name值不同,分别对应不同的文件。使用时,需要哪里处理哪个数组就可以。
文件名:<input type='text'name='filename'/><br />
商品图片:<input type='file' name='good_photo'/><br />
客户图片:<input type='file' name='client_photo'/><br />
其他图片:<input type='file' name='other_photo'/><br />
第二种情况是文件与name相关的,比如,以数组方式命名的。
商品图片一:<input type='file' name='good_photo[]'/><br />
商品图片二:<input type='file' name='good_photo[]'/><br />
商品图片三:<input type='file' name='good_photo[]'/><br />
此时$_FILES的结构是,将同name的整理到一个数组元素中,该数组元素对应5个元素(name,type,tmp_name,error,size)的数组,每个元素数组分别对应上传的文件的相应的信息,比如:
array(1) {
["good_photo"]=>
array(5) {
["name"]=>
array(3) {
[0]=>
string(27) "5a2f8658e38893.12819117.jpg"
[1]=>
string(27) "5a2fa199dfa456.11063548.jpg"
[2]=>
string(27) "5a2fa12acb8946.84517593.jpg"
}
["type"]=>
array(3) {
[0]=>
string(10) "image/jpeg"
[1]=>
string(10) "image/jpeg"
[2]=>
string(10) "image/jpeg"
}
["tmp_name"]=>
array(3) {
[0]=>
string(27) "C:\Windows\Temp\php89A3.tmp"
[1]=>
string(27) "C:\Windows\Temp\php8A21.tmp"
[2]=>
string(27) "C:\Windows\Temp\php8ABE.tmp"
}
["error"]=>
array(3) {
[0]=>
int(0)
[1]=>
int(0)
[2]=>
int(0)
}
["size"]=>
array(3) {
[0]=>
int(217792)
[1]=>
int(252477)
[2]=>
int(252477)
}
}
}
对于这种形式的,分别整理出来每个文件的5个信息,将每个文件都上传。在每个元素中,下标一致的属于同一个文件,只要获得每个下标,就可以通过下标获得对应的5个元素值。
<?php
header('Content-type:text/html; charset=utf-8');
require './checkfile.php';
$result = multiupload($_FILES['good_photo']);
var_dump($result);
/**
* 多文件上传函数,业务逻辑判断
* @param array $file_list 临时上传文件的5个信息的数组,由$_FILES中获得
* @return array,每个上传文件的结果,成功;false,失败
*/
function multiupload($file_list){
//遍历,使用name元素得到下标
foreach($file_list['name']as $key => $val){
//echo $key .'<br />';
$file_info['name'] =$file_list['name'][$key];
$file_info['type'] =$file_list['type'][$key];
$file_info['tmp_name']= $file_list['tmp_name'][$key];
$file_info['error']= $file_list['error'][$key];
$file_info['size'] =$file_list['size'][$key];
//上传文件,并存储每个文件的上传结果,与$key对应
$result_list[$key] =uploadFile($file_info);
}
return $result_list;
}
5. 上传文件的错误类型
0:没有错误
1:文件超过了php中对上传文件大小的设置。在php.ini中,upload_max_filesize = 2M。针对整个php服务器中的上传的文件。
2:文件过大,超过表单中元素。在表单中有个隐藏域设置,比如:<input type=’hidden’ name=’MAX_FILE_SIZE’ value=’10000’ />。精确到每个表单层面。
3:文件没有上传完。
4:没有上传文件。比如,没有选择文件,就提交了。
5:上传的文件大小为0,上传的为空文件。一般约定这个,但不是错误,php中没有定义5。
6:临时上传目录没有找到。默认下在操作系统的临时目录下。Php.ini中可以设置临时目录:upload_tmp_dir =
7:临时文件写入失败。比如,磁盘空间不足,或者权限不允许等。
6. 上传文件的其他相关配置
Php中限制一次请求上传文件的设置,max_file_uploads = 20
设置post数据的大小,post_max_size= 8M。超过post数据的总大小,php不处理post数据,$_POST,$_FILES就是空数组。文件只能以post方式提交。
http不适合上传超大文件。
设置是否开启上传功能,file_uploads = on。
7. 设计商品添加功能
根据控制器,模型,视图的顺序,依次进行。
控制器,在后台,新建一个商品控制器,完成商品相关的操作。
<?php
/**
* 商品控制器,做商品相关的操作
*/
class GoodsController extends PlatformController{
/**
* 展示商品表单页面功能
*/
public functionaddAction(){
require VIEW_PATH .'goods_add.html';
}
}
暂时不需要使用模型层。
视图,增加一个模版。
创建商品表
create table goods(
goods_id int unsigned notnull auto_increment,
goods_name varchar(64) notnull default '',
shop_price decimal(10,2)not null default 0,
goods_desc text,
goods_number int not nulldefault 0,
is_best tinyint not nulldefault 0,
is_new tinyint not nulldefault 1,
is_hot tinyint not nulldefault 0,
is_on_sale tinyint not nulldefault 1,
image_ori varchar(255) not null default ' ' comment '上传的原始图片',
admin_id int unsigned notnull default 0 comment '哪个管理员添加的商品',
create_time int not nulldefault 0 comment '添加时间,时间戳,整型',
primary key (goods_id)
)engine=myisam charset=utf8;
然后处理商品数据。
控制器
/**
* 处理商品添加数据
*/
public functionInsertAction(){
//收集表单数据,作为上传数据源
$data['goods_name']= $_POST['goods_name'];
$data['shop_price']= $_POST['shop_price'];
$data['goods_desc']= $_POST['goods_desc'];
$data['goods_number']= $_POST['goods_number'];
$data['is_best'] =isset($_POST['is_best']) ? '1':'0';
$data['is_new'] =isset($_POST['is_new']) ? '1':'0';
$data['is_hot'] =isset($_POST['is_hot']) ? '1':'0';
$data['is_on_sale']= isset($_POST['is_on_sale']) ? '1':'0';
//需要获取的数据
$data['admin_id'] =$_SESSION['admin_info']['id'];
$data['create_time']= time();
//利用模型插入数据
$m_goods =ModelFactory::MF('GoodsModel');
$result = $m_goods->InsertGoods($data);
//跳转到列表}页
if($result){
//插入成功,商品列表
header('Location:index.php?p=back&c=Goods&a=list');
die;
}else{
$this->GotoUrl('失败,失败原因', 'index.php?p=back&c=Goods&a=add');
die;
}
}
模型
增加goods操作模型。
<?php
/**
* 商品表goods的操作模型
*/
class GoodsModel extends BaseModel{
/**
* 插入商品方法
* @param array 商品信息的数据
* @return 成功
*/
public functionInsertGoods($data){
$sql = "insertinto goods values (null,'{$data['goods_name']}', '{$data['shop_price']}',
'{$data['goods_desc']}','{$data['goods_number']}', '{$data['is_best']}',
'{$data['is_new']}','{$data['is_hot']}', '{$data['is_on_sale']}',
'{$data['admin_id']}','{$data['create_time']}')";
$re = $this ->dao->exec($sql);
var_dump($re);
return $re;
}
}
8. 封装上传工具类
用于项目中的商品图片的处理。实现单个文件上传的方法和多个文件上传的方法。添加存储错误信息的属性和获取错误属性的方法,将可变的参数配置成属性的形式。关于后缀名和mime的关系,要求用户指定后缀名,根据后缀名和mime的映射关系,通过提供对照表,推算出mime。
如果使用工具类上传商品图片,为了便于管理,需要将商品与该商品的图片进行关联。那么,需要在商品表中添加一个字段,存储商品图片地址。那么,在商品控制类的插入商品方法中,如需将获取的上传图片的地址也插入商品表中。然后,在模型中,增加对上传地址的处理。
上传图片的工具类:
<?php
/**
* 上传图片的工具类
*/
class Upload{
//后缀名和mime的对照表
private $_ext_mime = array(
'.jpeg' =>'image/jpeg',
'.gif' =>'image/gif',
'.png' =>'image/png',
'.jpg' =>'image/jpeg',
);
private $_error;//存储当前错误信息
/**
* 获取错误信息的方法
* @return string 错误信息
*/
public function getError(){
return $this->_error;
}
private $_upload_path;
private $_prefix;
private $_max_size;
private $_ext_list;
private $_mime_list;
public functionsetUploadPath($upload_path){
if(is_dir($upload_path)){
$this->_upload_path = $upload_path;
}else{
trigger_error('上传目录不存在,采用默认');
}
}
public functionsetPrefix($prefix){
$this ->_prefix =$prefix;
}
public functionsetMax_size($max_size){
$this ->_max_size= $max_size;
}
public functionseText_list($ext_list){
$this ->_ext_list= $ext_list;
$this->_setMimeList($this ->_ext_list);
}
public function__construct(){
$this->_upload_path = './';
$this ->_prefix ='';
$this ->_max_size= 1*1024*1024;
$this ->_ext_list= array('.jpg', '.gif', '.png', '.jpeg');
$this->_setMimeList($this ->_ext_list);
}
private function_setMimeList($ext_list){
$mime_list =array();
foreach($ext_list as$ext){
$mime_list[]= $this ->_ext_mime[$ext];
}
$this ->_mime_list= $mime_list;
}
/**
* 文件上传函数,业务逻辑判断
* @param array $file_info 临时上传文件的5个信息,由$_FILES中获得
* @return string,目标文件名,成功;false,失败
*/
functionuploadFile($file_info){
//判断是否有错误
if($file_info['_error']= 0){
$this->_error = '文件上传错误';
return false;
}
//判断文件类型,后缀名
$ext_list = $this->_ext_list;//允许的后缀名列表
$ext =strrchr($file_info['name'], '.');
if(! in_array($ext,$ext_list)){
$this->_error = '类型、后缀不符合要求';
return false;
}
//MIEM
$mime_list = $this->_mime_list;
if(!in_array($file_info['type'], $mime_list)){
$this->_error = 'mime不符合要求';
return false;
}
//php检测mime
$finfo = newFinfo(FILEINFO_MIME_TYPE);
$mime_type = $finfo->file($file_info['tmp_name']);
if(!in_array($mime_type, $mime_list)){
$this->_error = 'php检测mime不合法';
return false;
}
//判断大小
$max_size = $this->_max_size;
if($file_info['size']> $max_size){
$this->_error = '大小不符合';
return false;
}
//设置上传文件目录
$upload_path = $this->_upload_path;
//采用子目录存储
//获取当前需要的子目录名(目录/小时)
$sub_dir =date('YmdH') . '/';
//是否存在
if(!is_dir($upload_path. $sub_dir)){
mkdir($upload_path. $sub_dir);
}
//目标文件名
$prefix = $this->_prefix;
$dst_name =uniqid($prefix, true) . $ext;
//是否为php上传文件的检测
if(! is_uploaded_file($file_info['tmp_name'])){
$this->_error = '不是上传的临时文件';
return false;
}
//移动
if(move_uploaded_file($file_info['tmp_name'],$upload_path . $sub_dir . $dst_name)){
$this->_error = '成功';
return$sub_dir . $dst_name;
}else{
$this->_error = '移动失败';
return false;
}
}
}
商品控制器类的插入商品的方法:
/**
* 处理商品添加数据
*/
public functionInsertAction(){
//收集表单数据,作为上传数据源
$data['goods_name']= $_POST['goods_name'];
$data['shop_price']= $_POST['shop_price'];
$data['goods_desc']= $_POST['goods_desc'];
$data['goods_number']= $_POST['goods_number'];
$data['is_best'] =isset($_POST['is_best']) ? '1':'0';
$data['is_new'] =isset($_POST['is_new']) ? '1':'0';
$data['is_hot'] =isset($_POST['is_hot']) ? '1':'0';
$data['is_on_sale']= isset($_POST['is_on_sale']) ? '1':'0';
//处理上传的图片
$t_upload = newUpload();
//设置上传图片相关的属性
$t_upload->setUploadPath(ROOT . 'upload/goods/');
$t_upload->setPrefix('goods_ori_');
if($result =$t_upload ->uploadFile($_FILES['goods_image_ori'])){
$data['image_ori']= $result;
}else{
$this->GotoUrl('文件上传失败,原因是:' . $t_upload ->getError(),'
index.php?p=back&c=Goods&a=Add');
die;
}
//需要获取的数据
$data['admin_id'] =$_SESSION['admin_info']['id'];
$data['create_time']= time();
//利用模型插入数据
$m_goods =ModelFactory::MF('GoodsModel');
$result = $m_goods->InsertGoods($data);
//跳转到列表}页
if($result){
//插入成功,商品列表
header('Location:index.php?p=back&c=Goods&a=list');
die;
}else{
$this->GotoUrl('失败,失败原因', 'index.php?p=back&c=Goods&a=add');
die;
}
}
在商品模型的插入方法中,添加处理商品的字段。
七、 文件和目录的操作
1. 关于目录操作函数
创建目录,mkdir(目录地址,权限,是否递归创建);权限类似linux文件权限的方式,如0755。比如:
<?php
$file_path = 'D:\php\test1\updownload\aa\mkdir';
$re = mkdir($file_path,0755,true);
var_dump($re);
注意,第二个参数,即权限参数,在windows下被忽略。
删除目录,rmdir(目录地址);,比如:
$re =rmdir($file_path);
var_dump($re);
注意,不允许删除非空目录。
获取目录内容,
句柄=opendir(目录地址);打开目录句柄,句柄就是php程序与文件系统数据流通道。
文件名=readdir(句柄);通过句柄,从目录中读取一个文件,包括文件和子目录。一次读取一个文件,并向下移动文件指针。每个目录下都存在.和..这两个虚拟目录,表示当前目录和上级目录。
配合循环结构可以查看目录下所有的文件名。比如:
$file_path = 'D:\php\test1\updownload';
$handle = opendir($file_path);
var_dump($handle);
echo '<br />';
while($file_name = readdir($handle)){
if($file_name == '.' ||$file_name == '..'){
continue;
}
var_dump($file_name);
echo '<br />';
}
由于当readdir获取不到文件时,指针为空,对应的$file_name为false,但是如果指针指向的文件名为0,那么$file_name=0,就是转换为$file_name=false,while循环终止,一般名字为0的文件是排在..文件后面的,这使得不能得到后面的文件名。所以一般采用以下方式判断:
$file_path = 'D:\php\test1\updownload';
$handle = opendir($file_path);
var_dump($handle);
echo '<br />';
while(false !== $file_name = readdir($handle)){
if($file_name == '.' ||$file_name == '..'){
continue;
}
var_dump($file_name);
echo '<br />';
}
closedir(句柄);关闭句柄。
rename(原始地址,目标地址);
$old_path = 'D:\php\test1\updownload';
$new_path = 'D:\php\test1\updownloadnew';
rename($old_path, $new_path);
2. 使用递归获取目录内全部内容
$file_path = 'D:\php\test1\updownloadnew';
echo '<br />';
function readf($file_path){
$handle =opendir($file_path);
while(false !== $file_name= readdir($handle)){
if($file_name == '.'|| $file_name == '..'){
continue;
}
if(is_dir($file_path. '/' . $file_name)){
readf($file_path. '/' . $file_name);
}else{
var_dump($file_name);
echo '<br/>';
}
}
closedir($handle);
}
readf($file_path);
3. 树状展示递归获取的结果
需要确定缩进级别,每当递归调用一次,就往前缩进一个级别。递归调用深度,就是该函数确定的文件的缩进级别。
$file_path = 'D:\php\test1';
//$handle = opendir($file_path);
//var_dump($handle);
echo '<br />';
/**
* @param string $path
* @param integer $deep
* @return
*/
function readf($file_path, $deep = 0){
$handle =opendir($file_path);
while(false !== $file_name= readdir($handle)){
if($file_name == '.'|| $file_name == '..'){
continue;
}
echostr_repeat(' ', $deep*4), $file_name;
echo '<br />';
if(is_dir($file_path. '/' . $file_name)){
readf($file_path. '/' . $file_name, $deep + 1);
}
}
closedir($handle);
}
readf($file_path);
4. 将递归获取的结果存储到数组中
需要记录下来每个文件名,缩进级别,是目录还是文件。
$file_path = 'D:\php\test1';
/**
* @param string $path
* @param integer $deep
* @return
*/
function readf($file_path, $deep = 0){
//static 保证在readf里面一直可以存在,保证每个递归调用操作的都是同一个数组
static $file_list =array();//存储所有的文件信息,是二维数组
$handle =opendir($file_path);
while(false !== $file_name= readdir($handle)){
if($file_name == '.'|| $file_name == '..'){
continue;
}
//echostr_repeat(' ', $deep*4), $file_name;
//echo '<br/>';
$fileinfo['filename']= $file_name;
$fileinfo['deep'] =$deep;
$file_list[] =$fileinfo;
if(is_dir($file_path. '/' . $file_name)){
readf($file_path. '/' . $file_name, $deep + 1);
}
}
closedir($handle);
return $file_list;
}
$re = readf($file_path);
echo '<pre>';
var_dump($re);
5. 递归删除
$file_path = 'D:/php/test1/del';
/**
* @param string $path
* @param integer $deep
* @return
*/
function readf($file_path){
$handle =opendir($file_path);
while(false !== $file_name= readdir($handle)){
if($file_name == '.'|| $file_name == '..'){
continue;
}
if(is_dir($file_path. '/' . $file_name)){
readf($file_path. '/' . $file_name);
}else{
unlink($file_path. '/' . $file_name);
}
}
closedir($handle);
//删除该目录
return rmdir($file_path);
}
$re = readf($file_path);
echo '<pre>';
var_dump($re);
6. 文件操作函数
读写,将文件作为内容容器。
file_put_contents(文件地址,内容);//将内容写入文件
默认为替换写,将原内容清空,然后写入,比如:
<?php
$file = './data.txt';
$data = date('H:i:s');
$result = file_put_contents($file, $data);
var_dump($result);
使用第三个参数,FILE_APPEND,表示追加,比如:
<?php
$file = './data.txt';
$data = date('H:i:s') . "\n";
$result = file_put_contents($file, $data, FILE_APPEND);
var_dump($result);
file_get_contents(文件地址);//将内容从文件中读取
提示浏览器换行,使用nl2br();将换行符转换为br。
<?php
$file = './data.txt';
$data = date('H:i:s') . "\n";
//$result = file_put_contents($file, $data, FILE_APPEND);
$result = file_get_contents($file);
echo nl2br($result);
var_dump($result);
unlink();//删除文件
rename();//文件移动(重命名)
filesize(文件地址);//获取文件大小
$file ='./data.txt';
$size =filesize($file);
var_dump($size);
file_exists(文件地址);//布尔值,文件是否存在。
时间戳=filemtime(文件地址);//文件最后修改的时间。
$file = './data.txt';
$size = filesize($file);
var_dump($size);
echo '<br />';
var_dump(file_exists($file));echo '<br />';
$time = filemtime($file);
echo date('H:s:i', $time);//将时间戳转化为指定的格式
var_dump(filemtime($file));echo '<br />';
7. 文件句柄操作函数
基本单位都是字节。一般,文件的读写,使用函数file_put_contents();file_get_contents();完成,少数情况下,不能使用这两个函数,比如文件过大时。只能一部分一部分的操作。
fopen();//打开文件句柄,php程序与文件数据通路,需要文件地址和开发模式两个参数。打开模式是打开该文件后,需要执行什么操作,如下的模式可以选择,r,w,a,表示,读模式,写操作(清空写),追加模式。
$file = './data.txt';
$handle = fopen($file, 'r');
var_dump($handle);
读取,fread();fgetc();fgets();
Fgetc(句柄);//char,字符,读取一个字节数据,指针自定下移
header('Content-Type: text/html; charset=utf-8');
$file = './data.txt';
$handle = fopen($file, 'r');
//var_dump($handle);
$c = fgetc($handle);
var_dump($c);
字符串=fgets(句柄,长度);//从文件指针位置,读取指定长度的字符串内容。获取的长度的内容为长度-1。指针会向后移动。如果读取是先读到了换行符,也会终止。
$file = './data.txt';
$handle = fopen($file, 'r');
//var_dump($handle);
$c = fgets($handle, 4);
var_dump($c);
这个函数,也叫读行函数,对于每行记录一组信息,该函数最常用。
使用循环结构,读取全部记录,配合feof() END of File,来判断是否到达文件末尾。
$file = './data.txt';
$handle = fopen($file, 'r');
//var_dump($handle);
while(!feof($handle)){
$line = fgets($handle,1024);
echo nl2br($line);
}
字符串=fread(句柄,长度);//依据长度读取内容,不受换行符的限制。
$file = './data.txt';
$handle = fopen($file, 'r');
$str = fread($handle, 40);
var_dump($str);
唯一的限制是最大不能超过8192。
写入操作,写入长度=fwrite(句柄,内容);//
$file = './data.txt';
$mode = 'a';
$handle = fopen($file, $mode);
$data = date('Y-m-d H:i:s') . "\n";
$result = fwrite($handle, $data);
var_dump($result);
注意清空写模式下,打开文件时就清空了。
$file = './data.txt';
$mode = 'w';
$handle = fopen($file, $mode);
$data = date('Y-m-d H:i:s') . "\n";
$result = fwrite($handle, $data);
var_dump($result);
写操作就是在文件指针位置进行写操作。如果是追加写模式,永远在末尾完成写操作。
关闭句柄,flcose(句柄);//虽然脚本结束时会自动关闭,但是这种涉及到内存资源的操作数据,最好即使关闭,释放资源。
8. 指针操作函数
fseek(句柄);//定位指针,位置从0开始,增加
ftell(句柄);//获取当前指针
$file = './data.txt';
$mode = 'r';
$handle = fopen($file, $mode);
echo '位置' . ftell($handle);
fseek($handle, 4);
echo '位置' . ftell($handle);
9. 关于文件的打开模式
基本模式:
R 读
W 清空写,文件存在直接打开同时清空
A 追加写,文件存在直接打开
X 新建写,只能新建文件进行操作,进行写操作,与w类似。
+扩展模式
扩展的操作,都可以完成读写操作。
R+ 读写,打开任意文件,无论是否存在,文件内容不会被清空。依据文件指针位置,进行读写操作,其中写会替换指针位置原有的字节。
$file = './data.txt';
$mode = 'r+';
$handle = fopen($file, $mode);
echo '位置' . ftell($handle);
$str = fgets($handle, 1024);
echo $str . '<br />';
echo '位置' . ftell($handle);
$data = 'asdvdf';
$result = fwrite($handle, $data);
echo $result;
W+ 读写,打开时,同时清空内容,之后指针在哪里,就在哪里完成读写。与r+的唯一区别是会清空文件内容。
A + 读写,打开任意文件,不会清空内容,指针仅影响读操作,不影响写操作,仅仅可以在末尾写。
X+ 读写,只能新建文件进行操作,依据文件指针位置,进行读写操作。
10. 文件的并发操作
当一个脚本在读取文件时,同时有另一个脚本在写入这个文本,如果前一个脚本的读取文件时,后一个脚本写入文件内容,导致文件的并发操作,可能出现脏读、幻读等情况。
默认的,php的文件操作函数,是无阻塞的,是自由操作状态的。如果需要一个脚本操作时阻塞另外的脚本操作,需要用到文件锁。
锁操作流程,先加锁,检测锁是否成功,如果成功再使用。
锁定类型有:s-lock读锁,或共享锁,读操作前,需要增加的锁定,允许并发读,阻塞额外的写操作。x-lock写锁,写操作前,尝试添加的锁定类型,导致其他脚本不能读也不能写。
还有一种概念是意向锁,所有的操作资源的脚本都遵循一个约定来使用文件锁。
Flock(句柄,类型)函数用于添加php的文件锁定,添加意向锁。类型为:LOCK_SH读锁,LOCK_EX写锁。
比如:
<?php
$file = './data.txt';
$mode = 'r+';
$handle = fopen($file, $mode);
//尝试加锁
$lock_result = flock($handle, LOCK_SH);
//判断锁定结果
if(!$lock_result){
//锁定失败,不能操作
trigger_error('不能锁定文件');
die;
}else{
$result = fgets($handle,1024);
var_dump($result);
sleep(5);
echo '<br />';
$result = fgets($handle,1024);
var_dump($result);
}
<?php
$file = './data.txt';
$mode = 'a';
$handle = fopen($file, $mode);
//尝试加锁
$lock_result = flock($handle, LOCK_EX);
//判断锁定结果
if(!$lock_result){
//锁定失败,不能操作
trigger_error('不能锁定文件');
die;
}else{
$data = 'aasdfb' ."\n";
$result = fwrite($handle,$data);
var_dump($result);
}
还有一个常量是LOCK_NB,就是不等待其他的锁。
比如:$lock_result = flock($handle, LOCK_EX | LOCK_NB);
使用flock($handle, LOCK_UN)可以强制解除锁。
Fclose()会自动解锁。
八、 关于HTTP协议
http是超文本传输协议,应用层的协议,规范。浏览器与服务器间数据交互格式。规范是请求数据格式和响应数据格式。
1. 请求
比如,以get形式的请求,浏览器会整理出一定的格式,向服务器发送。一般,包括请求行,请求头,请求体三个部分。
请求行,表示请求数据的第一行,请求的摘要信息。表示,请求的方法,请求的资源地址,使用的协议版本。
请求头,浏览器向服务器传输的请求属性信息。浏览器需要服务器知道的浏览器的状态。比如,User-Agent,用户代理,什么发出的请求。Accept-Language,可以接收的语言类型。Accept,可以接受的文件类型。Accept-Encoding,可以接收的编码类型,压缩相关的格式类型。Host,请求的主机名。Connection,连接类型。http/1.1新标准,响应结束是否立即断开tcp连接,值为keep-alive,短时间内保持连接。Cookie,携带的cookie。
请求主体,请求的主体数据。通常在使用post方式发出请求时,post数据就在请求主体中传输。
2. 操作请求
在服务器端受到浏览器请求后,利用相应的请求信息完成操作。比如,返回来源页就是利用请求头中的资源完成的。就是利用请求头中的Referer信息。比如,防盗链功能的设定。也是利用了Referer中的信息,如果Referer中的信息为空,说明这个访问不是通过合法的连接请求访问的,那么可以进行拒绝,比如:
<?php
header('Content-Type:text/html; charset=utf-8');
if(!isset($_SERVER['HTTP_REFERER'])){
echo '非法访问,请联系管理员';
}else{
echo '登陆成功';
}
比如,I18n程序,就是国际化程序。项目支持多语言的展示。这就需要知道浏览器需要哪种语言,浏览器请求时携带Accept-Language头,表示可以接受的语言。
首先,项目要支持多语言的版本,比如,
<?php
//项目中的语言支持
$support_list = getSupportLang();
//var_dump($support_list);
//获取浏览器接收的语言
$browser_list = getBrowserLang();
//var_dump($browser_list);
//获取当前浏览器需要的语言
$curr_lang = getLang($support_list, $browser_list);
//var_dump($curr_lang);
require './language/' . $curr_lang . '.php';
//找到浏览器所需要的语言
function getLang($s_l, $b_l){
$lang = 'zh_cn';
foreach($b_l as $l){
if(in_array($l,$s_l)){
$lang = $l;
break;
}
}
return $lang;
}
//获得项目支持的语言
function getSupportLang(){
$path = './language/';
$handle = opendir($path);
while($filename =readdir($handle)){
if($filename =='.' || $filename == '..'){
continue;
}
$lang_list[] =substr($filename, 0, strpos($filename, '.'));
}
return $lang_list;
}
//浏览器需要的语言
function getBrowserLang(){
$accept_language =$_SERVER['HTTP_ACCEPT_LANGUAGE'];
$lang_list = explode(',',$accept_language);
foreach($lang_list as$lang){
$tmp_arr =explode(';', $lang);
$tmp_lang =$tmp_arr[0];
$browser_list[] =str_replace('-', '_', strtolower($tmp_lang));
}
return $browser_list;
}
echo $lang['HELLO'], ' ',$_GET['name'];
3. 响应
分为三部分,响应行,响应的第一行;响应头,,服务器需要浏览器直到的一些信息;响应主体,主体数据,用于展示。
响应行,协议版本,响应状态码,状态消息。告知浏览器,当前响应的结果。常见的状态消息,200 ok,成功;400 Not Found,请求的资源不存在;403 Forbidden,请求被拒绝;302 Found,重定向;500 Server Internal Error,服务器内部错误。
响应头,Date,响应时间;Connection,连接类型;Keep-Alive,保持连接的时效;Content-Type,主体类型;Centent-Length,主体的长度。语法与请求头一致,CRLF行结尾,空行表示头结束。
响应主体,任何的输出都是响应主体,就是html代码和echo的内容。浏览器源代码中查看的内容,就是响应主体。
4. 操作响应
header();//头函数是常用的响应操作函数,比如,跳转,设置主体类型,设置cookie的值。
<?php
header('Set-Cookie:name=haha');
控制浏览器缓存的操作,Expires控制响应的有效期的,表示方式是特定格式的GMT时间:Expires: Wed, 13 Dec 2017 12 12:48:21 GMT
Date();//将一个时间戳,格式化成本地时间。
Gmdate();//将一个时间戳,格式化成GMT时间。
<?php
header('Content-Type: text/html; charset=utf-8');
//$expires = gmdate('D, d M Y H H:i:s', time()+3) . ' GMT';
//取消浏览器缓存
$expires = gmdate('D, d M Y H H:i:s', time()-1) . ' GMT';
header('Expires:' . $expires);
echo date('H:i:s'), '<br />';
?>
<hr />
<a href='huancun.php'>哈哈</a>
比如,强制命令浏览器不要缓存时,像验证码图片的取消缓存。
九、 文件的下载
HTTP下载,告知浏览器,将浏览器接收到的响应主体,以附件的形式进行存储。
通过响应头设置,比如:
header('Content-Disposition:attachment');指定下载的文件名header('Content-Disposition:attachment;filename=haha.jgp');
使用basename();可以取得地址中的文件名部分。
指定主体类型,比如header('Content-Type: image/jpeg');
文件大小,header('Content-Length: 1000');
比如:
<?php
$file = './http.rar';
header('Content-Disposition: attachment;filename=' . basename($file));
$finfo = new Finfo(FILEINFO_MIME_TYPE);
$mime = $finfo ->file($file);
header('Content-Type: ' . $mime);
header('Content-Length: ' . filesize($file));
$handle = fopen($file, 'r');
while(! feof($handle)){
echo fgets($handle, 1024);
}
fclose($handle);
转载自原文链接, 如需删除请联系管理员。
原文链接:phpWeb,转载请注明来源!