项目初始化
This commit is contained in:
356
app/common/library/Auth.php
Normal file
356
app/common/library/Auth.php
Normal file
@@ -0,0 +1,356 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\library;
|
||||
|
||||
use Throwable;
|
||||
use ba\Random;
|
||||
use support\think\Db;
|
||||
use app\common\model\User;
|
||||
use app\common\facade\Token;
|
||||
use Webman\Http\Request;
|
||||
|
||||
/**
|
||||
* 公共权限类(会员权限类)
|
||||
*/
|
||||
class Auth extends \ba\Auth
|
||||
{
|
||||
public const LOGIN_RESPONSE_CODE = 303;
|
||||
public const NEED_LOGIN = 'need login';
|
||||
public const LOGGED_IN = 'logged in';
|
||||
public const TOKEN_TYPE = 'user';
|
||||
|
||||
protected bool $loginEd = false;
|
||||
protected string $error = '';
|
||||
protected ?User $model = null;
|
||||
protected string $token = '';
|
||||
protected string $refreshToken = '';
|
||||
protected int $keepTime = 86400;
|
||||
protected int $refreshTokenKeepTime = 2592000;
|
||||
|
||||
protected array $allowFields = ['id', 'username', 'nickname', 'email', 'mobile', 'avatar', 'gender', 'birthday', 'money', 'score', 'join_time', 'motto', 'last_login_time', 'last_login_ip'];
|
||||
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
parent::__construct(array_merge([
|
||||
'auth_group' => 'user_group',
|
||||
'auth_group_access' => '',
|
||||
'auth_rule' => 'user_rule',
|
||||
], $config));
|
||||
|
||||
$this->setKeepTime((int)config('buildadmin.user_token_keep_time', 86400));
|
||||
}
|
||||
|
||||
public function __get($name): mixed
|
||||
{
|
||||
return $this->model?->$name;
|
||||
}
|
||||
|
||||
public static function instance(array $options = []): Auth
|
||||
{
|
||||
$request = $options['request'] ?? (function_exists('request') ? request() : null);
|
||||
unset($options['request']);
|
||||
if ($request && !isset($request->userAuth)) {
|
||||
$request->userAuth = new static($options);
|
||||
}
|
||||
return $request && isset($request->userAuth) ? $request->userAuth : new static($options);
|
||||
}
|
||||
|
||||
public function init($token): bool
|
||||
{
|
||||
$tokenData = Token::get($token);
|
||||
if ($tokenData) {
|
||||
Token::tokenExpirationCheck($tokenData);
|
||||
$userId = $tokenData['user_id'];
|
||||
if ($tokenData['type'] == self::TOKEN_TYPE && $userId > 0) {
|
||||
$this->model = User::where('id', $userId)->find();
|
||||
if (!$this->model) {
|
||||
$this->setError('Account not exist');
|
||||
return false;
|
||||
}
|
||||
if ($this->model->status != 'enable') {
|
||||
$this->setError('Account disabled');
|
||||
return false;
|
||||
}
|
||||
$this->token = $token;
|
||||
$this->loginSuccessful();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
$this->setError('Token login failed');
|
||||
$this->reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
public function register(string $username, string $password = '', string $mobile = '', string $email = '', int $group = 1, array $extend = []): bool
|
||||
{
|
||||
$request = function_exists('request') ? request() : null;
|
||||
$ip = $request ? $request->getRealIp() : '0.0.0.0';
|
||||
|
||||
if ($email && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$this->setError(__('Email'));
|
||||
return false;
|
||||
}
|
||||
if ($username && !preg_match('/^[a-zA-Z][a-zA-Z0-9_]{2,15}$/', $username)) {
|
||||
$this->setError(__('Username'));
|
||||
return false;
|
||||
}
|
||||
if (User::where('email', $email)->find() && $email) {
|
||||
$this->setError(__('Email') . ' ' . __('already exists'));
|
||||
return false;
|
||||
}
|
||||
if (User::where('username', $username)->find()) {
|
||||
$this->setError(__('Username') . ' ' . __('already exists'));
|
||||
return false;
|
||||
}
|
||||
|
||||
$nickname = preg_replace_callback('/1[3-9]\d{9}/', fn($m) => substr($m[0], 0, 3) . '****' . substr($m[0], 7), $username);
|
||||
$time = time();
|
||||
$data = [
|
||||
'group_id' => $group,
|
||||
'nickname' => $nickname,
|
||||
'join_ip' => $ip,
|
||||
'join_time' => $time,
|
||||
'last_login_ip' => $ip,
|
||||
'last_login_time' => $time,
|
||||
'status' => 'enable',
|
||||
];
|
||||
$data = array_merge(compact('username', 'password', 'mobile', 'email'), $data, $extend);
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
$this->model = User::create($data);
|
||||
$this->token = Random::uuid();
|
||||
Token::set($this->token, self::TOKEN_TYPE, $this->model->id, $this->keepTime);
|
||||
Db::commit();
|
||||
|
||||
if ($password) {
|
||||
$this->model->resetPassword($this->model->id, $password);
|
||||
}
|
||||
|
||||
event_trigger('userRegisterSuccess', $this->model);
|
||||
} catch (Throwable $e) {
|
||||
$this->setError($e->getMessage());
|
||||
Db::rollback();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function login(string $username, string $password, bool $keep): bool
|
||||
{
|
||||
$accountType = false;
|
||||
if (preg_match('/^1[3-9]\d{9}$/', $username)) $accountType = 'mobile';
|
||||
elseif (filter_var($username, FILTER_VALIDATE_EMAIL)) $accountType = 'email';
|
||||
elseif (preg_match('/^[a-zA-Z][a-zA-Z0-9_]{2,15}$/', $username)) $accountType = 'username';
|
||||
if (!$accountType) {
|
||||
$this->setError('Account not exist');
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->model = User::where($accountType, $username)->find();
|
||||
if (!$this->model) {
|
||||
$this->setError('Account not exist');
|
||||
return false;
|
||||
}
|
||||
if ($this->model->status == 'disable') {
|
||||
$this->setError('Account disabled');
|
||||
return false;
|
||||
}
|
||||
|
||||
$userLoginRetry = config('buildadmin.user_login_retry');
|
||||
if ($userLoginRetry && $this->model->last_login_time) {
|
||||
$lastLoginTs = is_numeric($this->model->last_login_time) ? (int)$this->model->last_login_time : strtotime($this->model->last_login_time);
|
||||
if ($this->model->login_failure > 0 && $lastLoginTs > 0 && time() - $lastLoginTs >= 86400) {
|
||||
$this->model->login_failure = 0;
|
||||
$this->model->save();
|
||||
$this->model = User::where($accountType, $username)->find();
|
||||
}
|
||||
if ($this->model->login_failure >= $userLoginRetry) {
|
||||
$this->setError('Please try again after 1 day');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!verify_password($password, $this->model->password, ['salt' => $this->model->salt])) {
|
||||
$this->loginFailed();
|
||||
$this->setError('Password is incorrect');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config('buildadmin.user_sso')) {
|
||||
Token::clear(self::TOKEN_TYPE, $this->model->id);
|
||||
Token::clear(self::TOKEN_TYPE . '-refresh', $this->model->id);
|
||||
}
|
||||
|
||||
if ($keep) $this->setRefreshToken($this->refreshTokenKeepTime);
|
||||
return $this->loginSuccessful();
|
||||
}
|
||||
|
||||
public function direct(int $userId): bool
|
||||
{
|
||||
$this->model = User::find($userId);
|
||||
if (!$this->model) return false;
|
||||
if (config('buildadmin.user_sso')) {
|
||||
Token::clear(self::TOKEN_TYPE, $this->model->id);
|
||||
Token::clear(self::TOKEN_TYPE . '-refresh', $this->model->id);
|
||||
}
|
||||
return $this->loginSuccessful();
|
||||
}
|
||||
|
||||
public function loginSuccessful(): bool
|
||||
{
|
||||
if (!$this->model) return false;
|
||||
$request = function_exists('request') ? request() : null;
|
||||
$ip = $request ? $request->getRealIp() : '0.0.0.0';
|
||||
if (!$this->token) {
|
||||
$this->token = Random::uuid();
|
||||
Token::set($this->token, self::TOKEN_TYPE, $this->model->id, $this->keepTime);
|
||||
}
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
$this->model->login_failure = 0;
|
||||
$this->model->last_login_time = time();
|
||||
$this->model->last_login_ip = $ip;
|
||||
$this->model->save();
|
||||
$this->loginEd = true;
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
if ($this->token) {
|
||||
Token::delete($this->token);
|
||||
$this->token = '';
|
||||
}
|
||||
$this->setError($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function loginFailed(): bool
|
||||
{
|
||||
if (!$this->model) return false;
|
||||
$request = function_exists('request') ? request() : null;
|
||||
$ip = $request ? $request->getRealIp() : '0.0.0.0';
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
$this->model->login_failure++;
|
||||
$this->model->last_login_time = time();
|
||||
$this->model->last_login_ip = $ip;
|
||||
$this->model->save();
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
}
|
||||
return $this->reset();
|
||||
}
|
||||
|
||||
public function logout(): bool
|
||||
{
|
||||
if (!$this->loginEd) {
|
||||
$this->setError('You are not logged in');
|
||||
return false;
|
||||
}
|
||||
return $this->reset();
|
||||
}
|
||||
|
||||
public function isLogin(): bool
|
||||
{
|
||||
return $this->loginEd;
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
public function getToken(): string
|
||||
{
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
public function setRefreshToken(int $keepTime = 0): void
|
||||
{
|
||||
$this->refreshToken = Random::uuid();
|
||||
Token::set($this->refreshToken, self::TOKEN_TYPE . '-refresh', $this->model->id, $keepTime);
|
||||
}
|
||||
|
||||
public function getRefreshToken(): string
|
||||
{
|
||||
return $this->refreshToken;
|
||||
}
|
||||
|
||||
public function getUserInfo(): array
|
||||
{
|
||||
if (!$this->model) return [];
|
||||
$info = $this->model->toArray();
|
||||
$info = array_intersect_key($info, array_flip($this->getAllowFields()));
|
||||
$info['token'] = $this->getToken();
|
||||
$info['refresh_token'] = $this->getRefreshToken();
|
||||
return $info;
|
||||
}
|
||||
|
||||
public function getAllowFields(): array
|
||||
{
|
||||
return $this->allowFields;
|
||||
}
|
||||
|
||||
public function setAllowFields($fields): void
|
||||
{
|
||||
$this->allowFields = $fields;
|
||||
}
|
||||
|
||||
public function setKeepTime(int $keepTime = 0): void
|
||||
{
|
||||
$this->keepTime = $keepTime;
|
||||
}
|
||||
|
||||
public function check(string $name, int $uid = 0, string $relation = 'or', string $mode = 'url'): bool
|
||||
{
|
||||
return parent::check($name, $uid ?: $this->id, $relation, $mode);
|
||||
}
|
||||
|
||||
public function getRuleList(int $uid = 0): array
|
||||
{
|
||||
return parent::getRuleList($uid ?: $this->id);
|
||||
}
|
||||
|
||||
public function getRuleIds(int $uid = 0): array
|
||||
{
|
||||
return parent::getRuleIds($uid ?: $this->id);
|
||||
}
|
||||
|
||||
public function getMenus(int $uid = 0): array
|
||||
{
|
||||
return parent::getMenus($uid ?: $this->id);
|
||||
}
|
||||
|
||||
public function isSuperUser(): bool
|
||||
{
|
||||
return in_array('*', $this->getRuleIds());
|
||||
}
|
||||
|
||||
public function setError(string $error): Auth
|
||||
{
|
||||
$this->error = $error;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getError(): string
|
||||
{
|
||||
return $this->error ? __($this->error) : '';
|
||||
}
|
||||
|
||||
protected function reset(bool $deleteToken = true): bool
|
||||
{
|
||||
if ($deleteToken && $this->token) {
|
||||
Token::delete($this->token);
|
||||
}
|
||||
$this->token = '';
|
||||
$this->loginEd = false;
|
||||
$this->model = null;
|
||||
$this->refreshToken = '';
|
||||
$this->setError('');
|
||||
$this->setKeepTime((int)config('buildadmin.user_token_keep_time', 86400));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
66
app/common/library/Email.php
Normal file
66
app/common/library/Email.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\library;
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
|
||||
/**
|
||||
* 邮件类(Webman 迁移版)
|
||||
*/
|
||||
class Email extends PHPMailer
|
||||
{
|
||||
public bool $configured = false;
|
||||
|
||||
public array $options = [
|
||||
'charset' => 'utf-8',
|
||||
'debug' => true,
|
||||
'lang' => 'zh_cn',
|
||||
];
|
||||
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
$this->options = array_merge($this->options, $options);
|
||||
|
||||
parent::__construct($this->options['debug']);
|
||||
|
||||
$langSet = function_exists('locale') ? locale() : 'zh_CN';
|
||||
if ($langSet == 'zh-cn' || $langSet == 'zh_CN' || !$langSet) {
|
||||
$langSet = 'zh_cn';
|
||||
}
|
||||
$this->options['lang'] = $this->options['lang'] ?: $langSet;
|
||||
|
||||
$langPath = root_path() . 'vendor' . DIRECTORY_SEPARATOR . 'phpmailer' . DIRECTORY_SEPARATOR . 'phpmailer' . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR;
|
||||
if (is_dir($langPath)) {
|
||||
$this->setLanguage($this->options['lang'], $langPath);
|
||||
}
|
||||
$this->CharSet = $this->options['charset'];
|
||||
|
||||
$sysMailConfig = get_sys_config('', 'mail');
|
||||
$this->configured = true;
|
||||
if (is_array($sysMailConfig)) {
|
||||
foreach ($sysMailConfig as $item) {
|
||||
if (!$item) {
|
||||
$this->configured = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->configured = false;
|
||||
}
|
||||
|
||||
if ($this->configured) {
|
||||
$this->Host = $sysMailConfig['smtp_server'];
|
||||
$this->SMTPAuth = true;
|
||||
$this->Username = $sysMailConfig['smtp_user'];
|
||||
$this->Password = $sysMailConfig['smtp_pass'];
|
||||
$this->SMTPSecure = ($sysMailConfig['smtp_verification'] ?? '') == 'SSL' ? self::ENCRYPTION_SMTPS : self::ENCRYPTION_STARTTLS;
|
||||
$this->Port = $sysMailConfig['smtp_port'] ?? 465;
|
||||
$this->setFrom($sysMailConfig['smtp_sender_mail'] ?? '', $sysMailConfig['smtp_user'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
public function setSubject($subject): void
|
||||
{
|
||||
$this->Subject = "=?utf-8?B?" . base64_encode($subject) . "?=";
|
||||
}
|
||||
}
|
||||
133
app/common/library/Menu.php
Normal file
133
app/common/library/Menu.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library;
|
||||
|
||||
use Throwable;
|
||||
use app\admin\model\AdminRule;
|
||||
use app\admin\model\UserRule;
|
||||
|
||||
/**
|
||||
* 菜单规则管理类(Webman 迁移版)
|
||||
*/
|
||||
class Menu
|
||||
{
|
||||
/**
|
||||
* @param array $menu
|
||||
* @param int|string $parent 父级规则name或id
|
||||
* @param string $mode 添加模式(规则重复时):cover=覆盖旧菜单,rename=重命名新菜单,ignore=忽略
|
||||
* @param string $position 位置:backend=后台,frontend=前台
|
||||
* @return void
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function create(array $menu, int|string $parent = 0, string $mode = 'cover', string $position = 'backend'): void
|
||||
{
|
||||
$pid = 0;
|
||||
$model = $position == 'backend' ? new AdminRule() : new UserRule();
|
||||
$parentRule = $model->where((is_numeric($parent) ? 'id' : 'name'), $parent)->find();
|
||||
if ($parentRule) {
|
||||
$pid = $parentRule['id'];
|
||||
}
|
||||
foreach ($menu as $item) {
|
||||
if (!self::requiredAttrCheck($item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$item['status'] = 1;
|
||||
if (!isset($item['pid'])) {
|
||||
$item['pid'] = $pid;
|
||||
}
|
||||
|
||||
$sameOldMenu = $model->where('name', $item['name'])->find();
|
||||
if ($sameOldMenu) {
|
||||
if ($mode == 'cover') {
|
||||
$sameOldMenu->save($item);
|
||||
} elseif ($mode == 'rename') {
|
||||
$count = $model->where('name', $item['name'])->count();
|
||||
$item['name'] = $item['name'] . '-CONFLICT-' . $count;
|
||||
$item['path'] = $item['path'] . '-CONFLICT-' . $count;
|
||||
$item['title'] = $item['title'] . '-CONFLICT-' . $count;
|
||||
$sameOldMenu = $model->create($item);
|
||||
} elseif ($mode == 'ignore') {
|
||||
$sameOldMenu = $model
|
||||
->where('name', $item['name'])
|
||||
->where('pid', $item['pid'])
|
||||
->find();
|
||||
|
||||
if (!$sameOldMenu) {
|
||||
$sameOldMenu = $model->create($item);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$sameOldMenu = $model->create($item);
|
||||
}
|
||||
if (!empty($item['children'])) {
|
||||
self::create($item['children'], $sameOldMenu['id'], $mode, $position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function delete(string|int $id, bool $recursion = false, string $position = 'backend'): bool
|
||||
{
|
||||
if (!$id) {
|
||||
return true;
|
||||
}
|
||||
$model = $position == 'backend' ? new AdminRule() : new UserRule();
|
||||
$menuRule = $model->where((is_numeric($id) ? 'id' : 'name'), $id)->find();
|
||||
if (!$menuRule) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$children = $model->where('pid', $menuRule['id'])->select()->toArray();
|
||||
if ($recursion && $children) {
|
||||
foreach ($children as $child) {
|
||||
self::delete($child['id'], true, $position);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$children || $recursion) {
|
||||
$menuRule->delete();
|
||||
self::delete($menuRule->pid, false, $position);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function enable(string|int $id, string $position = 'backend'): bool
|
||||
{
|
||||
$model = $position == 'backend' ? new AdminRule() : new UserRule();
|
||||
$menuRule = $model->where((is_numeric($id) ? 'id' : 'name'), $id)->find();
|
||||
if (!$menuRule) {
|
||||
return false;
|
||||
}
|
||||
$menuRule->status = 1;
|
||||
$menuRule->save();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function disable(string|int $id, string $position = 'backend'): bool
|
||||
{
|
||||
$model = $position == 'backend' ? new AdminRule() : new UserRule();
|
||||
$menuRule = $model->where((is_numeric($id) ? 'id' : 'name'), $id)->find();
|
||||
if (!$menuRule) {
|
||||
return false;
|
||||
}
|
||||
$menuRule->status = 0;
|
||||
$menuRule->save();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function requiredAttrCheck($menu): bool
|
||||
{
|
||||
$attrs = ['type', 'title', 'name'];
|
||||
foreach ($attrs as $attr) {
|
||||
if (!array_key_exists($attr, $menu)) {
|
||||
return false;
|
||||
}
|
||||
if (!$menu[$attr]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
146
app/common/library/Token.php
Normal file
146
app/common/library/Token.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use app\common\library\token\TokenExpirationException;
|
||||
|
||||
/**
|
||||
* Token 管理类
|
||||
*/
|
||||
class Token
|
||||
{
|
||||
public array $instance = [];
|
||||
public ?object $handler = null;
|
||||
protected string $namespace = '\\app\\common\\library\\token\\driver\\';
|
||||
|
||||
public function getDriver(?string $name = null): object
|
||||
{
|
||||
if ($this->handler !== null) {
|
||||
return $this->handler;
|
||||
}
|
||||
$name = $name ?: $this->getDefaultDriver();
|
||||
if ($name === null) {
|
||||
throw new InvalidArgumentException(sprintf('Unable to resolve NULL driver for [%s].', static::class));
|
||||
}
|
||||
return $this->createDriver($name);
|
||||
}
|
||||
|
||||
protected function createDriver(string $name): object
|
||||
{
|
||||
$type = $this->resolveType($name);
|
||||
$params = $this->resolveParams($name);
|
||||
$class = $this->resolveClass($type);
|
||||
|
||||
if (isset($this->instance[$type])) {
|
||||
return $this->instance[$type];
|
||||
}
|
||||
return new $class(...$params);
|
||||
}
|
||||
|
||||
protected function getDefaultDriver(): string
|
||||
{
|
||||
return $this->getConfig('default');
|
||||
}
|
||||
|
||||
protected function getConfig(?string $name = null, mixed $default = null): array|string
|
||||
{
|
||||
$config = config('buildadmin.token', []);
|
||||
if ($name === null) {
|
||||
return $config;
|
||||
}
|
||||
$keys = explode('.', $name);
|
||||
$val = $config;
|
||||
foreach ($keys as $k) {
|
||||
if (!is_array($val) || !array_key_exists($k, $val)) {
|
||||
return $default;
|
||||
}
|
||||
$val = $val[$k];
|
||||
}
|
||||
return $val;
|
||||
}
|
||||
|
||||
protected function resolveParams(string $name): array
|
||||
{
|
||||
$config = $this->getStoreConfig($name);
|
||||
return [$config];
|
||||
}
|
||||
|
||||
protected function resolveClass(string $type): string
|
||||
{
|
||||
$class = str_contains($type, '\\') ? $type : $this->namespace . $this->studly($type);
|
||||
if (class_exists($class)) {
|
||||
return $class;
|
||||
}
|
||||
throw new InvalidArgumentException("Driver [{$type}] not supported.");
|
||||
}
|
||||
|
||||
protected function getStoreConfig(string $store, ?string $name = null, mixed $default = null): array|string
|
||||
{
|
||||
$config = $this->getConfig("stores.{$store}");
|
||||
if ($config === null) {
|
||||
throw new InvalidArgumentException("Store [{$store}] not found.");
|
||||
}
|
||||
if ($name === null) {
|
||||
return $config;
|
||||
}
|
||||
$keys = explode('.', $name);
|
||||
$val = $config;
|
||||
foreach ($keys as $k) {
|
||||
if (!is_array($val) || !array_key_exists($k, $val)) {
|
||||
return $default;
|
||||
}
|
||||
$val = $val[$k];
|
||||
}
|
||||
return $val;
|
||||
}
|
||||
|
||||
protected function resolveType(string $name): string
|
||||
{
|
||||
return $this->getStoreConfig($name, 'type', 'Mysql');
|
||||
}
|
||||
|
||||
private function studly(string $value): string
|
||||
{
|
||||
$value = ucwords(str_replace(['-', '_'], ' ', $value));
|
||||
return str_replace(' ', '', $value);
|
||||
}
|
||||
|
||||
public function set(string $token, string $type, int $userId, ?int $expire = null): bool
|
||||
{
|
||||
return $this->getDriver()->set($token, $type, $userId, $expire);
|
||||
}
|
||||
|
||||
public function get(string $token, bool $expirationException = true): array
|
||||
{
|
||||
return $this->getDriver()->get($token);
|
||||
}
|
||||
|
||||
public function check(string $token, string $type, int $userId, bool $expirationException = true): bool
|
||||
{
|
||||
return $this->getDriver()->check($token, $type, $userId);
|
||||
}
|
||||
|
||||
public function delete(string $token): bool
|
||||
{
|
||||
return $this->getDriver()->delete($token);
|
||||
}
|
||||
|
||||
public function clear(string $type, int $userId): bool
|
||||
{
|
||||
return $this->getDriver()->clear($type, $userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Token 过期检查
|
||||
* @throws TokenExpirationException
|
||||
*/
|
||||
public function tokenExpirationCheck(array $token): void
|
||||
{
|
||||
if (isset($token['expire_time']) && $token['expire_time'] <= time()) {
|
||||
throw new TokenExpirationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
260
app/common/library/Upload.php
Normal file
260
app/common/library/Upload.php
Normal file
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library;
|
||||
|
||||
use ba\Random;
|
||||
use ba\Filesystem;
|
||||
use app\common\model\Attachment;
|
||||
use app\common\library\upload\Driver;
|
||||
use app\common\library\upload\WebmanUploadedFile;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* 上传(Webman 迁移版,使用 WebmanUploadedFile)
|
||||
*/
|
||||
class Upload
|
||||
{
|
||||
protected array $config = [];
|
||||
protected ?WebmanUploadedFile $file = null;
|
||||
protected bool $isImage = false;
|
||||
protected array $fileInfo = [];
|
||||
protected array $driver = [
|
||||
'name' => 'local',
|
||||
'handler' => [],
|
||||
'namespace' => '\\app\\common\\library\\upload\\driver\\',
|
||||
];
|
||||
protected string $topic = 'default';
|
||||
|
||||
public function __construct(?WebmanUploadedFile $file = null, array $config = [])
|
||||
{
|
||||
$upload = config('upload', []);
|
||||
$this->config = array_merge($upload, $config);
|
||||
|
||||
if ($file) {
|
||||
$this->setFile($file);
|
||||
}
|
||||
}
|
||||
|
||||
public function setFile(?WebmanUploadedFile $file): self
|
||||
{
|
||||
if (empty($file)) {
|
||||
throw new RuntimeException(__('No files were uploaded'));
|
||||
}
|
||||
|
||||
$suffix = strtolower($file->extension());
|
||||
$suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file';
|
||||
$this->fileInfo = [
|
||||
'suffix' => $suffix,
|
||||
'type' => $file->getMime(),
|
||||
'size' => $file->getSize(),
|
||||
'name' => $file->getOriginalName(),
|
||||
'sha1' => $file->sha1(),
|
||||
];
|
||||
$this->file = $file;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setDriver(string $driver): self
|
||||
{
|
||||
$this->driver['name'] = $driver;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDriver(?string $driver = null, bool $noDriveException = true): Driver|false
|
||||
{
|
||||
$driver = $driver ?? $this->driver['name'];
|
||||
if (!isset($this->driver['handler'][$driver])) {
|
||||
$class = $this->resolveDriverClass($driver);
|
||||
if ($class) {
|
||||
$this->driver['handler'][$driver] = new $class();
|
||||
} elseif ($noDriveException) {
|
||||
throw new InvalidArgumentException(__('Driver %s not supported', [$driver]));
|
||||
}
|
||||
}
|
||||
return $this->driver['handler'][$driver] ?? false;
|
||||
}
|
||||
|
||||
protected function resolveDriverClass(string $driver): string|false
|
||||
{
|
||||
if ($this->driver['namespace'] || str_contains($driver, '\\')) {
|
||||
$class = str_contains($driver, '\\') ? $driver : $this->driver['namespace'] . $this->studly($driver);
|
||||
if (class_exists($class)) {
|
||||
return $class;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function studly(string $value): string
|
||||
{
|
||||
$value = ucwords(str_replace(['-', '_'], ' ', $value));
|
||||
return str_replace(' ', '', $value);
|
||||
}
|
||||
|
||||
public function setTopic(string $topic): self
|
||||
{
|
||||
$this->topic = $topic;
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function checkIsImage(): bool
|
||||
{
|
||||
if (in_array($this->fileInfo['type'], ['image/gif', 'image/jpg', 'image/jpeg', 'image/bmp', 'image/png', 'image/webp'])
|
||||
|| in_array($this->fileInfo['suffix'], ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'webp'])) {
|
||||
$path = $this->file->getPathname();
|
||||
$imgInfo = is_file($path) ? getimagesize($path) : false;
|
||||
if (!$imgInfo || !isset($imgInfo[0]) || !isset($imgInfo[1])) {
|
||||
throw new RuntimeException(__('The uploaded image file is not a valid image'));
|
||||
}
|
||||
$this->fileInfo['width'] = $imgInfo[0];
|
||||
$this->fileInfo['height'] = $imgInfo[1];
|
||||
$this->isImage = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isImage(): bool
|
||||
{
|
||||
return $this->isImage;
|
||||
}
|
||||
|
||||
public function getSuffix(): string
|
||||
{
|
||||
return $this->fileInfo['suffix'] ?? 'file';
|
||||
}
|
||||
|
||||
public function getSaveName(?string $saveName = null, ?string $filename = null, ?string $sha1 = null): string
|
||||
{
|
||||
if ($filename) {
|
||||
$suffix = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
$suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file';
|
||||
} else {
|
||||
$suffix = $this->fileInfo['suffix'];
|
||||
}
|
||||
$filename = $filename ?? $this->fileInfo['name'];
|
||||
$sha1 = $sha1 ?? $this->fileInfo['sha1'];
|
||||
$replaceArr = [
|
||||
'{topic}' => $this->topic,
|
||||
'{year}' => date('Y'),
|
||||
'{mon}' => date('m'),
|
||||
'{day}' => date('d'),
|
||||
'{hour}' => date('H'),
|
||||
'{min}' => date('i'),
|
||||
'{sec}' => date('s'),
|
||||
'{random}' => Random::build(),
|
||||
'{random32}' => Random::build('alnum', 32),
|
||||
'{fileName}' => $this->getFileNameSubstr($filename, $suffix),
|
||||
'{suffix}' => $suffix,
|
||||
'{.suffix}' => $suffix ? '.' . $suffix : '',
|
||||
'{fileSha1}' => $sha1,
|
||||
];
|
||||
$saveName = $saveName ?? $this->config['save_name'];
|
||||
return Filesystem::fsFit(str_replace(array_keys($replaceArr), array_values($replaceArr), $saveName));
|
||||
}
|
||||
|
||||
public function validates(): void
|
||||
{
|
||||
if (empty($this->file)) {
|
||||
throw new RuntimeException(__('No files have been uploaded or the file size exceeds the upload limit of the server'));
|
||||
}
|
||||
|
||||
$size = Filesystem::fileUnitToByte($this->config['max_size'] ?? '10M');
|
||||
$mime = $this->checkConfig($this->config['allowed_mime_types'] ?? []);
|
||||
$suffix = $this->checkConfig($this->config['allowed_suffixes'] ?? '');
|
||||
|
||||
if ($this->fileInfo['size'] > $size) {
|
||||
throw new RuntimeException(__('The uploaded file is too large (%sMiB), Maximum file size:%sMiB', [
|
||||
round($this->fileInfo['size'] / pow(1024, 2), 2),
|
||||
round($size / pow(1024, 2), 2)
|
||||
]));
|
||||
}
|
||||
|
||||
if ($suffix && !in_array(strtolower($this->fileInfo['suffix']), $suffix)) {
|
||||
throw new RuntimeException(__('The uploaded file format is not allowed'));
|
||||
}
|
||||
|
||||
if ($mime && !in_array(strtolower($this->fileInfo['type']), $mime)) {
|
||||
throw new RuntimeException(__('The uploaded file format is not allowed'));
|
||||
}
|
||||
|
||||
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $this->topic)) {
|
||||
throw new RuntimeException(__('Topic format error'));
|
||||
}
|
||||
|
||||
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $this->driver['name'])) {
|
||||
throw new RuntimeException(__('Driver %s not supported', [$this->driver['name']]));
|
||||
}
|
||||
|
||||
if ($this->checkIsImage()) {
|
||||
$maxW = $this->config['image_max_width'] ?? 0;
|
||||
$maxH = $this->config['image_max_height'] ?? 0;
|
||||
if ($maxW && $this->fileInfo['width'] > $maxW) {
|
||||
throw new RuntimeException(__('The uploaded image file is not a valid image'));
|
||||
}
|
||||
if ($maxH && $this->fileInfo['height'] > $maxH) {
|
||||
throw new RuntimeException(__('The uploaded image file is not a valid image'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function upload(?string $saveName = null, int $adminId = 0, int $userId = 0): array
|
||||
{
|
||||
$this->validates();
|
||||
|
||||
$driver = $this->getDriver();
|
||||
if (!$driver) {
|
||||
throw new RuntimeException(__('Driver %s not supported', [$this->driver['name']]));
|
||||
}
|
||||
|
||||
$saveName = $saveName ?: $this->getSaveName();
|
||||
$params = [
|
||||
'topic' => $this->topic,
|
||||
'admin_id' => $adminId,
|
||||
'user_id' => $userId,
|
||||
'url' => $driver->url($saveName, false),
|
||||
'width' => $this->fileInfo['width'] ?? 0,
|
||||
'height' => $this->fileInfo['height'] ?? 0,
|
||||
'name' => $this->getFileNameSubstr($this->fileInfo['name'], $this->fileInfo['suffix'], 100) . '.' . $this->fileInfo['suffix'],
|
||||
'size' => $this->fileInfo['size'],
|
||||
'mimetype' => $this->fileInfo['type'],
|
||||
'storage' => $this->driver['name'],
|
||||
'sha1' => $this->fileInfo['sha1'],
|
||||
];
|
||||
|
||||
$attachment = Attachment::where('sha1', $params['sha1'])
|
||||
->where('topic', $params['topic'])
|
||||
->where('storage', $params['storage'])
|
||||
->find();
|
||||
|
||||
if ($attachment && $driver->exists($attachment->url)) {
|
||||
$attachment->quote++;
|
||||
$attachment->last_upload_time = time();
|
||||
} else {
|
||||
$driver->save($this->file, $saveName);
|
||||
$attachment = new Attachment();
|
||||
$attachment->data(array_filter($params));
|
||||
}
|
||||
$attachment->save();
|
||||
return $attachment->toArray();
|
||||
}
|
||||
|
||||
public function getFileNameSubstr(string $fileName, string $suffix, int $length = 15): string
|
||||
{
|
||||
$pattern = "/[\s:@#?&\/=',+]+/u";
|
||||
$fileName = str_replace(".$suffix", '', $fileName);
|
||||
$fileName = preg_replace($pattern, '', $fileName);
|
||||
return mb_substr(htmlspecialchars(strip_tags($fileName)), 0, $length);
|
||||
}
|
||||
|
||||
protected function checkConfig(mixed $configItem): array
|
||||
{
|
||||
if (is_array($configItem)) {
|
||||
return array_map('strtolower', $configItem);
|
||||
}
|
||||
return $configItem ? explode(',', strtolower((string)$configItem)) : [];
|
||||
}
|
||||
}
|
||||
66
app/common/library/token/Driver.php
Normal file
66
app/common/library/token/Driver.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library\token;
|
||||
|
||||
/**
|
||||
* Token 驱动抽象类
|
||||
*/
|
||||
abstract class Driver
|
||||
{
|
||||
/**
|
||||
* 具体驱动的句柄 Mysql|Redis
|
||||
* @var object
|
||||
*/
|
||||
protected object $handler;
|
||||
|
||||
/**
|
||||
* @var array 配置数据
|
||||
*/
|
||||
protected array $options = [];
|
||||
|
||||
/**
|
||||
* 设置 token
|
||||
*/
|
||||
abstract public function set(string $token, string $type, int $userId, ?int $expire = null): bool;
|
||||
|
||||
/**
|
||||
* 获取 token 的数据
|
||||
*/
|
||||
abstract public function get(string $token): array;
|
||||
|
||||
/**
|
||||
* 检查 token 是否有效
|
||||
*/
|
||||
abstract public function check(string $token, string $type, int $userId): bool;
|
||||
|
||||
/**
|
||||
* 删除一个 token
|
||||
*/
|
||||
abstract public function delete(string $token): bool;
|
||||
|
||||
/**
|
||||
* 清理一个用户的所有 token
|
||||
*/
|
||||
abstract public function clear(string $type, int $userId): bool;
|
||||
|
||||
/**
|
||||
* 返回句柄对象
|
||||
*/
|
||||
public function handler(): ?object
|
||||
{
|
||||
return $this->handler;
|
||||
}
|
||||
|
||||
protected function getEncryptedToken(string $token): string
|
||||
{
|
||||
$config = config('buildadmin.token');
|
||||
return hash_hmac($config['algo'], $token, $config['key']);
|
||||
}
|
||||
|
||||
protected function getExpiredIn(int $expireTime): int
|
||||
{
|
||||
return $expireTime ? max(0, $expireTime - time()) : 365 * 86400;
|
||||
}
|
||||
}
|
||||
27
app/common/library/token/TokenExpirationException.php
Normal file
27
app/common/library/token/TokenExpirationException.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library\token;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Token 过期异常
|
||||
*/
|
||||
class TokenExpirationException extends Exception
|
||||
{
|
||||
public function __construct(
|
||||
protected string $message = '',
|
||||
protected int $code = 409,
|
||||
protected array $data = [],
|
||||
?\Throwable $previous = null
|
||||
) {
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public function getData(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
}
|
||||
117
app/common/library/token/driver/Mysql.php
Normal file
117
app/common/library/token/driver/Mysql.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library\token\driver;
|
||||
|
||||
use support\think\Db;
|
||||
use app\common\library\token\Driver;
|
||||
|
||||
/**
|
||||
* Token Mysql 驱动
|
||||
* @see Driver
|
||||
*/
|
||||
class Mysql extends Driver
|
||||
{
|
||||
protected array $options = [];
|
||||
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
if (!empty($options)) {
|
||||
$this->options = array_merge($this->options, $options);
|
||||
}
|
||||
|
||||
if (!empty($this->options['name'])) {
|
||||
$this->handler = Db::connect($this->options['name'])->name($this->options['table']);
|
||||
} else {
|
||||
$this->handler = Db::name($this->options['table']);
|
||||
}
|
||||
}
|
||||
|
||||
public function set(string $token, string $type, int $userId, ?int $expire = null): bool
|
||||
{
|
||||
if ($expire === null) {
|
||||
$expire = $this->options['expire'] ?? 2592000;
|
||||
}
|
||||
$expireTime = $expire !== 0 ? time() + $expire : 0;
|
||||
$encryptedToken = $this->getEncryptedToken($token);
|
||||
$this->handler->insert([
|
||||
'token' => $encryptedToken,
|
||||
'type' => $type,
|
||||
'user_id' => $userId,
|
||||
'create_time' => time(),
|
||||
'expire_time' => $expireTime,
|
||||
]);
|
||||
|
||||
// 每隔 48 小时清理一次过期 Token
|
||||
$time = time();
|
||||
$lastCacheCleanupTime = $this->getLastCacheCleanupTime();
|
||||
if (!$lastCacheCleanupTime || $lastCacheCleanupTime < $time - 172800) {
|
||||
$this->setLastCacheCleanupTime($time);
|
||||
$this->handler->where('expire_time', '<', time())->where('expire_time', '>', 0)->delete();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function get(string $token): array
|
||||
{
|
||||
$data = $this->handler->where('token', $this->getEncryptedToken($token))->find();
|
||||
if (!$data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$data['token'] = $token;
|
||||
$data['expires_in'] = $this->getExpiredIn($data['expire_time'] ?? 0);
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function check(string $token, string $type, int $userId): bool
|
||||
{
|
||||
$data = $this->get($token);
|
||||
if (!$data || ($data['expire_time'] && $data['expire_time'] <= time())) {
|
||||
return false;
|
||||
}
|
||||
return $data['type'] == $type && $data['user_id'] == $userId;
|
||||
}
|
||||
|
||||
public function delete(string $token): bool
|
||||
{
|
||||
$this->handler->where('token', $this->getEncryptedToken($token))->delete();
|
||||
return true;
|
||||
}
|
||||
|
||||
public function clear(string $type, int $userId): bool
|
||||
{
|
||||
$this->handler->where('type', $type)->where('user_id', $userId)->delete();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用文件存储 last_cache_cleanup_time(兼容无 cache 插件环境)
|
||||
*/
|
||||
private function getLastCacheCleanupTime(): ?int
|
||||
{
|
||||
$path = $this->getCleanupTimePath();
|
||||
if (!is_file($path)) {
|
||||
return null;
|
||||
}
|
||||
$v = file_get_contents($path);
|
||||
return $v !== false && $v !== '' ? (int) $v : null;
|
||||
}
|
||||
|
||||
private function setLastCacheCleanupTime(int $time): void
|
||||
{
|
||||
$path = $this->getCleanupTimePath();
|
||||
$dir = dirname($path);
|
||||
if (!is_dir($dir)) {
|
||||
@mkdir($dir, 0755, true);
|
||||
}
|
||||
@file_put_contents($path, (string) $time);
|
||||
}
|
||||
|
||||
private function getCleanupTimePath(): string
|
||||
{
|
||||
$base = defined('RUNTIME_PATH') ? RUNTIME_PATH : (base_path() . DIRECTORY_SEPARATOR . 'runtime');
|
||||
return $base . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR . 'token_last_cleanup.txt';
|
||||
}
|
||||
}
|
||||
35
app/common/library/upload/Driver.php
Normal file
35
app/common/library/upload/Driver.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library\upload;
|
||||
|
||||
/**
|
||||
* 上传驱动抽象类(Webman 迁移版,支持 WebmanUploadedFile)
|
||||
*/
|
||||
abstract class Driver
|
||||
{
|
||||
protected array $options = [];
|
||||
|
||||
/**
|
||||
* 保存文件
|
||||
* @param WebmanUploadedFile $file
|
||||
* @param string $saveName
|
||||
*/
|
||||
abstract public function save(WebmanUploadedFile $file, string $saveName): bool;
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*/
|
||||
abstract public function delete(string $saveName): bool;
|
||||
|
||||
/**
|
||||
* 获取资源 URL 地址
|
||||
*/
|
||||
abstract public function url(string $saveName, string|bool $domain = true, string $default = ''): string;
|
||||
|
||||
/**
|
||||
* 文件是否存在
|
||||
*/
|
||||
abstract public function exists(string $saveName): bool;
|
||||
}
|
||||
66
app/common/library/upload/WebmanUploadedFile.php
Normal file
66
app/common/library/upload/WebmanUploadedFile.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library\upload;
|
||||
|
||||
use Webman\Http\UploadFile;
|
||||
|
||||
/**
|
||||
* Webman UploadFile 适配器,提供 ThinkPHP UploadedFile 兼容接口
|
||||
*/
|
||||
class WebmanUploadedFile
|
||||
{
|
||||
public function __construct(
|
||||
protected UploadFile $file
|
||||
) {
|
||||
}
|
||||
|
||||
public function extension(): string
|
||||
{
|
||||
$ext = $this->file->getUploadExtension();
|
||||
return $ext && preg_match("/^[a-zA-Z0-9]+$/", $ext) ? $ext : 'file';
|
||||
}
|
||||
|
||||
public function getMime(): string
|
||||
{
|
||||
return $this->file->getUploadMimeType() ?? '';
|
||||
}
|
||||
|
||||
public function getSize(): int
|
||||
{
|
||||
$size = $this->file->getSize();
|
||||
return $size !== false ? $size : 0;
|
||||
}
|
||||
|
||||
public function getOriginalName(): string
|
||||
{
|
||||
return $this->file->getUploadName() ?? '';
|
||||
}
|
||||
|
||||
public function sha1(): string
|
||||
{
|
||||
$path = $this->file->getPathname();
|
||||
return is_file($path) ? hash_file('sha1', $path) : '';
|
||||
}
|
||||
|
||||
public function getPathname(): string
|
||||
{
|
||||
return $this->file->getPathname();
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动文件(兼容 ThinkPHP move($dir, $name) 与 Webman move($fullPath))
|
||||
*/
|
||||
public function move(string $directory, ?string $name = null): self
|
||||
{
|
||||
$destination = rtrim($directory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . ($name ?? basename($this->file->getUploadName()));
|
||||
$this->file->move($destination);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUploadFile(): UploadFile
|
||||
{
|
||||
return $this->file;
|
||||
}
|
||||
}
|
||||
105
app/common/library/upload/driver/Local.php
Normal file
105
app/common/library/upload/driver/Local.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library\upload\driver;
|
||||
|
||||
use ba\Filesystem;
|
||||
use app\common\library\upload\Driver;
|
||||
use app\common\library\upload\WebmanUploadedFile;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* 上传到本地磁盘的驱动(Webman 迁移版)
|
||||
*/
|
||||
class Local extends Driver
|
||||
{
|
||||
protected array $options = [];
|
||||
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
$this->options = config('filesystem.disks.public', []);
|
||||
if (!empty($options)) {
|
||||
$this->options = array_merge($this->options, $options);
|
||||
}
|
||||
}
|
||||
|
||||
public function save(WebmanUploadedFile $file, string $saveName): bool
|
||||
{
|
||||
$savePathInfo = pathinfo($saveName);
|
||||
$saveFullPath = $this->getFullPath($saveName);
|
||||
$destination = rtrim($saveFullPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $savePathInfo['basename'];
|
||||
|
||||
$saveDir = dirname($destination);
|
||||
if (!is_dir($saveDir) && !@mkdir($saveDir, 0755, true)) {
|
||||
throw new RuntimeException(__('Failed to create upload directory'));
|
||||
}
|
||||
|
||||
$uploadFile = $file->getUploadFile();
|
||||
$uploadFile->move($destination);
|
||||
@chmod($destination, 0666 & ~umask());
|
||||
return true;
|
||||
}
|
||||
|
||||
public function delete(string $saveName): bool
|
||||
{
|
||||
$saveFullName = $this->getFullPath($saveName, true);
|
||||
if ($this->exists($saveFullName)) {
|
||||
@unlink($saveFullName);
|
||||
}
|
||||
Filesystem::delEmptyDir(dirname($saveFullName));
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url(string $saveName, string|bool $domain = true, string $default = ''): string
|
||||
{
|
||||
$saveName = $this->clearRootPath($saveName);
|
||||
$saveName = $saveName ? '/' . ltrim(str_replace('\\', '/', $saveName), '/') : '';
|
||||
|
||||
if ($domain === true) {
|
||||
$req = function_exists('request') ? request() : null;
|
||||
$domain = $req && method_exists($req, 'host') ? '//' . $req->host() : '';
|
||||
} elseif ($domain === false) {
|
||||
$domain = '';
|
||||
}
|
||||
|
||||
$saveName = $saveName ?: $default;
|
||||
if (!$saveName) return $domain;
|
||||
|
||||
$regex = "/^((?:[a-z]+:)?\/\/|data:image\/)(.*)/i";
|
||||
if (preg_match('/^http(s)?:\/\//', $saveName) || preg_match($regex, $saveName) || $domain === false) {
|
||||
return $saveName;
|
||||
}
|
||||
return str_replace('\\', '/', $domain . $saveName);
|
||||
}
|
||||
|
||||
public function exists(string $saveName): bool
|
||||
{
|
||||
$saveFullName = $this->getFullPath($saveName, true);
|
||||
return file_exists($saveFullName);
|
||||
}
|
||||
|
||||
public function getFullPath(string $saveName, bool $baseName = false): string
|
||||
{
|
||||
$savePathInfo = pathinfo($saveName);
|
||||
$root = $this->getRootPath();
|
||||
$dirName = $savePathInfo['dirname'] . '/';
|
||||
|
||||
if (str_starts_with($saveName, $root)) {
|
||||
return Filesystem::fsFit($baseName || !isset($savePathInfo['extension']) ? $saveName : $dirName);
|
||||
}
|
||||
|
||||
return Filesystem::fsFit($root . $dirName . ($baseName ? $savePathInfo['basename'] : ''));
|
||||
}
|
||||
|
||||
public function clearRootPath(string $saveName): string
|
||||
{
|
||||
return str_replace($this->getRootPath(), '', Filesystem::fsFit($saveName));
|
||||
}
|
||||
|
||||
public function getRootPath(): string
|
||||
{
|
||||
$root = $this->options['root'] ?? rtrim(base_path(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'public';
|
||||
return Filesystem::fsFit($root);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user