Safe: PHP 代码审计-PHP语言

主要内容:

PHP 简介

XAMPP如何下载及安装

XAMPP的介绍:

XAMPP是一个集(Apache+MySQL+PHP+PERL)为一体的功能强大的建站集成软件包。它可以在Windows、Linux、Solaris、Mac OS X 等多种操作系统下安装使用。支持多种语言:简体中文、繁体中文、英文、韩文、俄文等等。

这个功能强大的软件包原来的名字是 LAMPP,但是为了避免误解,最新的几个版本就改名为 XAMPP 了。它的使用简便快捷,在性能上易于操作和浏览。正因为它的简单而吸引了大量的人用其建网站,博客。

XAMPP的下载

下载地址:https://www.apachefriends.org/zh_cn/download.html

在这个页面上我们可以选择适合自己操作系统的安装包进行下载(Windows系统、Linux系统、Mac OS X 系统等),在本篇文章将以Windows系统为例

XAMPP可通过官网进行下载,下载好后解压最好选在D盘目录下安装,安装好打开xampp-control运行程序

添加系统变量

php 不同操作系统安装文档: https://www.php.net/manual/zh/install.php

windows

如果没有安装XAMPP,使用官方zip包也可以。 下好zip包,我这里下的是64位非线程安全的。下载好的zip包直接解压就可以使用。

把PHP.exe所在文件夹路径(“C:\XAMPP\php”)添加进环境变量-系统变量-Path中(直接搜索框搜索系统变量便可找到)。

在cmd中输入php -v,检查是否配置成功

或者下载PHP https://www.php.net/distributions/php-7.3.24.tar.gz

brew install openssl
brew install gettext
brew install zlib

tar -zvxf php-7.3.24.tar.gz
cd php-7.3.24
./configure --prefix=/usr/local/php/ \
--with-config-file-path=/usr/local/php/etc \
--with-config-file-scan-dir=/usr/local/php/etc/conf.d \
--enable-fpm \
--with-fpm-user=www \
--with-fpm-group=www \
--with-mysqli \
--with-pdo-mysql \
--with-iconv-dir \
--with-freetype-dir \
--with-zlib=/opt/homebrew/Cellar/zlib/1.2.11 \
--with-jpeg-dir=/opt/homebrew/Cellar/jpeg/9d/ \
--with-png-dir=/opt/homebrew/Cellar/libpng/1.6.37/ \
--with-libxml-dir=/usr/bin/xml2-config \
--enable-xml \
--disable-rpath \
--enable-bcmath \
--enable-shmop \
--enable-sysvsem \
--enable-inline-optimization \
--with-curl=/opt/homebrew/Cellar/curl/7.80.0/ \
--enable-mbregex \
--enable-mbstring \
--with-mcrypt \
--enable-ftp \
--with-gd \
--enable-gd-native-ttf \
--with-openssl=/opt/homebrew/Cellar/[email protected]/1.1.1k \
--with-mhash \
--enable-pcntl \
--enable-sockets \
--with-xmlrpc \
--enable-zip \
--enable-soap \
--without-pear \
--with-gettext \
--disable-fileinfo \
--enable-maintainer-zts \
--enable-mysqlnd
make && sudo make install

MAC安装PHP

# zsh 替换 brew bintray 镜像
echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.ustc.edu.cn/homebrew-bottles' >>~/.zshrc
source ~/.zshrc

# bash 替换 brew bintray 镜像
echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.ustc.edu.cn/homebrew-bottles' >>~/.bash_profile
source ~/.bash_profile

# 刷新源
brew update

# 搜索PHP
> brew search php
brew install [email protected]

安装位置

/usr/bin/php
# 最新版Mac系统
/opt/homebrew/opt/[email protected]/bin/
# 配置文件位置
/opt/homebrew/etc/php/8.1/php.ini

配置文件位置 vim /opt/homebrew/etc/php/8.1/php.ini

VS Code 的安装

VS Code 官网:https://code.visualstudio.com

VS Code 的安装很简单,直接去官网下载安装包,然后双击安装即可。

私人订制:VS Code 的常见配置

1、下载插件:PHP Server

指定php的路径:首选项->设置-> "PHP › Validate: Executable Path"

{
  "php.validate.executablePath": "/usr/bin/php",
  "phpserver.phpConfigPath": "/etc/php.ini",
}

#windows
{
  "php.validate.executablePath": "D:/xampp/php/php.exe",
  "phpserver.phpConfigPath": "D:/xampp/php/php.ini"
  "phpserver.phpPath": "D:/xampp/php/php.exe"
}

#mac
{
  "phpserver.phpConfigPath": "/opt/homebrew/etc/php/8.1/php.ini",
  "php.validate.executablePath": "/opt/homebrew/Cellar/[email protected]/8.1.22/bin/php"
}

安装 PHP Server后 , php文件中可以选择右键 PHP Server:Serve project,直接跳转到浏览器

2、下载插件:open in browser

安装 open in browser 插件后,在 HTML 文件中「右键选择 –> Open in Default Browser」,即可在浏览器中预览网页。

3、Code Runner 插件

可直接运行php代码。

以下为官方文档参考: https://www.php.net/manual/zh/

PHP 是什么?

PHP(“PHP: Hypertext Preprocessor”,超文本预处理器的字母缩写)是一种被广泛应用的开放源代码的多用途脚本语言,它可嵌入到 HTML中,尤其适合 web 开发。

以上是一个简单的回答,不过这是什么意思呢?请看如下例子:

示例 #1 一个介绍性的范例

<!DOCTYPE html>
<html>
    <head>
        <title>Example</title>
    </head>
    <body>

        <?php
        echo "Hi, I'm a PHP script!";
        ?>

    </body>
</html>

PHP 页面并不包含大量输出 HTML 的命令(如 C 或 Perl 中所示),而是包含嵌入代码的 HTML,这些代码可执行某些操作(在本例中为输出 Hi, I'm a PHP script!)。PHP 代码包含在特殊的开始和结束处理指令 <?php 和 ?> 中,允许跳入和退出 PHP 模式。

和客户端的 JavaScript 不同的是,PHP 代码是运行在服务端的。如果在服务器上建立了如上例类似的代码,则在运行该脚本后,客户端就能接收到其结果,但无法得知底层代码是什么。甚至可以将 web 服务器设置成让 PHP 来处理所有的 HTML 文件,这么一来,用户就无法知道正在使用 PHP。

使用 PHP 的最大的好处是它对于初学者来说极其简单,同时也给专业的程序员提供了各种高级的特性。当看到 PHP 长长的特性列表时,请不要害怕。使用 PHP,几乎任何人都可以快速上手并编写简单的脚本。

尽管 PHP 的开发主要侧重于服务器端脚本,但它可以做很多事情。

PHP 能做什么?

https://www.php.net/manual/zh/introduction.php

PHP 能做任何事。PHP 主要是用于服务端的脚本程序,因此可以用 PHP 来完成任何其它的 CGI 程序能够完成的工作,例如收集表单数据,生成动态网页,或者发送/接收 Cookies。但 PHP 的功能远不局限于此。

PHP 脚本主要用于以下两个领域:

  • 服务器端脚本。这是 PHP 使用最广泛、最主要的目标领域。开展这项工作需要具备以下三点:PHP 解析器(CGI 或服务器模块)、Web 服务器和 Web 浏览器。所有这些都可以在本地机器上运行,以便尝试 PHP 编程。有关更多信息,请参阅安装说明部分。
  • 命令行脚本。PHP 脚本无需任何服务器或浏览器即可运行,只需 PHP 解析器即可使用。这种方式非常适合使用 cron(在 Unix 或 macOS 上)或任务计划程序(在 Windows 上)定期执行的脚本。这些脚本还可用于简单的文本处理任务。有关更多信息,请参阅有关 PHP 的命令行用法的部分。

PHP 可用于所有主流操作系统,包括 Linux、许多 Unix 变体(包括 HP-UX、Solaris 和 OpenBSD)、Microsoft Windows、macOS、RISC OS 以及其他操作系统。PHP 还支持当今大多数 Web 服务器。这包括 Apache、IIS 和许多其他服务器。这包括任何可以使用 FastCGI PHP 二进制文件的 Web 服务器,如 lighttpd 和 nginx。PHP 既可以作为模块工作,也可以作为 CGI 处理器工作。

因此,使用 PHP,开发者可以自由地选择操作系统和 web 服务器。同时,还可以在开发时选择使用面向过程或者面对对象(OOP),或者两者的混和。

PHP 不仅限于输出 HTML。PHP 的功能包括输出丰富的文件类型,例如图像或 PDF 文件、加密数据和发送电子邮件。还可以轻松输出任何文本,例如 JSON 或 XML。PHP 可以自动生成这些文件,并将它们保存在文件系统中,而不是将其打印出来,从而形成动态内容的服务器端缓存。

PHP 最强大最显著的特性之一,是它支持很大范围的数据库。使用任何针对某数据库的扩展(例如 mysql)编写数据库支持的网页非常简单,或者使用抽象层如 PDO,或者通过 ODBC 扩展连接到任何支持 ODBC 标准的数据库。其它一些数据库也可能会用 cURL 或者 sockets,例如 CouchDB。

PHP 还支持使用 LDAP、IMAP、SNMP、NNTP、POP3、HTTP、COM(Windows 环境)等协议与其他服务通信,以及其他无数协议。还可以打开原始网络套接字并使用任何其他协议进行交互。PHP 支持几乎所有 web 开发语言之间的 WDDX 复杂数据交换。关于相互连接,PHP 已经支持了对 Java 对象的实例化,并且可以无缝的将其用作 PHP 对象。

PHP 具有极其有效的文本处理特性,包括 Perl 兼容正则表达式(PCRE)以及许多扩展和工具可用于解析和访问 XML 文档。PHP 将所有的 XML 功能标准化于坚实的 libxml2 扩展,并且还增加了 SimpleXMLXMLReader 以及 XMLWriter 支持以扩充其功能。

另外,还有很多其它有趣的扩展库,在此根据字母分类归类列出。还有一些附加的 PECL 扩展 可能有也可能没有在 PHP 手册中列出,例如 » XDebug

第一个 PHP 页面

https://www.php.net/manual/zh/tutorial.firstpage.php

在 web 服务器根目录(DOCUMENT_ROOT)下建立一个文件名为 hello.php,然后完成如下内容:

示例 #1 第一个 PHP 脚本:hello.php

<?php

echo "Hello World!";

?>

在浏览器的地址栏里输入 web 服务器的 URL 访问这个文件,在结尾加上“/hello.php”。如果本地开发,那么 URL 一般是 http://localhost/hello.php 或者 http://127.0.0.1/hello.php ,当然这取决于 web 服务器的设置。如果所有的设置都正确,那么这个文件将被 PHP 解析,浏览器会显示输出的“Hello World”。

PHP 可以嵌入到普通 HTML 网页中。这意味着可以在 HTML 文档中编写 PHP 语句,如下例所示:

<!DOCTYPE html>
<html>
    <head>
        <title>PHP Test</title>
    </head>
    <body>
        <?php echo '<p>Hello World</p>'; ?>
    </body>
</html>

这会产生以下输出:

<!DOCTYPE html>
<html>
    <head>
        <title>PHP Test</title>
    </head>
    <body>
        <p>Hello World</p>
    </body>
</html>

该程序非常的简单,它仅仅只是利用了 PHP 的 echo 语句显示了 Hello World。用户一定不会满足与此。请注意该文件无需被执行或以任何方式指定。服务器会找到该文件并提供给 PHP 进行解释,因为使用了“.php”的扩展名,服务器已被配置成自动传递有着“.php”扩展名的文件给 PHP。一个普通的 HTML 文件,加上了几个特别的标签,就可以做很多非常有趣的事情!

如果试过了这个例子,但是没有得到任何输出,或者浏览器弹出了下载框,或者浏览器以文本方式显示了源文件,可能的原因是服务器还没有支持 PHP,或者没有正确配置。

还要确认通过浏览器访问的 URL 确实指向了服务器上的这个文件。如果只是从本地文件系统调用这个文件,它不会被 PHP 解析。

以上例子的目的是为了显示 PHP 特殊标识符的格式。在这个例子中,用 <?php 来表示 PHP 标识符的起始,然后放入 PHP 语句并通过加上一个终止标识符 ?> 来退出 PHP 模式。可以根据自己的需要在 HTML 文件中像这样开启或关闭 PHP 模式。

注意: 关于换行

尽管换行在 HTML 中的实际意义不是很大,但适当地使用换行可以使 HTML 代码易读且美观。PHP 会在输出时自动删除其结束符 ?> 后的一个换行。该功能主要是针对在一个页面中嵌入多段 PHP 代码或者包含了无实质性输出的 PHP 文件而设计,与此同时也造成了一些疑惑。如果需要在 PHP 结束符 ?> 之后输出换行的话,可以在其后加一个空格,或者在最后的一个 echo/print 语句中加入一个换行。

现在已经成功建立了一个简单的 PHP 脚本,那么再来建立一个最著名的PHP 脚本!调用函数 phpinfo(),将会看到很多有关自己系统的有用信息,例如预定义变量、已经加载的PHP 模块和配置信息。请花一些时间来查看这些重要的信息。

示例 #2 从 PHP 获取系统信息

<?php phpinfo(); ?>

PS: 在vscode中右键,PHP Server: Serve project 运行代码。

实用的脚本

https://www.php.net/manual/zh/tutorial.useful.php

现在来编写一些更实用的脚本,比如检查浏览页面的访问者在用什么浏览器。要达到这个目的,需要检查用户的agent 字符串,它是浏览器发送的 HTTP 请求的一部分。该信息被存储在一个变量中。在 PHP中,变量总是以一个美元符开头。我们现在感兴趣的变量是 $_SERVER['HTTP_USER_AGENT']。

注意:

$_SERVER是一个特殊的 PHP 保留变量,它包含了 web服务器提供的所有信息,被称为超全局变量。请查阅本手册“超全局变量”中的有关内容以获取更多信息。

示例 #1 打印一个变量(数组元素)

<?php
echo $_SERVER['HTTP_USER_AGENT'];
?>

该脚本的输出可能是:

Mozilla/5.0 (Linux) Firefox/112.0

PHP 有多种不同类型的变量。以上例子是从 Array 变量中打印了一个元素。数组非常有用。

$_SERVER 只是 PHP 自动全局化的变量之一。可以查阅“预定义变量”一节来查看这些变量的列表,或者也可以通过上节例子中 phpinfo() 函数的输出来查看。

可以在一个 PHP 标识中加入多个 PHP 语句,也可以建立一个代码块来做比简单的 echo 更多的事情。例如,如果需要检测 Firefox,可以进行如下操作:

示例 #2 流程控制函数的使用

<?php
if (str_contains($_SERVER['HTTP_USER_AGENT'], 'Firefox')) {
    echo 'You are using Firefox.';
}
?>

该脚本的输出可能是:

You are using Firefox.

这里要介绍一些新的原理。上面用了一个 if 语句。如果用户对C 语言的基本语法比较熟悉,则应该对此很熟悉,否则,可能需要拿起任何一本PHP 介绍性的书籍并阅读前面的两三个章节,或者也可以阅读本手册的“语言参考”一章。

需要介绍的第二个原理,是对 str_contains()函数的调用。str_contains() 是 PHP的一个内置函数,其功能是确定指定字符串是否包含其它字符串。例如我们现在需要在 $_SERVER['HTTP_USER_AGENT']​(即所谓的 haystack)变量中寻找'Firefox'。如果在这个 haystack中该字符串(即所谓的 needle)被找到(“草里寻针”),则函数返回 true;如果没有,则返回 false。如果返回 true,则 if 会将条件判断为true 并运行其花括号 {} 内的代码;否则,则不运行这些代码。

可以自己尝试利用ifelse 以及其它的函数如strtoupper()strlen()来建立类似的脚本。在本手册中相关的页面也包含有范例。如果对如何使用函数不是很确定,可以阅读手册中有关“如何阅读函数的定义”和“函数”的有关章节。

以下我们进一步显示如何进出 PHP 模式,甚至是在一个 PHP 代码块的中间:

示例 #3 混和 HTML 和 PHP 模式

<?php
if (str_contains($_SERVER['HTTP_USER_AGENT'], 'Firefox')) {
?>
<h3>str_contains() returned true</h3>
<p>You are using Firefox</p>
<?php
} else {
?>
<h3>str_contains() returned false</h3>
<p>You are not using Firefox</p>
<?php
}
?>

该脚本的输出可能是:

<h3>str_contains() returned true</h3>
<p>You are using Firefox</p>

和使用 PHP echo 语句输出不同的是,以上示例跳出了 PHP 模式直接发送 HTML。这里需要特别注意的一点是脚本的逻辑流保持不变。根据 str_contains() 的结果,只会有一个 HTML 块将发送给浏览者。换句话说,取决于是否找到字符串 Firefox。

处理表单

https://www.php.net/manual/zh/tutorial.forms.php

PHP 一个很有用的特点体现在它处理 PHP 表单的方式。需要理解的非常重要的原理,是表单的任何元素都在 PHP 脚本中自动生效。请参阅本手册中“PHP 的外部变量”以获取关于在 PHP 中使用表单的详细信息及范例。以下是 HTML 表单的范例:

示例 #1 一个简单的 HTML 表单

<form action="action.php" method="post">
    <label for="name">Your name:</label>
    <input name="name" id="name" type="text">

    <label for="age">Your age:</label>
    <input name="age" id="age" type="number">

    <button type="submit">Submit</button>
</form>

该表单中并没有什么特殊的地方,其中没有使用任何特殊的标识符。当用户填写了该表单并点击了提交按钮,页面 action.php 将被调用。在该文件中,可以加入如下内容:

示例 #2 打印来自表单的数据

Hi <?php echo htmlspecialchars($_POST['name']); ?>.
You are <?php echo (int) $_POST['age']; ?> years old.

该脚本的输出可能是:

Hi Joe. You are 22 years old.

除了htmlspecialchars() 和 (int)部分,这段程序做什么用显而易见。htmlspecialchars() 使得 HTML 之中的特殊字符被正确的编码,从而不会被使用者在页面注入 HTML 标签或者Javascript 代码。例如 age 字段,我们明确知道他是一个数值,因此我们将它转换为一个int来自动的消除任何不必要的字符。也可以使用 PHP 的 filter 扩展来自动完成该工作。

PHP 将自动设置 $_POST['name'] 和 $_POST['age'] 变量。

在这之前我们使用了超全局变量 $_SERVER,现在我们引入了超全局变量 $_POST,它包含了所有的 POST 数据。请注意我们的表单提交数据的方法(method)。如果使用了 GET 方法,那么表单中的信息将被储存到超全局变量 $_GET中。

如果并不关心请求数据的来源,也可以用超全局变量 $_REQUEST,它包含了所有 GET、POST、COOKIE 和 FILE 的数据。

示例2

  • age 处有 (int) 转换不存在xss漏洞。
  • name 处存在xss漏洞
<script>alert(1)</script>

安全分享

xss防御
  • 输入过滤
    • 后端做特殊字符过滤
  • 输出编码
    • 转译特殊字符为html实体编码
      • 在PHP中,有htmlentities()和htmlspecialchars()两个函数可以满足安全要求。

action.php可以这样写

你好,<?php echo htmlspecialchars($_POST['name']); ?>。
你 <?php echo (int)$_POST['age']; ?> 岁了。

哪种防御手段更好,输出编码更好。

  • 如果做输入过滤,很可能存在被绕过的情况。因为js比较灵活,版本也在更新,过滤的可能不全。
  • 业务本身就要这些特殊符号,不能过滤。而输出编码做了html实体编码,只是输出在浏览器展示的时候,而且操作很简单只需要加个编码函数就行了,不存在任何绕过情况。
cookie

官方文档提到了COOKIE,那么cookie从哪得到了的?这分几种情况:

  • 第一次访问网站,给的cookie
  • 登录之后给一个
  • 登录时拿第一次给的cookie登录,这里就存在风险了。因为这个cookie可以是别人发给你的链接,比如拿了黑客给的cookie登录,这种就叫会话固定攻击。

PHP基本语法

PHP 标记

https://www.php.net/manual/zh/language.basic-syntax.phptags.php

当解析一个文件时,PHP 会寻找起始和结束标记,也就是 <?php?> ,这告诉 PHP 开始和停止解析二者之间的代码。此种解析方式使得 PHP 可以被嵌入到各种不同的文档中去,而任何起始和结束标记之外的部分都会被 PHP 解析器忽略。

PHP 有一个 echo 标记简写 <?=, 它是更完整的 <?php echo 的简写形式。

示例 #1 PHP 开始和结束标记

1.  <?php echo 'if you want to serve PHP code in XHTML or XML documents,
                use these tags'; ?>

2.  You can use the short echo tag to <?= 'print this string' ?>.
    It's equivalent to <?php echo 'print this string' ?>.

3.  <? echo 'this code is within short tags, but will only work '.
            'if short_open_tag is enabled'; ?>

短标记 (第三个例子) 是被默认开启的,但是也可以通过 short_open_tag php.ini 来直接禁用。如果 PHP 在被安装时使用了 --disable-short-tags 的配置,该功能则是被默认禁用的。

注意:

因为短标记可以被禁用,所以建议使用普通标记 (<?php ?> 和 <?= ?>) 来最大化兼容性。

如果文件内容仅仅包含 PHP 代码,最好在文件末尾删除 PHP 结束标记。这可以避免在 PHP 结束标记之后万一意外加入了空格或者换行符,会导致 PHP 开始输出这些空白,而脚本中此时并无输出的意图。

<?php
echo "Hello world";

// ... 更多代码

echo "Last statement";

// 脚本在此处结束,没有 PHP 结束标记

从 HTML 中分离

https://www.php.net/manual/zh/language.basic-syntax.phpmode.php

凡是在一对开始和结束标记之外的内容都会被 PHP 解析器忽略,这使得 PHP 文件可以具备混合内容。 可以使 PHP 嵌入到 HTML 文档中去,如下例所示。

<p>This is going to be ignored by PHP and displayed by the browser.</p>
<?php echo 'While this is going to be parsed.'; ?>
<p>This will also be ignored by PHP and displayed by the browser.</p>

这将如预期中的运行,因为当 PHP 解释器碰到 ?> 结束标记时就简单地将其后内容原样输出(除非马上紧接换行 - 见 指令分隔符)直到碰到下一个开始标记;例外是处于条件语句中间时,此时 PHP 解释器会根据条件判断来决定哪些输出,哪些跳过。见下例。

使用条件结构:

示例 #1 使用条件的高级分离术

<?php if ($expression == true): ?>
  This will show if the expression is true.
<?php else: ?>
  Otherwise this will show.
<?php endif; ?>

上例中 PHP 将跳过条件语句未达成的段落,即使该段落位于 PHP 开始和结束标记之外。由于PHP 解释器会在条件未达成时直接跳过该段条件语句块,因此 PHP 会根据条件来忽略之。

要输出大段文本时,跳出 PHP 解析模式通常比将文本通过 echoprint 输出更有效率。

指令分隔符

https://www.php.net/manual/zh/language.basic-syntax.instruction-separation.php

同 C 或 Perl 一样,PHP 需要在每个语句后用分号结束指令。一段 PHP 代码中的结束标记隐含表示了一个分号;在一个 PHP 代码段中的最后一行可以不用分号结束。如果后面还有新行,则代码段的结束标记包含了行结束。

示例 #1 包含末尾换行符的结束标记的例子

<?php echo "Some text"; ?>
No newline
<?= "But newline now" ?>

以上示例会输出:

Some textNo newline
But newline now

进入和退出 PHP 解析的例子:

<?php
    echo 'This is a test';
?>

<?php echo 'This is a test' ?>

<?php echo 'We omitted the last closing tag';

注释

https://www.php.net/manual/zh/language.basic-syntax.comments.php

PHP 支持 C,C++ 和 Unix Shell 风格(Perl 风格)的注释。例如:

<?php
    echo 'This is a test'; // 这是单行 c++ 样式注释
    /* 这是一条多行注释
       另一行也是注释 */
    echo 'This is yet another test';
    echo 'One Final Test'; # 这是单行 shell 风格的注释
?>

单行注释仅仅注释到行末或者当前的 PHP 代码块,视乎哪个首先出现。这意味着在 / … ?> 或者 # … ?> 之后的 HTML 代码将被显示出来:?> 跳出了 PHP 模式并返回了 HTML 模式,/ 或 # 并不能影响到这一点。

<h1>This is an <?php # echo 'simple';?> example</h1>
<p>The header above will say 'This is an  example'.</p>

C 风格的注释在碰到第一个 */ 时结束。要确保不要嵌套 C 风格的注释。试图注释掉一大块代码时很容易出现该错误。

<?php
 /*
    echo 'This is a test'; /* 这个注释会引发问题 */
 */
?>

PHP类型

简介

PHP 支持 10 种原始数据类型。

四种标量类型:

  • bool(布尔型)
  • int(整型)
  • float(浮点型,也称作 double)
  • string(字符串)

四种复合类型:

  • array(数组)
  • object(对象)
  • callable(可调用)
  • iterable(可迭代)

最后是两种特殊类型:

  • resource(资源)
  • NULL(无类型)

可能还会读到一些关于“双精度(double)”类型的参考。实际上 double 和 float 是相同的,由于一些历史的原因,这两个名称同时存在。

变量的类型通常不是由程序员设定的,确切地说,是由 PHP 根据该变量使用的上下文在运行时决定的。

注意: 使用类型转换,强制将表达式的值转换为某种类型。还可以使用 settype() 函数就地对变量进行类型转换

使用 var_dump() 函数检查表达式的值和类型。使用 get_debug_type() 检索表达式的值和类型。使用 is_type 检查表达式是否属于某种类型。

示例 #1 不同类型

<?php
$a_bool = true;   // a bool
$a_str  = "foo";  // a string
$a_str2 = 'foo';  // a string
$an_int = 12;     // an int
echo get_debug_type($a_bool), "\n"; // 输出: boolean
echo get_debug_type($a_str), "\n";  // 输出: string

// 如果是整型,就加上 4
if (is_int($an_int)) {
    $an_int += 4;
}
var_dump($an_int); // 输出: int(16)

// 如果 $a_bool 是字符串,就打印出来
if (is_string($a_bool)) {
    echo "String: $a_bool";
}
?>

以上示例在 PHP 8 中的输出:

bool
string
int(16)

什么漏洞只有php有,而其它语言没有?

  • php include函数会有文件包含漏洞。

Boolean 布尔类型

https://www.php.net/manual/zh/language.types.boolean.php

bool 仅有两个值,用于表达真(truth)值,不是 true 就是 false

语法

要指定一个 bool,使用常量 truefalse。两个都不区分大小写。

<?php
$foo = True; // 设置 $foo 为 TRUE
?>

通常运算符所返回的 bool 值结果会被传递给控制流程

<?php
// == 是一个操作符,它检测两个变量是否相等,并返回一个布尔值
if ($action == "show_version") {
    echo "The version is 1.23";
}

// 这样做是不必要的...
if ($show_separators == TRUE) {
    echo "<hr>\n";
}

// ...因为可以使用下面这种简单的方式:
if ($show_separators) {
    echo "<hr>\n";
}
?>

转换为布尔值

要明确地将值转换成 bool,可以用 (bool)强制转换。通常这不是必需的,因为值在逻辑上下文中使用将会自动解释为 bool 类型的值。更多信息请阅读类型转换页面。

参见类型转换的判别

当转换为 bool 时,以下值被认为是 false

  • 布尔false 本身
  • 整型值 0(零)
  • 浮点型值 0.0(零)-0.0(零)
  • 字符串 "",以及字符串 "0"
  • 不包括任何元素的数组
  • 原子类型 NULL(包括尚未赋值的变量)
  • 内部对象的强制转换行为重载为 bool。例如:由不带属性的空元素创建的 SimpleXML 对象。

所有其它值都被认为是 true(包括 资源NAN)。

警告 -1 和其它非零值(不论正负)一样,被认为是 true!

示例 #1 转换为布尔型

<?php
var_dump((bool) "");        // bool(false)
var_dump((bool) "0");       // bool(false)
var_dump((bool) 1);         // bool(true)
var_dump((bool) -2);        // bool(true)
var_dump((bool) "foo");     // bool(true)
var_dump((bool) 2.3e5);     // bool(true)
var_dump((bool) array(12)); // bool(true)
var_dump((bool) array());   // bool(false)
var_dump((bool) "false");   // bool(true)
?>

Integer 整型

https://www.php.net/manual/zh/language.types.integer.php

int 是集合 ℤ = {…, -2, -1, 0, 1, 2, …} 中的某个数。

参见

语法

Int 可以使用十进制,十六进制,八进制或二进制表示,前面可以加上可选的符号(- 或者 +)。可以用负运算符来表示一个负的 int

要使用八进制表达,数字前必须加上 0(零)。 PHP 8.1.0 起,八进制表达也可以在前面加上 0o 或者 0O 。 要使用十六进制表达,数字前必须加上 0x。要使用二进制表达,数字前必须加上 0b。

从 PHP 7.4.0 开始,整型数值可能会包含下划线 (_),为了更好的阅读体验,这些下划线在展示的时候,会被 PHP 过滤掉。

示例 #1 整数文字表达

<?php
$a = 1234; // 十进制数
$a = 0123; // 八进制数 (等于十进制 83)
$a = 0o123; // 八进制数 (PHP 8.1.0 起)
$a = 0x1A; // 十六进制数 (等于十进制 26)
$a = 0b11111111; // 二进制数字 (等于十进制 255)
$a = 1_234_567; // 整型数值 (PHP 7.4.0 以后)
?>

int literal 的结构形式从 PHP 8.1.0 开始是(之前不允许使用 0o 或 0O 八进制前缀,并且 PHP 7.4.0 之前不允许使用下划线):

decimal     : [1-9][0-9]*(_[0-9]+)*
            | 0

hexadecimal : 0[xX][0-9a-fA-F]+(_[0-9a-fA-F]+)*

octal       : 0[oO]?[0-7]+(_[0-7]+)*

binary      : 0[bB][01]+(_[01]+)*

integer     : decimal
            | hexadecimal
            | octal
            | binary

整型数 int 的字长和平台有关,尽管通常最大值是大约二十亿(32 位有符号)。64位平台下的最大值通常是大约 9E18。PHP 不支持无符号的 intint 值的字长可以用常量 PHP_INT_SIZE来表示,最大值可以用常量 PHP_INT_MAX 来表示,最小值可以用常量 PHP_INT_MIN 表示。

整数溢出

如果给定的一个数超出了 int 的范围,将会被解释为 float。同样如果执行的运算结果超出了 int 范围,也会返回 float

示例 #2 整数溢出

<?php
$large_number = 50000000000000000000;
var_dump($large_number);         // float(5.0E+19)
var_dump(PHP_INT_MAX + 1);       // 32-bit system: float(2147483648)
                                 // 64-bit system: float(9.2233720368548E+18)
?>

整数除法

PHP 没有 int 除法取整运算符,要使用 intdiv() 实现。1/2 产生出 float 0.5。值可以舍弃小数部分,强制转换为 int,或者使用 round() 函数可以更好地进行四舍五入。

示例 #3 Divisions

<?php
var_dump(25/7);         // float(3.5714285714286) 
var_dump((int) (25/7)); // int(3)
var_dump(round(25/7));  // float(4) 
?>

转换为整型

要明确地将一个值转换为 int,用 (int) 或(integer) 强制转换。不过大多数情况下都不需要强制转换,因为当运算符,函数或流程控制需要一个 int 参数时,值会自动转换。还可以通过函数 intval() 来将一个值转换成 int 整型。

resource 转换成 int 时,结果会是 PHP 运行时为 resource 分配的唯一资源号。

  • 从布尔值转换

    false 将产生出 0(零),true 将产生出 1(壹)。

  • 从浮点型转换

    当从浮点数 float 转换成整数 int时,将向零取整。自 PHP 8.1.0 起,当将非整数类型的 float 转换为失去精度的 int 时,会发出弃用通知。

示例 #4 从浮点数转换

<?php

function foo($value): int {
  return $value; 
}

var_dump(foo(8.1)); // 自 PHP 8.1.0 起:“Deprecated: Implicit conversion from float 8.1 to int loses precision”
var_dump(foo(8.1)); // PHP 8.1.0 之前为 8
var_dump(foo(8.0)); // 8 in both cases

var_dump((int) 8.1); // 8 in both cases
var_dump(intval(8.1)); // 8 in both cases
?>

执行结果

#Output of the above example in PHP 8.4.4:
Deprecated: Implicit conversion from float 8.1 to int loses precision in script on line 4
int(8)
Deprecated: Implicit conversion from float 8.1 to int loses precision in script on line 4
int(8)
int(8)
int(8)
int(8)

如果浮点数超出了 int 范围(32 位平台下通常为 +/- 2.15e+9 = 2^31,64 位平台下,通常为 +/- 9.22e+18 = 2^63),则结果为未定义, 因为没有足够的精度给出一个确切的 int 结果。 在此情况下没有警告,甚至没有任何通知!

注意: NaN、Inf 和 -Inf 在转换成 int 时是零。

警告 绝不要将未知的分数强制转换为 int,这样有时会导致不可预料的结果。

<?php
echo (int) ( (0.1+0.7) * 10 ); // 显示 7!
?>

参见关于浮点数精度的警告

  • 从 NULL 转换

    null 会转换为零(0)。

安全分享-整数溢出

<?php

$large_number = 9223372036854775807;
var_dump($large_number); //int(9223372036854775807

$large_number = 9223372036854775808;
var_dump($large_number); //float(9.223372036854776E+18)
echo (int)$large_number; //-9223372036854775808 整数溢出

电商网站,抓包做整数溢出

  • 修改商品价格,但平台一般有价格检验,修改商品价格肯定是修改不成功的。
  • 修改订单价格,但订单价格也有逻辑,订单价格=产品价格+不同产品
  • 最终订单价格中肯定有数量,我们可以挑一个最贵的新产品把数量改多,可以通过这种价格乘以数量的方式让整数超过最大值。

这种修改商品数量,一般很难找到问题,因为消费逻辑和功能逻辑都没问题。

防止整数溢出:

  • 限制数量,限制价格,但在有大活动时又不希望限制
  • 在整个电商系统中,对支付的数字使用专用的类型decimal,这是专门做支付精确价格的类型

我们在审计整数溢出漏洞时,关注以下几点:

  • 功能:如果是支付功能,有整数溢出就很严重了
  • 是否可以绕过限制逻辑。

利用针对代码的白盒扫描器发现,聚合整个代码里的业务逻辑特征结合这个变量的类型来判断。

Float 浮点型

https://www.php.net/manual/zh/language.types.float.php

浮点型(也叫浮点数 float,双精度数 double 或实数 real)可以用以下任一语法定义:

<?php
$a = 1.234; 
$b = 1.2e3; 
$c = 7E-10;
$d = 1_234.567; // 从 PHP 7.4.0 开始支持
?>

浮点数的形式表示(PHP 7.4.0 之前不支持下划线):

LNUM          [0-9]+(_[0-9]+)*
DNUM ({LNUM}?"."{LNUM}) | ({LNUM}"."{LNUM}?)
EXPONENT_DNUM (({LNUM} | {DNUM}) [eE][+-]? {LNUM})

浮点数的字长和平台相关,尽管通常最大值是 1.8e308 并具有 14 位十进制数字的精度(64 位 IEEE 格式)。

警告 浮点数的精度

浮点数的精度有限。尽管取决于系统,PHP 通常使用 IEEE 754 双精度格式,则由于取整而导致的最大相对误差为 1.11e-16。非基本数学运算可能会给出更大误差,并且要考虑到进行复合运算时的误差传递。

此外,以十进制能够精确表示的有理数如 0.1 或 0.7,无论有多少尾数都不能被内部所使用的二进制精确表示,因此不能在不丢失一点点精度的情况下转换为二进制的格式。这就会造成混乱的结果:例如,floor((0.1+0.7)*10) 通常会返回 7 而不是预期中的 8,因为该结果内部的表示其实是类似 7.9999999999999991118…。

所以永远不要相信浮点数结果精确到了最后一位,也永远不要比较两个浮点数是否相等。如果确实需要更高的精度,应该使用任意精度数学函数或者 gmp 函数

参见» 浮点数指南网页的简单解释。

转换为浮点数

  • 从 string 转换

    如果 string 是 numeric 或者前导数字, 则将它解析为相应的 float 值,否则将转换为零(0)。

  • 从其他类型转换

    对于其它类型的值,其情况类似于先将值转换成 int,然后再转换成 float。 请参阅“转换为整型”一节以获取更多信息。

    注意: 某些类型在转换成 int 时有未定义行为,转换为 float 时也会如此。

比较浮点数

如上述警告信息所言,由于内部表达方式的原因,比较两个浮点数是否相等是有问题的。不过还是有迂回的方法来比较浮点数值的。

要测试浮点数是否相等,要使用一个仅比该数值大一丁点的最小误差值。该值也被称为机器极小值(epsilon)或最小单元取整数,是计算中所能接受的最小的差别值。

$a 和 $b 在小数点后五位精度内都是相等的。

示例 #1 浮点比较

<?php
$a = 1.23456789;
$b = 1.23456780;
$epsilon = 0.00001;

if (abs($a - $b) < $epsilon) {
    echo "true";
}

// 执行结果 true
?>

NaN

某些数学运算会产生一个由常量 NAN 所代表的结果。此结果代表着一个在浮点数运算中未定义或不可表述的值。任何拿此值与其它任何值(除了 true)进行的松散或严格比较的结果都是 false

由于 NAN 代表着任何不同值,不应拿 NAN 去和其它值进行比较,包括其自身,应该用 is_nan() 来检查。

String 字符串

https://www.php.net/manual/zh/language.types.string.php

一个字符串 string 就是由一系列的字符组成,其中每个字符等同于一个字节。这意味着 PHP 只能支持 256 的字符集,因此不支持 Unicode 。详见字符串类型详解。

注意: 在 32 位版本中,string 最大可以达到 2GB(最多 2147483647 字节)。

语法

一个字符串可以用 4 种方式表达:

单引号

定义一个字符串的最简单的方法是用单引号把它包围起来(字符 ')。

要表达一个单引号自身,需在它的前面加个反斜线(\)来转义。要表达一个反斜线自身,则用两个反斜线(\\)。其它任何方式的反斜线都会被当成反斜线本身:也就是说如果想使用其它转义序列例如 \r 或者 \n,并不代表任何特殊含义,就单纯是这两个字符本身

注意: 不像双引号heredoc 语法结构,在单引号字符串中的变量和特殊字符的转义序列将不会被替换。

示例 #1 Syntax Variants

<?php
echo 'this is a simple string', PHP_EOL;

// 可以录入多行
echo 'You can also have embedded newlines in
strings this way as it is
okay to do', PHP_EOL;

// 输出: Arnold once said: "I'll be back"
echo 'Arnold once said: "I\'ll be back"', PHP_EOL;

// 输出: You deleted C:\*.*?
echo 'You deleted C:\\*.*?', PHP_EOL;

// 输出: You deleted C:\*.*?
echo 'You deleted C:\*.*?', PHP_EOL;

// 输出: This will not expand: \n a newline
echo 'This will not expand: \n a newline', PHP_EOL;

// 输出: Variables do not $expand $either
echo 'Variables do not $expand $either', PHP_EOL;
?>
双引号

如果字符串是包围在双引号("))中, PHP 将对以下特殊的字符进行解析:

Table 1: 转义字符
序列 含义
\n 换行(ASCII 字符集中的 LF 或 0x0A (10))
\r 回车(ASCII 字符集中的 CR 或 0x0D (13))
\t 水平制表符(ASCII 字符集中的 HT 或 0x09 (9))
\v 垂直制表符(ASCII 字符集中的 VT 或 0x0B (11))
\e Escape(ASCII 字符集中的 ESC 或 0x1B (27))
\f 换页(ASCII 字符集中的 FF 或 0x0C (12))
\\ 反斜线
\$ 美元标记
\" 双引号
\[0-7]{1,3} } 八进制:匹配正则表达式序列 [0-7]{1,3} 的是八进制表示法的字符序列(比如 "\101" — "A"),会静默溢出以适应一个字节(例如 "\400" — "\000")
\x[0-9A-Fa-f]{1,2} 十六进制:匹配正则表达式序列 [0-9A-Fa-f]{1,2} 的是十六进制表示法的一个字符(比如 "\x41" — "A")
\u{[0-9A-Fa-f]+} Unicode:匹配正则表达式 [0-9A-Fa-f]+ 的字符序列是 unicode 码位,该码位能作为 UTF-8 的表达方式输出字符串。序列中必须包含大括号。例如 "\u{41}" — "A"

和单引号字符串一样,转义任何其它字符都会导致反斜线被显示出来。

用双引号定义的字符串最重要的特征是变量会被解析,详见变量插值

Heredoc 结构

第三种表达字符串的方法是用 heredoc 句法结构:<<<。在该运算符之后要提供一个标识符,然后换行。接下来是字符串 string 本身,最后要用前面定义的标识符作为结束标志。

结束标识符可以使用空格或制表符(tab)缩进,此时文档字符串会删除所有缩进。 在 PHP 7.3.0 之前的版本中,结束时所引用的标识符必须在该行的第一列。

而且,标识符的命名也要像其它标签一样遵守 PHP 的规则:只能包含字母、数字和下划线,并且必须以字母和下划线作为开头。

示例 #2 PHP 7.3.0 之后的基础 Heredoc 示例

<?php
// 无缩进
echo <<<END
      a
     b
    c
\n
END;
// 4 空格缩进
echo <<<END
      a
     b
    c
    END;
#Output of the above example in PHP 8.4.4:
      a
     b
    c
  a
 b
c

如果结束标识符的缩进超过内容的任何一行的缩进,则将抛出 ParseError 异常:

示例 #3 结束标识符的缩进不能超过正文的任何一行

<?php
echo <<<END
  a
 b
c
   END;
#Output of the above example in PHP 8.4.4:
Parse error: Invalid body indentation level (expecting an indentation level of at least 3) in script on line 3

制表符也可以缩进结束标识符,但是,关于缩进结束标识符和内容, 制表符和空格不能混合使用。在以上任何情况下, 将会抛出 ParseError 异常。 之所以包含这些空白限制,是因为混合制表符和空格来缩进不利于易读性。

示例 #4 内容(空白)和结束标识符的不同缩进

<?php
// 以下所有代码都不起作用。
// 正文(空格)和结束标记(制表符),不同的缩进
{
    echo <<<END
     a
        END;
}
// 在正文中混合空格和制表符
{
    echo <<<END
        a
     END;
}
// 在结束标记中混合空格和制表符
{
    echo <<<END
          a
         END;
}
#以上示例在 PHP 7.3 中的输出:
Parse error: Invalid indentation - tabs and spaces cannot be mixed in example.php line 8

内容字符串的结束标识符后面不需要跟分号或者换行符。 例如,从 PHP 7.3.0 开始允许以下代码:

示例 #5 在结束标识符后继续表达式

<?php
$values = [<<<END
a
  b
    c
END, 'd e f'];
var_dump($values);
#以上示例在 PHP 7.3 中的输出:
array(2) {
  [0] =>
  string(11) "a
  b
    c"
  [1] =>
  string(5) "d e f"
}

Heredoc 结构就象是没有双引号的 string,这就是说在 heredoc 结构中单引号不用被转义,但是上文中列出的转义序列还可以使用。变量将被替换,但在 heredoc 结构中含有复杂的变量时要像 string 一样格外小心。

示例 #9 Heredoc 结构的字符串示例

<?php
$str = <<<EOD
Example of string
spanning multiple lines
using heredoc syntax.
EOD;

/* 含有变量的更复杂示例 */
class foo
{
    var $foo;
    var $bar;

    function __construct()
    {
        $this->foo = 'Foo';
        $this->bar = array('Bar1', 'Bar2', 'Bar3');
    }
}

$foo = new foo();
$name = 'MyName';

echo <<<EOT
My name is "$name". I am printing some $foo->foo.
Now, I am printing some {$foo->bar[1]}.
This should print a capital 'A': \x41
EOT;
?>

以上示例会输出:

My name is "MyName". I am printing some Foo.
Now, I am printing some Bar2.
This should print a capital 'A': A

也可以把 Heredoc 结构用在函数参数中来传递数据:

示例 #10 Heredoc 结构在参数中的示例

<?php
var_dump(array(<<<EOD
foobar!
EOD
));
?>
#Output of the above example in PHP 8.4.4:
array(1) {
  [0]=>
  string(7) "foobar!"
}

可以用 Heredoc 结构来初始化静态变量和类的属性和常量:

示例 #11 使用 Heredoc 结构来初始化静态值

<?php
// 静态变量
function foo()
{
    static $bar = <<<LABEL
Nothing in here...
LABEL;
}

// 类的常量、属性
class foo
{
    const BAR = <<<FOOBAR
Constant example
FOOBAR;

    public $baz = <<<FOOBAR
Property example
FOOBAR;
}
?>

还可以在 Heredoc 结构中用双引号来声明标识符:

示例 #12 在 heredoc 结构中使用双引号

<?php
echo <<<"FOOBAR"
Hello World!
FOOBAR;
?>
#Output of the above example in PHP 8.4.4:
Hello World!
Nowdoc 结构

就象 heredoc 结构类似于双引号字符串,Nowdoc 结构是类似于单引号字符串的。Nowdoc 结构很象 heredoc 结构,但是 nowdoc 中不进行字符串插值。这种结构很适合用于嵌入 PHP 代码或其它大段文本而无需对其中的特殊字符进行转义。与 SGML 的 <![CDATA[ ]]> 结构是用来声明大段的不用解析的文本类似,nowdoc 结构也有相同的特征。

一个 nowdoc 结构也用和 heredocs 结构一样的标记 <<<, 但是跟在后面的标识符要用单引号括起来,即 <<<'EOT'。Heredoc 结构的所有规则也同样适用于 nowdoc 结构,尤其是结束标识符的规则。

示例 #13 Nowdoc 结构字符串示例

<?php
echo <<<'EOD'
Example of string spanning multiple lines
using nowdoc syntax. Backslashes are always treated literally,
e.g. \\ and \'.
EOD;

以上示例会输出:

Example of string spanning multiple lines
using nowdoc syntax. Backslashes are always treated literally,
e.g. \\ and \'.

示例 #14 含变量引用的 Nowdoc 字符串示例

<?php

/* 含有变量的更复杂的示例 */
class foo
{
    public $foo;
    public $bar;

    function __construct()
    {
        $this->foo = 'Foo';
        $this->bar = array('Bar1', 'Bar2', 'Bar3');
    }
}

$foo = new foo();
$name = 'MyName';

echo <<<'EOT'
My name is "$name". I am printing some $foo->foo.
Now, I am printing some {$foo->bar[1]}.
This should not print a capital 'A': \x41
EOT;
?>

以上示例会输出:

My name is "$name". I am printing some $foo->foo.
Now, I am printing some {$foo->bar[1]}.
This should not print a capital 'A': \x41

示例 #15 静态数据的示例

<?php
class foo {
    public $bar = <<<'EOT'
bar
EOT;
}
?>

注意:Nowdoc 结构是在 PHP 5.3.0 中加入的

字符串插值

当字符串用双引号或 heredoc 结构定义时,其中的变量可以进行替换。

这里共有两种语法规则:一种基本规则,一种高级规则。基本的语法规则是最常用和最方便的,它可以用最少的代码在一个 string 中嵌入一个变量,一个 array 的值,或一个 object 的属性。

基本语法

如果遇到一个美元符号($),后面的字符会被解释为变量名,然后替换为变量的值。

示例 #16 String Interpolation

<?php
$juice = "apple";

echo "He drank some $juice juice." . PHP_EOL;

?>
He drank some apple juice.

从形式上讲,基本变量替换语法的结构如下:

string-variable::
     variable-name   (offset-or-property)?
   | ${   expression   }

offset-or-property::
     offset-in-string
   | property-in-string

offset-in-string::
     [   name   ]
   | [   variable-name   ]
   | [   integer-literal   ]

property-in-string::
     ->  name

variable-name::
     $   name

name::
     [a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*

示例 #17 插值数组或属性的第一个维度值

<?php
$juices = array("apple", "orange", "string_key" => "purple");

echo "He drank some $juices[0] juice.";
echo PHP_EOL;
echo "He drank some $juices[1] juice.";
echo PHP_EOL;
echo "He drank some $juices[string_key] juice.";
echo PHP_EOL;

class A {
    public $s = "string";
}

$o = new A();

echo "Object value: $o->s.";
?>

以上示例会输出:

He drank some apple juice.
He drank some orange juice.
He drank some purple juice.
Object value: string.

注意: 数组键必须是无引号的,因此不能用基本语法将常量作为键来引用。使用 高级 语法代替。

从 PHP 7.1.0 起,还支持负数字索引。

示例 #18 负数索引

<?php
$string = 'string';
echo "The character at index -2 is $string[-2].", PHP_EOL;
$string[-3] = 'o';
echo "Changing the character at index -3 to o gives $string.", PHP_EOL;
?>

以上示例会输出:

The character at index -2 is n.
Changing the character at index -3 to o gives strong.

对于更复杂的情况,可以使用 高级 语法。

高级(大括号)语法

高级语法允许使用任意访问器对变量进行插值。

任何标量变量、数组元素或对象属性(static 或非 static)都可以通过这种语法进行插值。表达式的写法和 在string之外的写法一样,然后用大括号{和 }包围。由于{不能被转义,所以这种语法只有在 紧跟在{后面的\(才会被识别。要得到一个字面的 {\),需要用{\$。以下是一些例子:

示例 #19 Curly Syntax

<?php
const DATA_KEY = 'const-key';
$great = 'fantastic';
$arr = [
    '1',
    '2',
    '3',
    [41, 42, 43],
    'key' => 'Indexed value',
    'const-key' => 'Key with minus sign',
    'foo' => ['foo1', 'foo2', 'foo3']
];

// 无效,输出 This is { fantastic}
echo "This is { $great}";

// 有效,输出 This is fantastic
echo "This is {$great}";

class Square {
    public $width;

    public function __construct(int $width) { $this->width = $width; }
}

$square = new Square(5);

// 有效
echo "This square is {$square->width}00 centimeters wide.";


// 有效,引用 key 仅使用花括号语法时有效
echo "This works: {$arr['key']}";


// 有效
echo "This works: {$arr[3][2]}";

echo "This works: {$arr[DATA_KEY]}";

// 使用多维数组时,在字符串内部时,始终使用括号括住数组
echo "This works: {$arr['foo'][2]}";

echo "This works: {$obj->values[3]->name}";

echo "This works: {$obj->$staticProp}";

// 无效,输出 C:\directory\{fantastic}.txt
echo "C:\directory\{$great}.txt";

// 有效,输出 C:\directory\fantastic.txt
echo "C:\\directory\\{$great}.txt";
?>
#Output of the above example in PHP 8.4.4:
This is { fantastic}This is fantasticThis square is 500 centimeters wide.This works: Indexed valueThis works: 43This works: Key with minus signThis works: foo3
Warning: Undefined variable $obj in script on line 44
Warning: Attempt to read property "values" on null in script on line 44
Warning: Trying to access array offset on null in script on line 44
Warning: Attempt to read property "name" on null in script on line 44
This works: 
Warning: Undefined variable $obj in script on line 46
Warning: Undefined variable $staticProp in script on line 46
Warning: Attempt to read property "" on null in script on line 46
This works: C:\directory\{fantastic}.txtC:\directory\fantastic.txt
存取和修改字符串中的字符

string 中的字符可以通过一个从 0 开始的下标,用类似 array 结构中的方括号包含对应的数字来访问和修改,比如 $str[42]。可以把 string 当成字符组成的 array。函数 substr()substr_replace() 可用于操作多于一个字符的情况。

注意: 从 PHP 7.1.0 开始,还支持 string 负偏移量。从 string 尾部到指定位置的偏移量。以前,负偏移量读取时(返回空 string)会发出 E_NOTICE, 写入时(string 保持不变)会发出 E_WARNING

注意: PHP 8.0.0 之前, 出于同样的目的,可以使用大括号访问 string,例如 $str{42}。 从 PHP 7.4.0 起,此大括号语法被弃用,自 PHP 8.0.0 开始不再受支持。

警告 用超出字符串长度的下标写入将会拉长该字符串并以空格填充。非整数类型下标会被转换成整数。非法下标类型会产生一个 E_WARNING 级别错误。 写入时只用到了赋值字符串的第一个字符。 PHP 7.1.0 开始,用空字符串赋值会导致 fatal 错误;在之前赋给的值是 NULL 字符。

警告 PHP 的字符串在内部是字节组成的数组。因此用花括号访问或修改字符串对多字节字符集很不安全。仅应对单字节编码例如 ISO-8859-1 的字符串进行此类操作。

注意: 从 PHP 7.1.0 开始,对空字符串应用空索引运算符会引发致命错误。 以前是空字符串会被静默转为数组。

示例 #20 一些字符串示例

<?php
// 取得字符串的第一个字符
$str = 'This is a test.';
$first = $str[0];
var_dump($first);

// 取得字符串的第三个字符
$third = $str[2];
var_dump($third);

// 取得字符串的最后一个字符
$str = 'This is still a test.';
$last = $str[strlen($str)-1];
var_dump($last);

// 修改字符串的最后一个字符
$str = 'Look at the sea';
$str[strlen($str)-1] = 'e';
var_dump($str);
?>
#Output of the above example in PHP 8.4.4:
string(1) "T"
string(1) "i"
string(1) "."
string(15) "Look at the see"

字符串下标必须为整数或可转换为整数的字符串,否则会发出警告。之前类似 "foo" 的下标会无声地转换成 0。

示例 #21 字符串无效下标的例子

<?php
$str = 'abc';

$keys = [ '1', '1.0', 'x', '1x' ];

foreach ($keys as $keyToTry) {
    var_dump(isset($str[$keyToTry]));

    try {
        var_dump($str[$keyToTry]);
    } catch (TypeError $e) {
        echo $e->getMessage(), PHP_EOL;
    }

    echo PHP_EOL;
}
?>

以上示例会输出:

bool(true)
string(1) "b"

bool(false)
Cannot access offset of type string on string

bool(false)
Cannot access offset of type string on string

bool(false)

Warning: Illegal string offset "1x" in Standard input code on line 10
string(1) "b"

注意: 用 [] 或 {} 访问任何其它类型(不包括数组或具有相应接口的对象实现)的变量只会无声地返回 null。

注意:可以直接在字符串原型中用 [] 或 {} 访问字符。

注意:PHP 7.4 中弃用在字符串字面量中使用 {} 来访问字符。 PHP 8.0 已移除。

有用的函数和运算符

字符串可以用 '.'(点)运算符连接起来,注意 '+'(加号)运算符没有这个功能。更多信息参考字符串运算符

对于 string 的操作有很多有用的函数。

可以参考字符串函数了解大部分函数,高级的查找与替换功能可以参考 Perl 兼容正则表达式函数

另外还有 URL 字符串函数,也有加密/解密字符串的函数(SodiumHash)。

最后,可以参考字符类型函数

转换成字符串

一个值可以通过在其前面加上 (string) 或用 strval() 函数来转变成字符串。在一个需要字符串的表达式中,会自动转换为 string。这发生在当使用 echo 或 print 函数,或当变量与 string 进行比较的时候。类型和类型转换可以更好的解释下面的事情,也可参考函数 settype()。

一个布尔值 bool 的 true 被转换成 string 的 "1"。bool 的 false 被转换成 ""(空字符串)。这种转换可以在 bool 和 string 之间相互进行。

一个整数 int 或浮点数 float 被转换为数字的字面样式的 string(包括 float 中的指数部分)。使用指数计数法的浮点数(4.1E+6)也可转换。

注意:PHP 8.0.0 起,十进制小数点字符都是一个句号(.)。 而在此之前的版本,在脚本的区域(category LC_NUMERIC) 中定义了十进制小数点字符。参见 setlocale()。

数组 array 总是转换成字符串 "Array",因此,echo 和 print 无法显示出该数组的内容。要显示某个单元,可以用 echo $arr['foo'] 这种结构。要显示整个数组内容见下文。

必须使用魔术方法 __toString 才能将 object 转换为 string。

资源 Resource 总会被转变成 "Resource id #1" 这种结构的字符串,其中的 1 是 PHP 在运行时分配给该 resource 的资源数字。 While the exact structure of this string should not be relied on and is subject to change, it will always be unique for a given resource within the lifetime of a script being executed (ie a Web request or CLI process) and won't be reused. 要得到一个 resource 的类型,可以用函数 get_resource_type()。

null 总是被转变成空字符串。

如上面所说的,直接把 array,object 或 resource 转换成 string 不会得到除了其类型之外的任何有用信息。可以使用函数 print_r() 和 var_dump() 列出这些类型的内容。

大部分的 PHP 值可以转变成 string 来永久保存,这被称作串行化,可以用函数 serialize() 来实现。

字符串类型详解

PHP 中的 string 的实现方式是一个由字节组成的数组再加上一个整数指明缓冲区长度。并无如何将字节转换成字符的信息,由程序员来决定。字符串由什么值来组成并无限制;特别的,其值为 0(“NUL bytes”)的字节可以处于字符串任何位置(不过有几个函数,在本手册中被称为非“二进制安全”的,也许会把 NUL 字节之后的数据全都忽略)。

字符串类型的此特性解释了为什么 PHP 中没有单独的“byte”类型 - 已经用字符串来代替了。返回非文本值的函数 - 例如从网络套接字读取的任意数据 - 仍会返回字符串。

由于 PHP 并不特别指明字符串的编码,那字符串到底是怎样编码的呢?例如字符串 "á" 到底是等于 "\xE1"(ISO-8859-1),"\xC3\xA1"(UTF-8,C form),"\x61\xCC\x81"(UTF-8,D form)还是任何其它可能的表达呢?答案是字符串会被按照该脚本文件相同的编码方式来编码。因此如果一个脚本的编码是 ISO-8859-1,则其中的字符串也会被编码为 ISO-8859-1,以此类推。不过这并不适用于激活了 Zend Multibyte 时;此时脚本可以是以任何方式编码的(明确指定或被自动检测)然后被转换为某种内部编码,然后字符串将被用此方式编码。注意脚本的编码有一些约束(如果激活了 Zend Multibyte 则是其内部编码)- 这意味着此编码应该是 ASCII 的兼容超集,例如 UTF-8 或 ISO-8859-1。不过要注意,依赖状态的编码其中相同的字节值可以用于首字母和非首字母而转换状态,这可能会造成问题。

当然了,要做到有用,操作文本的函数必须假定字符串是如何编码的。不幸的是,PHP 关于此的函数有很多变种:

  • 某些函数假定字符串是以单字节编码的,但并不需要将字节解释为特定的字符。例如 substr(),strpos(),strlen() 和 strcmp()。理解这些函数的另一种方法是它们作用于内存缓冲区,即按照字节和字节下标操作。
  • 某些函数被传递入了字符串的编码方式,也可能会假定默认无此信息。例如 htmlentities() 和 mbstring 扩展中的大部分函数。
  • 其它函数使用了当前区域(见 setlocale()),但是逐字节操作。
  • 最后一些函数会假定字符串是使用某特定编码的,通常是 UTF-8。intl 扩展和 PCRE(上例中仅在使用了 u 修饰符时)扩展中的大部分函数都是这样。

最后,要书写能够正确使用 Unicode 的程序依赖于很小心地避免那些可能会损坏数据的函数。要使用来自于 intl 和 mbstring 扩展的函数。不过使用能处理 Unicode 编码的函数只是个开始。不管用何种语言提供的函数,最基本的还是了解 Unicode 规格。例如一个程序如果假定只有大写和小写,那可是大错特错。

安全分享

$a = 1;
echo 'This will not expand: \n a newline $a' . "\n";  // php 中 . 点用于字符串拼接
echo "This will not expand: \n a newline $a";   // "" 内变量也是一种拼接

字符串拼接容易出现sql注入。

select username from users where id = 1 and age = 12 order by name limit 1

如果sql注入拼接了,什么情况下不能用预编译:

  • sql语句中变量不能加单引号的地方不能用预编译,预编译会加引号的。

预编译:

  • 本身包含的引号做转义
  • 两侧加单引号

sql注入在不同位置的修复方式:

order by 和 limit 后是不能加引号的。

#like后用concat来注入
like concat('%',?'%')

#limit 有注入怎么办? 强制整数转换

#order by 有注入怎么办? 做映射 1 对就 name;做列名白名单

#动态表名能预编译吗? 不能,因为他不用单引号。 但支持反引号 select * from `?`。防注入:1.表名加反引号,2.过滤反引号

#如果做了预编译,但预编译前面做了拼接还是会有注入。如下伪代码
$a = 'id';
$id = 1;
$sql = "select * from users where " . "$a = $id";
if ($input.concans('id')){ // 输入里包含id就做sql拼接
  $sql = $sql . "id = $id"
}
$sql = prepare("select * from users where id = ? and age = x");
$sql.set($id)

$sql = $sql . "name = ?";

query($sql);

Array 数组

https://www.php.net/manual/zh/language.types.array.php

PHP 中的 array 实际上是一个有序映射。映射是一种把 values 关联到 keys 的类型。此类型针对多种不同用途进行了优化; 它可以被视为数组、列表(向量)、哈希表(映射的实现)、字典、集合、堆栈、队列等等。 由于 array 的值可以是其它 array 所以树形结构和多维 array 也是允许的。

解释这些数据结构超出了本手册的范围,但对每种结构至少会提供一个例子。 要得到这些数据结构的更多信息,建议参考有关此广阔主题的其它文献。

语法

定义数组 array()

可以用 array() 语言结构来新建一个 array。它接受任意数量用逗号分隔的 键(key) => 值(value) 对。

array(
    key  => value,
    key2 => value2,
    key3 => value3,
    ...
)

最后一个数组单元之后的逗号可以省略。通常用于单行数组定义中,例如常用 array(1, 2) 而不是 array(1, 2, )。对多行数组定义通常保留最后一个逗号,这样要添加一个新单元时更方便。

注意: 可以用短数组语法 [] 替代 array() 。

示例 #1 一个简单数组

<?php
$array1 = array(
    "foo" => "bar",
    "bar" => "foo",
);

// 使用短数组语法
$array2 = [
    "foo" => "bar",
    "bar" => "foo",
];

var_dump($array1, $array2);
?>
#Output of the above example in PHP 8.4.4:
array(2) {
  ["foo"]=>
  string(3) "bar"
  ["bar"]=>
  string(3) "foo"
}
array(2) {
  ["foo"]=>
  string(3) "bar"
  ["bar"]=>
  string(3) "foo"
}

key 可以是 integer 或者 string。value 可以是任意类型。

此外 key 会有如下的强制转换:

  • String 中包含有效的十进制 int,除非数字前面有一个 + 号,否则将被转换为 int 类型。例如键名 "8" 实际会被储存为8。另外, "08"不会被强制转换,因为它不是一个有效的十进制整数。
  • Float 也会被转换为 int ,意味着其小数部分会被舍去。例如键名8.7 实际会被储存为 8。
  • Bool 也会被转换成 int。即键名 true 实际会被储存为 1而键名 false 会被储存为 0。
  • Null 会被转换为空字符串,即键名 null 实际会被储存为 ""。
  • Arrayobject 不能被用为键名。坚持这么做会导致警告:Illegal offset type。

如果在数组定义时多个元素都使用相同键名,那么只有最后一个会被使用,其它的元素都会被覆盖。

示例 #2 类型转换与覆盖的示例

<?php
$array = array(
    1    => "a",
    "1"  => "b",
    1.5  => "c",
    true => "d",
);
var_dump($array);
?>

以上示例会输出:

array(1) {
  [1]=>
  string(1) "d"
}

上例中所有的键名都被强制转换为 1,则每一个新单元都会覆盖前一个的值,最后剩下的只有一个 "d"。

PHP 数组可以同时含有 int 和 string 类型的键名,因为 PHP 实际并不区分索引数组和关联数组。

示例 #3 混合 int 和 string 键名

<?php
$array = array(
    "foo" => "bar",
    "bar" => "foo",
    100   => -100,
    -100  => 100,
);
var_dump($array);
?>

以上示例会输出:

array(4) {
  ["foo"]=>
  string(3) "bar"
  ["bar"]=>
  string(3) "foo"
  [100]=>
  int(-100)
  [-100]=>
  int(100)
}

key 为可选项。如果未指定,PHP 将自动使用之前用过的最大 int 键名加上 1 作为新的键名。

示例 #4 没有键名的索引数组

<?php
$array = array("foo", "bar", "hello", "world");
var_dump($array);
?>

以上示例会输出:

array(4) {
  [0]=>
  string(3) "foo"
  [1]=>
  string(3) "bar"
  [2]=>
  string(5) "hello"
  [3]=>
  string(5) "world"
}

还可以只对某些单元指定键名而对其它的空置:

示例 #5 仅对部分单元指定键名

<?php
$array = array(
         "a",
         "b",
    6 => "c",
         "d",
);
var_dump($array);
?>

以上示例会输出:

array(4) {
  [0]=>
  string(1) "a"
  [1]=>
  string(1) "b"
  [6]=>
  string(1) "c"
  [7]=>
  string(1) "d"
}

可以看到最后一个值 "d" 被自动赋予了键名 7。这是由于之前最大的整数键名是 6。

示例 #6 复杂类型转换和覆盖的例子

这个例子包括键名类型转换和元素覆盖的所有变化。

<?php
$array = array(
    1    => 'a',
    '1'  => 'b', // 值 "a" 会被 "b" 覆盖
    1.5  => 'c', // 值 "b" 会被 "c" 覆盖
    -1 => 'd',
    '01'  => 'e', // 由于这不是整数字符串,因此不会覆盖键名 1
    '1.5' => 'f', // 由于这不是整数字符串,因此不会覆盖键名 1
    true => 'g', // 值 "c" 会被 "g" 覆盖
    false => 'h',
    '' => 'i',
    null => 'j', // 值 "i" 会被 "j" 覆盖
    'k', // 值 “k” 的键名被分配为 2。这是因为之前最大的整数键是 1
    2 => 'l', // 值 "k" 会被 "l" 覆盖
);

var_dump($array);
?>

以上示例会输出:

array(7) {
  [1]=>
  string(1) "g"
  [-1]=>
  string(1) "d"
  ["01"]=>
  string(1) "e"
  ["1.5"]=>
  string(1) "f"
  [0]=>
  string(1) "h"
  [""]=>
  string(1) "j"
  [2]=>
  string(1) "l"
}

示例 #7 负数索引示例

当分配负整数 key n 时,PHP 会将下一个 key 分配给 n+1。

<?php
$array = [];

$array[-5] = 1;
$array[] = 2;

var_dump($array);
?>

以上示例会输出:

array(2) {
  [-5]=>
  int(1)
  [-4]=>
  int(2)
}

警告PHP 8.3.0 之前,分配负整数 key n 会将下一个 key 分配给 0,因此前面的示例将输出:

array(2) {
  [-5]=>
  int(1)
  [0]=>
  int(2)
}
用方括号语法访问数组单元

数组单元可以通过 array[key] 语法来访问。

示例 #8 访问数组单元

<?php
$array = array(
    "foo" => "bar",
    42    => 24,
    "multi" => array(
         "dimensional" => array(
             "array" => "foo"
         )
    )
);

var_dump($array["foo"]);
var_dump($array[42]);
var_dump($array["multi"]["dimensional"]["array"]);
?>

以上示例会输出:

string(3) "bar"
int(24)
string(3) "foo"

注意: 在 PHP 8.0.0 之前,方括号和花括号可以互换使用来访问数组单元(例如 $array[42] 和 $array{42} 在上例中效果相同)。 花括号语法在 PHP 7.4.0 中已弃用,在 PHP 8.0.0 中不再支持。

示例 #9 数组解引用

<?php
function getArray() {
    return array(1, 2, 3);
}

$secondElement = getArray()[1];

var_dump($secondElement);
?>
#Output of the above example in PHP 8.4.4:
int(2)

补充: 示例 #8 数组解引用

// 或
<?php

function getArray() {
    return array(1,2,3);
}

//$sendElement = getArray()[1];

list(, $sendElement) = getArray();
echo $sendElement;
用方括号的语法新建/修改

可以通过明示地设定其中的值来修改现有的 array 。

这是通过在方括号内指定键名来给 array 赋值实现的。也可以省略键名,在这种情况下给变量名加上一对空的方括号([])。

$arr[key] = value;
$arr[] = value;
// key 可以是 int 或 string
// value 可以是任意类型的值

如果 $arr 不存在或者设置为 null 或者 false,将会新建它,这也是另一种创建 array 的方法。不过并不鼓励这样做,因为如果 $arr 已经包含有值(例如来自请求变量的 string)则此值会保留而 [] 实际上代表着字符串访问运算符。初始化变量的最好方式是直接给其赋值。

注意: 从 PHP 7.1.0 起,对字符串应用空索引操作符会抛出致命错误。以前,字符串被静默地转换为数组。

注意: 从 PHP 8.1.0 起,弃用从 false 值中创建一个新数组。 但仍然允许从 null 或者未定义的变量中创建新数组。

要修改某个值,通过其键名给该单元赋一个新值。要删除某键值对,对其调用 unset() 函数。

示例 #10 Using Square Brackets with Arrays

<?php
$arr = array(5 => 1, 12 => 2);

$arr[] = 56;    // 这与 $arr[13] = 56 相同;
                // 在脚本的这一点上

$arr["x"] = 42; // 添加一个新元素
                // 键名使用 "x"

unset($arr[5]); // 从数组中删除元素

unset($arr);    // 删除整个数组
?>

注意: 如上所述,如果给出方括号但没有指定键名,则取当前最大 int 索引值,新的键名将是该值加上 1(但是最小为 0)。如果当前还没有 int 索引,则键名将为 0 。

注意这里所使用的最大整数键名目前不需要存在于 array 中。 它只要在上次 array 重新生成索引后曾经存在于 array 就行了。以下面的例子来说明:

<?php
// 创建一个简单的数组
$array = array(1, 2, 3, 4, 5);
print_r($array);

// 现在删除其中的所有元素,但保持数组本身不变:
foreach ($array as $i => $value) {
    unset($array[$i]);
}
print_r($array);

// 添加一个单元(注意新的键名是 5,而不是你可能以为的 0)
$array[] = 6;
print_r($array);

// 重新索引:
$array = array_values($array);
$array[] = 7;
print_r($array);
?>

以上示例会输出:

Array
(
    [0] => 1
    [1] => 2
    [2] => 3
    [3] => 4
    [4] => 5
)
Array
(
)
Array
(
    [5] => 6
)
Array
(
    [0] => 6
    [1] => 7
)
数组解包

可以使用 [] (自 PHP 7.1.0 起)或者 list() 语言结构解包数组。这些结构可用于将数组解包为不同的变量。

示例 #11 Array Destructuring

<?php
$source_array = ['foo', 'bar', 'baz'];

[$foo, $bar, $baz] = $source_array;

echo $foo, PHP_EOL;    // 打印 "foo"
echo $bar, PHP_EOL;    // 打印 "bar"
echo $baz, PHP_EOL;    // 打印 "baz"
?>
#Output of the above example in PHP 8.4.4:
foo
bar
baz

数组解包可用于 foreach 在迭代多维数组时对其进行解包。

示例 #12 Array Destructuring in Foreach

<?php
$source_array = [
    [1, 'John'],
    [2, 'Jane'],
];

foreach ($source_array as [$id, $name]) {
    echo "{$id}: '{$name}'\n";
}
?>
#Output of the above example in PHP 8.4.4:
1: 'John'
2: 'Jane'

如果变量未提供,数组元素将会被忽略。数组解包始终从索引 0 开始。

示例 #13 Ignoring Elements

<?php
$source_array = ['foo', 'bar', 'baz'];

// 将索引 2 的元素分配给变量 $baz
[, , $baz] = $source_array;

echo $baz;    // 打印 "baz"
?>
#Output of the above example in PHP 8.4.4:
baz

自 PHP 7.1.0 起,也可以解包关联数组。这在数字索引数组中更容易选择正确的元素,因为可以显式指定索引。

示例 #14 Destructuring Associative Arrays

<?php
$source_array = ['foo' => 1, 'bar' => 2, 'baz' => 3];

// 将索引 'baz' 处的元素分配给变量 $three
['baz' => $three] = $source_array;

echo $three, PHP_EOL;    // 打印 3

$source_array = ['foo', 'bar', 'baz'];

// 将索引 2 处的元素分配给变量 $baz
[2 => $baz] = $source_array;

echo $baz, PHP_EOL;    // 打印 "baz"
?>
#Output of the above example in PHP 8.4.4:
3
baz

数组解包可以方便的用于两个变量交换。

示例 #15 Swapping Two Variable

<?php
$a = 1;
$b = 2;

[$b, $a] = [$a, $b];

echo $a, PHP_EOL;    // 打印 2
echo $b, PHP_EOL;    // 打印 1
?>

注意: 分配时不支持展开运算符(…)。

注意: 尝试访问未定义的数组键与访问任何未定义的变量相同:都将发出 E_WARNING 级别的错误消息(PHP 8.0.0 之前是 E_NOTICE 级别),结果将是 null。

实用函数

有很多操作数组的函数,参见 数组函数 一节。

注意: unset() 函数允许删除 array 中的某个键。但要注意数组将不会重建索引。如果需要删除后重建索引,可以用 array_values() 函数重建 array 索引。

示例 #16 Unsetting Intermediate Elements

<?php
$a = array(1 => 'one', 2 => 'two', 3 => 'three');

/* 该数组将被定义为
   $a = array(1 => 'one', 3 => 'three');
   而不是
   $a = array(1 => 'one', 2 =>'three');
*/
unset($a[2]);
var_dump($a);

$b = array_values($a);
// Now $b is array(0 => 'one', 1 =>'three')
var_dump($b);
?>
Output of the above example in PHP 8.4.4:

array(2) {
  [1]=>
  string(3) "one"
  [3]=>
  string(5) "three"
}
array(2) {
  [0]=>
  string(3) "one"
  [1]=>
  string(5) "three"
}

foreach 控制结构是专门用于 array 的。它提供了一个简单的方法来遍历 array。

数组做什么和不做什么

为什么 $foo[bar] 错了?

应该始终在用字符串表示的数组索引上加上引号。例如用 $foo['bar'] 而不是 $foo[bar]。但是为什么呢?可能在老的脚本中见过如下语法:

<?php
$foo[bar] = 'enemy';
echo $foo[bar];
// 及其它
?>

这样是错的,但可以正常运行。那么为什么错了呢?原因是此代码中有一个未定义的常量( bar )而不是 string('bar'-注意引号)。而 PHP 可能会在以后定义此常量,不幸的是你的代码中有同样的名字。它能运行,是因为 PHP 自动将裸字符串(没有引号的 string 且不对应于任何已知符号)转换成一个其值为该裸 string 的 string。例如,如果没有常量定义为 bar,那么PHP 将在 string 中替代为 'bar' 并使用之。

警告: 将未定义的常量当作裸字符串的回退会触发 E_NOTICE 级别错误。 从 PHP 7.2.0 起已废弃,并触发 E_WARNING 级别错误。 从 PHP 8.0.0 起被移除,并触发 Error 异常。

这并不意味着总是给键名加上引号。用不着给键名为 常量变量 的加上引号,否则会使 PHP 不能解析它们。

示例 #17 Key Quoting

<?php
error_reporting(E_ALL);
ini_set('display_errors', true);
ini_set('html_errors', false);

// 简单数组:
$array = array(1, 2);
$count = count($array);

for ($i = 0; $i < $count; $i++) {
    echo "\n检查 $i: \n";
    echo "坏的: " . $array['$i'] . "\n";
    echo "好的: " . $array[$i] . "\n";
    echo "坏的: {$array['$i']}\n";
    echo "好的: {$array[$i]}\n";
}
?>

以上示例会输出:

检查 0:
Notice: Undefined index:  $i in /path/to/script.html on line 9
坏的:
好的: 1
Notice: Undefined index:  $i in /path/to/script.html on line 11
坏的:
好的: 1

检查 1:
Notice: Undefined index:  $i in /path/to/script.html on line 9
坏的:
好的: 2
Notice: Undefined index:  $i in /path/to/script.html on line 11
坏的:
好的: 2

演示此行为的更多例子:

示例 #18 More Examples

<?php
// 显示所有错误
error_reporting(E_ALL);

$arr = array('fruit' => 'apple', 'veggie' => 'carrot');

// Correct
echo $arr['fruit'], PHP_EOL;  // apple
echo $arr['veggie'], PHP_EOL; // carrot

// Incorrect. This works but also throws a PHP Error because
// of an undefined constant named fruit
//
// Error: Undefined constant "fruit"
try {
    echo $arr[fruit];    // apple
} catch (Error $e) {
    echo get_class($e), ': ', $e->getMessage(), PHP_EOL;
}

// This defines a constant to demonstrate what's going on.  The value 'veggie'
// is assigned to a constant named fruit.
define('fruit', 'veggie');

// Notice the difference now
echo $arr['fruit'], PHP_EOL;  // apple
echo $arr[fruit], PHP_EOL;    // carrot

// The following is okay, as it's inside a string. Constants are not looked for
// within strings, so no E_NOTICE occurs here
echo "Hello $arr[fruit]", PHP_EOL;      // Hello apple

// With one exception: braces surrounding arrays within strings allows constants
// to be interpreted
echo "Hello {$arr[fruit]}", PHP_EOL;    // Hello carrot
echo "Hello {$arr['fruit']}", PHP_EOL;  // Hello apple

// Concatenation is another option
echo "Hello " . $arr['fruit'], PHP_EOL; // Hello apple
?>
#Output of the above example in PHP 8.4.4:
apple
carrot
Error: Undefined constant "fruit"
apple
carrot
Hello apple
Hello carrot
Hello apple
Hello apple
// 这将不起作用,并会导致解析错误,例如:
// Parse error: parse error, expecting T_STRING' or T_VARIABLE' or T_NUM_STRING'
// 这当然也适用于在字符串中使用超全局变量
print "Hello $arr['fruit']";
print "Hello $_GET['foo']";
?>

当打开 error_reporting 来显示 E_NOTICE 级别的错误(将其设为 E_ALL)时将看到这些错误。默认情况下 error_reporting 被关闭不显示这些。

和在 语法 一节中规定的一样,在方括号([” 和 “]”)之间必须有一个表达式。这意味着可以这样写:

<?php
echo $arr[somefunc($bar)];
?>

这是一个用函数返回值作为数组索引的例子。PHP 也可以用已知常量,可能之前已经见过:

<?php
$error_descriptions[E_ERROR]   = "A fatal error has occurred";
$error_descriptions[E_WARNING] = "PHP issued a warning";
$error_descriptions[E_NOTICE]  = "This is just an informal notice";
?>

注意 E_ERROR 也是个合法的标识符,就和第一个例子中的 bar 一样。但是上一个例子实际上和如下写法是一样的:

<?php
$error_descriptions[1] = "A fatal error has occurred";
$error_descriptions[2] = "PHP issued a warning";
$error_descriptions[8] = "This is just an informal notice";
?>

因为 E_ERROR 等于 1,等等。

那么为什么这样做不好?

也许有一天,PHP 开发小组可能会想新增一个常量或者关键字,或者用户可能希望以后在自己的程序中引入新的常量,那就有麻烦了。例如已经不能这样用 empty 和 default 这两个词了,因为他们是 保留关键字

注意: 重申一次,在双引号字符串中,不给索引加上引号是合法的因此 "$foo[bar]" 是合法的(“合法”的原文为 valid。在实际测试中,这么做确实可以访问数组的该元素,但是会报一个常量未定义的 notice。无论如何,强烈建议不要使用 $foo[bar]这样的写法,而要使用 $foo['bar'] 来访问数组中元素。–haohappy 注)。至于为什么参见以上的例子和 字符串中的变量解析 中的解释。

转换为数组

对于任意 int,float, string,bool 和 resource 类型,如果将一个值转换为 array,将得到一个仅有一个元素的数组,其下标为 0,该元素即为此标量的值。换句话说,(array) $scalarValue 与 array($scalarValue) 完全一样。

如果将 object 类型转换为 array,则结果为一个数组,其单元为该对象的属性。键名将为成员变量名,不过有几点例外:整数属性不可访问; 私有变量前会加上类名作前缀;保护变量前会加上一个 '*' 做前缀。这些前缀的前后都各有一个 NUL 字节。 未初始化的类型属性将会被丢弃。

示例 #19 Converting to an Array

<?php

class A {
    private $B;
    protected $C;
    public $D;
    function __construct()
    {
        $this->{1} = null;
    }
}

var_export((array) new A());
?>

以上示例会输出:

array (
  '' . "\0" . 'A' . "\0" . 'B' => NULL,
  '' . "\0" . '*' . "\0" . 'C' => NULL,
  'D' => NULL,
  1 => NULL,
)

这些 NUL 会导致一些意想不到的行为:

示例 #20 Casting an Object to an Array

<?php

class A {
    private $A; // 将变为 '\0A\0A'
}

class B extends A {
    private $A; // 将变为 '\0B\0A'
    public $AA; // 将变为 'AA'
}

var_dump((array) new B());
?>

以上示例会输出:

array(3) { ["BA"]=> NULL ["AA"]=> NULL ["AA"]=> NULL } 上例会有两个键名为 'AA',不过其中一个实际上是 '\0A\0A'。

将 null 转换为 array 会得到一个空的数组。

比较

可以用 array_diff() 函数和 数组运算符 来比较数组。

数组解包

在 array 定义时,用 … 前缀的一个 array 可以被展开到当前位置。 只有实现了 Traversable 的数组和对象才能被展开。 PHP 7.4.0 开始可以使用 … 解包 array。

它可以多次使用,在 … 操作符前后都可以添加常规元素:

示例 #21 简单的数组解包

<?php
// 使用简短的数组语法。
// 亦可用于 array() 语法
$arr1 = [1, 2, 3];
$arr2 = [...$arr1]; // [1, 2, 3]
$arr3 = [0, ...$arr1]; // [0, 1, 2, 3]
$arr4 = [...$arr1, ...$arr2, 111]; // [1, 2, 3, 1, 2, 3, 111]
$arr5 = [...$arr1, ...$arr1]; // [1, 2, 3, 1, 2, 3]

function getArr() {
  return ['a', 'b'];
}
$arr6 = [...getArr(), 'c' => 'd']; // ['a', 'b', 'c' => 'd']

var_dump($arr1, $arr2, $arr3, $arr4, $arr5, $arr6);
?>

… 操作符解包 array 时也遵守函数 array_merge() 的语义。 也就是说,key 为字符时,后面的字符键会覆盖之前的字符键;key 为 integer 时则会重新编号:

示例 #22 重复 key 的数组解包

<?php
// string key
$arr1 = ["a" => 1];
$arr2 = ["a" => 2];
$arr3 = ["a" => 0, ...$arr1, ...$arr2];
var_dump($arr3); // ["a" => 2]

// integer key
$arr4 = [1, 2, 3];
$arr5 = [4, 5, 6];
$arr6 = [...$arr4, ...$arr5];
var_dump($arr6); // [1, 2, 3, 4, 5, 6]
// 即 [0 => 1, 1 => 2, 2 => 3, 3 => 4, 4 => 5, 5 => 6]
// 也就是原始的 integer key 不再保留
?>

注意: 既不是 integer 也不是 string 的 key 会抛出 TypeError。 这类 key 只能通过 Traversable 对象生成。

注意: 在 PHP 8.1 之前,带有 string 键的 array 无法解包:

<?php

$arr1 = [1, 2, 3];
$arr2 = ['a' => 4];
$arr3 = [...$arr1, ...$arr2];
// Fatal error: Uncaught Error: Cannot unpack array with string keys in example.php:5

$arr4 = [1, 2, 3];
$arr5 = [4, 5];
$arr6 = [...$arr4, ...$arr5]; // works. [1, 2, 3, 4, 5]
?>

示例

PHP 中的数组类型有非常多的用途。以下是一些示例:

示例 #23 Array Versatility

<?php
// This:
$a = array( 'color' => 'red',
            'taste' => 'sweet',
            'shape' => 'round',
            'name'  => 'apple',
            4        // 键名为 0
          );

$b = array('a', 'b', 'c');

var_dump($a, $b);

// . . .完全等同于:
$a = array();
$a['color'] = 'red';
$a['taste'] = 'sweet';
$a['shape'] = 'round';
$a['name']  = 'apple';
$a[]        = 4;        // 键名为 0

$b = array();
$b[] = 'a';
$b[] = 'b';
$b[] = 'c';

// 执行上述代码后,数组 $a 将是
// array('color' => 'red', 'taste' => 'sweet', 'shape' => 'round',
// 'name' => 'apple', 0 => 4), 数组 $b 将是
// array(0 => 'a', 1 => 'b', 2 => 'c'), 或简单的 array('a', 'b', 'c').
?>
#Output of the above example in PHP 8.4.4:
array(5) {
  ["color"]=>
  string(3) "red"
  ["taste"]=>
  string(5) "sweet"
  ["shape"]=>
  string(5) "round"
  ["name"]=>
  string(5) "apple"
  [0]=>
  int(4)
}
array(3) {
  [0]=>
  string(1) "a"
  [1]=>
  string(1) "b"
  [2]=>
  string(1) "c"
}

示例 #24 使用 array()

<?php
// Array as (property-)map
$map = array( 'version'    => 4,
              'OS'         => 'Linux',
              'lang'       => 'english',
              'short_tags' => true
            );
var_dump($map);

// strictly numerical keys
// this is the same as array(0 => 7, 1 => 8, ...)
$array = array( 7,
                8,
                0,
                156,
                -10
              );
var_dump($array);

$switching = array(         10, // key = 0
                    5    =>  6,
                    3    =>  7,
                    'a'  =>  4,
                            11, // key = 6 (整数索引的最大值为 5)
                    '8'  =>  2, // key = 8 (整数!)
                    '02' => 77, // key = '02'
                    0    => 12  // 值 10 被 12 覆盖
                  );
var_dump($switching);

// 空数组
$empty = array();
var_dump($empty);
?>

示例 #25 集合

<?php
$colors = array('red', 'blue', 'green', 'yellow');

foreach ($colors as $color) {
    echo "Do you like $color?\n";
}

?>

以上示例会输出:

Do you like red?
Do you like blue?
Do you like green?
Do you like yellow?

可以通过引用传递 array 的值来直接更改数组的值。

示例 #26 在循环中改变单元

<?php
$colors = array('red', 'blue', 'green', 'yellow');

foreach ($colors as &$color) {
    $color = mb_strtoupper($color);
}
unset($color); /* 确保后面对
$color 的写入不会修改最后一个数组元素 */

print_r($colors);
?>

以上示例会输出:

Array
(
    [0] => RED
    [1] => BLUE
    [2] => GREEN
    [3] => YELLOW
)

本例生成一个下标从 1 开始的数组。

示例 #27 下标从 1 开始的数组

<?php
$firstquarter = array(1 => 'January', 'February', 'March');
print_r($firstquarter);
?>

以上示例会输出:

Array
(
    [1] => January
    [2] => February
    [3] => March
)

示例 #28 填充数组

<?php
// 把指定目录中的所有项填充到数组
$handle = opendir('.');
while (false !== ($file = readdir($handle))) {
    $files[] = $file;
}
closedir($handle);

var_dump($files);
?>
#Output of the above example in PHP 8.4.4:
array(7) {
  [0]=>
  string(1) "."
  [1]=>
  string(2) ".."
  [2]=>
  string(3) "tmp"
  [3]=>
  string(4) "home"
  [4]=>
  string(3) "dev"
  [5]=>
  string(4) "proc"
  [6]=>
  string(8) "examples"
}

Array 是有序的。也可以使用不同的排序函数来改变顺序。更多信息参见 数组函数。可以用 count() 函数来统计出 array 中元素的个数。

示例 #29 数组排序

<?php
sort($files);
print_r($files);
?>

因为 array 中的值可以为任意值,也可是另一个 array。这样可以产生递归或多维 array。

示例 #30 递归和多维数组

<?php
$fruits = array ( "fruits"  => array ( "a" => "orange",
                                       "b" => "banana",
                                       "c" => "apple"
                                     ),
                  "numbers" => array ( 1,
                                       2,
                                       3,
                                       4,
                                       5,
                                       6
                                     ),
                  "holes"   => array (      "first",
                                       5 => "second",
                                            "third"
                                     )
                );
var_dump($fruits);

// 处理上面数组中的值的一些例子
echo $fruits["holes"][5];    // 打印 "second"
echo $fruits["fruits"]["a"]; // 打印 "orange"
unset($fruits["holes"][0]);  // 删除 "first"

// 创建一个新的多维数组
$juices["apple"]["green"] = "good";
var_dump($juices);
?>

Array 的赋值总是会涉及到值的拷贝。使用 引用运算符 通过引用来拷贝 array。

示例 #31 Array Copying

<?php
$arr1 = array(2, 3);
$arr2 = $arr1;
$arr2[] = 4; // $arr2 已更改,
             // $arr1 仍然是 array(2, 3)

$arr3 = &$arr1;
$arr3[] = 4; // 现在 $arr1 和 $arr3 是一样的

var_dump($arr1, $arr2, $arr3);
?>

安全分享-变量覆盖

存在数组覆盖问题。比如用户输入把已有的覆盖: 伪代码

$array = [
    1 => $SESSION['id'],
];

$input = $_GET['name'];
$array = $array . $input;
if ($array[1] == 1) {
    echo 'admin';
};

Iterable 可迭代对象

https://www.php.net/manual/zh/language.types.iterable.php

Iterable 是内置编译时 array|Traversable 的类型别名。从 PHP 7.1.0 到 PHP 8.2.0 之间的描述来看,iterable 是内置伪类型,充当上述类型别名,也可以用于类型声明。iterable 类型可用于 foreach 或在生成器中使用 yield from。

注意:

将可迭代对象声明为返回类型的函数也可能是 生成器。

示例 #1 可迭代生成器返回类型的示例

<?php

function gen(): iterable {
    yield 1;
    yield 2;
    yield 3;
}

foreach(gen() as $value) {
    echo $value, "\n";
}
?>
#Output of the above example in PHP 8.4.4:
1
2
3

补充:

使用可迭代对象

可迭代对象可以用作参数类型,表示函数需要一组值, 但是不会关心值集的形式,因为它将与 foreach 一起使用。如果一个值不是数组或 Traversable 的实例,则会抛出一个 TypeError

示例 #1 Iterable 可迭代参数类型示例

<?php
function foo(iterable $iterable) {
    foreach ($iterable as $value) {
        // ...
    }
}

$xx_students = array("1", "2", "3");
foreach ($xx_students as $student) {
    echo $student . PHP_EOL; }
?>

声明为可迭代的参数可能会使用 null 或者一个数组作为默认值。

示例 #2 可迭代参数默认值示例

<?php
function foo(iterable $iterable = []) {
    // ...
}
?>

可迭代对象还可以用作返回类型,表示函数将返回一个可迭代的值。 如果返回值不是数组或 Traversable 的实例,则会抛出一个 TypeError。

示例 #3 可迭代返回类型示例

<?php
function bar(): iterable {
    return [1, 2, 3];
}

$a = bar();
print_r($a);
?>

这样做为了代码规范,更易读。

Object 对象

对象初始化

要创建一个新的对象 object,使用 new 语句实例化一个类:

示例 #1 Object Construction

<?php
class foo
{
    function do_foo()
    {
        echo "Doing foo.";
    }
}

$bar = new foo;
$bar->do_foo();
?>
#Output of the above example in PHP 8.4.4:
Doing foo.

详细讨论参见手册中类与对象章节。

转换为对象

如果将一个对象转换成对象,它将不会有任何变化。如果其它任何类型的值被转换成对象,将会创建一个内置类 stdClass 的实例。如果该值为 null,则新的实例为空。 array 转换成 object 将使键名成为属性名并具有相对应的值。注意:在这个例子里, 使用 PHP 7.2.0 之前的版本,数字键只能通过迭代访问。

示例 #2 Casting to an Object

<?php
$obj = (object) array('1' => 'foo');
var_dump(isset($obj->{'1'})); // outputs 'bool(true)'

// Deprecated as of PHP 8.1
var_dump(key($obj)); // outputs 'string(1) "1"' 
?>

对于其他值,会包含进成员变量名 scalar。

示例 #3 (object) cast

<?php
$obj = (object) 'ciao';
echo $obj->scalar;  // 输出 'ciao'
?>

Resource 资源类型

https://www.php.net/manual/zh/language.types.resource.php

资源 resource 是一种特殊变量,保存了到外部资源的一个引用。资源是通过专门的函数来建立和使用的。所有这些函数及其相应资源类型见附录

参见 get_resource_type()

转换为资源

由于资源类型变量保存有为打开文件、数据库连接、图形画布区域等的特殊句柄,因此将其它类型的值转换为资源没有意义。

释放资源

引用计数系统是 Zend 引擎的一部分,可以自动检测到一个资源不再被引用了(和 Java 一样)。这种情况下此资源使用的所有外部资源都会被垃圾回收系统释放。因此,很少需要手工释放内存。

注意: 持久数据库连接比较特殊,它们不会被垃圾回收系统销毁。参见数据库永久连接一章。

NULL

https://www.php.net/manual/zh/language.types.null.php

null 类型是 PHP 的原子类型(unit type),也就是说,它仅有一个值 null。

未定义和 unset() 的变量都将解析为值 null。

语法

null 类型只有一个值,就是不区分大小写的常量 null。

<?php
$var = NULL;       
?>

转换到 null

警告: 本特性自 PHP 7.2.0 起废弃,并且自 PHP 8.0.0 起被移除。 强烈建议不要使用本特性。

使用 (unset) $var 将一个变量转换为 null 将不会删除该变量或 unset 其值。仅是返回 null 值而已。

参见

PHP流程控制

if

else

elseif/else if

流程控制的替代语法

while

do-while

for

foreach

break

continue

switch

return

require

include

安全分享

什么情况下会有文件包含漏洞? include的参数不是写死的。如

include $_POST['filename']

require_once

include_once

函数

用户自定义函数

函数的参数

返回值

可变函数

内部(内置)函数

匿名函数

箭头函数

文件系统安全

https://www.php.net/manual/zh/security.filesystem.php

补充: 示例 #1 不对变量检查会导致

<?php
// 从用户主目录移除一个文件
$username = $_POST['user_submitted_name'];
$userfile = $_POST['user_submitted_filename'];
$homedir  = "/home/$username";

unlink("$homedir/$userfile");

echo "The file has been deleted!";
?>
  1. 可以传其它用户名,比如root
  2. 不能直接使用用户输入来删除文件。比如输入 ../../../../../../etc/passwd

如何做好权限控制:

  1. 系统权限划分-服务本身的权限控制
    • 上例中 unlink 用的是启动web程序用户的权限。需要创建一个普通用户来启动服务。
  2. 服务端代码逻辑控制-用户权限控制:
    • 敏感文件只有管理员有权限
    • 用户只能操作自身的文件

代码审计查看接口的逻辑。

  • 查看是否有危险函数。如unlink
  • 参数是否是用户输入的
  • 判断代码逻辑中是否存在过滤或者安全防护代码

提供安全修复建议:

  • 过滤 ../
    • 发现有../,把/斜杠替换为空

文件上传处理

POST 方法上传

安全分享

文件上传的危害:

  • 可以getshell
  • 上传大文件,写满磁盘 - dos攻击
  • 上传html文件
    • URL跳转(可钓鱼)
    • 网信办责令关闭:赌博页面-在线发牌、黄色页面、涉恐涉政页面

能getshell的条件,及防御:

  • 上传可执行代码文件,php文件
    • 判断文件类型,推荐白名单限制(jpg,png,gif),黑名单容易被绕过
  • 能够执行这个文件
    • 文件上传的目录去掉可执行权限。-x
  • 知道文件位置
    • 不返回文件路径。
    • 修改上传文件的文件名,如随机文件名

上传html文件位置:

  • 服务器上存储,过滤html
  • 对象存储,有图片、html
    • 对象存储是否和web服务同域名,如果同域名一旦文件有安全问题,可能域名面临关闭
    • 管理员才能上传,并做好日志记录
    • 不传html,文件名有没有后缀,对象存储会自动指定以html解析。关闭该功能,规范文件

错误信息说明

常见缺陷

上传多个文件

对 PUT 方法的支持

mysql数据库安全

设计数据库

第一步一般都是创建数据库,除非是使用第三方的数据库服务。

当创建一个数据库的时候,会指定一个所有者来执行和新建语句。通常,只有所有者(或超级用户)才有权对数据库中的对象进行任意操作。如果想让其他用户使用,就必须赋予他们权限。

应用程序永远不要使用数据库所有者或超级用户帐号来连接数据库,因为这些帐号可以执行任意的操作,比如说修改数据库结构(例如删除一个表)或者清空整个数据库的内容。

应该为程序的每个方面创建不同的数据库帐号,并赋予对数据库对象的极有限的权限。

仅分配给能完成其功能所需的权限,避免同一个用户可以完成另一个用户的事情。这样即使攻击者利用程序漏洞取得了数据库的访问权限,也最多只能做到和该程序一样的影响范围。

PHP连接MySQL

  • PDO — PHP 数据对象

(PHP 5 >= 5.1.0, PHP 7, PHP 8, PECL pdo >= 0.2.0)

PDO::query — 执行 SQL 语句,以 PDOStatement 对象形式返回结果集

<?php
$servername = "127.0.0.1:33060"; //mysql的地址
$username = "root";
$password = "";
$dbname = "dvwa";

try {
    $conn = new PDO("mysql:host=$servername;dbname=$dbname", $username, $password);
    echo "连接成功\r";
} catch (PDOException $e) {
    echo $e->getMessage();
}

允许MySQL远程登录

GRANT ALL PRIVILEGES ON . TO 'root'@'%' IDENTIFIED BY '' WITH GRANT OPTION;
FLUSH PRIVILEGES;

PHP执行SQL语句

$sql = "select * from xxx_students";
foreach ($conn->query($sql) as $row) {
print $row['name'] . "\t";
print $row['age'] . "\t";
}

PHP使用预编译的方式执行SQL语句

#1.预编译
$sql2 = $conn->prepare("select * from xxx_students where age=?");
$age = 27;
#2.绑定参数
$sql2->bindParam(1, $age);
#3.执行
$sql2->execute();
#4.获取结果
$res = $sql2->fetchAll();
print_r($res);

PDO::prepare方法

预编译防止SQL注入的原理是提前编译SQL语句,将所有的用户输入都当做数据,而非语法。

mysql提供预编译 预编译原理:提前进行sql语句的编译,在sql语句执行之前就确定sql的语义,保证用户输入的内容只当作变量执行

emacs

Emacs

org-mode

Orgmode

Donations

打赏

Copyright

© 2025 Jasper Hsu

Creative Commons

Creative Commons

Attribute

Attribute

Noncommercial

Noncommercial

Share Alike

Share Alike