first commit

This commit is contained in:
root
2026-03-21 17:04:18 +08:00
commit 3c38481573
617 changed files with 65539 additions and 0 deletions

1517
var/Typecho/Common.php Executable file

File diff suppressed because it is too large Load Diff

249
var/Typecho/Config.php Executable file
View File

@@ -0,0 +1,249 @@
<?php
namespace Typecho;
/**
* 配置管理类
*
* @category typecho
* @package Config
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Config extends \stdClass implements \Iterator, \ArrayAccess
{
/**
* 当前配置
*
* @access private
* @var array
*/
private array $currentConfig = [];
/**
* 实例化一个当前配置
*
* @access public
* @param array|string|null $config 配置列表
*/
public function __construct($config = [])
{
/** 初始化参数 */
$this->setDefault($config);
}
/**
* 工厂模式实例化一个当前配置
*
* @access public
*
* @param array|string|null $config 配置列表
*
* @return Config
*/
public static function factory($config = []): Config
{
return new self($config);
}
/**
* 设置默认的配置
*
* @access public
*
* @param mixed $config 配置信息
* @param boolean $replace 是否替换已经存在的信息
*
* @return void
*/
public function setDefault($config, bool $replace = false)
{
if (empty($config)) {
return;
}
/** 初始化参数 */
if (is_string($config)) {
parse_str($config, $params);
} else {
$params = $config;
}
/** 设置默认参数 */
foreach ($params as $name => $value) {
if ($replace || !array_key_exists($name, $this->currentConfig)) {
$this->currentConfig[$name] = $value;
}
}
}
/**
* @return bool
*/
public function isEmpty(): bool
{
return empty($this->currentConfig);
}
/**
* 重设指针
*
* @access public
* @return void
*/
public function rewind(): void
{
reset($this->currentConfig);
}
/**
* 返回当前值
*
* @access public
* @return mixed
*/
#[\ReturnTypeWillChange]
public function current()
{
return current($this->currentConfig);
}
/**
* 指针后移一位
*
* @access public
* @return void
*/
public function next(): void
{
next($this->currentConfig);
}
/**
* 获取当前指针
*
* @access public
* @return mixed
*/
#[\ReturnTypeWillChange]
public function key()
{
return key($this->currentConfig);
}
/**
* 验证当前值是否到达最后
*
* @access public
* @return boolean
*/
public function valid(): bool
{
return false !== $this->current();
}
/**
* 魔术函数获取一个配置值
*
* @access public
* @param string $name 配置名称
* @return mixed
*/
public function __get(string $name)
{
return $this->offsetGet($name);
}
/**
* 魔术函数设置一个配置值
*
* @access public
* @param string $name 配置名称
* @param mixed $value 配置值
* @return void
*/
public function __set(string $name, $value)
{
$this->offsetSet($name, $value);
}
/**
* 直接输出默认配置值
*
* @access public
* @param string $name 配置名称
* @param array|null $args 参数
* @return void
*/
public function __call(string $name, ?array $args)
{
echo $this->currentConfig[$name];
}
/**
* 判断当前配置值是否存在
*
* @access public
* @param string $name 配置名称
* @return boolean
*/
public function __isSet(string $name): bool
{
return $this->offsetExists($name);
}
/**
* 魔术方法,打印当前配置数组
*
* @access public
* @return string
*/
public function __toString(): string
{
return json_encode($this->currentConfig);
}
/**
* @return array
*/
public function toArray(): array
{
return $this->currentConfig;
}
/**
* @param mixed $offset
* @return bool
*/
public function offsetExists($offset): bool
{
return isset($this->currentConfig[$offset]);
}
/**
* @param mixed $offset
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
return $this->currentConfig[$offset] ?? null;
}
/**
* @param mixed $offset
* @param mixed $value
*/
public function offsetSet($offset, $value): void
{
$this->currentConfig[$offset] = $value;
}
/**
* @param mixed $offset
*/
public function offsetUnset($offset): void
{
unset($this->currentConfig[$offset]);
}
}

170
var/Typecho/Cookie.php Executable file
View File

@@ -0,0 +1,170 @@
<?php
namespace Typecho;
/**
* cookie支持
*
* @author qining
* @category typecho
* @package Cookie
*/
class Cookie
{
/**
* 前缀
*
* @var string
* @access private
*/
private static string $prefix = '';
/**
* 路径
*
* @var string
* @access private
*/
private static string $path = '/';
/**
* @var string
* @access private
*/
private static string $domain = '';
/**
* @var bool
* @access private
*/
private static bool $secure = false;
/**
* @var bool
* @access private
*/
private static bool $httponly = false;
/**
* 获取前缀
*
* @access public
* @return string
*/
public static function getPrefix(): string
{
return self::$prefix;
}
/**
* 设置前缀
*
* @param string $url
*
* @access public
* @return void
*/
public static function setPrefix(string $url)
{
self::$prefix = md5($url);
$parsed = parse_url($url);
self::$domain = $parsed['host'];
/** 在路径后面强制加上斜杠 */
self::$path = empty($parsed['path']) ? '/' : Common::url(null, $parsed['path']);
}
/**
* 获取目录
*
* @access public
* @return string
*/
public static function getPath(): string
{
return self::$path;
}
/**
* @access public
* @return string
*/
public static function getDomain(): string
{
return self::$domain;
}
/**
* @access public
* @return bool
*/
public static function getSecure(): bool
{
return self::$secure ?: false;
}
/**
* 设置额外的选项
*
* @param array $options
* @return void
*/
public static function setOptions(array $options)
{
self::$domain = $options['domain'] ?: self::$domain;
self::$secure = !!$options['secure'];
self::$httponly = !!$options['httponly'];
}
/**
* 获取指定的COOKIE值
*
* @param string $key 指定的参数
* @param string|null $default 默认的参数
* @return mixed
*/
public static function get(string $key, ?string $default = null)
{
$key = self::$prefix . $key;
$value = $_COOKIE[$key] ?? $default;
return is_array($value) ? $default : $value;
}
/**
* 设置指定的COOKIE值
*
* @param string $key 指定的参数
* @param mixed $value 设置的值
* @param integer $expire 过期时间,默认为0,表示随会话时间结束
*/
public static function set(string $key, $value, int $expire = 0)
{
$key = self::$prefix . $key;
$_COOKIE[$key] = $value;
Response::getInstance()->setCookie(
$key,
$value,
$expire,
self::$path,
self::$domain,
self::$secure,
self::$httponly
);
}
/**
* 删除指定的COOKIE值
*
* @param string $key 指定的参数
*/
public static function delete(string $key)
{
$key = self::$prefix . $key;
if (!isset($_COOKIE[$key])) {
return;
}
Response::getInstance()->setCookie($key, '', -1, self::$path, self::$domain, self::$secure, self::$httponly);
unset($_COOKIE[$key]);
}
}

128
var/Typecho/Date.php Executable file
View File

@@ -0,0 +1,128 @@
<?php
namespace Typecho;
/**
* 日期处理
*
* @author qining
* @category typecho
* @package Date
*/
class Date
{
/**
* 期望时区偏移
*
* @access public
* @var integer
*/
public static int $timezoneOffset = 0;
/**
* 服务器时区偏移
*
* @access public
* @var integer
*/
public static int $serverTimezoneOffset = 0;
/**
* 当前的服务器时间戳
*
* @access public
* @var integer
*/
public static int $serverTimeStamp = 0;
/**
* 可以被直接转换的时间戳
*
* @access public
* @var integer
*/
public int $timeStamp = 0;
/**
* @var string
*/
public string $year;
/**
* @var string
*/
public string $month;
/**
* @var string
*/
public string $day;
/**
* 初始化参数
*
* @param integer|null $time 时间戳
*/
public function __construct(?int $time = null)
{
$this->timeStamp = (null === $time ? self::time() : $time)
+ (self::$timezoneOffset - self::$serverTimezoneOffset);
$this->year = date('Y', $this->timeStamp);
$this->month = date('m', $this->timeStamp);
$this->day = date('d', $this->timeStamp);
}
/**
* 设置当前期望的时区偏移
*
* @param integer $offset
*/
public static function setTimezoneOffset(int $offset)
{
self::$timezoneOffset = $offset;
self::$serverTimezoneOffset = idate('Z');
}
/**
* 获取格式化时间
*
* @param string $format 时间格式
* @return string
*/
public function format(string $format): string
{
return date($format, $this->timeStamp);
}
/**
* 获取国际化偏移时间
*
* @return string
*/
public function word(): string
{
return I18n::dateWord($this->timeStamp, self::time() + (self::$timezoneOffset - self::$serverTimezoneOffset));
}
/**
* 获取GMT时间
*
* @deprecated
* @return int
*/
public static function gmtTime(): int
{
return self::time();
}
/**
* 获取服务器时间
*
* @return int
*/
public static function time(): int
{
return self::$serverTimeStamp ?: (self::$serverTimeStamp = time());
}
}

465
var/Typecho/Db.php Executable file
View File

@@ -0,0 +1,465 @@
<?php
namespace Typecho;
use Typecho\Db\Adapter;
use Typecho\Db\Query;
use Typecho\Db\Exception as DbException;
/**
* 包含获取数据支持方法的类.
* 必须定义__TYPECHO_DB_HOST__, __TYPECHO_DB_PORT__, __TYPECHO_DB_NAME__,
* __TYPECHO_DB_USER__, __TYPECHO_DB_PASS__, __TYPECHO_DB_CHAR__
*
* @package Db
*/
class Db
{
/** 读取数据库 */
public const READ = 1;
/** 写入数据库 */
public const WRITE = 2;
/** 升序方式 */
public const SORT_ASC = 'ASC';
/** 降序方式 */
public const SORT_DESC = 'DESC';
/** 表内连接方式 */
public const INNER_JOIN = 'INNER';
/** 表外连接方式 */
public const OUTER_JOIN = 'OUTER';
/** 表左连接方式 */
public const LEFT_JOIN = 'LEFT';
/** 表右连接方式 */
public const RIGHT_JOIN = 'RIGHT';
/** 数据库查询操作 */
public const SELECT = 'SELECT';
/** 数据库更新操作 */
public const UPDATE = 'UPDATE';
/** 数据库插入操作 */
public const INSERT = 'INSERT';
/** 数据库删除操作 */
public const DELETE = 'DELETE';
/**
* 数据库适配器
* @var Adapter
*/
private Adapter $adapter;
/**
* 默认配置
*
* @var array
*/
private array $config;
/**
* 已经连接
*
* @access private
* @var array
*/
private array $connectedPool;
/**
* 前缀
*
* @access private
* @var string
*/
private string $prefix;
/**
* 适配器名称
*
* @access private
* @var string
*/
private string $adapterName;
/**
* 实例化的数据库对象
* @var Db
*/
private static Db $instance;
/**
* 数据库类构造函数
*
* @param mixed $adapterName 适配器名称
* @param string $prefix 前缀
*
* @throws DbException
*/
public function __construct($adapterName, string $prefix = 'typecho_')
{
/** 获取适配器名称 */
$adapterName = $adapterName == 'Mysql' ? 'Mysqli' : $adapterName;
$this->adapterName = $adapterName;
/** 数据库适配器 */
$adapterName = '\Typecho\Db\Adapter\\' . str_replace('_', '\\', $adapterName);
if (!call_user_func([$adapterName, 'isAvailable'])) {
throw new DbException("Adapter {$adapterName} is not available");
}
$this->prefix = $prefix;
/** 初始化内部变量 */
$this->connectedPool = [];
$this->config = [
self::READ => [],
self::WRITE => []
];
//实例化适配器对象
$this->adapter = new $adapterName();
}
/**
* @return Adapter
*/
public function getAdapter(): Adapter
{
return $this->adapter;
}
/**
* 获取适配器名称
*
* @access public
* @return string
*/
public function getAdapterName(): string
{
return $this->adapterName;
}
/**
* 获取表前缀
*
* @access public
* @return string
*/
public function getPrefix(): string
{
return $this->prefix;
}
/**
* @param Config $config
* @param int $op
*/
public function addConfig(Config $config, int $op)
{
if ($op & self::READ) {
$this->config[self::READ][] = $config;
}
if ($op & self::WRITE) {
$this->config[self::WRITE][] = $config;
}
}
/**
* getConfig
*
* @param int $op
*
* @return Config
* @throws DbException
*/
public function getConfig(int $op): Config
{
if (empty($this->config[$op])) {
/** DbException */
throw new DbException('Missing Database Connection');
}
$key = array_rand($this->config[$op]);
return $this->config[$op][$key];
}
/**
* 重置连接池
*
* @return void
*/
public function flushPool()
{
$this->connectedPool = [];
}
/**
* 选择数据库
*
* @param int $op
*
* @return mixed
* @throws DbException
*/
public function selectDb(int $op)
{
if (!isset($this->connectedPool[$op])) {
$selectConnectionConfig = $this->getConfig($op);
$selectConnectionHandle = $this->adapter->connect($selectConnectionConfig);
$this->connectedPool[$op] = $selectConnectionHandle;
}
return $this->connectedPool[$op];
}
/**
* 获取SQL词法构建器实例化对象
*
* @return Query
*/
public function sql(): Query
{
return new Query($this->adapter, $this->prefix);
}
/**
* 为多数据库提供支持
*
* @access public
* @param array $config 数据库实例
* @param integer $op 数据库操作
* @return void
*/
public function addServer(array $config, int $op)
{
$this->addConfig(Config::factory($config), $op);
$this->flushPool();
}
/**
* 获取版本
*
* @param int $op
*
* @return string
* @throws DbException
*/
public function getVersion(int $op = self::READ): string
{
return $this->adapter->getVersion($this->selectDb($op));
}
/**
* 设置默认数据库对象
*
* @access public
* @param Db $db 数据库对象
* @return void
*/
public static function set(Db $db)
{
self::$instance = $db;
}
/**
* 获取数据库实例化对象
* 用静态变量存储实例化的数据库对象,可以保证数据连接仅进行一次
*
* @return Db
* @throws DbException
*/
public static function get(): Db
{
if (empty(self::$instance)) {
/** DbException */
throw new DbException('Missing Database Object');
}
return self::$instance;
}
/**
* 选择查询字段
*
* @param ...$ags
*
* @return Query
* @throws DbException
*/
public function select(...$ags): Query
{
$this->selectDb(self::READ);
$args = func_get_args();
return call_user_func_array([$this->sql(), 'select'], $args ?: ['*']);
}
/**
* 更新记录操作(UPDATE)
*
* @param string $table 需要更新记录的表
*
* @return Query
* @throws DbException
*/
public function update(string $table): Query
{
$this->selectDb(self::WRITE);
return $this->sql()->update($table);
}
/**
* 删除记录操作(DELETE)
*
* @param string $table 需要删除记录的表
*
* @return Query
* @throws DbException
*/
public function delete(string $table): Query
{
$this->selectDb(self::WRITE);
return $this->sql()->delete($table);
}
/**
* 插入记录操作(INSERT)
*
* @param string $table 需要插入记录的表
*
* @return Query
* @throws DbException
*/
public function insert(string $table): Query
{
$this->selectDb(self::WRITE);
return $this->sql()->insert($table);
}
/**
* @param $table
* @throws DbException
*/
public function truncate($table)
{
$table = preg_replace("/^table\./", $this->prefix, $table);
$this->adapter->truncate($table, $this->selectDb(self::WRITE));
}
/**
* 执行查询语句
*
* @param mixed $query 查询语句或者查询对象
* @param int $op 数据库读写状态
* @param string $action 操作动作
*
* @return mixed
* @throws DbException
*/
public function query($query, int $op = self::READ, string $action = self::SELECT)
{
$table = null;
/** 在适配器中执行查询 */
if ($query instanceof Query) {
$action = $query->getAttribute('action');
$table = $query->getAttribute('table');
$op = (self::UPDATE == $action || self::DELETE == $action
|| self::INSERT == $action) ? self::WRITE : self::READ;
} elseif (!is_string($query)) {
/** 如果query不是对象也不是字符串,那么将其判断为查询资源句柄,直接返回 */
return $query;
}
/** 选择连接池 */
$handle = $this->selectDb($op);
/** 如果是查询对象,则将其转换为查询语句 */
$sql = $query instanceof Query ? $query->prepare($query) : $query;
/** 提交查询 */
$resource = $this->adapter->query($sql, $handle, $op, $action, $table);
if ($action) {
//根据查询动作返回相应资源
switch ($action) {
case self::UPDATE:
case self::DELETE:
return $this->adapter->affectedRows($resource, $handle);
case self::INSERT:
return $this->adapter->lastInsertId($resource, $handle);
case self::SELECT:
default:
return $resource;
}
} else {
//如果直接执行查询语句则返回资源
return $resource;
}
}
/**
* 一次取出所有行
*
* @param mixed $query 查询对象
* @param callable|null $filter 行过滤器函数,将查询的每一行作为第一个参数传入指定的过滤器中
*
* @return array
* @throws DbException
*/
public function fetchAll($query, ?callable $filter = null): array
{
//执行查询
$resource = $this->query($query);
$result = $this->adapter->fetchAll($resource);
return $filter ? array_map($filter, $result) : $result;
}
/**
* 一次取出一行
*
* @param mixed $query 查询对象
* @param callable|null $filter 行过滤器函数,将查询的每一行作为第一个参数传入指定的过滤器中
* @return array|null
* @throws DbException
*/
public function fetchRow($query, ?callable $filter = null): ?array
{
$resource = $this->query($query);
return ($rows = $this->adapter->fetch($resource)) ?
($filter ? call_user_func($filter, $rows) : $rows) :
null;
}
/**
* 一次取出一个对象
*
* @param mixed $query 查询对象
* @param array|null $filter 行过滤器函数,将查询的每一行作为第一个参数传入指定的过滤器中
* @return \stdClass|null
* @throws DbException
*/
public function fetchObject($query, ?array $filter = null): ?\stdClass
{
$resource = $this->query($query);
return ($rows = $this->adapter->fetchObject($resource)) ?
($filter ? call_user_func($filter, $rows) : $rows) :
null;
}
}

134
var/Typecho/Db/Adapter.php Executable file
View File

@@ -0,0 +1,134 @@
<?php
namespace Typecho\Db;
use Typecho\Config;
use Typecho\Db;
/**
* Typecho数据库适配器
* 定义通用的数据库适配接口
*
* @package Db
*/
interface Adapter
{
/**
* 判断适配器是否可用
*
* @access public
* @return boolean
*/
public static function isAvailable(): bool;
/**
* 数据库连接函数
*
* @param Config $config 数据库配置
* @return mixed
*/
public function connect(Config $config);
/**
* 获取数据库版本
*
* @param mixed $handle
* @return string
*/
public function getVersion($handle): string;
/**
* 获取数据库类型
*
* @return string
*/
public function getDriver(): string;
/**
* 清空数据表
*
* @param string $table 数据表名
* @param mixed $handle 连接对象
*/
public function truncate(string $table, $handle);
/**
* 执行数据库查询
*
* @param string $query 数据库查询SQL字符串
* @param mixed $handle 连接对象
* @param integer $op 数据库读写状态
* @param string|null $action 数据库动作
* @param string|null $table 数据表
* @return resource
*/
public function query(string $query, $handle, int $op = Db::READ, ?string $action = null, ?string $table = null);
/**
* 将数据查询的其中一行作为数组取出,其中字段名对应数组键值
*
* @param resource $resource 查询的资源数据
* @return array|null
*/
public function fetch($resource): ?array;
/**
* 将数据查询的结果作为数组全部取出,其中字段名对应数组键值
*
* @param resource $resource 查询的资源数据
* @return array
*/
public function fetchAll($resource): array;
/**
* 将数据查询的其中一行作为对象取出,其中字段名对应对象属性
*
* @param resource $resource 查询的资源数据
* @return \stdClass|null
*/
public function fetchObject($resource): ?\stdClass;
/**
* 引号转义函数
*
* @param mixed $string 需要转义的字符串
* @return string
*/
public function quoteValue($string): string;
/**
* 对象引号过滤
*
* @access public
* @param string $string
* @return string
*/
public function quoteColumn(string $string): string;
/**
* 合成查询语句
*
* @access public
* @param array $sql 查询对象词法数组
* @return string
*/
public function parseSelect(array $sql): string;
/**
* 取出最后一次查询影响的行数
*
* @param resource $resource 查询的资源数据
* @param mixed $handle 连接对象
* @return integer
*/
public function affectedRows($resource, $handle): int;
/**
* 取出最后一次插入返回的主键值
*
* @param resource $resource 查询的资源数据
* @param mixed $handle 连接对象
* @return integer
*/
public function lastInsertId($resource, $handle): int;
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Typecho\Db\Adapter;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
use Typecho\Db\Exception as DbException;
/**
* 数据库连接异常类
*
* @package Db
*/
class ConnectionException extends DbException
{
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Typecho\Db\Adapter;
trait MysqlTrait
{
use QueryTrait;
/**
* 清空数据表
*
* @param string $table
* @param mixed $handle 连接对象
* @throws SQLException
*/
public function truncate(string $table, $handle)
{
$this->query('TRUNCATE TABLE ' . $this->quoteColumn($table), $handle);
}
/**
* 合成查询语句
*
* @access public
* @param array $sql 查询对象词法数组
* @return string
*/
public function parseSelect(array $sql): string
{
return $this->buildQuery($sql);
}
/**
* @return string
*/
public function getDriver(): string
{
return 'mysql';
}
}

216
var/Typecho/Db/Adapter/Mysqli.php Executable file
View File

@@ -0,0 +1,216 @@
<?php
namespace Typecho\Db\Adapter;
use Typecho\Config;
use Typecho\Db;
use Typecho\Db\Adapter;
use mysqli_sql_exception;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 数据库Mysqli适配器
*
* @package Db
*/
class Mysqli implements Adapter
{
use MysqlTrait;
/**
* 数据库连接字符串标示
*
* @access private
* @var \mysqli
*/
private \mysqli $dbLink;
/**
* 判断适配器是否可用
*
* @access public
* @return boolean
*/
public static function isAvailable(): bool
{
return extension_loaded('mysqli');
}
/**
* 数据库连接函数
*
* @param Config $config 数据库配置
* @return \mysqli
* @throws ConnectionException
*/
public function connect(Config $config): \mysqli
{
$mysqli = mysqli_init();
if ($mysqli) {
try {
if (!empty($config->sslCa)) {
$mysqli->ssl_set(null, null, $config->sslCa, null, null);
if (isset($config->sslVerify)) {
$mysqli->options(MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, $config->sslVerify);
}
}
$host = $config->host;
$port = empty($config->port) ? null : $config->port;
$socket = null;
if (strpos($host, '/') !== false) {
$socket = $host;
$host = 'localhost';
$port = null;
}
$mysqli->real_connect(
$host,
$config->user,
$config->password,
$config->database,
$port,
$socket
);
$this->dbLink = $mysqli;
if ($config->charset) {
$this->dbLink->query("SET NAMES '{$config->charset}'");
}
} catch (mysqli_sql_exception $e) {
throw new ConnectionException($e->getMessage(), $e->getCode());
}
return $this->dbLink;
}
/** 数据库异常 */
throw new ConnectionException("Couldn't connect to database.", mysqli_connect_errno());
}
/**
* 获取数据库版本
*
* @param mixed $handle
* @return string
*/
public function getVersion($handle): string
{
return $this->dbLink->server_version;
}
/**
* 执行数据库查询
*
* @param string $query 数据库查询SQL字符串
* @param mixed $handle 连接对象
* @param integer $op 数据库读写状态
* @param string|null $action 数据库动作
* @param string|null $table 数据表
* @throws SQLException
*/
public function query(
string $query,
$handle,
int $op = Db::READ,
?string $action = null,
?string $table = null
) {
try {
if ($resource = $this->dbLink->query($query)) {
return $resource;
}
} catch (mysqli_sql_exception $e) {
/** 数据库异常 */
throw new SQLException($e->getMessage(), $e->getCode());
}
/** 数据库异常 */
throw new SQLException($this->dbLink->error, $this->dbLink->errno);
}
/**
* 对象引号过滤
*
* @access public
* @param string $string
* @return string
*/
public function quoteColumn(string $string): string
{
return '`' . $string . '`';
}
/**
* 将数据查询的其中一行作为数组取出,其中字段名对应数组键值
*
* @param \mysqli_result $resource 查询返回资源标识
* @return array|null
*/
public function fetch($resource): ?array
{
return $resource->fetch_assoc();
}
/**
* 将数据查询的结果作为数组全部取出,其中字段名对应数组键值
*
* @param \mysqli_result $resource 查询返回资源标识
* @return array
*/
public function fetchAll($resource): array
{
return $resource->fetch_all(MYSQLI_ASSOC);
}
/**
* 将数据查询的其中一行作为对象取出,其中字段名对应对象属性
*
* @param \mysqli_result $resource 查询的资源数据
* @return \stdClass|null
*/
public function fetchObject($resource): ?\stdClass
{
return $resource->fetch_object();
}
/**
* 引号转义函数
*
* @param mixed $string 需要转义的字符串
* @return string
*/
public function quoteValue($string): string
{
return "'" . $this->dbLink->real_escape_string($string) . "'";
}
/**
* 取出最后一次查询影响的行数
*
* @param mixed $resource 查询的资源数据
* @param \mysqli $handle 连接对象
* @return integer
*/
public function affectedRows($resource, $handle): int
{
return $handle->affected_rows;
}
/**
* 取出最后一次插入返回的主键值
*
* @param mixed $resource 查询的资源数据
* @param \mysqli $handle 连接对象
* @return integer
*/
public function lastInsertId($resource, $handle): int
{
return $handle->insert_id;
}
}

184
var/Typecho/Db/Adapter/Pdo.php Executable file
View File

@@ -0,0 +1,184 @@
<?php
namespace Typecho\Db\Adapter;
use Typecho\Config;
use Typecho\Db;
use Typecho\Db\Adapter;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 数据库PDOMysql适配器
*
* @package Db
*/
abstract class Pdo implements Adapter
{
/**
* 数据库对象
*
* @access protected
* @var \PDO
*/
protected \PDO $object;
/**
* 最后一次操作的数据表
*
* @access protected
* @var string|null
*/
protected ?string $lastTable;
/**
* 判断适配器是否可用
*
* @access public
* @return boolean
*/
public static function isAvailable(): bool
{
return class_exists('PDO');
}
/**
* 数据库连接函数
*
* @param Config $config 数据库配置
* @return \PDO
* @throws ConnectionException
*/
public function connect(Config $config): \PDO
{
try {
$this->object = $this->init($config);
$this->object->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
return $this->object;
} catch (\PDOException $e) {
/** 数据库异常 */
throw new ConnectionException($e->getMessage(), $e->getCode());
}
}
/**
* 初始化数据库
*
* @param Config $config 数据库配置
* @abstract
* @access public
* @return \PDO
*/
abstract public function init(Config $config): \PDO;
/**
* 获取数据库版本
*
* @param mixed $handle
* @return string
*/
public function getVersion($handle): string
{
return $handle->getAttribute(\PDO::ATTR_SERVER_VERSION);
}
/**
* 执行数据库查询
*
* @param string $query 数据库查询SQL字符串
* @param \PDO $handle 连接对象
* @param integer $op 数据库读写状态
* @param string|null $action 数据库动作
* @param string|null $table 数据表
* @return \PDOStatement
* @throws SQLException
*/
public function query(
string $query,
$handle,
int $op = Db::READ,
?string $action = null,
?string $table = null
): \PDOStatement {
try {
$this->lastTable = $table;
$resource = $handle->prepare($query);
$resource->execute();
} catch (\PDOException $e) {
/** 数据库异常 */
throw new SQLException($e->getMessage(), $e->getCode());
}
return $resource;
}
/**
* 将数据查询的结果作为数组全部取出,其中字段名对应数组键值
*
* @param \PDOStatement $resource 查询的资源数据
* @return array
*/
public function fetchAll($resource): array
{
return $resource->fetchAll(\PDO::FETCH_ASSOC);
}
/**
* 将数据查询的其中一行作为数组取出,其中字段名对应数组键值
*
* @param \PDOStatement $resource 查询返回资源标识
* @return array|null
*/
public function fetch($resource): ?array
{
return $resource->fetch(\PDO::FETCH_ASSOC) ?: null;
}
/**
* 将数据查询的其中一行作为对象取出,其中字段名对应对象属性
*
* @param \PDOStatement $resource 查询的资源数据
* @return \stdClass|null
*/
public function fetchObject($resource): ?\stdClass
{
return $resource->fetchObject() ?: null;
}
/**
* 引号转义函数
*
* @param mixed $string 需要转义的字符串
* @return string
*/
public function quoteValue($string): string
{
return $this->object->quote($string);
}
/**
* 取出最后一次查询影响的行数
*
* @param \PDOStatement $resource 查询的资源数据
* @param \PDO $handle 连接对象
* @return integer
*/
public function affectedRows($resource, $handle): int
{
return $resource->rowCount();
}
/**
* 取出最后一次插入返回的主键值
*
* @param \PDOStatement $resource 查询的资源数据
* @param \PDO $handle 连接对象
* @return integer
*/
public function lastInsertId($resource, $handle): int
{
return $handle->lastInsertId();
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace Typecho\Db\Adapter\Pdo;
use Typecho\Config;
use Typecho\Db\Adapter\MysqlTrait;
use Typecho\Db\Adapter\Pdo;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 数据库Pdo_Mysql适配器
*
* @package Db
*/
class Mysql extends Pdo
{
use MysqlTrait;
/**
* 判断适配器是否可用
*
* @access public
* @return boolean
*/
public static function isAvailable(): bool
{
return parent::isAvailable() && in_array('mysql', \PDO::getAvailableDrivers());
}
/**
* 对象引号过滤
*
* @access public
* @param string $string
* @return string
*/
public function quoteColumn(string $string): string
{
return '`' . $string . '`';
}
/**
* 初始化数据库
*
* @param Config $config 数据库配置
* @access public
* @return \PDO
*/
public function init(Config $config): \PDO
{
$options = [];
if (!empty($config->sslCa)) {
$options[\PDO::MYSQL_ATTR_SSL_CA] = $config->sslCa;
if (isset($config->sslVerify)) {
// FIXME: https://github.com/php/php-src/issues/8577
$options[\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = $config->sslVerify;
}
}
$dsn = !empty($config->dsn)
? $config->dsn
: (strpos($config->host, '/') !== false
? "mysql:dbname={$config->database};unix_socket={$config->host}"
: "mysql:dbname={$config->database};host={$config->host};port={$config->port}");
$pdo = new \PDO(
$dsn,
$config->user,
$config->password,
$options
);
if (class_exists('\Pdo\Mysql')) {
// 新版本写法
$pdo->setAttribute(\Pdo\Mysql::ATTR_USE_BUFFERED_QUERY, true);
} else {
// 兼容旧版本
$pdo->setAttribute(\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true);
}
if ($config->charset) {
$pdo->exec("SET NAMES '{$config->charset}'");
}
return $pdo;
}
/**
* 引号转义函数
*
* @param mixed $string 需要转义的字符串
* @return string
*/
public function quoteValue($string): string
{
return '\'' . str_replace(['\'', '\\'], ['\'\'', '\\\\'], $string) . '\'';
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace Typecho\Db\Adapter\Pdo;
use Typecho\Config;
use Typecho\Db;
use Typecho\Db\Adapter\SQLException;
use Typecho\Db\Adapter\Pdo;
use Typecho\Db\Adapter\PgsqlTrait;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 数据库Pdo_Pgsql适配器
*
* @package Db
*/
class Pgsql extends Pdo
{
use PgsqlTrait;
/**
* 判断适配器是否可用
*
* @access public
* @return boolean
*/
public static function isAvailable(): bool
{
return parent::isAvailable() && in_array('pgsql', \PDO::getAvailableDrivers());
}
/**
* 执行数据库查询
*
* @param string $query 数据库查询SQL字符串
* @param \PDO $handle 连接对象
* @param integer $op 数据库读写状态
* @param string|null $action 数据库动作
* @param string|null $table 数据表
* @return \PDOStatement
* @throws SQLException
*/
public function query(
string $query,
$handle,
int $op = Db::READ,
?string $action = null,
?string $table = null
): \PDOStatement {
$this->prepareQuery($query, $handle, $action, $table);
return parent::query($query, $handle, $op, $action, $table);
}
/**
* 初始化数据库
*
* @param Config $config 数据库配置
* @return \PDO
*/
public function init(Config $config): \PDO
{
$dsn = "pgsql:dbname={$config->database};host={$config->host};port={$config->port}";
if ($config->sslVerify) {
$dsn .= ';sslmode=require';
}
$pdo = new \PDO(
$dsn,
$config->user,
$config->password
);
if ($config->charset) {
$pdo->exec("SET NAMES '{$config->charset}'");
}
return $pdo;
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace Typecho\Db\Adapter\Pdo;
use Typecho\Config;
use Typecho\Db\Adapter\Pdo;
use Typecho\Db\Adapter\SQLiteTrait;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 数据库Pdo_SQLite适配器
*
* @package Db
*/
class SQLite extends Pdo
{
use SQLiteTrait;
/**
* 判断适配器是否可用
*
* @access public
* @return boolean
*/
public static function isAvailable(): bool
{
return parent::isAvailable() && in_array('sqlite', \PDO::getAvailableDrivers());
}
/**
* 初始化数据库
*
* @param Config $config 数据库配置
* @access public
* @return \PDO
*/
public function init(Config $config): \PDO
{
$pdo = new \PDO("sqlite:{$config->file}");
$this->isSQLite2 = version_compare($pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '3.0.0', '<');
return $pdo;
}
/**
* 将数据查询的其中一行作为对象取出,其中字段名对应对象属性
*
* @param \PDOStatement $resource 查询的资源数据
* @return \stdClass|null
*/
public function fetchObject($resource): ?\stdClass
{
$result = $this->fetch($resource);
return $result ? (object) $result : null;
}
/**
* 将数据查询的其中一行作为数组取出,其中字段名对应数组键值
*
* @param \PDOStatement $resource 查询返回资源标识
* @return array|null
*/
public function fetch($resource): ?array
{
$result = parent::fetch($resource);
return $result ? $this->filterColumnName($result) : null;
}
/**
* 将数据查询的结果作为数组全部取出,其中字段名对应数组键值
*
* @param \PDOStatement $resource 查询的资源数据
* @return array
*/
public function fetchAll($resource): array
{
return array_map([$this, 'filterColumnName'], parent::fetchAll($resource));
}
}

151
var/Typecho/Db/Adapter/Pgsql.php Executable file
View File

@@ -0,0 +1,151 @@
<?php
namespace Typecho\Db\Adapter;
use Typecho\Config;
use Typecho\Db;
use Typecho\Db\Adapter;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 数据库Pgsql适配器
*
* @package Db
*/
class Pgsql implements Adapter
{
use PgsqlTrait;
/**
* 判断适配器是否可用
*
* @access public
* @return boolean
*/
public static function isAvailable(): bool
{
return extension_loaded('pgsql');
}
/**
* 数据库连接函数
*
* @param Config $config 数据库配置
* @return resource
* @throws ConnectionException
*/
public function connect(Config $config)
{
$dsn = "host={$config->host} port={$config->port}"
. " dbname={$config->database} user={$config->user} password={$config->password}";
if ($config->sslVerify) {
$dsn .= ' sslmode=require';
}
if ($config->charset) {
$dsn .= " options='--client_encoding={$config->charset}'";
}
if ($dbLink = @pg_connect($dsn)) {
return $dbLink;
}
/** 数据库异常 */
throw new ConnectionException("Couldn't connect to database.");
}
/**
* 获取数据库版本
*
* @param mixed $handle
* @return string
*/
public function getVersion($handle): string
{
$version = pg_version($handle);
return $version['server'];
}
/**
* 执行数据库查询
*
* @param string $query 数据库查询SQL字符串
* @param resource $handle 连接对象
* @param integer $op 数据库读写状态
* @param string|null $action 数据库动作
* @param string|null $table 数据表
* @return resource
* @throws SQLException
*/
public function query(string $query, $handle, int $op = Db::READ, ?string $action = null, ?string $table = null)
{
$this->prepareQuery($query, $handle, $action, $table);
if ($resource = pg_query($handle, $query)) {
return $resource;
}
/** 数据库异常 */
throw new SQLException(
@pg_last_error($handle),
pg_result_error_field(pg_get_result($handle), PGSQL_DIAG_SQLSTATE)
);
}
/**
* 将数据查询的其中一行作为数组取出,其中字段名对应数组键值
*
* @param resource $resource 查询返回资源标识
* @return array|null
*/
public function fetch($resource): ?array
{
return pg_fetch_assoc($resource) ?: null;
}
/**
* 将数据查询的其中一行作为对象取出,其中字段名对应对象属性
*
* @param resource $resource 查询的资源数据
* @return \stdClass|null
*/
public function fetchObject($resource): ?\stdClass
{
return pg_fetch_object($resource) ?: null;
}
/**
* @param resource $resource
* @return array
*/
public function fetchAll($resource): array
{
return pg_fetch_all($resource, PGSQL_ASSOC) ?: [];
}
/**
* 取出最后一次查询影响的行数
*
* @param resource $resource 查询的资源数据
* @param resource $handle 连接对象
* @return integer
*/
public function affectedRows($resource, $handle): int
{
return pg_affected_rows($resource);
}
/**
* 引号转义函数
*
* @param mixed $string 需要转义的字符串
* @return string
*/
public function quoteValue($string): string
{
return '\'' . str_replace('\'', '\'\'', $string) . '\'';
}
}

View File

@@ -0,0 +1,175 @@
<?php
namespace Typecho\Db\Adapter;
use Typecho\Db;
trait PgsqlTrait
{
use QueryTrait;
/**
* @var array
*/
private array $pk = [];
/**
* @var bool
*/
private bool $compatibleInsert = false;
/**
* @var string|null
*/
private ?string $lastInsertTable = null;
/**
* 清空数据表
*
* @param string $table
* @param resource $handle 连接对象
* @throws SQLException
*/
public function truncate(string $table, $handle)
{
$this->query('TRUNCATE TABLE ' . $this->quoteColumn($table) . ' RESTART IDENTITY', $handle);
}
/**
* 合成查询语句
*
* @access public
* @param array $sql 查询对象词法数组
* @return string
*/
public function parseSelect(array $sql): string
{
return $this->buildQuery($sql);
}
/**
* 对象引号过滤
*
* @access public
* @param string $string
* @return string
*/
public function quoteColumn(string $string): string
{
return '"' . $string . '"';
}
/**
* @param string $query
* @param $handle
* @param string|null $action
* @param string|null $table
* @throws SQLException
*/
protected function prepareQuery(string &$query, $handle, ?string $action = null, ?string $table = null)
{
if (Db::INSERT == $action && !empty($table)) {
$this->compatibleInsert = false;
if (!isset($this->pk[$table])) {
$resource = $this->query("SELECT
pg_attribute.attname,
format_type(pg_attribute.atttypid, pg_attribute.atttypmod)
FROM pg_index, pg_class, pg_attribute, pg_namespace
WHERE
pg_class.oid = " . $this->quoteValue($table) . "::regclass AND
indrelid = pg_class.oid AND
nspname = 'public' AND
pg_class.relnamespace = pg_namespace.oid AND
pg_attribute.attrelid = pg_class.oid AND
pg_attribute.attnum = any(pg_index.indkey)
AND indisprimary", $handle, Db::READ, Db::SELECT, $table);
$result = $this->fetch($resource);
if (!empty($result)) {
$this->pk[$table] = $result['attname'];
}
}
// 使用兼容模式监听插入结果
if (isset($this->pk[$table])) {
$this->compatibleInsert = true;
$this->lastInsertTable = $table;
$query .= ' RETURNING ' . $this->quoteColumn($this->pk[$table]);
}
} else {
$this->lastInsertTable = null;
}
}
/**
* 取出最后一次插入返回的主键值
*
* @param resource $resource 查询的资源数据
* @param resource $handle 连接对象
* @return integer
* @throws SQLException
*/
public function lastInsertId($resource, $handle): int
{
$lastTable = $this->lastInsertTable;
if ($this->compatibleInsert) {
$result = $this->fetch($resource);
$pk = $this->pk[$lastTable];
if (!empty($result) && isset($result[$pk])) {
return (int) $result[$pk];
}
} else {
$resource = $this->query(
'SELECT oid FROM pg_class WHERE relname = '
. $this->quoteValue($lastTable . '_seq'),
$handle,
Db::READ,
Db::SELECT,
$lastTable
);
$result = $this->fetch($resource);
if (!empty($result)) {
$resource = $this->query(
'SELECT CURRVAL(' . $this->quoteValue($lastTable . '_seq') . ') AS last_insert_id',
$handle,
Db::READ,
Db::SELECT,
$lastTable
);
$result = $this->fetch($resource);
if (!empty($result)) {
return (int) $result['last_insert_id'];
}
}
}
return 0;
}
/**
* @return string
*/
public function getDriver(): string
{
return 'pgsql';
}
abstract public function query(
string $query,
$handle,
int $op = Db::READ,
?string $action = null,
?string $table = null
);
abstract public function quoteValue(string $string): string;
abstract public function fetch($resource): ?array;
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Typecho\Db\Adapter;
/**
* Build Sql
*/
trait QueryTrait
{
/**
* @param array $sql
* @return string
*/
private function buildQuery(array $sql): string
{
if (!empty($sql['join'])) {
foreach ($sql['join'] as $val) {
[$table, $condition, $op] = $val;
$sql['table'] = "{$sql['table']} {$op} JOIN {$table} ON {$condition}";
}
}
$sql['limit'] = isset($sql['limit']) ? ' LIMIT ' . $sql['limit'] : '';
$sql['offset'] = isset($sql['offset']) ? ' OFFSET ' . $sql['offset'] : '';
return 'SELECT ' . $sql['fields'] . ' FROM ' . $sql['table'] .
$sql['where'] . $sql['group'] . $sql['having'] . $sql['order'] . $sql['limit'] . $sql['offset'];
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Typecho\Db\Adapter;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
use Typecho\Db\Exception as DbException;
/**
* 数据库连接异常类
*
* @package Db
*/
class SQLException extends DbException
{
}

166
var/Typecho/Db/Adapter/SQLite.php Executable file
View File

@@ -0,0 +1,166 @@
<?php
namespace Typecho\Db\Adapter;
use Typecho\Config;
use Typecho\Db;
use Typecho\Db\Adapter;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 数据库SQLite适配器
*
* @package Db
*/
class SQLite implements Adapter
{
use SQLiteTrait;
/**
* 判断适配器是否可用
*
* @access public
* @return boolean
*/
public static function isAvailable(): bool
{
return extension_loaded('sqlite3');
}
/**
* 数据库连接函数
*
* @param Config $config 数据库配置
* @return \SQLite3
* @throws ConnectionException
*/
public function connect(Config $config): \SQLite3
{
try {
$dbHandle = new \SQLite3($config->file);
$this->isSQLite2 = version_compare(\SQLite3::version()['versionString'], '3.0.0', '<');
} catch (\Exception $e) {
throw new ConnectionException($e->getMessage(), $e->getCode());
}
return $dbHandle;
}
/**
* 获取数据库版本
*
* @param mixed $handle
* @return string
*/
public function getVersion($handle): string
{
return \SQLite3::version()['versionString'];
}
/**
* 执行数据库查询
*
* @param string $query 数据库查询SQL字符串
* @param \SQLite3 $handle 连接对象
* @param integer $op 数据库读写状态
* @param string|null $action 数据库动作
* @param string|null $table 数据表
* @return \SQLite3Result
* @throws SQLException
*/
public function query(
string $query,
$handle,
int $op = Db::READ,
?string $action = null,
?string $table = null
): \SQLite3Result {
if ($stm = $handle->prepare($query)) {
if ($resource = $stm->execute()) {
return $resource;
}
}
/** 数据库异常 */
throw new SQLException($handle->lastErrorMsg(), $handle->lastErrorCode());
}
/**
* 将数据查询的其中一行作为对象取出,其中字段名对应对象属性
*
* @param \SQLite3Result $resource 查询的资源数据
* @return \stdClass|null
*/
public function fetchObject($resource): ?\stdClass
{
$result = $this->fetch($resource);
return $result ? (object) $result : null;
}
/**
* 将数据查询的其中一行作为数组取出,其中字段名对应数组键值
*
* @param \SQLite3Result $resource 查询返回资源标识
* @return array|null
*/
public function fetch($resource): ?array
{
$result = $resource->fetchArray(SQLITE3_ASSOC);
return $result ? $this->filterColumnName($result) : null;
}
/**
* 将数据查询的结果作为数组全部取出,其中字段名对应数组键值
*
* @param \SQLite3Result $resource 查询的资源数据
* @return array
*/
public function fetchAll($resource): array
{
$result = [];
while ($row = $this->fetch($resource)) {
$result[] = $row;
}
return $result;
}
/**
* 引号转义函数
*
* @param mixed $string 需要转义的字符串
* @return string
*/
public function quoteValue($string): string
{
return '\'' . str_replace('\'', '\'\'', $string) . '\'';
}
/**
* 取出最后一次查询影响的行数
*
* @param \SQLite3Result $resource 查询的资源数据
* @param \SQLite3 $handle 连接对象
* @return integer
*/
public function affectedRows($resource, $handle): int
{
return $handle->changes();
}
/**
* 取出最后一次插入返回的主键值
*
* @param \SQLite3Result $resource 查询的资源数据
* @param \SQLite3 $handle 连接对象
* @return integer
*/
public function lastInsertId($resource, $handle): int
{
return $handle->lastInsertRowID();
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace Typecho\Db\Adapter;
/**
* SQLite Special Util
*/
trait SQLiteTrait
{
use QueryTrait;
private bool $isSQLite2 = false;
/**
* 清空数据表
*
* @param string $table
* @param mixed $handle 连接对象
* @throws SQLException
*/
public function truncate(string $table, $handle)
{
$this->query('DELETE FROM ' . $this->quoteColumn($table), $handle);
}
/**
* 对象引号过滤
*
* @param string $string
* @return string
*/
public function quoteColumn(string $string): string
{
return '"' . $string . '"';
}
/**
* 过滤字段名
*
* @access private
*
* @param array $result
*
* @return array
*/
private function filterColumnName(array $result): array
{
/** 如果结果为空,直接返回 */
if (empty($result)) {
return $result;
}
$tResult = [];
/** 遍历数组 */
foreach ($result as $key => $val) {
/** 按点分隔 */
if (false !== ($pos = strpos($key, '.'))) {
$key = substr($key, $pos + 1);
}
$tResult[trim($key, '"')] = $val;
}
return $tResult;
}
/**
* 处理sqlite2的distinct count
*
* @param string $sql
*
* @return string
*/
private function filterCountQuery(string $sql): string
{
if (preg_match("/SELECT\s+COUNT\(DISTINCT\s+([^\)]+)\)\s+(AS\s+[^\s]+)?\s*FROM\s+(.+)/is", $sql, $matches)) {
return 'SELECT COUNT(' . $matches[1] . ') ' . $matches[2] . ' FROM SELECT DISTINCT '
. $matches[1] . ' FROM ' . $matches[3];
}
return $sql;
}
/**
* 合成查询语句
*
* @access public
* @param array $sql 查询对象词法数组
* @return string
*/
public function parseSelect(array $sql): string
{
$query = $this->buildQuery($sql);
if ($this->isSQLite2) {
$query = $this->filterCountQuery($query);
}
return $query;
}
/**
* @return string
*/
public function getDriver(): string
{
return 'sqlite';
}
}

18
var/Typecho/Db/Exception.php Executable file
View File

@@ -0,0 +1,18 @@
<?php
namespace Typecho\Db;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
use Typecho\Exception as TypechoException;
/**
* 数据库异常类
*
* @package Db
*/
class Exception extends TypechoException
{
}

558
var/Typecho/Db/Query.php Executable file
View File

@@ -0,0 +1,558 @@
<?php
namespace Typecho\Db;
use Typecho\Db;
/**
* Typecho数据库查询语句构建类
* 使用方法:
* $query = new Query(); //或者使用DB积累的sql方法返回实例化对象
* $query->select('posts', 'post_id, post_title')
* ->where('post_id = %d', 1)
* ->limit(1);
* echo $query;
* 打印的结果将是
* SELECT post_id, post_title FROM posts WHERE 1=1 AND post_id = 1 LIMIT 1
*
*
* @package Db
*/
class Query
{
/** 数据库关键字 */
private const KEYWORDS = '*PRIMARY|AND|OR|LIKE|ILIKE|BINARY|BY|DISTINCT|AS|IN|NOT|IS|NULL';
/**
* 默认字段
*
* @var array
* @access private
*/
private static array $default = [
'action' => null,
'table' => null,
'fields' => '*',
'join' => [],
'where' => null,
'limit' => null,
'offset' => null,
'order' => null,
'group' => null,
'having' => null,
'rows' => [],
];
/**
* 数据库适配器
*
* @var Adapter
*/
private Adapter $adapter;
/**
* 查询语句预结构,由数组构成,方便组合为SQL查询字符串
*
* @var array
*/
private array $sqlPreBuild;
/**
* 前缀
*
* @access private
* @var string
*/
private string $prefix;
/**
* @var array
*/
private array $params = [];
/**
* 构造函数,引用数据库适配器作为内部数据
*
* @param Adapter $adapter 数据库适配器
* @param string $prefix 前缀
*/
public function __construct(Adapter $adapter, string $prefix)
{
$this->adapter = &$adapter;
$this->prefix = $prefix;
$this->sqlPreBuild = self::$default;
}
/**
* set default params
*
* @param array $default
*/
public static function setDefault(array $default)
{
self::$default = array_merge(self::$default, $default);
}
/**
* 获取参数
*
* @return array
*/
public function getParams(): array
{
return $this->params;
}
/**
* 获取查询字串属性值
*
* @access public
* @param string $attributeName 属性名称
* @return string
*/
public function getAttribute(string $attributeName): ?string
{
return $this->sqlPreBuild[$attributeName] ?? null;
}
/**
* 清除查询字串属性值
*
* @access public
* @param string $attributeName 属性名称
* @return Query
*/
public function cleanAttribute(string $attributeName): Query
{
if (isset($this->sqlPreBuild[$attributeName])) {
$this->sqlPreBuild[$attributeName] = self::$default[$attributeName];
}
return $this;
}
/**
* 连接表
*
* @param string $table 需要连接的表
* @param string $condition 连接条件
* @param string $op 连接方法(LEFT, RIGHT, INNER)
* @return Query
*/
public function join(string $table, string $condition, string $op = Db::INNER_JOIN): Query
{
$this->sqlPreBuild['join'][] = [$this->filterPrefix($table), $this->filterColumn($condition), $op];
return $this;
}
/**
* 过滤表前缀,表前缀由table.构成
*
* @param string $string 需要解析的字符串
* @return string
*/
private function filterPrefix(string $string): string
{
return (0 === strpos($string, 'table.')) ? substr_replace($string, $this->prefix, 0, 6) : $string;
}
/**
* 过滤数组键值
*
* @access private
* @param string $str 待处理字段值
* @return string
*/
private function filterColumn(string $str): string
{
$str = $str . ' 0';
$length = strlen($str);
$lastIsAlnum = false;
$result = '';
$word = '';
$split = '';
$quotes = 0;
for ($i = 0; $i < $length; $i++) {
$cha = $str[$i];
if (ctype_alnum($cha) || false !== strpos('_*', $cha)) {
if (!$lastIsAlnum) {
if (
$quotes > 0 && !ctype_digit($word) && '.' != $split
&& false === strpos(self::KEYWORDS, strtoupper($word))
) {
$word = $this->adapter->quoteColumn($word);
} elseif ('.' == $split && 'table' == $word) {
$word = $this->prefix;
$split = '';
}
$result .= $word . $split;
$word = '';
$quotes = 0;
}
$word .= $cha;
$lastIsAlnum = true;
} else {
if ($lastIsAlnum) {
if (0 == $quotes) {
if (false !== strpos(' ,)=<>.+-*/', $cha)) {
$quotes = 1;
} elseif ('(' == $cha) {
$quotes = - 1;
}
}
$split = '';
}
$split .= $cha;
$lastIsAlnum = false;
}
}
return $result;
}
/**
* AND条件查询语句
*
* @param ...$args
* @return $this
*/
public function where(...$args): Query
{
[$condition] = $args;
$condition = str_replace('?', "%s", $this->filterColumn($condition));
$operator = empty($this->sqlPreBuild['where']) ? ' WHERE ' : ' AND';
if (count($args) <= 1) {
$this->sqlPreBuild['where'] .= $operator . ' (' . $condition . ')';
} else {
array_shift($args);
$this->sqlPreBuild['where'] .= $operator . ' (' . vsprintf($condition, $this->quoteValues($args)) . ')';
}
return $this;
}
/**
* 转义参数
*
* @param array $values
* @access protected
* @return array
*/
protected function quoteValues(array $values): array
{
foreach ($values as &$value) {
if (is_array($value)) {
$value = '(' . implode(',', array_map([$this, 'quoteValue'], $value)) . ')';
} else {
$value = $this->quoteValue($value);
}
}
return $values;
}
/**
* 延迟转义
*
* @param $value
* @return string
*/
public function quoteValue($value): string
{
$this->params[] = $value;
return '#param:' . (count($this->params) - 1) . '#';
}
/**
* OR条件查询语句
*
* @param ...$args
* @return Query
*/
public function orWhere(...$args): Query
{
[$condition] = $args;
$condition = str_replace('?', "%s", $this->filterColumn($condition));
$operator = empty($this->sqlPreBuild['where']) ? ' WHERE ' : ' OR';
if (func_num_args() <= 1) {
$this->sqlPreBuild['where'] .= $operator . ' (' . $condition . ')';
} else {
array_shift($args);
$this->sqlPreBuild['where'] .= $operator . ' (' . vsprintf($condition, $this->quoteValues($args)) . ')';
}
return $this;
}
/**
* 查询行数限制
*
* @param mixed $limit 需要查询的行数
* @return Query
*/
public function limit($limit): Query
{
$this->sqlPreBuild['limit'] = intval($limit);
return $this;
}
/**
* 查询行数偏移量
*
* @param mixed $offset 需要偏移的行数
* @return Query
*/
public function offset($offset): Query
{
$this->sqlPreBuild['offset'] = intval($offset);
return $this;
}
/**
* 分页查询
*
* @param mixed $page 页数
* @param mixed $pageSize 每页行数
* @return Query
*/
public function page($page, $pageSize): Query
{
$pageSize = intval($pageSize);
$this->sqlPreBuild['limit'] = $pageSize;
$this->sqlPreBuild['offset'] = (max(intval($page), 1) - 1) * $pageSize;
return $this;
}
/**
* 指定需要写入的栏目及其值
*
* @param array $rows
* @return Query
*/
public function rows(array $rows): Query
{
foreach ($rows as $key => $row) {
$this->sqlPreBuild['rows'][$this->filterColumn($key)]
= is_null($row) ? 'NULL' : $this->adapter->quoteValue($row);
}
return $this;
}
/**
* 指定需要写入栏目及其值
* 单行且不会转义引号
*
* @param string $key 栏目名称
* @param mixed $value 指定的值
* @param bool $escape 是否转义
* @return Query
*/
public function expression(string $key, $value, bool $escape = true): Query
{
$this->sqlPreBuild['rows'][$this->filterColumn($key)] = $escape ? $this->filterColumn($value) : $value;
return $this;
}
/**
* 排序顺序(ORDER BY)
*
* @param string $orderBy 排序的索引
* @param string $sort 排序的方式(ASC, DESC)
* @return Query
*/
public function order(string $orderBy, string $sort = Db::SORT_ASC): Query
{
if (empty($this->sqlPreBuild['order'])) {
$this->sqlPreBuild['order'] = ' ORDER BY ';
} else {
$this->sqlPreBuild['order'] .= ', ';
}
$this->sqlPreBuild['order'] .= $this->filterColumn($orderBy) . (empty($sort) ? null : ' ' . $sort);
return $this;
}
/**
* 集合聚集(GROUP BY)
*
* @param string $key 聚集的键值
* @return Query
*/
public function group(string $key): Query
{
$this->sqlPreBuild['group'] = ' GROUP BY ' . $this->filterColumn($key);
return $this;
}
/**
* @param string $condition
* @param ...$args
* @return $this
*/
public function having(string $condition, ...$args): Query
{
$condition = str_replace('?', "%s", $this->filterColumn($condition));
$operator = empty($this->sqlPreBuild['having']) ? ' HAVING ' : ' AND';
if (count($args) == 0) {
$this->sqlPreBuild['having'] .= $operator . ' (' . $condition . ')';
} else {
$this->sqlPreBuild['having'] .= $operator . ' (' . vsprintf($condition, $this->quoteValues($args)) . ')';
}
return $this;
}
/**
* 选择查询字段
*
* @param mixed ...$args 查询字段
* @return $this
*/
public function select(...$args): Query
{
$this->sqlPreBuild['action'] = Db::SELECT;
$this->sqlPreBuild['fields'] = $this->getColumnFromParameters($args);
return $this;
}
/**
* 从参数中合成查询字段
*
* @access private
* @param array $parameters
* @return string
*/
private function getColumnFromParameters(array $parameters): string
{
$fields = [];
foreach ($parameters as $value) {
if (is_array($value)) {
foreach ($value as $key => $val) {
$fields[] = $key . ' AS ' . $val;
}
} else {
$fields[] = $value;
}
}
return $this->filterColumn(implode(' , ', $fields));
}
/**
* 查询记录操作(SELECT)
*
* @param string $table 查询的表
* @return Query
*/
public function from(string $table): Query
{
$this->sqlPreBuild['table'] = $this->filterPrefix($table);
return $this;
}
/**
* 更新记录操作(UPDATE)
*
* @param string $table 需要更新记录的表
* @return Query
*/
public function update(string $table): Query
{
$this->sqlPreBuild['action'] = Db::UPDATE;
$this->sqlPreBuild['table'] = $this->filterPrefix($table);
return $this;
}
/**
* 删除记录操作(DELETE)
*
* @param string $table 需要删除记录的表
* @return Query
*/
public function delete(string $table): Query
{
$this->sqlPreBuild['action'] = Db::DELETE;
$this->sqlPreBuild['table'] = $this->filterPrefix($table);
return $this;
}
/**
* 插入记录操作(INSERT)
*
* @param string $table 需要插入记录的表
* @return Query
*/
public function insert(string $table): Query
{
$this->sqlPreBuild['action'] = Db::INSERT;
$this->sqlPreBuild['table'] = $this->filterPrefix($table);
return $this;
}
/**
* @param string $query
* @return string
*/
public function prepare(string $query): string
{
$params = $this->params;
$adapter = $this->adapter;
return preg_replace_callback("/#param:([0-9]+)#/", function ($matches) use ($params, $adapter) {
if (array_key_exists($matches[1], $params)) {
return is_null($params[$matches[1]]) ? 'NULL' : $adapter->quoteValue($params[$matches[1]]);
} else {
return $matches[0];
}
}, $query);
}
/**
* 构造最终查询语句
*
* @return string
*/
public function __toString()
{
switch ($this->sqlPreBuild['action']) {
case Db::SELECT:
return $this->adapter->parseSelect($this->sqlPreBuild);
case Db::INSERT:
return 'INSERT INTO '
. $this->sqlPreBuild['table']
. '(' . implode(' , ', array_keys($this->sqlPreBuild['rows'])) . ')'
. ' VALUES '
. '(' . implode(' , ', array_values($this->sqlPreBuild['rows'])) . ')'
. $this->sqlPreBuild['limit'];
case Db::DELETE:
return 'DELETE FROM '
. $this->sqlPreBuild['table']
. $this->sqlPreBuild['where'];
case Db::UPDATE:
$columns = [];
if (isset($this->sqlPreBuild['rows'])) {
foreach ($this->sqlPreBuild['rows'] as $key => $val) {
$columns[] = "$key = $val";
}
}
return 'UPDATE '
. $this->sqlPreBuild['table']
. ' SET ' . implode(' , ', $columns)
. $this->sqlPreBuild['where'];
default:
return null;
}
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Typecho\Db\Query;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
use Typecho\Db\Exception as DbException;
/**
* 数据库查询异常类
*
* @package Db
*/
class Exception extends DbException
{
}

19
var/Typecho/Exception.php Executable file
View File

@@ -0,0 +1,19 @@
<?php
namespace Typecho;
/**
* Typecho异常基类
* 主要重载异常打印函数
*
* @package Exception
*/
class Exception extends \Exception
{
public function __construct($message, $code = 0)
{
$this->message = $message;
$this->code = $code;
}
}

412
var/Typecho/Feed.php Executable file
View File

@@ -0,0 +1,412 @@
<?php
namespace Typecho;
/**
* Feed
*
* @package Feed
*/
class Feed
{
/** 定义RSS 1.0类型 */
public const RSS1 = 'RSS 1.0';
/** 定义RSS 2.0类型 */
public const RSS2 = 'RSS 2.0';
/** 定义ATOM 1.0类型 */
public const ATOM1 = 'ATOM 1.0';
/** 定义RSS时间格式 */
public const DATE_RFC822 = 'r';
/** 定义ATOM时间格式 */
public const DATE_W3CDTF = 'c';
/** 定义行结束符 */
public const EOL = "\n";
/**
* feed状态
*
* @access private
* @var string
*/
private string $type;
/**
* 字符集编码
*
* @access private
* @var string
*/
private string $charset;
/**
* 语言状态
*
* @access private
* @var string
*/
private string $lang;
/**
* 聚合地址
*
* @access private
* @var string
*/
private string $feedUrl;
/**
* 基本地址
*
* @access private
* @var string
*/
private string $baseUrl;
/**
* 聚合标题
*
* @access private
* @var string
*/
private string $title;
/**
* 聚合副标题
*
* @access private
* @var string|null
*/
private ?string $subTitle;
/**
* 版本信息
*
* @access private
* @var string
*/
private string $version;
/**
* 所有的items
*
* @access private
* @var array
*/
private array $items = [];
/**
* 创建Feed对象
*
* @param $version
* @param string $type
* @param string $charset
* @param string $lang
*/
public function __construct($version, string $type = self::RSS2, string $charset = 'UTF-8', string $lang = 'en')
{
$this->version = $version;
$this->type = $type;
$this->charset = $charset;
$this->lang = $lang;
}
/**
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* 设置标题
*
* @param string $title 标题
*/
public function setTitle(string $title)
{
$this->title = $title;
}
/**
* 设置副标题
*
* @param string|null $subTitle 副标题
*/
public function setSubTitle(?string $subTitle)
{
$this->subTitle = $subTitle;
}
/**
* 设置聚合地址
*
* @param string $feedUrl 聚合地址
*/
public function setFeedUrl(string $feedUrl)
{
$this->feedUrl = $feedUrl;
}
/**
* @return string
*/
public function getFeedUrl(): string
{
return $this->feedUrl;
}
/**
* 设置主页
*
* @param string $baseUrl 主页地址
*/
public function setBaseUrl(string $baseUrl)
{
$this->baseUrl = $baseUrl;
}
/**
* $item的格式为
* <code>
* array (
* 'title' => 'xxx',
* 'content' => 'xxx',
* 'excerpt' => 'xxx',
* 'date' => 'xxx',
* 'link' => 'xxx',
* 'author' => 'xxx',
* 'comments' => 'xxx',
* 'commentsUrl'=> 'xxx',
* 'commentsFeedUrl' => 'xxx',
* )
* </code>
*
* @param array $item
*/
public function addItem(array $item)
{
$this->items[] = $item;
}
/**
* 输出字符串
*
* @return string
*/
public function __toString(): string
{
$result = '<?xml version="1.0" encoding="' . $this->charset . '"?>' . self::EOL;
if (self::RSS1 == $this->type) {
$result .= '<rdf:RDF
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://purl.org/rss/1.0/"
xmlns:dc="http://purl.org/dc/elements/1.1/">' . self::EOL;
$content = '';
$links = [];
$lastUpdate = 0;
foreach ($this->items as $item) {
$content .= '<item rdf:about="' . $item['link'] . '">' . self::EOL;
$content .= '<title>' . htmlspecialchars($item['title']) . '</title>' . self::EOL;
$content .= '<link>' . $item['link'] . '</link>' . self::EOL;
$content .= '<dc:date>' . $this->dateFormat($item['date']) . '</dc:date>' . self::EOL;
$content .= '<description>' . strip_tags($item['content']) . '</description>' . self::EOL;
if (!empty($item['suffix'])) {
$content .= $item['suffix'];
}
$content .= '</item>' . self::EOL;
$links[] = $item['link'];
if ($item['date'] > $lastUpdate) {
$lastUpdate = $item['date'];
}
}
$result .= '<channel rdf:about="' . $this->feedUrl . '">
<title>' . htmlspecialchars($this->title) . '</title>
<link>' . $this->baseUrl . '</link>
<description>' . htmlspecialchars($this->subTitle ?? '') . '</description>
<items>
<rdf:Seq>' . self::EOL;
foreach ($links as $link) {
$result .= '<rdf:li resource="' . $link . '"/>' . self::EOL;
}
$result .= '</rdf:Seq>
</items>
</channel>' . self::EOL;
$result .= $content . '</rdf:RDF>';
} elseif (self::RSS2 == $this->type) {
$result .= '<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:wfw="http://wellformedweb.org/CommentAPI/">
<channel>' . self::EOL;
$content = '';
$lastUpdate = 0;
foreach ($this->items as $item) {
$content .= '<item>' . self::EOL;
$content .= '<title>' . htmlspecialchars($item['title']) . '</title>' . self::EOL;
$content .= '<link>' . $item['link'] . '</link>' . self::EOL;
$content .= '<guid>' . $item['link'] . '</guid>' . self::EOL;
$content .= '<pubDate>' . $this->dateFormat($item['date']) . '</pubDate>' . self::EOL;
$content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName)
. '</dc:creator>' . self::EOL;
if (!empty($item['category']) && is_array($item['category'])) {
foreach ($item['category'] as $category) {
$content .= '<category><![CDATA[' . $category['name'] . ']]></category>' . self::EOL;
}
}
if (!empty($item['excerpt'])) {
$content .= '<description><![CDATA[' . strip_tags($item['excerpt'])
. ']]></description>' . self::EOL;
}
if (!empty($item['content'])) {
$content .= '<content:encoded xml:lang="' . $this->lang . '"><![CDATA['
. self::EOL .
$item['content'] . self::EOL .
']]></content:encoded>' . self::EOL;
}
if (isset($item['comments']) && strlen($item['comments']) > 0) {
$content .= '<slash:comments>' . $item['comments'] . '</slash:comments>' . self::EOL;
}
$content .= '<comments>' . $item['link'] . '#comments</comments>' . self::EOL;
if (!empty($item['commentsFeedUrl'])) {
$content .= '<wfw:commentRss>' . $item['commentsFeedUrl'] . '</wfw:commentRss>' . self::EOL;
}
if (!empty($item['suffix'])) {
$content .= $item['suffix'];
}
$content .= '</item>' . self::EOL;
if ($item['date'] > $lastUpdate) {
$lastUpdate = $item['date'];
}
}
$result .= '<title>' . htmlspecialchars($this->title) . '</title>
<link>' . $this->baseUrl . '</link>
<atom:link href="' . $this->feedUrl . '" rel="self" type="application/rss+xml" />
<language>' . $this->lang . '</language>
<description>' . htmlspecialchars($this->subTitle ?? '') . '</description>
<lastBuildDate>' . $this->dateFormat($lastUpdate) . '</lastBuildDate>
<pubDate>' . $this->dateFormat($lastUpdate) . '</pubDate>' . self::EOL;
$result .= $content . '</channel>
</rss>';
} elseif (self::ATOM1 == $this->type) {
$result .= '<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:thr="http://purl.org/syndication/thread/1.0"
xml:lang="' . $this->lang . '"
xml:base="' . $this->baseUrl . '"
>' . self::EOL;
$content = '';
$lastUpdate = 0;
foreach ($this->items as $item) {
$content .= '<entry>' . self::EOL;
$content .= '<title type="html"><![CDATA[' . $item['title'] . ']]></title>' . self::EOL;
$content .= '<link rel="alternate" type="text/html" href="' . $item['link'] . '" />' . self::EOL;
$content .= '<id>' . $item['link'] . '</id>' . self::EOL;
$content .= '<updated>' . $this->dateFormat($item['date']) . '</updated>' . self::EOL;
$content .= '<published>' . $this->dateFormat($item['date']) . '</published>' . self::EOL;
$content .= '<author>
<name>' . $item['author']->screenName . '</name>
<uri>' . $item['author']->url . '</uri>
</author>' . self::EOL;
if (!empty($item['category']) && is_array($item['category'])) {
foreach ($item['category'] as $category) {
$content .= '<category scheme="' . $category['permalink'] . '" term="'
. $category['name'] . '" />' . self::EOL;
}
}
if (!empty($item['excerpt'])) {
$content .= '<summary type="html"><![CDATA[' . htmlspecialchars($item['excerpt'])
. ']]></summary>' . self::EOL;
}
if (!empty($item['content'])) {
$content .= '<content type="html" xml:base="' . $item['link']
. '" xml:lang="' . $this->lang . '"><![CDATA['
. self::EOL .
$item['content'] . self::EOL .
']]></content>' . self::EOL;
}
if (isset($item['comments']) && strlen($item['comments']) > 0) {
$content .= '<link rel="replies" type="text/html" href="' . $item['link']
. '#comments" thr:count="' . $item['comments'] . '" />' . self::EOL;
if (!empty($item['commentsFeedUrl'])) {
$content .= '<link rel="replies" type="application/atom+xml" href="'
. $item['commentsFeedUrl'] . '" thr:count="' . $item['comments'] . '"/>' . self::EOL;
}
}
if (!empty($item['suffix'])) {
$content .= $item['suffix'];
}
$content .= '</entry>' . self::EOL;
if ($item['date'] > $lastUpdate) {
$lastUpdate = $item['date'];
}
}
$result .= '<title type="text">' . htmlspecialchars($this->title) . '</title>
<subtitle type="text">' . htmlspecialchars($this->subTitle ?? '') . '</subtitle>
<updated>' . $this->dateFormat($lastUpdate) . '</updated>
<generator uri="https://typecho.org/" version="' . $this->version . '">Typecho</generator>
<link rel="alternate" type="text/html" href="' . $this->baseUrl . '" />
<id>' . $this->feedUrl . '</id>
<link rel="self" type="application/atom+xml" href="' . $this->feedUrl . '" />
';
$result .= $content . '</feed>';
}
return $result;
}
/**
* 获取Feed时间格式
*
* @param integer $stamp 时间戳
* @return string
*/
public function dateFormat(int $stamp): string
{
if (self::RSS2 == $this->type) {
return date(self::DATE_RFC822, $stamp);
} elseif (self::RSS1 == $this->type || self::ATOM1 == $this->type) {
return date(self::DATE_W3CDTF, $stamp);
}
return '';
}
}

398
var/Typecho/Http/Client.php Executable file
View File

@@ -0,0 +1,398 @@
<?php
namespace Typecho\Http;
use Typecho\Common;
use Typecho\Http\Client\Exception;
/**
* Http客户端
*
* @category typecho
* @package Http
*/
class Client
{
/** POST方法 */
public const METHOD_POST = 'POST';
/** GET方法 */
public const METHOD_GET = 'GET';
/** PUT方法 */
public const METHOD_PUT = 'PUT';
/** DELETE方法 */
public const METHOD_DELETE = 'DELETE';
/**
* 方法名
*
* @var string
*/
private string $method = self::METHOD_GET;
/**
* User-Agent
*
* @var string
*/
public string $agent;
/**
* 传递参数
*
* @var string
*/
private string $query;
/**
* 设置超时
*
* @var integer
*/
private int $timeout = 3;
/**
* @var bool
*/
private bool $multipart = true;
/**
* 需要在body中传递的值
*
* @var array|string
*/
private $data = [];
/**
* 头信息参数
*
* @access private
* @var array
*/
private array $headers = [];
/**
* cookies
*
* @var array
*/
private array $cookies = [];
/**
* @var array
*/
private array $options = [];
/**
* 回执头部信息
*
* @var array
*/
private array $responseHeader = [];
/**
* 回执代码
*
* @var integer
*/
private int $responseStatus;
/**
* 回执身体
*
* @var string
*/
private string $responseBody;
/**
* 设置指定的COOKIE值
*
* @param string $key 指定的参数
* @param mixed $value 设置的值
* @return $this
*/
public function setCookie(string $key, $value): Client
{
$this->cookies[$key] = $value;
$this->setHeader('Cookie', str_replace('&', '; ', http_build_query($this->cookies)));
return $this;
}
/**
* 设置传递参数
*
* @param mixed $query 传递参数
* @return $this
*/
public function setQuery($query): Client
{
$query = is_array($query) ? http_build_query($query) : $query;
$this->query = empty($this->query) ? $query : $this->query . '&' . $query;
return $this;
}
/**
* 设置需要POST的数据
*
* @param array|string $data 需要POST的数据
* @param string $method
* @return $this
*/
public function setData($data, string $method = self::METHOD_POST): Client
{
if (is_array($data) && is_array($this->data)) {
$this->data = array_merge($this->data, $data);
} else {
$this->data = $data;
}
$this->setMethod($method);
return $this;
}
/**
* 设置需要请求的Json数据
*
* @param $data
* @param string $method
* @return $this
*/
public function setJson($data, string $method = self::METHOD_POST): Client
{
$this->setData(json_encode($data), $method)
->setMultipart(true)
->setHeader('Content-Type', 'application/json');
return $this;
}
/**
* 设置方法名
*
* @param string $method
* @return $this
*/
public function setMethod(string $method): Client
{
$this->method = $method;
return $this;
}
/**
* 设置需要POST的文件
*
* @param array $files 需要POST的文件
* @param string $method
* @return $this
*/
public function setFiles(array $files, string $method = self::METHOD_POST): Client
{
if (is_array($this->data)) {
foreach ($files as $name => $file) {
$this->data[$name] = new \CURLFile($file);
}
}
$this->setMethod($method);
return $this;
}
/**
* 设置超时时间
*
* @param integer $timeout 超时时间
* @return $this
*/
public function setTimeout(int $timeout): Client
{
$this->timeout = $timeout;
return $this;
}
/**
* setAgent
*
* @param string $agent
* @return $this
*/
public function setAgent(string $agent): Client
{
$this->agent = $agent;
return $this;
}
/**
* @param bool $multipart
* @return $this
*/
public function setMultipart(bool $multipart): Client
{
$this->multipart = $multipart;
return $this;
}
/**
* @param int $key
* @param mixed $value
* @return $this
*/
public function setOption(int $key, $value): Client
{
$this->options[$key] = $value;
return $this;
}
/**
* 设置头信息参数
*
* @param string $key 参数名称
* @param string $value 参数值
* @return $this
*/
public function setHeader(string $key, string $value): Client
{
$key = str_replace(' ', '-', ucwords(str_replace('-', ' ', $key)));
if ($key == 'User-Agent') {
$this->setAgent($value);
} else {
$this->headers[$key] = $value;
}
return $this;
}
/**
* 发送请求
*
* @param string $url 请求地址
* @throws Exception
*/
public function send(string $url)
{
$params = parse_url($url);
$query = empty($params['query']) ? '' : $params['query'];
if (!empty($this->query)) {
$query = empty($query) ? $this->query : '&' . $this->query;
}
if (!empty($query)) {
$params['query'] = $query;
}
$url = Common::buildUrl($params);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLINFO_HEADER_OUT, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FRESH_CONNECT, true);
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->method);
if (isset($this->agent)) {
curl_setopt($ch, CURLOPT_USERAGENT, $this->agent);
}
/** 设置header信息 */
if (!empty($this->headers)) {
$headers = [];
foreach ($this->headers as $key => $val) {
$headers[] = $key . ': ' . $val;
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
}
if (!empty($this->data)) {
$data = $this->data;
if (!$this->multipart) {
curl_setopt($ch, CURLOPT_POST, true);
$data = is_array($data) ? http_build_query($data) : $data;
}
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
}
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($ch, $header) {
$parts = explode(':', $header, 2);
if (count($parts) == 2) {
[$key, $value] = $parts;
$this->responseHeader[strtolower(trim($key))] = trim($value);
}
return strlen($header);
});
foreach ($this->options as $key => $val) {
curl_setopt($ch, $key, $val);
}
$response = curl_exec($ch);
if (false === $response) {
$error = curl_error($ch);
if (PHP_VERSION_ID >= 80000) {
unset($ch);
} else {
curl_close($ch);
}
throw new Exception($error, 500);
}
$this->responseStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$this->responseBody = $response;
if (PHP_VERSION_ID >= 80000) {
unset($ch);
} else {
curl_close($ch);
}
}
/**
* 获取回执的头部信息
*
* @param string $key 头信息名称
* @return ?string
*/
public function getResponseHeader(string $key): ?string
{
$key = strtolower($key);
return $this->responseHeader[$key] ?? null;
}
/**
* 获取回执代码
*
* @return integer
*/
public function getResponseStatus(): int
{
return $this->responseStatus;
}
/**
* 获取回执身体
*
* @return string
*/
public function getResponseBody(): string
{
return $this->responseBody;
}
/**
* 获取可用的连接
*
* @return ?Client
*/
public static function get(): ?Client
{
return extension_loaded('curl') ? new static() : null;
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Typecho\Http\Client;
use Typecho\Exception as TypechoException;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* Http客户端异常类
*
* @package Http
*/
class Exception extends TypechoException
{
}

169
var/Typecho/I18n.php Executable file
View File

@@ -0,0 +1,169 @@
<?php
namespace Typecho;
use Typecho\I18n\GetTextMulti;
/**
* 国际化字符翻译
*
* @package I18n
*/
class I18n
{
/**
* 是否已经载入的标志位
*
* @access private
* @var GetTextMulti|null
*/
private static ?GetTextMulti $loaded = null;
/**
* 语言文件
*
* @access private
* @var string|null
*/
private static ?string $lang = null;
/**
* 翻译文字
*
* @access public
*
* @param string $string 待翻译的文字
*
* @return string
*/
public static function translate(string $string): string
{
self::init();
return self::$loaded ? self::$loaded->translate($string) : $string;
}
/**
* 初始化语言文件
*
* @access private
*/
private static function init()
{
/** GetText支持 */
if (!isset(self::$loaded) && self::$lang && file_exists(self::$lang)) {
self::$loaded = new GetTextMulti(self::$lang);
}
}
/**
* 针对复数形式的翻译函数
*
* @param string $single 单数形式的翻译
* @param string $plural 复数形式的翻译
* @param integer $number 数字
* @return string
*/
public static function ngettext(string $single, string $plural, int $number): string
{
self::init();
return self::$loaded ? self::$loaded->ngettext($single, $plural, $number) : ($number > 1 ? $plural : $single);
}
/**
* 词义化时间
*
* @access public
*
* @param int $from 起始时间
* @param int $now 终止时间
*
* @return string
*/
public static function dateWord(int $from, int $now): string
{
$between = $now - $from;
/** 如果是一天 */
if ($between >= 0 && $between < 86400 && date('d', $from) == date('d', $now)) {
/** 如果是一小时 */
if ($between < 3600) {
/** 如果是一分钟 */
if ($between < 60) {
if (0 == $between) {
return _t('刚刚');
} else {
return str_replace('%d', $between, _n('一秒前', '%d秒前', $between));
}
}
$min = floor($between / 60);
return str_replace('%d', $min, _n('一分钟前', '%d分钟前', $min));
}
$hour = floor($between / 3600);
return str_replace('%d', $hour, _n('一小时前', '%d小时前', $hour));
}
/** 如果是昨天 */
if (
$between > 0
&& $between < 172800
&& (date('z', $from) + 1 == date('z', $now) // 在同一年的情况
|| date('z', $from) + 1 == date('L') + 365 + date('z', $now))
) { // 跨年的情况
return _t('昨天 %s', date('H:i', $from));
}
/** 如果是一个星期 */
if ($between > 0 && $between < 604800) {
$day = floor($between / 86400);
return str_replace('%d', $day, _n('一天前', '%d天前', $day));
}
/** 如果是 */
if (date('Y', $from) == date('Y', $now)) {
return date(_t('n月j日'), $from);
}
return date(_t('Y年m月d日'), $from);
}
/**
* 增加语言项
*
* @access public
*
* @param string $lang 语言名称
*
* @return void
*/
public static function addLang(string $lang)
{
self::$loaded->addFile($lang);
}
/**
* 获取语言项
*
* @access public
* @return string
*/
public static function getLang(): ?string
{
return self::$lang;
}
/**
* 设置语言项
*
* @access public
*
* @param string $lang 配置信息
*
* @return void
*/
public static function setLang(string $lang)
{
self::$lang = $lang;
}
}

407
var/Typecho/I18n/GetText.php Executable file
View File

@@ -0,0 +1,407 @@
<?php
namespace Typecho\I18n;
/*
Copyright (c) 2003 Danilo Segan <danilo@kvota.net>.
Copyright (c) 2005 Nico Kaiser <nico@siriux.net>
This file is part of PHP-gettext.
PHP-gettext is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
PHP-gettext is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with PHP-gettext; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
/**
* This file is part of PHP-gettext
*
* @author Danilo Segan <danilo@kvota.net>, Nico Kaiser <nico@siriux.net>
* @category typecho
* @package I18n
*/
class GetText
{
//public:
public int $error = 0; // public variable that holds error code (0 if no error)
//private:
private int $BYTE_ORDER = 0; // 0: low endian, 1: big endian
private $STREAM = null;
private bool $short_circuit = false;
private bool $enable_cache = false;
private ?int $originals = null; // offset of original table
private ?int $translations = null; // offset of translation table
private ?string $pluralHeader = null; // cache header field for plural forms
private int $total = 0; // total string count
private ?array $table_originals = null; // table for original strings (offsets)
private ?array $table_translations = null; // table for translated strings (offsets)
private ?array $cache_translations = null; // original -> translation mapping
/* Methods */
/**
* Constructor
*
* @param string $file file name
* @param boolean $enable_cache Enable or disable caching of strings (default on)
*/
public function __construct(string $file, bool $enable_cache = true)
{
// If there isn't a StreamReader, turn on short circuit mode.
if (!file_exists($file)) {
$this->short_circuit = true;
return;
}
// Caching can be turned off
$this->enable_cache = $enable_cache;
$this->STREAM = @fopen($file, 'rb');
$unpacked = unpack('c', $this->read(4));
$magic = array_shift($unpacked);
if (-34 == $magic) {
$this->BYTE_ORDER = 0;
} elseif (-107 == $magic) {
$this->BYTE_ORDER = 1;
} else {
$this->error = 1; // not MO file
return;
}
// FIXME: Do we care about revision? We should.
$revision = $this->readInt();
$this->total = $this->readInt();
$this->originals = $this->readInt();
$this->translations = $this->readInt();
}
/**
* Translates a string
*
* @access public
* @param string $string to be translated
* @param integer|null $num found string number
* @return string translated string (or original, if not found)
*/
public function translate($string, ?int &$num): string
{
if ($this->short_circuit) {
return $string;
}
$this->loadTables();
if ($this->enable_cache) {
// Caching enabled, get translated string from cache
if (array_key_exists($string, $this->cache_translations)) {
return $this->cache_translations[$string];
} else {
return $string;
}
} else {
// Caching not enabled, try to find string
$num = $this->findString($string);
if ($num == -1) {
return $string;
} else {
return $this->getTranslationString($num);
}
}
}
/**
* Plural version of gettext
*
* @access public
* @param string single
* @param string plural
* @param string number
* @param integer|null $num found string number
* @return string plural form
*/
public function ngettext($single, $plural, $number, ?int &$num): string
{
$number = intval($number);
if ($this->short_circuit) {
if ($number != 1) {
return $plural;
} else {
return $single;
}
}
// find out the appropriate form
$select = $this->selectString($number);
// this should contains all strings separated by NULLs
$key = $single . chr(0) . $plural;
if ($this->enable_cache) {
if (!array_key_exists($key, $this->cache_translations)) {
return ($number != 1) ? $plural : $single;
} else {
$result = $this->cache_translations[$key];
$list = explode(chr(0), $result);
return $list[$select] ?? '';
}
} else {
$num = $this->findString($key);
if ($num == -1) {
return ($number != 1) ? $plural : $single;
} else {
$result = $this->getTranslationString($num);
$list = explode(chr(0), $result);
return $list[$select] ?? '';
}
}
}
/**
* 关闭文件句柄
*
* @access public
* @return void
*/
public function __destruct()
{
fclose($this->STREAM);
}
/**
* read
*
* @param mixed $count
* @access private
* @return false|string
*/
private function read($count)
{
$count = abs($count);
if ($count > 0) {
return fread($this->STREAM, $count);
}
return false;
}
/**
* Reads a 32bit Integer from the Stream
*
* @access private
* @return Integer from the Stream
*/
private function readInt(): int
{
$end = unpack($this->BYTE_ORDER == 0 ? 'V' : 'N', $this->read(4));
return array_shift($end);
}
/**
* Loads the translation tables from the MO file into the cache
* If caching is enabled, also loads all strings into a cache
* to speed up translation lookups
*
* @access private
*/
private function loadTables()
{
if (
is_array($this->cache_translations) &&
is_array($this->table_originals) &&
is_array($this->table_translations)
) {
return;
}
/* get original and translations tables */
fseek($this->STREAM, $this->originals);
$this->table_originals = $this->readIntArray($this->total * 2);
fseek($this->STREAM, $this->translations);
$this->table_translations = $this->readIntArray($this->total * 2);
if ($this->enable_cache) {
$this->cache_translations = ['' => null];
/* read all strings in the cache */
for ($i = 0; $i < $this->total; $i++) {
if ($this->table_originals[$i * 2 + 1] > 0) {
fseek($this->STREAM, $this->table_originals[$i * 2 + 2]);
$original = fread($this->STREAM, $this->table_originals[$i * 2 + 1]);
fseek($this->STREAM, $this->table_translations[$i * 2 + 2]);
$translation = fread($this->STREAM, $this->table_translations[$i * 2 + 1]);
$this->cache_translations[$original] = $translation;
}
}
}
}
/**
* Reads an array of Integers from the Stream
*
* @param int $count How many elements should be read
* @return array of Integers
*/
private function readIntArray(int $count): array
{
return unpack(($this->BYTE_ORDER == 0 ? 'V' : 'N') . $count, $this->read(4 * $count));
}
/**
* Binary search for string
*
* @access private
* @param string $string
* @param int $start (internally used in recursive function)
* @param int $end (internally used in recursive function)
* @return int string number (offset in originals table)
*/
private function findString(string $string, int $start = -1, int $end = -1): int
{
if (($start == -1) or ($end == -1)) {
// findString is called with only one parameter, set start end end
$start = 0;
$end = $this->total;
}
if (abs($start - $end) <= 1) {
// We're done, now we either found the string, or it doesn't exist
$txt = $this->getOriginalString($start);
if ($string == $txt) {
return $start;
} else {
return -1;
}
} elseif ($start > $end) {
// start > end -> turn around and start over
return $this->findString($string, $end, $start);
} else {
// Divide table in two parts
$half = (int)(($start + $end) / 2);
$cmp = strcmp($string, $this->getOriginalString($half));
if ($cmp == 0) {
// string is exactly in the middle => return it
return $half;
} elseif ($cmp < 0) {
// The string is in the upper half
return $this->findString($string, $start, $half);
} else { // The string is in the lower half
return $this->findString($string, $half, $end);
}
}
}
/**
* Returns a string from the "originals" table
*
* @access private
* @param int $num Offset number of original string
* @return string Requested string if found, otherwise ''
*/
private function getOriginalString(int $num): string
{
$length = $this->table_originals[$num * 2 + 1];
$offset = $this->table_originals[$num * 2 + 2];
if (!$length) {
return '';
}
fseek($this->STREAM, $offset);
$data = fread($this->STREAM, $length);
return (string)$data;
}
/**
* Returns a string from the "translations" table
*
* @access private
* @param int $num Offset number of original string
* @return string Requested string if found, otherwise ''
*/
private function getTranslationString(int $num): string
{
$length = $this->table_translations[$num * 2 + 1];
$offset = $this->table_translations[$num * 2 + 2];
if (!$length) {
return '';
}
fseek($this->STREAM, $offset);
$data = fread($this->STREAM, $length);
return (string)$data;
}
/**
* Detects which plural form to take
*
* @param int $n count
* @return int array index of the right plural form
*/
private function selectString(int $n): int
{
$string = $this->getPluralForms();
$string = str_replace('nplurals', "\$total", $string);
$string = str_replace("n", $n, $string);
$string = str_replace('plural', "\$plural", $string);
$total = 0;
$plural = 0;
eval("$string");
if ($plural >= $total) {
$plural = $total - 1;
}
return $plural;
}
/**
* Get possible plural forms from MO header
*
* @access private
* @return string plural form header
*/
private function getPluralForms(): string
{
// lets assume message number 0 is header
// this is true, right?
$this->loadTables();
// cache header field for plural forms
if (!is_string($this->pluralHeader)) {
if ($this->enable_cache) {
$header = $this->cache_translations[""];
} else {
$header = $this->getTranslationString(0);
}
if (!is_null($header) && preg_match("/plural\-forms: ([^\n]*)\n/i", $header, $regs)) {
$expr = $regs[1];
} else {
$expr = "nplurals=2; plural=n == 1 ? 0 : 1;";
}
$this->pluralHeader = $expr;
}
return $this->pluralHeader;
}
}

102
var/Typecho/I18n/GetTextMulti.php Executable file
View File

@@ -0,0 +1,102 @@
<?php
namespace Typecho\I18n;
/**
* 用于解决一个多个mo文件带来的读写问题
* 我们重写了一个文件读取类
*
* @author qining
* @category typecho
* @package I18n
*/
class GetTextMulti
{
/**
* 所有的文件读写句柄
*
* @access private
* @var GetText[]
*/
private array $handlers = [];
/**
* 构造函数
*
* @access public
* @param string $fileName 语言文件名
* @return void
*/
public function __construct(string $fileName)
{
$this->addFile($fileName);
}
/**
* 增加一个语言文件
*
* @access public
* @param string $fileName 语言文件名
* @return void
*/
public function addFile(string $fileName)
{
$this->handlers[] = new GetText($fileName, true);
}
/**
* Translates a string
*
* @access public
* @param string $string string to be translated
* @return string translated string (or original, if not found)
*/
public function translate(string $string): string
{
foreach ($this->handlers as $handle) {
$string = $handle->translate($string, $count);
if (- 1 != $count) {
break;
}
}
return $string;
}
/**
* Plural version of gettext
*
* @access public
* @param string $single single
* @param string $plural plural
* @param int $number number
* @return string translated plural form
*/
public function ngettext(string $single, string $plural, int $number): string
{
$count = - 1;
foreach ($this->handlers as $handler) {
$string = $handler->ngettext($single, $plural, $number, $count);
if (- 1 != $count) {
break;
}
}
return $string;
}
/**
* 关闭所有句柄
*
* @access public
* @return void
*/
public function __destruct()
{
foreach ($this->handlers as $handler) {
/** 显示的释放内存 */
unset($handler);
}
}
}

491
var/Typecho/Plugin.php Executable file
View File

@@ -0,0 +1,491 @@
<?php
namespace Typecho;
use Typecho\Plugin\Exception as PluginException;
/**
* 插件处理类
*
* @category typecho
* @package Plugin
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Plugin
{
/**
* 所有启用的插件
*
* @var array
*/
private static array $plugin = [];
/**
* 实例化的插件对象
*
* @var array
*/
private static array $instances;
/**
* 临时存储变量
*
* @var array
*/
private static array $tmp = [];
/**
* 唯一句柄
*
* @var string
*/
private string $handle;
/**
* 组件
*
* @var string
*/
private $component;
/**
* 是否触发插件的信号
*
* @var boolean
*/
private bool $signal = false;
/**
* 插件初始化
*
* @param string $handle 插件
*/
public function __construct(string $handle)
{
if (defined('__TYPECHO_CLASS_ALIASES__')) {
$alias = array_search('\\' . ltrim($handle, '\\'), __TYPECHO_CLASS_ALIASES__);
$handle = $alias ?: $handle;
}
$this->handle = Common::nativeClassName($handle);
}
/**
* 插件初始化
*
* @param array $plugins 插件列表
*/
public static function init(array $plugins)
{
$plugins['activated'] = array_key_exists('activated', $plugins) ? $plugins['activated'] : [];
$plugins['handles'] = array_key_exists('handles', $plugins) ? $plugins['handles'] : [];
/** 初始化变量 */
self::$plugin = $plugins;
}
/**
* 获取实例化插件对象
*
* @param string $handle 插件
* @return Plugin
*/
public static function factory(string $handle): Plugin
{
return self::$instances[$handle] ?? (self::$instances[$handle] = new self($handle));
}
/**
* 启用插件
*
* @param string $pluginName 插件名称
*/
public static function activate(string $pluginName)
{
self::$plugin['activated'][$pluginName] = self::$tmp;
self::$tmp = [];
}
/**
* 禁用插件
*
* @param string $pluginName 插件名称
*/
public static function deactivate(string $pluginName)
{
/** 去掉所有相关回调函数 */
if (
isset(self::$plugin['activated'][$pluginName]['handles'])
&& is_array(self::$plugin['activated'][$pluginName]['handles'])
) {
foreach (self::$plugin['activated'][$pluginName]['handles'] as $handle => $handles) {
self::$plugin['handles'][$handle] = self::pluginHandlesDiff(
empty(self::$plugin['handles'][$handle]) ? [] : self::$plugin['handles'][$handle],
empty($handles) ? [] : $handles
);
if (empty(self::$plugin['handles'][$handle])) {
unset(self::$plugin['handles'][$handle]);
}
}
}
/** 禁用当前插件 */
unset(self::$plugin['activated'][$pluginName]);
}
/**
* 插件handle比对
*
* @param array $pluginHandles
* @param array $otherPluginHandles
* @return array
*/
private static function pluginHandlesDiff(array $pluginHandles, array $otherPluginHandles): array
{
foreach ($otherPluginHandles as $handle) {
while (false !== ($index = array_search($handle, $pluginHandles))) {
unset($pluginHandles[$index]);
}
}
return $pluginHandles;
}
/**
* 导出当前插件设置
*
* @return array
*/
public static function export(): array
{
return self::$plugin;
}
/**
* 获取插件文件的头信息
*
* @param string $pluginFile 插件文件路径
* @return array
*/
public static function parseInfo(string $pluginFile): array
{
$tokens = token_get_all(file_get_contents($pluginFile));
$isDoc = false;
$isFunction = false;
$isClass = false;
$isInClass = false;
$isInFunction = false;
$isDefined = false;
$current = null;
/** 初始信息 */
$info = [
'description' => '',
'title' => '',
'author' => '',
'homepage' => '',
'version' => '',
'since' => '',
'activate' => false,
'deactivate' => false,
'config' => false,
'personalConfig' => false
];
$map = [
'package' => 'title',
'author' => 'author',
'link' => 'homepage',
'since' => 'since',
'version' => 'version'
];
foreach ($tokens as $token) {
/** 获取doc comment */
if (!$isDoc && is_array($token) && T_DOC_COMMENT == $token[0]) {
/** 分行读取 */
$described = false;
$lines = preg_split("([\r\n])", $token[1]);
foreach ($lines as $line) {
$line = trim($line);
if (!empty($line) && '*' == $line[0]) {
$line = trim(substr($line, 1));
if (!$described && !empty($line) && '@' == $line[0]) {
$described = true;
}
if (!$described && !empty($line)) {
$info['description'] .= $line . "\n";
} elseif ($described && !empty($line) && '@' == $line[0]) {
$info['description'] = trim($info['description']);
$line = trim(substr($line, 1));
$args = explode(' ', $line);
$key = array_shift($args);
if (isset($map[$key])) {
$info[$map[$key]] = trim(implode(' ', $args));
}
}
}
}
$isDoc = true;
}
if (is_array($token)) {
switch ($token[0]) {
case T_FUNCTION:
$isFunction = true;
break;
case T_IMPLEMENTS:
$isClass = true;
break;
case T_WHITESPACE:
case T_COMMENT:
case T_DOC_COMMENT:
break;
case T_STRING:
$string = strtolower($token[1]);
switch ($string) {
case 'typecho_plugin_interface':
case 'plugininterface':
$isInClass = $isClass;
break;
case 'activate':
case 'deactivate':
case 'config':
case 'personalconfig':
if ($isFunction) {
$current = ('personalconfig' == $string ? 'personalConfig' : $string);
}
break;
default:
if (!empty($current) && $isInFunction && $isInClass) {
$info[$current] = true;
}
break;
}
break;
default:
if (!empty($current) && $isInFunction && $isInClass) {
$info[$current] = true;
}
break;
}
} else {
$token = strtolower($token);
switch ($token) {
case '{':
if ($isDefined) {
$isInFunction = true;
}
break;
case '(':
if ($isFunction && !$isDefined) {
$isDefined = true;
}
break;
case '}':
case ';':
$isDefined = false;
$isFunction = false;
$isInFunction = false;
$current = null;
break;
default:
if (!empty($current) && $isInFunction && $isInClass) {
$info[$current] = true;
}
break;
}
}
}
return $info;
}
/**
* 获取插件路径和类名
* 返回值为一个数组
* 第一项为插件路径,第二项为类名
*
* @param string $pluginName 插件名
* @param string $path 插件目录
* @return array
* @throws PluginException
*/
public static function portal(string $pluginName, string $path): array
{
switch (true) {
case file_exists($pluginFileName = $path . '/' . $pluginName . '/Plugin.php'):
$className = "\\" . PLUGIN_NAMESPACE . "\\{$pluginName}\\Plugin";
break;
case file_exists($pluginFileName = $path . '/' . $pluginName . '.php'):
$className = "\\" . PLUGIN_NAMESPACE . "\\" . $pluginName;
break;
default:
throw new PluginException('Missing Plugin ' . $pluginName, 404);
}
return [$pluginFileName, $className];
}
/**
* 版本依赖性检测
*
* @param string|null $version 插件版本
* @return boolean
*/
public static function checkDependence(?string $version): bool
{
//如果没有检测规则,直接掠过
if (empty($version)) {
return true;
}
return version_compare(Common::VERSION, $version, '>=');
}
/**
* 判断插件是否存在
*
* @param string $pluginName 插件名称
* @return bool
*/
public static function exists(string $pluginName): bool
{
return array_key_exists($pluginName, self::$plugin['activated']);
}
/**
* 插件调用后的触发器
*
* @param boolean|null $signal 触发器
* @return Plugin
*/
public function trigger(?bool &$signal): Plugin
{
$signal = false;
$this->signal = &$signal;
return $this;
}
/**
* 通过魔术函数设置当前组件位置
*
* @param string $component 当前组件
* @return Plugin
*/
public function __get(string $component)
{
$this->component = $component;
return $this;
}
/**
* 设置回调函数
*
* @param string $component 当前组件
* @param callable $value 回调函数
*/
public function __set(string $component, callable $value)
{
$weight = 0;
if (strpos($component, '_') > 0) {
$parts = explode('_', $component, 2);
[$component, $weight] = $parts;
$weight = intval($weight) - 10;
}
$component = $this->handle . ':' . $component;
if (!isset(self::$plugin['handles'][$component])) {
self::$plugin['handles'][$component] = [];
}
if (!isset(self::$tmp['handles'][$component])) {
self::$tmp['handles'][$component] = [];
}
foreach (self::$plugin['handles'][$component] as $key => $val) {
$key = floatval($key);
if ($weight > $key) {
break;
} elseif ($weight == $key) {
$weight += 0.001;
}
}
self::$plugin['handles'][$component][strval($weight)] = $value;
self::$tmp['handles'][$component][] = $value;
ksort(self::$plugin['handles'][$component], SORT_NUMERIC);
}
/**
* 回调处理函数
*
* @param string $component 当前组件
* @param array $args 参数
* @return mixed
*/
public function call(string $component, ...$args)
{
$componentKey = $this->handle . ':' . $component;
if (!isset(self::$plugin['handles'][$componentKey])) {
return null;
}
$return = null;
$this->signal = true;
foreach (self::$plugin['handles'][$componentKey] as $callback) {
$return = call_user_func_array($callback, $args);
}
return $return;
}
/**
* 过滤处理函数
*
* @param string $component 当前组件
* @param mixed $value 值
* @param array $args 参数
* @return mixed
*/
public function filter(string $component, $value, ...$args)
{
$componentKey = $this->handle . ':' . $component;
if (!isset(self::$plugin['handles'][$componentKey])) {
return $value;
}
$result = $value;
$this->signal = true;
foreach (self::$plugin['handles'][$componentKey] as $callback) {
$currentArgs = array_merge([$result], $args, [$result]);
$result = call_user_func_array($callback, $currentArgs);
}
return $result;
}
/**
* @deprecated ^1.3.0
* @param string $component
* @param array $args
* @return false|mixed|null
*/
public function __call(string $component, array $args)
{
return $this->call($component, ... $args);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Typecho\Plugin;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
use Typecho\Exception as TypechoException;
/**
* Typecho Blog Platform
*
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
* @version $Id: WidgetException.php 46 2008-03-10 13:59:36Z magike.net $
*/
/**
* 插件异常
*
* @package Plugin
*/
class Exception extends TypechoException
{
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Typecho\Plugin;
use Typecho\Widget\Helper\Form;
/**
* 插件接口
*
* @package Plugin
* @abstract
*/
interface PluginInterface
{
/**
* 启用插件方法,如果启用失败,直接抛出异常
*
* @static
* @access public
* @return void
*/
public static function activate();
/**
* 禁用插件方法,如果禁用失败,直接抛出异常
*
* @static
* @access public
* @return void
*/
public static function deactivate();
/**
* 获取插件配置面板
*
* @param Form $form 配置面板
*/
public static function config(Form $form);
/**
* 个人用户的配置面板
*
* @param Form $form
*/
public static function personalConfig(Form $form);
}

687
var/Typecho/Request.php Executable file
View File

@@ -0,0 +1,687 @@
<?php
namespace Typecho;
/**
* 服务器请求处理类
*
* @package Request
*/
class Request
{
/**
* 单例句柄
*
* @access private
* @var Request
*/
private static Request $instance;
/**
* 沙箱参数
*
* @access private
* @var Config|null
*/
private ?Config $sandbox;
/**
* 用户参数
*
* @access private
* @var Config|null
*/
private ?Config $params;
/**
* 路径信息
*
* @access private
* @var string|null
*/
private ?string $pathInfo = null;
/**
* requestUri
*
* @var string|null
* @access private
*/
private ?string $requestUri = null;
/**
* requestRoot
*
* @var string|null
* @access private
*/
private ?string $requestRoot = null;
/**
* 获取baseurl
*
* @var string|null
* @access private
*/
private ?string $baseUrl = null;
/**
* 客户端ip地址
*
* @access private
* @var string|null
*/
private ?string $ip = null;
/**
* 域名前缀
*
* @var string|null
*/
private ?string $urlPrefix = null;
/**
* 获取单例句柄
*
* @access public
* @return Request
*/
public static function getInstance(): Request
{
if (!isset(self::$instance)) {
self::$instance = new self();
}
return self::$instance;
}
/**
* 初始化变量
*
* @return $this
*/
public function beginSandbox(Config $sandbox): Request
{
$this->sandbox = $sandbox;
return $this;
}
/**
* @return $this
*/
public function endSandbox(): Request
{
$this->sandbox = null;
return $this;
}
/**
* @param Config $params
* @return $this
*/
public function proxy(Config $params): Request
{
$this->params = $params;
return $this;
}
/**
* @return $this
*/
public function endProxy(): Request
{
if (isset($this->params)) {
$this->params = null;
}
return $this;
}
/**
* 获取实际传递参数
*
* @param string $key 指定参数
* @param mixed $default 默认参数 (default: NULL)
* @param bool|null $exists detect exists
* @return mixed
*/
public function get(string $key, $default = null, ?bool &$exists = true)
{
$value = null;
switch (true) {
case isset($this->params) && isset($this->params[$key]):
$value = $this->params[$key];
break;
case isset($this->sandbox):
if (isset($this->sandbox[$key])) {
$value = $this->sandbox[$key];
}
break;
case $key === '@json':
if ($this->isJson()) {
$body = file_get_contents('php://input');
if (false !== $body) {
$value = json_decode($body, true, 16);
$default = $default ?? $value;
}
}
break;
case isset($_GET[$key]):
$value = $_GET[$key];
break;
case isset($_POST[$key]):
$value = $_POST[$key];
break;
default:
break;
}
if (isset($value) && $value !== '') {
$exists = true;
if (is_array($default) == is_array($value)) {
return $value;
} else {
return $default;
}
} else {
$exists = false;
return $default;
}
}
/**
* 获取实际传递参数(magic)
*
* @deprecated ^1.3.0
* @param string $key 指定参数
* @return mixed
*/
public function __get(string $key)
{
return $this->get($key);
}
/**
* 判断参数是否存在
*
* @deprecated ^1.3.0
* @param string $key 指定参数
* @return boolean
*/
public function __isset(string $key)
{
$this->get($key, null, $exists);
return $exists;
}
/**
* 获取一个数组
*
* @param $key
* @return array
*/
public function getArray($key): array
{
$result = $this->get($key, [], $exists);
if (!empty($result) || !$exists) {
return $result;
}
return [$this->get($key)];
}
/**
* 从参数列表指定的值中获取http传递参数
*
* @param mixed $params 指定的参数
* @return array
*/
public function from($params): array
{
$result = [];
$args = is_array($params) ? $params : func_get_args();
foreach ($args as $arg) {
$result[$arg] = $this->get($arg);
}
return $result;
}
/**
* getRequestRoot
*
* @return string
*/
public function getRequestRoot(): string
{
if (null === $this->requestRoot) {
$root = rtrim($this->getUrlPrefix() . $this->getBaseUrl(), '/') . '/';
$pos = strrpos($root, '.php/');
if ($pos) {
$root = dirname(substr($root, 0, $pos));
}
$this->requestRoot = rtrim($root, '/');
}
return $this->requestRoot;
}
/**
* 获取当前请求url
*
* @return string
*/
public function getRequestUrl(): string
{
return $this->getUrlPrefix() . $this->getRequestUri();
}
/**
* 根据当前uri构造指定参数的uri
*
* @param mixed $parameter 指定的参数
* @return string
*/
public function makeUriByRequest($parameter = null): string
{
/** 初始化地址 */
$requestUri = $this->getRequestUrl();
$parts = parse_url($requestUri);
/** 初始化参数 */
if (is_string($parameter)) {
parse_str($parameter, $args);
} elseif (is_array($parameter)) {
$args = $parameter;
} else {
return $requestUri;
}
/** 构造query */
if (isset($parts['query'])) {
parse_str($parts['query'], $currentArgs);
$args = array_merge($currentArgs, $args);
}
$parts['query'] = http_build_query($args);
/** 返回地址 */
return Common::buildUrl($parts);
}
/**
* 获取当前pathinfo
*
* @return string
*/
public function getPathInfo(): ?string
{
/** 缓存信息 */
if (null !== $this->pathInfo) {
return $this->pathInfo;
}
//参考Zend Framework对pathinfo的处理, 更好的兼容性
$pathInfo = null;
//处理requestUri
$requestUri = $this->getRequestUri();
$finalBaseUrl = $this->getBaseUrl();
// Remove the query string from REQUEST_URI
if ($pos = strpos($requestUri, '?')) {
$requestUri = substr($requestUri, 0, $pos);
}
if (
(null !== $finalBaseUrl)
&& (false === ($pathInfo = substr($requestUri, strlen($finalBaseUrl))))
) {
// If substr() returns false then PATH_INFO is set to an empty string
$pathInfo = '/';
} elseif (null === $finalBaseUrl) {
$pathInfo = $requestUri;
}
if (!empty($pathInfo)) {
//针对iis的utf8编码做强制转换
$pathInfo = defined('__TYPECHO_PATHINFO_ENCODING__') ?
mb_convert_encoding($pathInfo, 'UTF-8', __TYPECHO_PATHINFO_ENCODING__) : $pathInfo;
} else {
$pathInfo = '/';
}
// fix issue 456
return ($this->pathInfo = '/' . ltrim(urldecode($pathInfo), '/'));
}
/**
* 获取请求的内容类型
*
* @return string|null
*/
public function getContentType(): ?string
{
return $this->getHeader('Content-Type');
}
/**
* 获取环境变量
*
* @param string $name 获取环境变量名
* @param string|null $default
* @return string|null
*/
public function getServer(string $name, ?string $default = null): ?string
{
return $_SERVER[$name] ?? $default;
}
/**
* 获取ip地址
*
* @return string
*/
public function getIp(): string
{
if (null === $this->ip) {
$header = defined('__TYPECHO_IP_SOURCE__') ? __TYPECHO_IP_SOURCE__ : 'X-Forwarded-For';
$ip = $this->getHeader($header, $this->getHeader('Client-Ip', $this->getServer('REMOTE_ADDR')));
if (!empty($ip)) {
[$ip] = array_map('trim', explode(',', $ip));
$ip = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6);
}
if (!empty($ip)) {
$this->ip = $ip;
} else {
$this->ip = 'unknown';
}
}
return $this->ip;
}
/**
* get header value
*
* @param string $key
* @param string|null $default
* @return string|null
*/
public function getHeader(string $key, ?string $default = null): ?string
{
$key = strtoupper(str_replace('-', '_', $key));
// Content-Type 和 Content-Length 这两个 header 还需要从不带 HTTP_ 的 key 尝试获取
if (in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH'])) {
$default = $this->getServer($key, $default);
}
return $this->getServer('HTTP_' . $key, $default);
}
/**
* 获取客户端
*
* @return string
*/
public function getAgent(): ?string
{
return $this->getHeader('User-Agent');
}
/**
* 获取客户端
*
* @return string|null
*/
public function getReferer(): ?string
{
return $this->getHeader('Referer');
}
/**
* 判断是否为https
*
* @return bool
*/
public function isSecure(): bool
{
return (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && !strcasecmp('https', $_SERVER['HTTP_X_FORWARDED_PROTO']))
|| (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && !strcasecmp('quic', $_SERVER['HTTP_X_FORWARDED_PROTO']))
|| (!empty($_SERVER['HTTP_X_FORWARDED_PORT']) && 443 == $_SERVER['HTTP_X_FORWARDED_PORT'])
|| (!empty($_SERVER['HTTPS']) && 'off' != strtolower($_SERVER['HTTPS']))
|| (!empty($_SERVER['SERVER_PORT']) && 443 == $_SERVER['SERVER_PORT'])
|| (defined('__TYPECHO_SECURE__') && __TYPECHO_SECURE__);
}
/**
* @return bool
*/
public function isCli(): bool
{
return php_sapi_name() == 'cli';
}
/**
* 判断是否为get方法
*
* @return boolean
*/
public function isGet(): bool
{
return 'GET' == $this->getServer('REQUEST_METHOD');
}
/**
* 判断是否为post方法
*
* @return boolean
*/
public function isPost(): bool
{
return 'POST' == $this->getServer('REQUEST_METHOD');
}
/**
* 判断是否为put方法
*
* @return boolean
*/
public function isPut(): bool
{
return 'PUT' == $this->getServer('REQUEST_METHOD');
}
/**
* 判断是否为ajax
*
* @return boolean
*/
public function isAjax(): bool
{
return 'XMLHttpRequest' == $this->getHeader('X-Requested-With');
}
/**
* 判断是否为Json请求
*
* @return bool
*/
public function isJson(): bool
{
return !!preg_match(
"/^\s*application\/json(;|$)/i",
$this->getContentType() ?? ''
);
}
/**
* 判断输入是否满足要求
*
* @param mixed $query 条件
* @return boolean
*/
public function is($query): bool
{
$validated = false;
/** 解析串 */
if (is_string($query)) {
parse_str($query, $params);
} elseif (is_array($query)) {
$params = $query;
}
/** 验证串 */
if (!empty($params)) {
$validated = true;
foreach ($params as $key => $val) {
$param = $this->get($key, null, $exists);
$validated = empty($val) ? $exists : ($val == $param);
if (!$validated) {
break;
}
}
}
return $validated;
}
/**
* 获取请求资源地址
*
* @return string|null
*/
public function getRequestUri(): ?string
{
if (!empty($this->requestUri)) {
return $this->requestUri;
}
//处理requestUri
$requestUri = '/';
if (isset($_SERVER['HTTP_X_REWRITE_URL'])) { // check this first so IIS will catch
$requestUri = $_SERVER['HTTP_X_REWRITE_URL'];
} elseif (
// IIS7 with URL Rewrite: make sure we get the unencoded url (double slash problem)
isset($_SERVER['IIS_WasUrlRewritten'])
&& $_SERVER['IIS_WasUrlRewritten'] == '1'
&& isset($_SERVER['UNENCODED_URL'])
&& $_SERVER['UNENCODED_URL'] != ''
) {
$requestUri = $_SERVER['UNENCODED_URL'];
} elseif (isset($_SERVER['REQUEST_URI'])) {
$requestUri = $_SERVER['REQUEST_URI'];
$parts = @parse_url($requestUri);
if (isset($_SERVER['HTTP_HOST']) && strstr($requestUri, $_SERVER['HTTP_HOST'])) {
if (false !== $parts) {
$requestUri = (empty($parts['path']) ? '' : $parts['path'])
. ((empty($parts['query'])) ? '' : '?' . $parts['query']);
}
} elseif (!empty($_SERVER['QUERY_STRING']) && empty($parts['query'])) {
// fix query missing
$requestUri .= '?' . $_SERVER['QUERY_STRING'];
}
} elseif (isset($_SERVER['ORIG_PATH_INFO'])) { // IIS 5.0, PHP as CGI
$requestUri = $_SERVER['ORIG_PATH_INFO'];
if (!empty($_SERVER['QUERY_STRING'])) {
$requestUri .= '?' . $_SERVER['QUERY_STRING'];
}
}
return $this->requestUri = $requestUri;
}
/**
* 获取url前缀
*
* @return string|null
*/
public function getUrlPrefix(): ?string
{
if (empty($this->urlPrefix)) {
if (defined('__TYPECHO_URL_PREFIX__')) {
$this->urlPrefix = __TYPECHO_URL_PREFIX__;
} elseif (php_sapi_name() != 'cli') {
$this->urlPrefix = ($this->isSecure() ? 'https' : 'http') . '://'
. ($_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME']);
}
}
return $this->urlPrefix;
}
/**
* getBaseUrl
*
* @return string
*/
private function getBaseUrl(): ?string
{
if (null !== $this->baseUrl) {
return $this->baseUrl;
}
//处理baseUrl
$filename = (isset($_SERVER['SCRIPT_FILENAME'])) ? basename($_SERVER['SCRIPT_FILENAME']) : '';
if (isset($_SERVER['SCRIPT_NAME']) && basename($_SERVER['SCRIPT_NAME']) === $filename) {
$baseUrl = $_SERVER['SCRIPT_NAME'];
} elseif (isset($_SERVER['PHP_SELF']) && basename($_SERVER['PHP_SELF']) === $filename) {
$baseUrl = $_SERVER['PHP_SELF'];
} elseif (isset($_SERVER['ORIG_SCRIPT_NAME']) && basename($_SERVER['ORIG_SCRIPT_NAME']) === $filename) {
$baseUrl = $_SERVER['ORIG_SCRIPT_NAME']; // 1and1 shared hosting compatibility
} else {
// Backtrack up the script_filename to find the portion matching
// php_self
$path = $_SERVER['PHP_SELF'] ?? '';
$file = $_SERVER['SCRIPT_FILENAME'] ?? '';
$segs = explode('/', trim($file, '/'));
$segs = array_reverse($segs);
$index = 0;
$last = count($segs);
$baseUrl = '';
do {
$seg = $segs[$index];
$baseUrl = '/' . $seg . $baseUrl;
++$index;
} while (($last > $index) && (false !== ($pos = strpos($path, $baseUrl))) && (0 != $pos));
}
// Does the baseUrl have anything in common with the request_uri?
$finalBaseUrl = null;
$requestUri = $this->getRequestUri();
if (0 === strpos($requestUri, $baseUrl)) {
// full $baseUrl matches
$finalBaseUrl = $baseUrl;
} elseif (0 === strpos($requestUri, dirname($baseUrl))) {
// directory portion of $baseUrl matches
$finalBaseUrl = rtrim(dirname($baseUrl), '/');
} elseif (!strpos($requestUri, basename($baseUrl))) {
// no match whatsoever; set it blank
$finalBaseUrl = '';
} elseif (
(strlen($requestUri) >= strlen($baseUrl))
&& ((false !== ($pos = strpos($requestUri, $baseUrl))) && ($pos !== 0))
) {
// If using mod_rewrite or ISAPI_Rewrite strip the script filename
// out of baseUrl. $pos !== 0 makes sure it is not matching a value
// from PATH_INFO or QUERY_STRING
$baseUrl = substr($requestUri, 0, $pos + strlen($baseUrl));
}
return ($this->baseUrl = (null === $finalBaseUrl) ? rtrim($baseUrl, '/') : $finalBaseUrl);
}
}

354
var/Typecho/Response.php Executable file
View File

@@ -0,0 +1,354 @@
<?php
namespace Typecho;
use Typecho\Widget\Terminal;
/**
* Typecho公用方法
*
* @category typecho
* @package Response
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Response
{
/**
* http code
*
* @access private
* @var array
*/
private const HTTP_CODE = [
100 => 'Continue',
101 => 'Switching Protocols',
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
307 => 'Temporary Redirect',
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Timeout',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Request Entity Too Large',
414 => 'Request-URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Requested Range Not Satisfiable',
417 => 'Expectation Failed',
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported'
];
//默认的字符编码
/**
* 单例句柄
*
* @access private
* @var Response
*/
private static Response $instance;
/**
* 字符编码
*
* @var string
*/
private string $charset = 'UTF-8';
/**
* @var string
*/
private string $contentType = 'text/html';
/**
* @var callable[]
*/
private array $responders = [];
/**
* @var array
*/
private array $cookies = [];
/**
* @var array
*/
private array $headers = [];
/**
* @var int
*/
private int $status = 200;
/**
* @var bool
*/
private bool $enableAutoSendHeaders = true;
/**
* @var bool
*/
private bool $sandbox = false;
/**
* init responder
*/
public function __construct()
{
$this->clean();
}
/**
* 获取单例句柄
*
* @return Response
*/
public static function getInstance(): Response
{
if (!isset(self::$instance)) {
self::$instance = new self();
}
return self::$instance;
}
/**
* @return $this
*/
public function beginSandbox(): Response
{
$this->sandbox = true;
return $this;
}
/**
* @return $this
*/
public function endSandbox(): Response
{
$this->sandbox = false;
return $this;
}
/**
* @param bool $enable
*/
public function enableAutoSendHeaders(bool $enable = true)
{
$this->enableAutoSendHeaders = $enable;
}
/**
* clean all
*/
public function clean()
{
$this->headers = [];
$this->cookies = [];
$this->status = 200;
$this->responders = [];
$this->setContentType('text/html');
}
/**
* send all headers
*/
public function sendHeaders()
{
if ($this->sandbox) {
return;
}
$sentHeaders = [];
foreach (headers_list() as $header) {
[$key] = explode(':', $header, 2);
$sentHeaders[] = strtolower(trim($key));
}
header('HTTP/1.1 ' . $this->status . ' ' . self::HTTP_CODE[$this->status], true, $this->status);
// set header
foreach ($this->headers as $name => $value) {
if (!in_array(strtolower($name), $sentHeaders)) {
header($name . ': ' . $value);
}
}
// set cookie
foreach ($this->cookies as $cookie) {
[$key, $value, $timeout, $path, $domain, $secure, $httponly] = $cookie;
if ($timeout > 0) {
$now = time();
$timeout += $timeout > $now - 86400 ? 0 : $now;
} elseif ($timeout < 0) {
$timeout = 1;
}
setrawcookie($key, rawurlencode($value), $timeout, $path, $domain, $secure, $httponly);
}
}
/**
* respond data
* @throws Terminal
*/
public function respond()
{
if ($this->sandbox) {
throw new Terminal('sandbox mode');
}
if ($this->enableAutoSendHeaders) {
$this->sendHeaders();
}
foreach ($this->responders as $responder) {
call_user_func($responder, $this);
}
exit;
}
/**
* 设置HTTP状态
*
* @access public
* @param integer $code http代码
* @return $this
*/
public function setStatus(int $code): Response
{
if (!$this->sandbox) {
$this->status = $code;
}
return $this;
}
/**
* 设置http头
*
* @param string $name 名称
* @param string $value 对应值
* @return $this
*/
public function setHeader(string $name, string $value): Response
{
if (!$this->sandbox) {
$name = str_replace(' ', '-', ucwords(str_replace('-', ' ', $name)));
$this->headers[$name] = $value;
}
return $this;
}
/**
* 设置指定的COOKIE值
*
* @param string $key 指定的参数
* @param mixed $value 设置的值
* @param integer $timeout 过期时间,默认为0,表示随会话时间结束
* @param string $path 路径信息
* @param string|null $domain 域名信息
* @param bool $secure 是否仅可通过安全的 HTTPS 连接传给客户端
* @param bool $httponly 是否仅可通过 HTTP 协议访问
* @return $this
*/
public function setCookie(
string $key,
$value,
int $timeout = 0,
string $path = '/',
string $domain = '',
bool $secure = false,
bool $httponly = false
): Response {
if (!$this->sandbox) {
$this->cookies[] = [$key, $value, $timeout, $path, $domain, $secure, $httponly];
}
return $this;
}
/**
* 在http头部请求中声明类型和字符集
*
* @param string $contentType 文档类型
* @return $this
*/
public function setContentType(string $contentType): Response
{
if (!$this->sandbox) {
$this->contentType = $contentType;
$this->setHeader('Content-Type', $this->contentType . '; charset=' . $this->charset);
}
return $this;
}
/**
* 获取字符集
*
* @return string
*/
public function getCharset(): string
{
return $this->charset;
}
/**
* 设置默认回执编码
*
* @param string $charset 字符集
* @return $this
*/
public function setCharset(string $charset): Response
{
if (!$this->sandbox) {
$this->charset = $charset;
$this->setHeader('Content-Type', $this->contentType . '; charset=' . $this->charset);
}
return $this;
}
/**
* add responder
*
* @param callable $responder
* @return $this
*/
public function addResponder(callable $responder): Response
{
if (!$this->sandbox) {
$this->responders[] = $responder;
}
return $this;
}
}

191
var/Typecho/Router.php Executable file
View File

@@ -0,0 +1,191 @@
<?php
namespace Typecho;
use Typecho\Router\ParamsDelegateInterface;
use Typecho\Router\Parser;
use Typecho\Router\Exception as RouterException;
/**
* Typecho组件基类
*
* @package Router
*/
class Router
{
/**
* 当前路由名称
*
* @var string
*/
public static string $current;
/**
* 已经解析完毕的路由表配置
*
* @var array
*/
private static array $routingTable = [];
/**
* 是否已经匹配过,防止递归匹配
*
* @var bool
*/
private static bool $matched = false;
/**
* 解析路径
*
* @param string $pathInfo 全路径
* @param mixed $parameter 输入参数
* @param bool $once 是否只匹配一次
* @return false|Widget
* @throws \Exception
*/
public static function match(string $pathInfo, $parameter = null, bool $once = true)
{
if ($once && self::$matched) {
throw new RouterException("Path '{$pathInfo}' not found", 404);
}
self::$matched = true;
foreach (self::route($pathInfo) as $result) {
[$route, $params] = $result;
try {
return Widget::widget($route['widget'], $parameter, $params);
} catch (\Exception $e) {
if (404 == $e->getCode()) {
Widget::destroy($route['widget']);
continue;
}
throw $e;
}
}
return false;
}
/**
* 路由分发函数
*
* @throws RouterException|\Exception
*/
public static function dispatch()
{
/** 获取PATHINFO */
$pathInfo = Request::getInstance()->getPathInfo();
foreach (self::route($pathInfo) as $result) {
[$route, $params] = $result;
try {
$widget = Widget::widget($route['widget'], null, $params);
if (isset($route['action'])) {
$widget->{$route['action']}();
}
return;
} catch (\Exception $e) {
if (404 == $e->getCode()) {
Widget::destroy($route['widget']);
continue;
}
throw $e;
}
}
/** 载入路由异常支持 */
throw new RouterException("Path '{$pathInfo}' not found", 404);
}
/**
* 路由反解析函数
*
* @param string $name 路由配置表名称
* @param mixed $value 路由填充值
* @param string|null $prefix 最终合成路径的前缀
* @return string
*/
public static function url(
string $name,
$value = null,
?string $prefix = null
): string {
if (!isset(self::$routingTable[$name])) {
return '#';
}
$route = self::$routingTable[$name];
//交换数组键值
$pattern = [];
foreach ($route['params'] as $param) {
if (is_array($value) && isset($value[$param])) {
$pattern[$param] = $value[$param];
} elseif ($value instanceof ParamsDelegateInterface) {
$pattern[$param] = $value->getRouterParam($param);
} else {
$pattern[$param] = '{' . $param . '}';
}
}
return Common::url(vsprintf($route['format'], $pattern), $prefix);
}
/**
* 设置路由器默认配置
*
* @param mixed $routes 配置信息
* @return void
*/
public static function setRoutes($routes)
{
if (isset($routes[0])) {
self::$routingTable = $routes[0];
} else {
/** 解析路由配置 */
$parser = new Parser($routes);
self::$routingTable = $parser->parse();
}
}
/**
* 获取路由信息
*
* @param string $routeName 路由名称
* @return mixed
*/
public static function get(string $routeName)
{
return self::$routingTable[$routeName] ?? null;
}
/**
* @param string $pathInfo
* @return \Generator
* @throws \Exception
*/
private static function route(string $pathInfo): \Generator
{
foreach (self::$routingTable as $key => $route) {
if (preg_match($route['regx'], $pathInfo, $matches)) {
self::$current = $key;
/** 载入参数 */
$params = null;
if (!empty($route['params'])) {
unset($matches[0]);
$params = array_combine($route['params'], $matches);
}
yield [$route, $params];
}
}
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Typecho\Router;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
use Typecho\Exception as TypechoException;
/**
* 路由异常类
*
* @package Router
*/
class Exception extends TypechoException
{
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Typecho\Router;
interface ParamsDelegateInterface
{
public function getRouterParam(string $key): string;
}

115
var/Typecho/Router/Parser.php Executable file
View File

@@ -0,0 +1,115 @@
<?php
namespace Typecho\Router;
/**
* 路由器解析器
*
* @category typecho
* @package Router
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Parser
{
/**
* 默认匹配表
*
* @access private
* @var array
*/
private array $defaultRegex;
/**
* 路由器映射表
*
* @access private
* @var array
*/
private array $routingTable;
/**
* 参数表
*
* @access private
* @var array
*/
private array $params;
/**
* 设置路由表
*
* @access public
* @param array $routingTable 路由器映射表
*/
public function __construct(array $routingTable)
{
$this->routingTable = $routingTable;
$this->defaultRegex = [
'string' => '(.%s)',
'char' => '([^/]%s)',
'digital' => '([0-9]%s)',
'alpha' => '([_0-9a-zA-Z-]%s)',
'alphaslash' => '([_0-9a-zA-Z-/]%s)',
'split' => '((?:[^/]+/)%s[^/]+)',
];
}
/**
* 局部匹配并替换正则字符串
*
* @access public
* @param array $matches 匹配部分
* @return string
*/
public function match(array $matches): string
{
$params = explode(' ', $matches[1]);
$paramsNum = count($params);
$this->params[] = $params[0];
if (1 == $paramsNum) {
return sprintf($this->defaultRegex['char'], '+');
} elseif (2 == $paramsNum) {
return sprintf($this->defaultRegex[$params[1]], '+');
} elseif (3 == $paramsNum) {
return sprintf($this->defaultRegex[$params[1]], $params[2] > 0 ? '{' . $params[2] . '}' : '*');
} elseif (4 == $paramsNum) {
return sprintf($this->defaultRegex[$params[1]], '{' . $params[2] . ',' . $params[3] . '}');
}
return $matches[0];
}
/**
* 解析路由表
*
* @access public
* @return array
*/
public function parse(): array
{
$result = [];
foreach ($this->routingTable as $key => $route) {
$this->params = [];
$route['regx'] = preg_replace_callback(
"/%([^%]+)%/",
[$this, 'match'],
preg_quote(str_replace(['[', ']', ':'], ['%', '%', ' '], $route['url']))
);
/** 处理斜线 */
$route['regx'] = rtrim($route['regx'], '/');
$route['regx'] = '|^' . $route['regx'] . '[/]?$|';
$route['format'] = preg_replace("/\[([^\]]+)\]/", "%s", $route['url']);
$route['params'] = $this->params;
$result[$key] = $route;
}
return $result;
}
}

341
var/Typecho/Validate.php Executable file
View File

@@ -0,0 +1,341 @@
<?php
namespace Typecho;
/**
* 验证类
*
* @package Validate
*/
class Validate
{
/**
* 内部数据
*
* @access private
* @var array
*/
private array $data;
/**
* 当前验证指针
*
* @access private
* @var string
*/
private string $key;
/**
* 验证规则数组
*
* @access private
* @var array
*/
private array $rules = [];
/**
* 中断模式,一旦出现验证错误即抛出而不再继续执行
*
* @access private
* @var boolean
*/
private bool $break = false;
/**
* 最小长度
*
* @access public
*
* @param string $str 待处理的字符串
* @param integer $length 最小长度
*
* @return boolean
*/
public static function minLength(string $str, int $length): bool
{
return (Common::strLen($str) >= $length);
}
/**
* 枚举类型判断
*
* @access public
*
* @param string $str 待处理的字符串
* @param array $params 枚举值
*
* @return bool
*/
public static function enum(string $str, array $params): bool
{
$keys = array_flip($params);
return isset($keys[$str]);
}
/**
* Max Length
*
* @param string $str
* @param int $length
*
* @return bool
*/
public static function maxLength(string $str, int $length): bool
{
return (Common::strLen($str) < $length);
}
/**
* Valid Email
*
* @access public
*
* @param string $str
*
* @return boolean
*/
public static function email(string $str): bool
{
$email = filter_var($str, FILTER_SANITIZE_EMAIL);
return !!filter_var($str, FILTER_VALIDATE_EMAIL) && ($email === $str);
}
/**
* 验证是否为网址
*
* @access public
*
* @param string $str
*
* @return boolean
*/
public static function url(string $str): bool
{
$url = Common::safeUrl($str);
return !!filter_var($str, FILTER_VALIDATE_URL) && ($url === $str);
}
/**
* Alpha
*
* @access public
*
* @param string $str
*
* @return boolean
*/
public static function alpha(string $str): bool
{
return ctype_alpha($str);
}
/**
* Alpha-numeric
*
* @access public
*
* @param string $str
*
* @return boolean
*/
public static function alphaNumeric(string $str): bool
{
return ctype_alnum($str);
}
/**
* Alpha-numeric with underscores and dashes
*
* @access public
*
* @param string $str
*
* @return boolean
*/
public static function alphaDash(string $str): bool
{
return !!preg_match("/^([_a-z0-9-])+$/i", $str);
}
/**
* 对xss字符串的检测
*
* @access public
*
* @param string $str
*
* @return boolean
*/
public static function xssCheck(string $str): bool
{
$search = 'abcdefghijklmnopqrstuvwxyz';
$search .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
$search .= '1234567890!@#$%^&*()';
$search .= '~`";:?+/={}[]-_|\'\\';
for ($i = 0; $i < strlen($search); $i++) {
// ;? matches the ;, which is optional
// 0{0,7} matches any padded zeros, which are optional and go up to 8 chars
// &#x0040 @ search for the hex values
$str = preg_replace('/(&#[xX]0{0,8}' . dechex(ord($search[$i])) . ';?)/i', $search[$i], $str); // with a ;
// &#00064 @ 0{0,7} matches '0' zero to seven times
$str = preg_replace('/(&#0{0,8}' . ord($search[$i]) . ';?)/', $search[$i], $str); // with a ;
}
return !preg_match('/(\(|\)|\\\|"|<|>|[\x00-\x08]|[\x0b-\x0c]|[\x0e-\x19]|' . "\r|\n|\t" . ')/', $str);
}
/**
* Numeric
*
* @access public
*
* @param mixed $str
*
* @return boolean
*/
public static function isFloat($str): bool
{
return filter_var($str, FILTER_VALIDATE_FLOAT) !== false;
}
/**
* Is Numeric
*
* @access public
*
* @param mixed $str
*
* @return boolean
*/
public static function isInteger($str): bool
{
return filter_var($str, FILTER_VALIDATE_INT) !== false;
}
/**
* 正则表达式验证
*
* @param string $str
* @param string $pattern
* @return bool
*/
public static function regexp(string $str, string $pattern): bool
{
return preg_match($pattern, $str) === 1;
}
/**
* 增加验证规则
*
* @access public
*
* @param string $key 数值键值
* @param string|callable $rule 规则名称
* @param string $message 错误字符串
*
* @return $this
*/
public function addRule(string $key, $rule, string $message): Validate
{
if (func_num_args() <= 3) {
$this->rules[$key][] = [$rule, $message];
} else {
$params = func_get_args();
$params = array_splice($params, 3);
$this->rules[$key][] = array_merge([$rule, $message], $params);
}
return $this;
}
/**
* 设置为中断模式
*
* @access public
* @return void
*/
public function setBreak()
{
$this->break = true;
}
/**
* Run the Validator
* This function does all the work.
*
* @access public
*
* @param array $data 需要验证的数据
* @param array|null $rules 验证数据遵循的规则
*
* @return array
*/
public function run(array $data, ?array $rules = null): array
{
$result = [];
$this->data = $data;
$rules = empty($rules) ? $this->rules : $rules;
// Cycle through the rules and test for errors
foreach ($rules as $key => $rule) {
$this->key = $key;
$data[$key] = (is_array($data[$key]) ? 0 == count($data[$key])
: 0 == strlen($data[$key] ?? '')) ? null : $data[$key];
foreach ($rule as $params) {
$method = $params[0];
if ('required' != $method && 'confirm' != $method && 0 == strlen($data[$key] ?? '')) {
continue;
}
$message = $params[1];
$params[1] = $data[$key];
$params = array_slice($params, 1);
if (!call_user_func_array(is_callable($method) ? $method : [$this, $method], $params)) {
$result[$key] = $message;
break;
}
}
/** 开启中断 */
if ($this->break && $result) {
break;
}
}
return $result;
}
/**
* 验证输入是否一致
*
* @access public
*
* @param string|null $str 待处理的字符串
* @param string $key 需要一致性检查的键值
*
* @return boolean
*/
public function confirm(?string $str, string $key): bool
{
return !empty($this->data[$key]) ? ($str == $this->data[$key]) : empty($str);
}
/**
* 是否为空
*
* @access public
*
* @return boolean
*/
public function required(): bool
{
return array_key_exists($this->key, $this->data) &&
(is_array($this->data[$this->key]) ? 0 < count($this->data[$this->key])
: 0 < strlen($this->data[$this->key] ?? ''));
}
}

551
var/Typecho/Widget.php Executable file
View File

@@ -0,0 +1,551 @@
<?php
namespace Typecho;
use Typecho\Widget\Helper\EmptyClass;
use Typecho\Widget\Request as WidgetRequest;
use Typecho\Widget\Response as WidgetResponse;
use Typecho\Widget\Terminal;
/**
* Typecho组件基类
*
* @property $sequence
* @property $length
* @property-read $request
* @property-read $response
* @property-read $parameter
*/
abstract class Widget
{
/**
* widget对象池
*
* @var array
*/
private static array $widgetPool = [];
/**
* widget别名
*
* @var array
*/
private static array $widgetAlias = [];
/**
* request对象
*
* @var WidgetRequest
*/
protected WidgetRequest $request;
/**
* response对象
*
* @var WidgetResponse
*/
protected WidgetResponse $response;
/**
* 数据堆栈
*
* @var array
*/
protected array $stack = [];
/**
* 当前队列指针顺序值,从1开始
*
* @var integer
*/
protected int $sequence = 0;
/**
* 队列长度
*
* @var integer
*/
protected int $length = 0;
/**
* config对象
*
* @var Config
*/
protected Config $parameter;
/**
* 数据堆栈每一行
*
* @var array
*/
protected array $row = [];
/**
* 构造函数,初始化组件
*
* @param WidgetRequest $request request对象
* @param WidgetResponse $response response对象
* @param mixed $params 参数列表
*/
public function __construct(WidgetRequest $request, WidgetResponse $response, $params = null)
{
//设置函数内部对象
$this->request = $request;
$this->response = $response;
$this->parameter = Config::factory($params);
$this->init();
}
/**
* init method
*/
protected function init()
{
}
/**
* widget别名
*
* @param string $widgetClass
* @param string $aliasClass
*/
public static function alias(string $widgetClass, string $aliasClass)
{
self::$widgetAlias[$widgetClass] = $aliasClass;
}
/**
* 工厂方法,将类静态化放置到列表中
*
* @param class-string $alias 组件别名
* @param mixed $params 传递的参数
* @param mixed $request 前端参数
* @param bool|callable $disableSandboxOrCallback 回调
* @return Widget
*/
public static function widget(
string $alias,
$params = null,
$request = null,
$disableSandboxOrCallback = true
): Widget {
[$className] = explode('@', $alias);
$key = Common::nativeClassName($alias);
if (isset(self::$widgetAlias[$className])) {
$className = self::$widgetAlias[$className];
}
$sandbox = false;
if ($disableSandboxOrCallback === false || is_callable($disableSandboxOrCallback)) {
$sandbox = true;
Request::getInstance()->beginSandbox(new Config($request));
Response::getInstance()->beginSandbox();
}
if ($sandbox || !isset(self::$widgetPool[$key])) {
$requestObject = new WidgetRequest(Request::getInstance(), isset($request) ? new Config($request) : null);
$responseObject = new WidgetResponse(Request::getInstance(), Response::getInstance());
try {
$widget = new $className($requestObject, $responseObject, $params);
$widget->execute();
if ($sandbox && is_callable($disableSandboxOrCallback)) {
call_user_func($disableSandboxOrCallback, $widget);
}
} catch (Terminal $e) {
$widget = $widget ?? null;
} finally {
if ($sandbox) {
Response::getInstance()->endSandbox();
Request::getInstance()->endSandbox();
return $widget;
}
}
self::$widgetPool[$key] = $widget;
}
return self::$widgetPool[$key];
}
/**
* alloc widget instance
*
* @param mixed $params
* @param mixed $request
* @param bool|callable $disableSandboxOrCallback
* @return $this
*/
public static function alloc($params = null, $request = null, $disableSandboxOrCallback = true): Widget
{
return self::widget(static::class, $params, $request, $disableSandboxOrCallback);
}
/**
* alloc widget instance with alias
*
* @param string|null $alias
* @param mixed $params
* @param mixed $request
* @param bool|callable $disableSandboxOrCallback
* @return $this
*/
public static function allocWithAlias(
?string $alias,
$params = null,
$request = null,
$disableSandboxOrCallback = true
): Widget {
return self::widget(
static::class . (isset($alias) ? '@' . $alias : ''),
$params,
$request,
$disableSandboxOrCallback
);
}
/**
* 释放组件
*
* @param string $alias 组件名称
* @deprecated alias for destroy
*/
public static function destory(string $alias)
{
self::destroy($alias);
}
/**
* 释放组件
*
* @param string|null $alias 组件名称
*/
public static function destroy(?string $alias = null)
{
if (Common::nativeClassName(static::class) == 'Typecho_Widget') {
if (isset($alias)) {
unset(self::$widgetPool[$alias]);
} else {
self::$widgetPool = [];
}
} else {
$alias = static::class . (isset($alias) ? '@' . $alias : '');
unset(self::$widgetPool[$alias]);
}
}
/**
* execute function.
*/
public function execute()
{
}
/**
* post事件触发
*
* @param boolean $condition 触发条件
*
* @return $this|EmptyClass
*/
public function on(bool $condition)
{
if ($condition) {
return $this;
} else {
return new EmptyClass();
}
}
/**
* 将类本身赋值
*
* @param mixed $variable 变量名
* @return $this
*/
public function to(&$variable): Widget
{
return $variable = $this;
}
/**
* 按模版渲染
*
* @param string $template 模版
* @return string
*/
public function template(string $template): string
{
return preg_replace_callback(
"/\{([_a-z0-9]+)\}/i",
function (array $matches) {
return $this->{$matches[1]};
},
$template
);
}
/**
* 格式化解析堆栈内的所有数据
*
* @param string $template 模版
*/
public function parse(string $template)
{
while ($this->next()) {
echo $this->template($template);
}
}
/**
* @param string|array $column
* @return array|mixed|null
*/
public function toColumn($column)
{
if (is_array($column)) {
$item = [];
foreach ($column as $key) {
$item[$key] = $this->{$key};
}
return $item;
} else {
return $this->{$column};
}
}
/**
* @param string|array $column
* @return array
*/
public function toArray($column): array
{
$result = [];
while ($this->next()) {
$result[] = $this->toColumn($column);
}
return $result;
}
/**
* 返回堆栈每一行的值
*
* @return mixed
*/
public function next()
{
$key = key($this->stack);
if ($key !== null && isset($this->stack[$key])) {
$this->row = current($this->stack);
next($this->stack);
$this->sequence++;
} else {
reset($this->stack);
$this->sequence = 0;
return false;
}
return $this->row;
}
/**
* 将每一行的值压入堆栈
*
* @param array $value 每一行的值
* @return mixed
*/
public function push(array $value)
{
//将行数据按顺序置位
$this->row = $value;
$this->length++;
$this->stack[] = $value;
return $value;
}
/**
* 将所有行的值压入堆栈
*
* @param array $values 所有行的值
*/
public function pushAll(array $values)
{
foreach ($values as $value) {
$this->push($value);
}
}
/**
* 根据余数输出
*
* @param mixed ...$args
*/
public function alt(...$args)
{
$this->altBy($this->sequence, ...$args);
}
/**
* 根据余数输出
*
* @param int $current
* @param mixed ...$args
*/
public function altBy(int $current, ...$args)
{
$num = count($args);
$split = $current % $num;
echo $args[(0 == $split ? $num : $split) - 1];
}
/**
* 返回堆栈是否为空
*
* @return boolean
*/
public function have(): bool
{
return !empty($this->stack);
}
/**
* 魔术函数,用于挂接其它函数
*
* @param string $name 函数名
* @param array $args 函数参数
*/
public function __call(string $name, array $args)
{
$method = 'call' . ucfirst($name);
self::pluginHandle()->trigger($plugged)->call($method, $this, $args);
if (!$plugged) {
echo $this->{$name};
}
}
/**
* 获取对象插件句柄
*
* @return Plugin
*/
public static function pluginHandle(): Plugin
{
return Plugin::factory(static::class);
}
/**
* 魔术函数,用于获取内部变量
*
* @param string $name 变量名
* @return mixed
*/
public function __get(string $name)
{
$method = '___' . $name;
$key = '#' . $name;
if (array_key_exists($key, $this->row)) {
return $this->row[$key];
} elseif (method_exists($this, $method)) {
$this->row[$key] = $this->$method();
return $this->row[$key];
} elseif (array_key_exists($name, $this->row)) {
return $this->row[$name];
} else {
$return = self::pluginHandle()->trigger($plugged)->call($method, $this);
if ($plugged) {
return $return;
}
}
return null;
}
/**
* 设定堆栈每一行的值
*
* @param string $name 值对应的键值
* @param mixed $value 相应的值
*/
public function __set(string $name, $value)
{
$method = '___' . $name;
$key = '#' . $name;
if (isset($this->row[$key]) || method_exists($this, $method)) {
$this->row[$key] = $value;
} else {
$this->row[$name] = $value;
}
}
/**
* 验证堆栈值是否存在
*
* @param string $name
* @return boolean
*/
public function __isSet(string $name)
{
$method = '___' . $name;
$key = '#' . $name;
return isset($this->row[$key]) || method_exists($this, $method) || isset($this->row[$name]);
}
/**
* 输出顺序值
*
* @return int
*/
public function ___sequence(): int
{
return $this->sequence;
}
/**
* 输出数据长度
*
* @return int
*/
public function ___length(): int
{
return $this->length;
}
/**
* @return WidgetRequest
*/
public function ___request(): WidgetRequest
{
return $this->request;
}
/**
* @return WidgetResponse
*/
public function ___response(): WidgetResponse
{
return $this->response;
}
/**
* @return Config
*/
public function ___parameter(): Config
{
return $this->parameter;
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Typecho\Widget;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
use Typecho\Exception as TypechoException;
/**
* 组件异常类
*
* @package Widget
*/
class Exception extends TypechoException
{
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Typecho\Widget\Helper;
/**
* widget对象帮手,用于处理空对象方法
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class EmptyClass
{
/**
* 单例句柄
*
* @access private
* @var EmptyClass
*/
private static ?EmptyClass $instance = null;
/**
* 获取单例句柄
*
* @access public
* @return EmptyClass
*/
public static function getInstance(): EmptyClass
{
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* 所有方法请求直接返回
*
* @access public
* @param string $name 方法名
* @param array $args 参数列表
* @return $this
*/
public function __call(string $name, array $args)
{
return $this;
}
}

View File

@@ -0,0 +1,241 @@
<?php
namespace Typecho\Widget\Helper;
use Typecho\Cookie;
use Typecho\Request;
use Typecho\Validate;
use Typecho\Widget\Helper\Form\Element;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 表单处理帮手
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Form extends Layout
{
/** 表单post方法 */
public const POST_METHOD = 'post';
/** 表单get方法 */
public const GET_METHOD = 'get';
/** 标准编码方法 */
public const STANDARD_ENCODE = 'application/x-www-form-urlencoded';
/** 混合编码 */
public const MULTIPART_ENCODE = 'multipart/form-data';
/** 文本编码 */
public const TEXT_ENCODE = 'text/plain';
/**
* 输入元素列表
*
* @access private
* @var array
*/
private array $inputs = [];
/**
* 构造函数,设置基本属性
*
* @access public
*/
public function __construct($action = null, $method = self::GET_METHOD, $enctype = self::STANDARD_ENCODE)
{
/** 设置表单标签 */
parent::__construct('form');
/** 关闭自闭合 */
$this->setClose(false);
/** 设置表单属性 */
$this->setAction($action);
$this->setMethod($method);
$this->setEncodeType($enctype);
}
/**
* 设置表单提交目的
*
* @param string|null $action 表单提交目的
* @return $this
*/
public function setAction(?string $action): Form
{
$this->setAttribute('action', $action);
return $this;
}
/**
* 设置表单提交方法
*
* @param string $method 表单提交方法
* @return $this
*/
public function setMethod(string $method): Form
{
$this->setAttribute('method', $method);
return $this;
}
/**
* 设置表单编码方案
*
* @param string $enctype 编码方法
* @return $this
*/
public function setEncodeType(string $enctype): Form
{
$this->setAttribute('enctype', $enctype);
return $this;
}
/**
* 增加输入元素
*
* @access public
* @param Element $input 输入元素
* @return $this
*/
public function addInput(Element $input): Form
{
$this->inputs[$input->name] = $input;
$this->addItem($input);
return $this;
}
/**
* 获取输入项
*
* @param string $name 输入项名称
* @return mixed
*/
public function getInput(string $name)
{
return $this->inputs[$name];
}
/**
* 获取所有输入项的提交值
*
* @return array
*/
public function getAllRequest(): array
{
return $this->getParams(array_keys($this->inputs));
}
/**
* 获取此表单的所有输入项固有值
*
* @return array
*/
public function getValues(): array
{
$values = [];
foreach ($this->inputs as $name => $input) {
$values[$name] = $input->value;
}
return $values;
}
/**
* 获取此表单的所有输入项
*
* @return array
*/
public function getInputs(): array
{
return $this->inputs;
}
/**
* 验证表单
*
* @return array
*/
public function validate(): array
{
$validator = new Validate();
$rules = [];
foreach ($this->inputs as $name => $input) {
$rules[$name] = $input->rules;
}
$id = md5(implode('"', array_keys($this->inputs)));
/** 表单值 */
$formData = $this->getParams(array_keys($rules));
$error = $validator->run($formData, $rules);
if ($error) {
/** 利用session记录错误 */
Cookie::set('__typecho_form_message_' . $id, json_encode($error));
/** 利用session记录表单值 */
Cookie::set('__typecho_form_record_' . $id, json_encode($formData));
}
return $error;
}
/**
* 获取提交数据源
*
* @param array $params 数据参数集
* @return array
*/
public function getParams(array $params): array
{
$result = [];
$request = Request::getInstance();
foreach ($params as $param) {
$result[$param] = $request->get($param, is_array($this->getInput($param)->value) ? [] : null);
}
return $result;
}
/**
* 显示表单
*
* @return void
*/
public function render()
{
$id = md5(implode('"', array_keys($this->inputs)));
$record = Cookie::get('__typecho_form_record_' . $id);
$message = Cookie::get('__typecho_form_message_' . $id);
/** 恢复表单值 */
if (!empty($record)) {
$record = json_decode($record, true);
$message = json_decode($message, true);
foreach ($this->inputs as $name => $input) {
$input->value($record[$name] ?? $input->value);
/** 显示错误消息 */
if (isset($message[$name])) {
$input->message($message[$name]);
}
}
Cookie::delete('__typecho_form_record_' . $id);
}
parent::render();
Cookie::delete('__typecho_form_message_' . $id);
}
}

View File

@@ -0,0 +1,330 @@
<?php
namespace Typecho\Widget\Helper\Form;
use Typecho\Widget\Helper\Layout;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 表单元素抽象类
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
abstract class Element extends Layout
{
/**
* 单例唯一id
*
* @access protected
* @var integer
*/
protected static int $uniqueId = 0;
/**
* 表单元素容器
*
* @access public
* @var Layout
*/
public Layout $container;
/**
* 输入栏
*
* @access public
* @var Layout|null
*/
public ?Layout $input;
/**
* inputs
*
* @var array
* @access public
*/
public array $inputs = [];
/**
* 表单标题
*
* @access public
* @var Layout
*/
public Layout $label;
/**
* 表单验证器
*
* @access public
* @var array
*/
public array $rules = [];
/**
* 表单名称
*
* @access public
* @var string
*/
public ?string $name;
/**
* 表单值
*
* @access public
* @var mixed
*/
public $value;
/**
* 表单描述
*
* @access private
* @var Layout
*/
protected Layout $description;
/**
* 表单消息
*
* @access protected
* @var Layout
*/
protected Layout $message;
/**
* 多行输入
*
* @access public
* @var array()
*/
protected array $multiline = [];
/**
* 构造函数
*
* @param string|null $name 表单输入项名称
* @param array|null $options 选择项
* @param mixed $value 表单默认值
* @param string|null $label 表单标题
* @param string|null $description 表单描述
* @return void
*/
public function __construct(
?string $name = null,
?array $options = null,
$value = null,
?string $label = null,
?string $description = null
) {
/** 创建html元素,并设置class */
parent::__construct(
'ul',
['class' => 'typecho-option', 'id' => 'typecho-option-item-' . $name . '-' . self::$uniqueId]
);
$this->name = $name;
self::$uniqueId++;
/** 运行自定义初始函数 */
$this->init();
/** 初始化表单标题 */
if (null !== $label) {
$this->label($label);
}
/** 初始化表单项 */
$this->input = $this->input($name, $options);
/** 初始化表单值 */
if (null !== $value) {
$this->value($value);
}
/** 初始化表单描述 */
if (null !== $description) {
$this->description($description);
}
}
/**
* 自定义初始函数
*
* @return void
*/
public function init()
{
}
/**
* 创建表单标题
*
* @param string $value 标题字符串
* @return $this
*/
public function label(string $value): Element
{
/** 创建标题元素 */
if (empty($this->label)) {
$this->label = new Layout('label', ['class' => 'typecho-label']);
$this->container($this->label);
}
$this->label->html($value);
return $this;
}
/**
* 在容器里增加元素
*
* @param Layout $item 表单元素
* @return $this
*/
public function container(Layout $item): Element
{
/** 创建表单容器 */
if (empty($this->container)) {
$this->container = new Layout('li');
$this->addItem($this->container);
}
$this->container->addItem($item);
return $this;
}
/**
* 初始化当前输入项
*
* @param string|null $name 表单元素名称
* @param array|null $options 选择项
* @return Layout|null
*/
abstract public function input(?string $name = null, ?array $options = null): ?Layout;
/**
* 设置表单元素值
*
* @param mixed $value 表单元素值
* @return Element
*/
public function value($value): Element
{
$this->value = $value;
$this->inputValue($value);
return $this;
}
/**
* 设置描述信息
*
* @param string $description 描述信息
* @return Element
*/
public function description(string $description): Element
{
/** 创建描述元素 */
if (empty($this->description)) {
$this->description = new Layout('p', ['class' => 'description']);
$this->container($this->description);
}
$this->description->html($description);
return $this;
}
/**
* 设置提示信息
*
* @param string $message 提示信息
* @return Element
*/
public function message(string $message): Element
{
if (empty($this->message)) {
$this->message = new Layout('p', ['class' => 'message error']);
$this->container($this->message);
}
$this->message->html($message);
return $this;
}
/**
* 多行输出模式
*
* @return Layout
*/
public function multiline(): Layout
{
$item = new Layout('span');
$this->multiline[] = $item;
return $item;
}
/**
* 多行输出模式
*
* @return Element
*/
public function multiMode(): Element
{
foreach ($this->multiline as $item) {
$item->setAttribute('class', 'multiline');
}
return $this;
}
/**
* 增加验证器
*
* @param mixed ...$rules
* @return $this
*/
public function addRule(...$rules): Element
{
$this->rules[] = $rules;
return $this;
}
/**
* 统一设置所有输入项的属性值
*
* @param string $attributeName
* @param mixed $attributeValue
*/
public function setInputsAttribute(string $attributeName, $attributeValue)
{
foreach ($this->inputs as $input) {
$input->setAttribute($attributeName, $attributeValue);
}
}
/**
* 设置表单元素值
*
* @param mixed $value 表单元素值
*/
abstract protected function inputValue($value);
/**
* filterValue
*
* @param string $value
* @return string
*/
protected function filterValue(string $value): string
{
if (preg_match_all('/[_0-9a-z-]+/i', $value, $matches)) {
return implode('-', $matches[0]);
}
return '';
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Typecho\Widget\Helper\Form\Element;
use Typecho\Widget\Helper\Form\Element;
use Typecho\Widget\Helper\Layout;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 多选框帮手类
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Checkbox extends Element
{
/**
* 选择值
*
* @var array
*/
private array $options = [];
/**
* 初始化当前输入项
*
* @param string|null $name 表单元素名称
* @param array|null $options 选择项
* @return Layout|null
*/
public function input(?string $name = null, ?array $options = null): ?Layout
{
foreach ($options as $value => $label) {
$this->options[$value] = new Layout('input');
$item = $this->multiline();
$id = $this->name . '-' . $this->filterValue($value);
$this->inputs[] = $this->options[$value];
$item->addItem($this->options[$value]->setAttribute('name', $this->name . '[]')
->setAttribute('type', 'checkbox')
->setAttribute('value', $value)
->setAttribute('id', $id));
$labelItem = new Layout('label');
$item->addItem($labelItem->setAttribute('for', $id)->html($label));
$this->container($item);
}
return current($this->options) ?: null;
}
/**
* 设置表单元素值
*
* @param mixed $value 表单元素值
*/
protected function inputValue($value)
{
$values = isset($value) ? (is_array($value) ? $value : [$value]) : [];
foreach ($this->options as $option) {
$option->removeAttribute('checked');
}
foreach ($values as $value) {
if (isset($this->options[$value])) {
$this->options[$value]->setAttribute('checked', 'true');
}
}
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Typecho\Widget\Helper\Form\Element;
use Typecho\Widget\Helper\Form\Element;
use Typecho\Widget\Helper\Layout;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 虚拟域帮手类
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Fake extends Element
{
/**
* 构造函数
*
* @param string $name 表单输入项名称
* @param mixed $value 表单默认值
*/
public function __construct(string $name, $value)
{
$this->name = $name;
self::$uniqueId++;
/** 运行自定义初始函数 */
$this->init();
/** 初始化表单项 */
$this->input = $this->input($name);
/** 初始化表单值 */
if (null !== $value) {
$this->value($value);
}
}
/**
* 初始化当前输入项
*
* @param string|null $name 表单元素名称
* @param array|null $options 选择项
* @return Layout|null
*/
public function input(?string $name = null, ?array $options = null): ?Layout
{
$input = new Layout('input');
$this->inputs[] = $input;
return $input;
}
/**
* 设置表单项默认值
*
* @param mixed $value 表单项默认值
*/
protected function inputValue($value)
{
$this->input->setAttribute('value', $value);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Typecho\Widget\Helper\Form\Element;
use Typecho\Widget\Helper\Form\Element;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 隐藏域帮手类
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Hidden extends Element
{
use TextInputTrait;
/**
* 自定义初始函数
*
* @return void
*/
public function init()
{
/** 隐藏此行 */
$this->setAttribute('style', 'display:none');
}
/**
* @param string $value
* @return string
*/
protected function filterValue(string $value): string
{
return htmlspecialchars($value);
}
/**
* @return string
*/
protected function getType(): string
{
return 'hidden';
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Typecho\Widget\Helper\Form\Element;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
class Number extends Text
{
/**
* @return string
*/
protected function getType(): string
{
return 'number';
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Typecho\Widget\Helper\Form\Element;
use Typecho\Widget\Helper\Form\Element;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 密码输入表单项帮手类
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Password extends Element
{
use TextInputTrait;
/**
* @param string $value
* @return string
*/
protected function filterValue(string $value): string
{
return htmlspecialchars($value);
}
/**
* @return string
*/
protected function getType(): string
{
return 'password';
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace Typecho\Widget\Helper\Form\Element;
use Typecho\Widget\Helper\Form\Element;
use Typecho\Widget\Helper\Layout;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 单选框帮手类
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Radio extends Element
{
/**
* 选择值
*
* @var array
*/
private array $options = [];
/**
* 初始化当前输入项
*
* @param string|null $name 表单元素名称
* @param array|null $options 选择项
* @return Layout|null
*/
public function input(?string $name = null, ?array $options = null): ?Layout
{
foreach ($options as $value => $label) {
$this->options[$value] = new Layout('input');
$item = $this->multiline();
$id = $this->name . '-' . $this->filterValue($value);
$this->inputs[] = $this->options[$value];
$item->addItem($this->options[$value]->setAttribute('name', $this->name)
->setAttribute('type', 'radio')
->setAttribute('value', $value)
->setAttribute('id', $id));
$labelItem = new Layout('label');
$item->addItem($labelItem->setAttribute('for', $id)->html($label));
$this->container($item);
}
return current($this->options) ?: null;
}
/**
* 设置表单元素值
*
* @param mixed $value 表单元素值
*/
protected function inputValue($value)
{
foreach ($this->options as $option) {
$option->removeAttribute('checked');
}
if (isset($value) && isset($this->options[$value])) {
$this->value = $value;
$this->options[$value]->setAttribute('checked', 'true');
$this->input = $this->options[$value];
}
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Typecho\Widget\Helper\Form\Element;
use Typecho\Widget\Helper\Form\Element;
use Typecho\Widget\Helper\Layout;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 下拉选择框帮手类
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Select extends Element
{
/**
* 选择值
*
* @var array
*/
private array $options = [];
/**
* 初始化当前输入项
*
* @param string|null $name 表单元素名称
* @param array|null $options 选择项
* @return Layout|null
*/
public function input(?string $name = null, ?array $options = null): ?Layout
{
$input = new Layout('select');
$this->container($input->setAttribute('name', $name)
->setAttribute('id', $name . '-0-' . self::$uniqueId));
$this->label->setAttribute('for', $name . '-0-' . self::$uniqueId);
$this->inputs[] = $input;
foreach ($options as $value => $label) {
$this->options[$value] = new Layout('option');
$input->addItem($this->options[$value]->setAttribute('value', $value)->html($label));
}
return $input;
}
/**
* 设置表单元素值
*
* @param mixed $value 表单元素值
*/
protected function inputValue($value)
{
foreach ($this->options as $option) {
$option->removeAttribute('selected');
}
if (isset($value) && isset($this->options[$value])) {
$this->options[$value]->setAttribute('selected', 'true');
}
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Typecho\Widget\Helper\Form\Element;
use Typecho\Widget\Helper\Form\Element;
use Typecho\Widget\Helper\Layout;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 提交按钮表单项帮手类
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Submit extends Element
{
/**
* 初始化当前输入项
*
* @param string|null $name 表单元素名称
* @param array|null $options 选择项
* @return Layout|null
*/
public function input(?string $name = null, ?array $options = null): ?Layout
{
$this->setAttribute('class', 'typecho-option typecho-option-submit');
$input = new Layout('button', ['type' => 'submit']);
$this->container($input);
$this->inputs[] = $input;
return $input;
}
/**
* 设置表单元素值
*
* @param mixed $value 表单元素值
*/
protected function inputValue($value)
{
$this->input->html($value ?? 'Submit');
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Typecho\Widget\Helper\Form\Element;
use Typecho\Widget\Helper\Form\Element;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 文字输入表单项帮手类
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Text extends Element
{
use TextInputTrait;
/**
* @param string $value
* @return string
*/
protected function filterValue(string $value): string
{
return htmlspecialchars($value);
}
/**
* @return string
*/
protected function getType(): string
{
return 'text';
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Typecho\Widget\Helper\Form\Element;
use Typecho\Widget\Helper\Layout;
trait TextInputTrait
{
/**
* 初始化当前输入项
*
* @param string|null $name 表单元素名称
* @param array|null $options 选择项
* @return Layout|null
*/
public function input(?string $name = null, ?array $options = null): ?Layout
{
$input = new Layout('input', [
'id' => $name . '-0-' . self::$uniqueId,
'name' => $name,
'type' => $this->getType(),
'class' => 'text'
]);
$this->container($input);
$this->inputs[] = $input;
if (isset($this->label)) {
$this->label->setAttribute('for', $name . '-0-' . self::$uniqueId);
}
return $input;
}
/**
* 设置表单项默认值
*
* @param mixed $value 表单项默认值
*/
protected function inputValue($value)
{
if (isset($value)) {
$this->input->setAttribute('value', $this->filterValue($value));
} else {
$this->input->removeAttribute('value');
}
}
/**
* @param string $value
* @return string
*/
abstract protected function filterValue(string $value): string;
/**
* @return string
*/
abstract protected function getType(): string;
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Typecho\Widget\Helper\Form\Element;
use Typecho\Widget\Helper\Form\Element;
use Typecho\Widget\Helper\Layout;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 多行文字域帮手类
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Textarea extends Element
{
/**
* 初始化当前输入项
*
* @param string|null $name 表单元素名称
* @param array|null $options 选择项
* @return Layout|null
*/
public function input(?string $name = null, ?array $options = null): ?Layout
{
$input = new Layout('textarea', ['id' => $name . '-0-' . self::$uniqueId, 'name' => $name]);
$this->label->setAttribute('for', $name . '-0-' . self::$uniqueId);
$this->container($input->setClose(false));
$this->inputs[] = $input;
return $input;
}
/**
* 设置表单项默认值
*
* @param mixed $value 表单项默认值
*/
protected function inputValue($value)
{
$this->input->html(htmlspecialchars($value ?? ''));
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Typecho\Widget\Helper\Form\Element;
use Typecho\Common;
use Typecho\Widget\Helper\Form\Element;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* Url 表单项帮手类
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Url extends Element
{
use TextInputTrait;
/**
* @param string $value
* @return string
*/
protected function filterValue(string $value): string
{
return htmlspecialchars(Common::idnToUtf8($value));
}
/**
* @return string
*/
protected function getType(): string
{
return 'url';
}
}

View File

@@ -0,0 +1,323 @@
<?php
namespace Typecho\Widget\Helper;
/**
* HTML布局帮手类
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Layout
{
/**
* 元素列表
*
* @access private
* @var array
*/
private array $items = [];
/**
* 表单属性列表
*
* @access private
* @var array
*/
private array $attributes = [];
/**
* 标签名称
*
* @access private
* @var string
*/
private string $tagName = 'div';
/**
* 是否自闭合
*
* @access private
* @var boolean
*/
private bool $close = false;
/**
* 是否强制自闭合
*
* @access private
* @var boolean|null
*/
private ?bool $forceClose = null;
/**
* 内部数据
*
* @access private
* @var string
*/
private string $html;
/**
* 父节点
*
* @access private
* @var Layout
*/
private $parent;
/**
* 构造函数,设置标签名称
*
* @param string $tagName 标签名称
* @param array|null $attributes 属性列表
*
*/
public function __construct(string $tagName = 'div', ?array $attributes = null)
{
$this->setTagName($tagName);
if (!empty($attributes)) {
foreach ($attributes as $attributeName => $attributeValue) {
$this->setAttribute($attributeName, (string)$attributeValue);
}
}
}
/**
* 设置表单属性
*
* @param string $attributeName 属性名称
* @param mixed $attributeValue 属性值
* @return $this
*/
public function setAttribute(string $attributeName, $attributeValue): Layout
{
$this->attributes[$attributeName] = (string) $attributeValue;
return $this;
}
/**
* 删除元素
*
* @param Layout $item 元素
* @return $this
*/
public function removeItem(Layout $item): Layout
{
unset($this->items[array_search($item, $this->items)]);
return $this;
}
/**
* getItems
*
* @return array
*/
public function getItems(): array
{
return $this->items;
}
/**
* getTagName
*
* @return string
*/
public function getTagName(): string
{
return $this->tagName;
}
/**
* 设置标签名
*
* @param string $tagName 标签名
*/
public function setTagName(string $tagName)
{
$this->tagName = $tagName;
}
/**
* 移除某个属性
*
* @param string $attributeName 属性名称
* @return $this
*/
public function removeAttribute(string $attributeName): Layout
{
if (isset($this->attributes[$attributeName])) {
unset($this->attributes[$attributeName]);
}
return $this;
}
/**
* 获取属性
*
* @access public
*
* @param string $attributeName 属性名
* @return string|null
*/
public function getAttribute(string $attributeName): ?string
{
return $this->attributes[$attributeName] ?? null;
}
/**
* 设置是否自闭合
*
* @param boolean $close 是否自闭合
* @return $this
*/
public function setClose(bool $close): Layout
{
$this->forceClose = $close;
return $this;
}
/**
* 获取父节点
*
* @return Layout
*/
public function getParent(): Layout
{
return $this->parent;
}
/**
* 设置父节点
*
* @param Layout $parent 父节点
* @return $this
*/
public function setParent(Layout $parent): Layout
{
$this->parent = $parent;
return $this;
}
/**
* 增加到某布局元素集合中
*
* @param Layout $parent 布局对象
* @return $this
*/
public function appendTo(Layout $parent): Layout
{
$parent->addItem($this);
return $this;
}
/**
* 增加元素
*
* @param Layout $item 元素
* @return $this
*/
public function addItem(Layout $item): Layout
{
$item->setParent($this);
$this->items[] = $item;
return $this;
}
/**
* 获取属性
*
* @param string $name 属性名称
* @return string|null
*/
public function __get(string $name): ?string
{
return $this->attributes[$name] ?? null;
}
/**
* 设置属性
*
* @param string $name 属性名称
* @param string $value 属性值
*/
public function __set(string $name, string $value)
{
$this->attributes[$name] = $value;
}
/**
* 输出所有元素
*/
public function render()
{
if (empty($this->items) && empty($this->html)) {
$this->close = true;
}
if (null !== $this->forceClose) {
$this->close = $this->forceClose;
}
$this->start();
$this->html();
$this->end();
}
/**
* 开始标签
*/
public function start()
{
/** 输出标签 */
echo $this->tagName ? "<{$this->tagName}" : null;
/** 输出属性 */
foreach ($this->attributes as $attributeName => $attributeValue) {
echo " {$attributeName}=\"{$attributeValue}\"";
}
/** 支持自闭合 */
if (!$this->close && $this->tagName) {
echo ">\n";
}
}
/**
* 设置内部数据
*
* @param string|null $html 内部数据
* @return void|$this
*/
public function html(?string $html = null)
{
if (null === $html) {
if (empty($this->html)) {
foreach ($this->items as $item) {
$item->render();
}
} else {
echo $this->html;
}
} else {
$this->html = $html;
return $this;
}
}
/**
* 结束标签
*
* @return void
*/
public function end()
{
if ($this->tagName) {
echo $this->close ? " />\n" : "</{$this->tagName}>\n";
}
}
}

View File

@@ -0,0 +1,115 @@
<?php
namespace Typecho\Widget\Helper;
use Typecho\Widget\Exception;
/**
* 内容分页抽象类
*
* @package Widget
*/
abstract class PageNavigator
{
/**
* 记录总数
*
* @var integer
*/
protected int $total;
/**
* 页面总数
*
* @var integer
*/
protected int $totalPage;
/**
* 当前页面
*
* @var integer
*/
protected int $currentPage;
/**
* 每页内容数
*
* @var integer
*/
protected int $pageSize;
/**
* 页面链接模板
*
* @var string
*/
protected string $pageTemplate;
/**
* 链接锚点
*
* @var string
*/
protected string $anchor = '';
/**
* 页面占位符
*
* @var mixed
*/
protected $pageHolder = ['{page}', '%7Bpage%7D'];
/**
* 构造函数,初始化页面基本信息
*
* @param integer $total 记录总数
* @param integer $currentPage 当前页面
* @param integer $pageSize 每页记录数
* @param string $pageTemplate 页面链接模板
* @throws Exception
*/
public function __construct(int $total, int $currentPage, int $pageSize, string $pageTemplate)
{
$this->total = $total;
$this->totalPage = ceil($total / $pageSize);
$this->currentPage = $currentPage;
$this->pageSize = $pageSize;
$this->pageTemplate = $pageTemplate;
if (($currentPage > $this->totalPage || $currentPage < 1) && $total > 0) {
throw new Exception('Page Not Exists', 404);
}
}
/**
* 设置页面占位符
*
* @param string $holder 页面占位符
*/
public function setPageHolder(string $holder)
{
$this->pageHolder = ['{' . $holder . '}',
str_replace(['{', '}'], ['%7B', '%7D'], $holder)];
}
/**
* 设置锚点
*
* @param string $anchor 锚点
*/
public function setAnchor(string $anchor)
{
$this->anchor = '#' . $anchor;
}
/**
* 输出方法
*
* @throws Exception
*/
public function render()
{
throw new Exception('Method Not Implemented', 500);
}
}

View File

@@ -0,0 +1,136 @@
<?php
namespace Typecho\Widget\Helper\PageNavigator;
use Typecho\Widget\Helper\PageNavigator;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 盒状分页样式
*
* @author qining
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Box extends PageNavigator
{
/**
* 输出盒装样式分页栏
*
* @access public
* @param string $prevWord 上一页文字
* @param string $nextWord 下一页文字
* @param int $splitPage 分割范围
* @param string $splitWord 分割字符
* @param array $template
* @return void
*/
public function render(
string $prevWord = 'PREV',
string $nextWord = 'NEXT',
int $splitPage = 3,
string $splitWord = '...',
array $template = []
) {
if ($this->total < 1) {
return;
}
$default = [
'itemTag' => 'li',
'textTag' => 'span',
'currentClass' => 'current',
'prevClass' => 'prev',
'nextClass' => 'next'
];
$template = array_merge($default, $template);
extract($template);
// 定义item
$itemBegin = empty($itemTag) ? '' : ('<' . $itemTag . '>');
$itemCurrentBegin = empty($itemTag) ? '' : ('<' . $itemTag
. (empty($currentClass) ? '' : ' class="' . $currentClass . '"') . '>');
$itemPrevBegin = empty($itemTag) ? '' : ('<' . $itemTag
. (empty($prevClass) ? '' : ' class="' . $prevClass . '"') . '>');
$itemNextBegin = empty($itemTag) ? '' : ('<' . $itemTag
. (empty($nextClass) ? '' : ' class="' . $nextClass . '"') . '>');
$itemEnd = empty($itemTag) ? '' : ('</' . $itemTag . '>');
$textBegin = empty($textTag) ? '' : ('<' . $textTag . '>');
$textEnd = empty($textTag) ? '' : ('</' . $textTag . '>');
$linkBegin = '<a href="%s">';
$linkCurrentBegin = empty($itemTag) ? ('<a href="%s"'
. (empty($currentClass) ? '' : ' class="' . $currentClass . '"') . '>')
: $linkBegin;
$linkPrevBegin = empty($itemTag) ? ('<a href="%s"'
. (empty($prevClass) ? '' : ' class="' . $prevClass . '"') . '>')
: $linkBegin;
$linkNextBegin = empty($itemTag) ? ('<a href="%s"'
. (empty($nextClass) ? '' : ' class="' . $nextClass . '"') . '>')
: $linkBegin;
$linkEnd = '</a>';
$from = max(1, $this->currentPage - $splitPage);
$to = min($this->totalPage, $this->currentPage + $splitPage);
//输出上一页
if ($this->currentPage > 1) {
echo $itemPrevBegin . sprintf(
$linkPrevBegin,
str_replace($this->pageHolder, $this->currentPage - 1, $this->pageTemplate) . $this->anchor
)
. $prevWord . $linkEnd . $itemEnd;
}
//输出第一页
if ($from > 1) {
echo $itemBegin
. sprintf($linkBegin, str_replace($this->pageHolder, 1, $this->pageTemplate) . $this->anchor)
. '1' . $linkEnd . $itemEnd;
if ($from > 2) {
//输出省略号
echo $itemBegin . $textBegin . $splitWord . $textEnd . $itemEnd;
}
}
//输出中间页
for ($i = $from; $i <= $to; $i++) {
$current = ($i == $this->currentPage);
echo ($current ? $itemCurrentBegin : $itemBegin) . sprintf(
($current ? $linkCurrentBegin : $linkBegin),
str_replace($this->pageHolder, $i, $this->pageTemplate) . $this->anchor
)
. $i . $linkEnd . $itemEnd;
}
//输出最后页
if ($to < $this->totalPage) {
if ($to < $this->totalPage - 1) {
echo $itemBegin . $textBegin . $splitWord . $textEnd . $itemEnd;
}
echo $itemBegin
. sprintf(
$linkBegin,
str_replace($this->pageHolder, $this->totalPage, $this->pageTemplate) . $this->anchor
)
. $this->totalPage . $linkEnd . $itemEnd;
}
//输出下一页
if ($this->currentPage < $this->totalPage) {
echo $itemNextBegin . sprintf(
$linkNextBegin,
str_replace($this->pageHolder, $this->currentPage + 1, $this->pageTemplate) . $this->anchor
)
. $nextWord . $linkEnd . $itemEnd;
}
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Typecho\Widget\Helper\PageNavigator;
use Typecho\Widget\Helper\PageNavigator;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 经典分页样式
*
* @author qining
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Classic extends PageNavigator
{
/**
* 输出经典样式的分页
*
* @access public
* @param string $prevWord 上一页文字
* @param string $nextWord 下一页文字
* @return void
*/
public function render(string $prevWord = 'PREV', string $nextWord = 'NEXT')
{
$this->prev($prevWord);
$this->next($nextWord);
}
/**
* 输出上一页
*
* @access public
* @param string $prevWord 上一页文字
* @return void
*/
public function prev(string $prevWord = 'PREV')
{
//输出上一页
if ($this->total > 0 && $this->currentPage > 1) {
echo '<a class="prev" href="'
. str_replace($this->pageHolder, $this->currentPage - 1, $this->pageTemplate)
. $this->anchor . '">'
. $prevWord . '</a>';
}
}
/**
* 输出下一页
*
* @access public
* @param string $nextWord 下一页文字
* @return void
*/
public function next(string $nextWord = 'NEXT')
{
//输出下一页
if ($this->total > 0 && $this->currentPage < $this->totalPage) {
echo '<a class="next" title="" href="'
. str_replace($this->pageHolder, $this->currentPage + 1, $this->pageTemplate)
. $this->anchor . '">'
. $nextWord . '</a>';
}
}
}

390
var/Typecho/Widget/Request.php Executable file
View File

@@ -0,0 +1,390 @@
<?php
namespace Typecho\Widget;
use Typecho\Config;
use Typecho\Request as HttpRequest;
/**
* Widget Request Wrapper
*/
class Request
{
/**
* 支持的过滤器列表
*
* @access private
* @var string
*/
private const FILTERS = [
'int' => 'intval',
'integer' => 'intval',
'encode' => 'urlencode',
'html' => 'htmlspecialchars',
'search' => ['\Typecho\Common', 'filterSearchQuery'],
'xss' => ['\Typecho\Common', 'removeXSS'],
'url' => ['\Typecho\Common', 'safeUrl'],
'slug' => ['\Typecho\Common', 'slugName']
];
/**
* 当前过滤器
*
* @access private
* @var array
*/
private array $filter = [];
/**
* @var HttpRequest
*/
private HttpRequest $request;
/**
* @var Config
*/
private Config $params;
/**
* @param HttpRequest $request
* @param Config|null $params
*/
public function __construct(HttpRequest $request, ?Config $params = null)
{
$this->request = $request;
$this->params = $params ?? new Config();
}
/**
* 设置http传递参数
*
* @access public
*
* @param string $name 指定的参数
* @param mixed $value 参数值
*
* @return void
*/
public function setParam(string $name, $value)
{
$this->params[$name] = $value;
}
/**
* 设置多个参数
*
* @access public
*
* @param mixed $params 参数列表
*
* @return void
*/
public function setParams($params)
{
$this->params->setDefault($params);
}
/**
* Add filter to request
*
* @param string|callable ...$filters
* @return $this
*/
public function filter(...$filters): Request
{
foreach ($filters as $filter) {
$this->filter[] = $this->wrapFilter(
is_string($filter) && isset(self::FILTERS[$filter])
? self::FILTERS[$filter] : $filter
);
}
return $this;
}
/**
* 获取实际传递参数(magic)
*
* @deprecated ^1.3.0
* @param string $key 指定参数
* @return mixed
*/
public function __get(string $key)
{
return $this->get($key);
}
/**
* 判断参数是否存在
*
* @deprecated ^1.3.0
* @param string $key 指定参数
* @return boolean
*/
public function __isset(string $key)
{
$this->get($key, null, $exists);
return $exists;
}
/**
* @param string $key
* @param null $default
* @param bool|null $exists detect exists
* @return mixed
*/
public function get(string $key, $default = null, ?bool &$exists = true)
{
return $this->applyFilter($this->request->proxy($this->params)->get($key, $default, $exists));
}
/**
* @param $key
* @return array
*/
public function getArray($key): array
{
return $this->applyFilter($this->request->proxy($this->params)->getArray($key));
}
/**
* @param ...$params
* @return array
*/
public function from(...$params): array
{
return $this->applyFilter(call_user_func_array([$this->request->proxy($this->params), 'from'], $params));
}
/**
* 判断输入是否满足要求
*
* @param mixed $query 条件
* @return boolean
*/
public function is($query): bool
{
$result = $this->request->proxy($this->params)->is($query);
$this->request->endProxy();
return $result;
}
/**
* @return string
*/
public function getRequestRoot(): string
{
return $this->request->getRequestRoot();
}
/**
* 获取当前完整的请求url
*
* @return string
*/
public function getRequestUrl(): string
{
return $this->request->getRequestUrl();
}
/**
* 获取请求资源地址
*
* @return string|null
*/
public function getRequestUri(): ?string
{
return $this->request->getRequestUri();
}
/**
* 获取当前pathinfo
*
* @return string|null
*/
public function getPathInfo(): ?string
{
return $this->request->getPathInfo();
}
/**
* 获取url前缀
*
* @return string|null
*/
public function getUrlPrefix(): ?string
{
return $this->request->getUrlPrefix();
}
/**
* 根据当前uri构造指定参数的uri
*
* @param mixed $parameter 指定的参数
* @return string
*/
public function makeUriByRequest($parameter = null): string
{
return $this->request->makeUriByRequest($parameter);
}
/**
* 获取请求的内容类型
*
* @return string|null
*/
public function getContentType(): ?string
{
return $this->request->getContentType();
}
/**
* 获取环境变量
*
* @param string $name 获取环境变量名
* @param string|null $default
* @return string|null
*/
public function getServer(string $name, ?string $default = null): ?string
{
return $this->request->getServer($name, $default);
}
/**
* 获取ip地址
*
* @return string
*/
public function getIp(): string
{
return $this->request->getIp();
}
/**
* get header value
*
* @param string $key
* @param string|null $default
* @return string|null
*/
public function getHeader(string $key, ?string $default = null): ?string
{
return $this->request->getHeader($key, $default);
}
/**
* 获取客户端
*
* @return string
*/
public function getAgent(): ?string
{
return $this->request->getAgent();
}
/**
* 获取客户端
*
* @return string|null
*/
public function getReferer(): ?string
{
return $this->request->getReferer();
}
/**
* 判断是否为https
*
* @return bool
*/
public function isSecure(): bool
{
return $this->request->isSecure();
}
/**
* 判断是否为get方法
*
* @return boolean
*/
public function isGet(): bool
{
return $this->request->isGet();
}
/**
* 判断是否为post方法
*
* @return boolean
*/
public function isPost(): bool
{
return $this->request->isPost();
}
/**
* 判断是否为put方法
*
* @return boolean
*/
public function isPut(): bool
{
return $this->request->isPut();
}
/**
* 判断是否为ajax
*
* @return boolean
*/
public function isAjax(): bool
{
return $this->request->isAjax();
}
/**
* 判断是否为json
*
* @return boolean
*/
public function isJson(): bool
{
return $this->request->isJson();
}
/**
* 应用过滤器
*
* @param mixed $value
*
* @return mixed
*/
private function applyFilter($value)
{
if ($this->filter) {
foreach ($this->filter as $filter) {
$value = is_array($value) ? array_map($filter, $value) :
call_user_func($filter, $value);
}
$this->filter = [];
}
$this->request->endProxy();
return $value;
}
/**
* Wrap a filter to make sure it always receives a string.
*
* @param callable $filter
*
* @return callable
*/
private function wrapFilter(callable $filter): callable
{
return function ($value) use ($filter) {
return call_user_func($filter, $value ?? '');
};
}
}

245
var/Typecho/Widget/Response.php Executable file
View File

@@ -0,0 +1,245 @@
<?php
namespace Typecho\Widget;
use Typecho\Common;
use Typecho\Request as HttpRequest;
use Typecho\Response as HttpResponse;
/**
* Widget Response Wrapper
*/
class Response
{
/**
* @var HttpRequest
*/
private HttpRequest $request;
/**
* @var HttpResponse
*/
private HttpResponse $response;
/**
* @param HttpRequest $request
* @param HttpResponse $response
*/
public function __construct(HttpRequest $request, HttpResponse $response)
{
$this->request = $request;
$this->response = $response;
}
/**
* @param int $code
* @return $this
*/
public function setStatus(int $code): Response
{
$this->response->setStatus($code);
return $this;
}
/**
* @param string $name
* @param $value
* @return $this
*/
public function setHeader(string $name, $value): Response
{
$this->response->setHeader($name, (string)$value);
return $this;
}
/**
* 设置默认回执编码
*
* @param string $charset 字符集
* @return $this
*/
public function setCharset(string $charset): Response
{
$this->response->setCharset($charset);
return $this;
}
/**
* @param string $contentType
* @return $this
*/
public function setContentType(string $contentType = 'text/html'): Response
{
$this->response->setContentType($contentType);
return $this;
}
/**
* @param callable $callback
* @param string $contentType
*/
public function throwCallback(callable $callback, string $contentType = 'text/html')
{
$this->response->setContentType($contentType)
->addResponder($callback)
->respond();
}
/**
* @return void
*/
public function throwFinish()
{
$isFastCGI = function_exists('fastcgi_finish_request');
if (!$isFastCGI) {
ob_end_clean();
ob_start();
$this->setHeader('Connection', 'close');
$this->setHeader('Content-Encoding', 'none');
$this->setHeader('Content-Length', ob_get_length());
}
$this->response->sendHeaders();
if (!$isFastCGI) {
ob_end_flush();
flush();
} else {
fastcgi_finish_request();
}
}
/**
* @param string $content
* @param string $contentType
*/
public function throwContent(string $content, string $contentType = 'text/html')
{
$this->throwCallback(function () use ($content) {
echo $content;
}, $contentType);
}
/**
* @param mixed $message
*/
public function throwXml($message)
{
$this->throwCallback(function () use ($message) {
echo '<?xml version="1.0" encoding="' . $this->response->getCharset() . '"?>',
'<response>',
$this->parseXml($message),
'</response>';
}, 'text/xml');
}
/**
* 抛出json回执信息
*
* @param mixed $message 消息体
*/
public function throwJson($message)
{
$this->throwCallback(function () use ($message) {
echo json_encode($message);
}, 'application/json');
}
/**
* @param $file
* @param string|null $contentType
*/
public function throwFile($file, ?string $contentType = null)
{
if (!empty($contentType)) {
$this->response->setContentType($contentType);
}
$this->response->setHeader('Content-Length', filesize($file))
->addResponder(function () use ($file) {
readfile($file);
})
->respond();
}
/**
* 重定向函数
*
* @param string $location 重定向路径
* @param boolean $isPermanently 是否为永久重定向
*/
public function redirect(string $location, bool $isPermanently = false)
{
$location = Common::safeUrl($location);
$this->response->setStatus($isPermanently ? 301 : 302)
->setHeader('Location', $location)
->respond();
}
/**
* 返回来路
*
* @param string|null $suffix 附加地址
* @param string|null $default 默认来路
*/
public function goBack(?string $suffix = null, ?string $default = null)
{
//获取来源
$referer = $this->request->getReferer();
//判断来源
if (!empty($referer)) {
// ~ fix Issue 38
if (!empty($suffix)) {
$parts = parse_url($referer);
$myParts = parse_url($suffix);
if (isset($myParts['fragment'])) {
$parts['fragment'] = $myParts['fragment'];
}
if (isset($myParts['query'])) {
$args = [];
if (isset($parts['query'])) {
parse_str($parts['query'], $args);
}
parse_str($myParts['query'], $currentArgs);
$args = array_merge($args, $currentArgs);
$parts['query'] = http_build_query($args);
}
$referer = Common::buildUrl($parts);
}
$this->redirect($referer);
} else {
$this->redirect($default ?: '/');
}
}
/**
* 解析ajax回执的内部函数
*
* @param mixed $message 格式化数据
* @return string
*/
private function parseXml($message): string
{
/** 对于数组型则继续递归 */
if (is_array($message)) {
$result = '';
foreach ($message as $key => $val) {
$tagName = is_int($key) ? 'item' : $key;
$result .= '<' . $tagName . '>' . $this->parseXml($val) . '</' . $tagName . '>';
}
return $result;
} else {
return preg_match("/^[^<>]+$/is", $message) ? $message : '<![CDATA[' . $message . ']]>';
}
}
}

14
var/Typecho/Widget/Terminal.php Executable file
View File

@@ -0,0 +1,14 @@
<?php
namespace Typecho\Widget;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* Special exception to break executor
*/
class Terminal extends Exception
{
}