first commit

This commit is contained in:
JaguarJack
2022-12-05 23:01:12 +08:00
commit 0024080c28
322 changed files with 27698 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
<?php
namespace Catch\Support;
use Illuminate\Support\Composer as LaravelComposer;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\File;
use Laravel\SerializableClosure\Exceptions\PhpVersionNotSupportedException;
class Composer extends LaravelComposer
{
protected bool $ignorePlatformReqs = false;
/**
* require package
* @param string $package
* @return string
* @throws PhpVersionNotSupportedException
*/
public function require(string $package): string
{
$this->checkPHPVersion();
$command = ['require', $package];
return $this->runCommand($command);
}
/**
* require dev-package
*
* @param string $package
* @return string
* @throws PhpVersionNotSupportedException
*/
public function requireDev(string $package): string
{
$this->checkPHPVersion();
$command = ['require', '--dev', $package];
return $this->runCommand($command);
}
/**
* remove
*
* @param string $package
*/
public function remove(string $package)
{
$this->runCommand([
'remove', $package
]);
}
/**
*
* @param array $command
* @return string
*/
protected function runCommand(array $command): string
{
$command = array_merge($this->findComposer(), $command);
if ($this->ignorePlatformReqs) {
$command[] = '--ignore-platform-reqs';
}
$process = $this->getProcess($command);
$process->run();
return $process->getOutput();
}
/**
*
* @throws PhpVersionNotSupportedException
* @return void
*/
protected function checkPHPVersion(): void
{
$composerJson = json_decode(File::get(base_path().DIRECTORY_SEPARATOR.'composer.json'), true);
$phpVersion = PHP_VERSION;
$needPHPVersion = Str::of($composerJson['require']['php'])->remove('^');
if (version_compare($phpVersion, $needPHPVersion, '<') && ! $this->ignorePlatformReqs) {
throw new PhpVersionNotSupportedException("PHP $phpVersion 版本太低, 需要 PHP {$needPHPVersion}!如果想忽略版本要求, s可使用 {ignorePlatFormReqs} 方法然后安装");
}
}
/**
*
* @return $this
*/
public function ignorePlatFormReqs(): static
{
$this->ignorePlatformReqs = true;
return $this;
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Catch\Support\DB;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
class Query
{
/**
* @var string|null
*/
protected static string|null $log = null;
/**
* @return void
*/
public static function listen(): void
{
DB::listen(function ($query) {
$sql = str_replace(
'?',
'%s',
sprintf('[%s] '.$query->sql.' | %s ms'.PHP_EOL, date('Y-m-d H:i'), $query->time)
);
static::$log .= vsprintf($sql, $query->bindings);
});
}
/**
* @return void
*/
public static function log(): void
{
if (static::$log) {
$sqlLogPath = storage_path('logs'.DIRECTORY_SEPARATOR.'query'.DIRECTORY_SEPARATOR);
if (! File::isDirectory($sqlLogPath)) {
File::makeDirectory($sqlLogPath, 0777, true);
}
$logFile = $sqlLogPath.date('Ymd').'.log';
if (! File::exists($logFile)) {
File::put($logFile, '', true);
}
file_put_contents($logFile, static::$log.PHP_EOL, LOCK_EX | FILE_APPEND);
static::$log = null;
}
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Catch\Support\DB;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class SoftDelete extends SoftDeletingScope
{
/**
* @param Builder $builder
* @param Model $model
*/
public function apply(Builder $builder, Model $model)
{
$builder->where($model->getQualifiedDeletedAtColumn(), '=', 0);
}
}

View File

@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace Catch\Support\Macros;
use Illuminate\Database\Schema\Blueprint as LaravelBlueprint;
class Blueprint
{
/**
* boot;
*/
public static function boot(): void
{
$bluePrint = new static();
$bluePrint->createdAt();
$bluePrint->updatedAt();
$bluePrint->deletedAt();
$bluePrint->status();
$bluePrint->creatorId();
$bluePrint->unixTimestamp();
$bluePrint->parentId();
$bluePrint->sort();
}
/**
* created unix timestamp
*
* @return void
*/
public function createdAt(): void
{
LaravelBlueprint::macro(__FUNCTION__, function () {
$this->unsignedInteger('created_at')->default(0)->comment('created time');
});
}
/**
* update unix timestamp
*
* @return void
*/
public function updatedAt(): void
{
LaravelBlueprint::macro(__FUNCTION__, function () {
$this->unsignedInteger('updated_at')->default(0)->comment('updated time');
});
}
/**
* soft delete
*
* @return void
*/
public function deletedAt(): void
{
LaravelBlueprint::macro(__FUNCTION__, function () {
$this->unsignedInteger('deleted_at')->default(0)->comment('delete time');
});
}
/**
* unix timestamp
*
* @param bool $softDeleted
* @return void
*/
public function unixTimestamp(bool $softDeleted = true): void
{
LaravelBlueprint::macro(__FUNCTION__, function () use ($softDeleted) {
$this->createdAt();
$this->updatedAt();
if ($softDeleted) {
$this->deletedAt();
}
});
}
/**
* creator id
*
* @return void
*/
public function creatorId(): void
{
LaravelBlueprint::macro(__FUNCTION__, function () {
$this->unsignedInteger('creator_id')->default(0)->comment('creator id');
});
}
/**
* parent ID
*
* @return void
*/
public function parentId(): void
{
LaravelBlueprint::macro(__FUNCTION__, function () {
$this->unsignedInteger('parent_id')->default(0)->comment('parent id');
});
}
/**
* status
*
* @return void
*/
public function status(): void
{
LaravelBlueprint::macro(__FUNCTION__, function ($default = 1) {
$this->tinyInteger('status')->default($default)->comment('1:normal 2: forbidden');
});
}
/**
* sort
*
* @param int $default
* @return void
*/
public function sort(int $default = 1): void
{
LaravelBlueprint::macro(__FUNCTION__, function () use ($default) {
$this->integer('sort')->comment('sort')->default($default);
});
}
}

View File

@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace Catch\Support\Macros;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Builder as LaravelBuilder;
class Builder
{
/**
* boot
*/
public static function boot(): void
{
$builder = new static();
$builder->whereLike();
$builder->quickSearch();
$builder->tree();
}
/**
* where like
*
* @return void
*/
public function whereLike(): void
{
LaravelBuilder::macro(__FUNCTION__, function ($filed, $value) {
return $this->where($filed, 'like', "%$value%");
});
}
/**
* quick search
*
* @return void
*/
public function quickSearch(): void
{
LaravelBuilder::macro(__FUNCTION__, function (array $params = []) {
$params = array_merge(request()->all(), $params);
if (! property_exists($this->model, 'searchable')) {
return $this;
}
// filter null & empty string
$params = array_filter($params, function ($value) {
return (is_string($value) && strlen($value)) || is_numeric($value);
});
$wheres = [];
if (! empty($this->model->searchable)) {
foreach ($this->model->searchable as $field => $op) {
// 临时变量
$_field = $field;
// contains alias
if (str_contains($field, '.')) {
[, $_field] = explode('.', $field);
}
if (isset($params[$_field])) {
$opString = Str::of($op)->lower();
if ($opString->exactly('op')) {
$value = implode(',', $params[$_field]);
} elseif ($opString->exactly('like')) {
$value = "%{$params[$_field]}%";
} elseif ($opString->exactly('rlike')) {
$value = "{$params[$_field]}%";
} elseif ($opString->exactly('llike')) {
$value = "%{$params[$_field]}";
} else {
$value = $params[$_field];
}
$wheres[] = [$field, $op, $value];
}
}
}
$this->where($wheres);
return $this;
});
}
/**
* where like
*
* @time 2021年08月06日
* @return void
*/
public function tree(): void
{
LaravelBuilder::macro(__FUNCTION__, function (string $id, string $parentId, ...$fields) {
$fields = array_merge([$id, $parentId], $fields);
return $this->get($fields)->toTree(0, $parentId);
});
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Catch\Support\Macros;
use Catch\Support\Tree;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Collection as LaravelCollection;
class Collection
{
/**
* boot
*/
public static function boot(): void
{
$collection = new static();
$collection->toOptions();
$collection->toTree();
}
/**
* collection to tree
*
* @return void
*/
public function toTree(): void
{
LaravelCollection::macro(__FUNCTION__, function (int $pid = 0, string $pidField = 'parent_id', string $child = 'children') {
return Tree::done($this->all(), $pid, $pidField, $child);
});
}
/**
* toOptions
*
* @return void
*/
public function toOptions(): void
{
LaravelCollection::macro(__FUNCTION__, function () {
return $this->transform(function ($item, $key) use (&$options) {
if ($item instanceof Arrayable) {
$item = $item->toArray();
}
if (is_array($item)) {
$item = array_values($item);
return [
'value' => $item[0],
'label' => $item[1]
];
} else {
return [
'value' => $key,
'label' => $item
];
}
})->values();
});
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Catch\Support\Macros;
/**
* boot
*/
class Register
{
/**
* macros boot
*/
public static function boot(): void
{
Blueprint::boot();
Collection::boot();
Builder::boot();
}
}

View File

@@ -0,0 +1,171 @@
<?php
// +----------------------------------------------------------------------
// | CatchAdmin [Just Like ]
// +----------------------------------------------------------------------
// | Copyright (c) 2017~2021 https://catchadmin.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( https://github.com/JaguarJack/catchadmin-laravel/blob/master/LICENSE.md )
// +----------------------------------------------------------------------
// | Author: JaguarJack [ njphper@gmail.com ]
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace Catch\Support\Module\Driver;
use Catch\CatchAdmin;
use Catch\Contracts\ModuleRepositoryInterface;
use Catch\Enums\Status;
use Catch\Exceptions\FailedException;
use Illuminate\Container\Container;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
/**
* DatabaseDriver
*/
class DatabaseDriver implements ModuleRepositoryInterface
{
protected Model $model;
public function __construct()
{
$this->model = $this->createModuleModel();
}
/**
* all
*
* @param array $search
* @return Collection
*/
public function all(array $search): Collection
{
return $this->model::query()
->when($search['name'] ?? false, function ($query) use ($search) {
$query->where('name', 'like', '%'.$search['name'].'%');
})->get();
}
/**
* create module json
*
* @param array $module
* @return bool|int
*/
public function create(array $module): bool|int
{
$this->hasSameModule($module);
return $this->model->save([
'name' => $module['name'],
'path' => $module['path'],
'description' => $module['desc'],
'keywords' => $module['keywords'],
'service' => sprintf('\\%s%s', CatchAdmin::getModuleNamespace($module['name']), ucfirst($module['name']).'ServiceProvider'),
]);
}
/**
* module info
*
* @param string $name
* @return Collection
*/
public function show(string $name): Collection
{
return $this->model->where('name', $name)->first();
}
/**
* update module json
*
* @param string $name
* @param array $module
* @return bool|int
*/
public function update(string $name, array $module): bool|int
{
return $this->model->where('name', $name)
->update([
'name' => $module['name'],
'alias' => $module['alias'],
'description' => $module['desc'],
'keywords' => $module['keywords'],
]);
}
/**
* delete module json
*
* @param string $name
* @return bool|int
*/
public function delete(string $name): bool|int
{
return $this->model->where('name', $name)->delete();
}
/**
* disable or enable
*
* @param $name
* @return bool|int
*/
public function disOrEnable($name): bool|int
{
$module = $this->show($name);
$module->status = (int) $module->status;
return $module->save();
}
/**
* get enabled
*
* @return Collection
*/
public function getEnabled(): Collection
{
// TODO: Implement getEnabled() method.
return $this->model->where('status', Status::Enable->value())->get();
}
/**
*
* @param array $module
* @return void
*/
protected function hasSameModule(array $module): void
{
if ($this->model->where('name', $module['name'])->first()) {
throw new FailedException(sprintf('Module [%s] has been created', $module['name']));
}
if ($this->model->where('alias', $module['alias'])->first()) {
throw new FailedException(sprintf('Module Alias [%s] has been exised', $module['alias']));
}
}
/**
* create model
* @return Model
*/
protected function createModuleModel(): Model
{
return new class () extends Model {
protected $table;
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->table = Container::getInstance()->make('config')->get('catch.module.driver.table_name');
}
};
}
}

View File

@@ -0,0 +1,193 @@
<?php
// +----------------------------------------------------------------------
// | CatchAdmin [Just Like ]
// +----------------------------------------------------------------------
// | Copyright (c) 2017~2021 https://catchadmin.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( https://github.com/JaguarJack/catchadmin-laravel/blob/master/LICENSE.md )
// +----------------------------------------------------------------------
// | Author: JaguarJack [ njphper@gmail.com ]
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace Catch\Support\Module\Driver;
use Catch\CatchAdmin;
use Catch\Contracts\ModuleRepositoryInterface;
use Catch\Exceptions\FailedException;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
/**
* FileDriver
*/
class FileDriver implements ModuleRepositoryInterface
{
protected string $moduleJson;
/**
* construct
*/
public function __construct()
{
$this->moduleJson = storage_path('app').DIRECTORY_SEPARATOR.'modules.json';
}
/**
* all
*
* @param array $search
* @return Collection
*/
public function all(array $search = []): Collection
{
if (! File::exists($this->moduleJson)) {
return Collection::make([]);
}
if (! Str::length(File::get($this->moduleJson))) {
return Collection::make([]);
}
$modules = Collection::make(\json_decode(File::get($this->moduleJson), true))->values();
$name = $search['name'] ?? '';
if (! $name) {
return $modules;
}
return $modules->filter(function ($module) use ($name) {
return Str::of($module['name'])->contains($name);
});
}
/**
* create module json
*
* @param array $module
* @return bool
*/
public function create(array $module): bool
{
$modules = $this->all();
$this->hasSameModule($module, $modules);
$module['service'] = sprintf('\\%s', CatchAdmin::getModuleServiceProvider($module['path']));
$module['version'] = '1.0.0';
$module['enable'] = true;
File::put($this->moduleJson, $modules->push($module)->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
return true;
}
/**
* module info
*
* @param string $name
* @return Collection
*/
public function show(string $name): Collection
{
foreach ($this->all() as $module) {
if (Str::of($module['name'])->exactly($name)) {
return Collection::make($module);
}
}
throw new FailedException("Module [$name] not Found");
}
/**
* update module json
*
* @param string $name
* @param array $module
* @return bool
*/
public function update(string $name, array $module): bool
{
File::put($this->moduleJson, $this->all()->map(function ($m) use ($module, $name) {
if (Str::of($name)->exactly($m['name'])) {
$m['path'] = $module['path'];
$m['name'] = $module['name'];
$m['description'] = $module['description'] ?? '';
$m['keywords'] = $module['keywords'] ?? '';
$m['enable'] = $module['enable'];
}
return $m;
})->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
return true;
}
/**
* delete module json
*
* @param string $name
* @return bool
*/
public function delete(string $name): bool
{
File::put($this->moduleJson, $this->all()->filter(function ($module) use ($name) {
if (! Str::of($name)->exactly($module['name'])) {
return $module;
}
})->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
return true;
}
/**
* disable or enable
*
* @param $name
* @return bool|int
*/
public function disOrEnable($name): bool|int
{
return File::put($this->moduleJson, $this->all()->map(function ($module) use ($name) {
if (Str::of($module['name'])->exactly($name)) {
$module['enable'] = ! $module['enable'];
}
return $module;
})->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
/**
* get enabled
*
* @return Collection
*/
public function getEnabled(): Collection
{
// TODO: Implement getEnabled() method.
return $this->all()->where('enable', true)->values();
}
/**
*
* @param array $module
* @param Collection $modules
* @return void
*/
protected function hasSameModule(array $module, Collection $modules): void
{
if ($modules->count()) {
if ($modules->pluck('name')->contains($module['name'])) {
throw new FailedException(sprintf('Module [%s] has been created', $module['name']));
}
if ($modules->pluck('path')->contains($module['path'])) {
throw new FailedException(sprintf('Module path [%s] has been existed', $module['path']));
}
}
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace Catch\Support\Module;
use Catch\Contracts\ModuleRepositoryInterface;
use Catch\Support\Composer;
/**
* installer
*/
abstract class Installer
{
/**
* construct
*
* @param ModuleRepositoryInterface $moduleRepository
*/
public function __construct(protected ModuleRepositoryInterface $moduleRepository)
{
}
/**
* module info
*
* @return array
*/
abstract protected function info(): array;
/**
* migration
*
* @return string
*/
abstract protected function migration(): string;
/**
* seed
*
* @return string
*/
abstract protected function seeder(): string;
/**
* require packages
*
* @return void
*/
abstract protected function requirePackages(): void;
/**
* remove packages
*
* @return void
*/
abstract protected function removePackages(): void;
/**
* uninstall
*
* @return void
*/
public function uninstall(): void
{
$this->moduleRepository->delete($this->info()['name']);
$this->removePackages();
}
/**
* invoke
*
* @return void
*/
public function __invoke(): void
{
// TODO: Implement __invoke() method.
$this->moduleRepository->create($this->info());
// migration
// seed
$this->requirePackages();
}
/**
* composer installer
*
* @return Composer
*/
protected function composer(): Composer
{
return app(Composer::class);
}
}

View File

@@ -0,0 +1,63 @@
<?php
// +----------------------------------------------------------------------
// | CatchAdmin [Just Like ]
// +----------------------------------------------------------------------
// | Copyright (c) 2017~2021 https://catchadmin.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( https://github.com/JaguarJack/catchadmin-laravel/blob/master/LICENSE.md )
// +----------------------------------------------------------------------
// | Author: JaguarJack [ njphper@gmail.com ]
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace Catch\Support\Module;
use Catch\Support\Module\Driver\DatabaseDriver;
use Catch\Support\Module\Driver\FileDriver;
use Illuminate\Contracts\Container\Container;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Manager;
class ModuleManager extends Manager
{
public function __construct(Container|\Closure $container)
{
if ($container instanceof \Closure) {
$container = $container();
}
parent::__construct($container);
}
/**
* @return string
*/
public function getDefaultDriver(): string
{
// TODO: Implement getDefaultDriver() method.
return $this->config->get('catch.module.driver.default');
}
/**
* create file driver
*
* @return FileDriver
*/
public function createFileDriver(): FileDriver
{
return new FileDriver();
}
/**
* create database driver
*
* @return DatabaseDriver
*/
public function createDatabaseDriver(): DatabaseDriver
{
return new DatabaseDriver();
}
}

View File

@@ -0,0 +1,144 @@
<?php
// +----------------------------------------------------------------------
// | CatchAdmin [Just Like ]
// +----------------------------------------------------------------------
// | Copyright (c) 2017~2021 https://catchadmin.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( https://github.com/JaguarJack/catchadmin-laravel/blob/master/LICENSE.md )
// +----------------------------------------------------------------------
// | Author: JaguarJack [ njphper@gmail.com ]
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace Catch\Support\Module;
use Catch\Contracts\ModuleRepositoryInterface;
use Catch\Events\Module\Created;
use Catch\Events\Module\Creating;
use Catch\Events\Module\Deleted;
use Catch\Events\Module\Updated;
use Catch\Events\Module\Updating;
use Exception;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Event;
/**
* FileDriver
*/
class ModuleRepository
{
protected ModuleRepositoryInterface $moduleRepository;
/**
* construct
*/
public function __construct(ModuleRepositoryInterface $moduleRepository)
{
$this->moduleRepository = $moduleRepository;
}
/**
* all
*
* @param array $search
* @return Collection
*/
public function all(array $search): Collection
{
return $this->moduleRepository->all($search);
}
/**
* create module json
*
* @param array $module
* @return bool
*/
public function create(array $module): bool
{
Event::dispatch(new Creating($module));
$this->moduleRepository->create($module);
Event::dispatch(new Created($module));
return true;
}
/**
* module info
*
* @param string $name
* @return Collection
* @throws Exception
*/
public function show(string $name): Collection
{
try {
return $this->moduleRepository->show($name);
} catch (Exception $e) {
throw new $e();
}
}
/**
* update module json
*
* @param string $name
* @param array $module
* @return bool
*/
public function update(string $name, array $module): bool
{
Event::dispatch(new Updating($name, $module));
$this->moduleRepository->update($name, $module);
Event::dispatch(new Updated($name, $module));
return true;
}
/**
* delete module json
*
* @param string $name
* @return bool
* @throws Exception
*/
public function delete(string $name): bool
{
$module = $this->show($name);
$this->moduleRepository->delete($name);
Event::dispatch(new Deleted($module));
return true;
}
/**
* disable or enable
*
* @param string $name
* @return bool|int
*/
public function disOrEnable(string $name): bool|int
{
return $this->moduleRepository->disOrEnable($name);
}
/**
* get enabled
*
* @return Collection
*/
public function getEnabled(): Collection
{
// TODO: Implement getEnabled() method.
return $this->moduleRepository->getEnabled();
}
}

View File

@@ -0,0 +1,61 @@
<?php
// +----------------------------------------------------------------------
// | CatchAdmin [Just Like ]
// +----------------------------------------------------------------------
// | Copyright (c) 2017~2021 https://catchadmin.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( https://github.com/JaguarJack/catchadmin-laravel/blob/master/LICENSE.md )
// +----------------------------------------------------------------------
// | Author: JaguarJack [ njphper@gmail.com ]
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace Catch\Support;
class Tree
{
protected static string $pk = 'id';
/**
*
* @param string $pk
* @return Tree
*/
public static function setPk(string $pk): Tree
{
self::$pk = $pk;
return new self();
}
/**
* return done
*
* @param array $items
* @param int $pid
* @param string $pidField
* @param string $child
* @return array
*/
public static function done(array $items, int $pid = 0, string $pidField = 'parent_id', string $child = 'children'): array
{
$tree = [];
foreach ($items as $item) {
if ($item[$pidField] == $pid) {
$children = self::done($items, $item[self::$pk], $pidField, $child);
if (count($children)) {
$item[$child] = $children;
}
$tree[] = $item;
}
}
return $tree;
}
}

View File

@@ -0,0 +1,200 @@
<?php
// +----------------------------------------------------------------------
// | CatchAdmin [Just Like ]
// +----------------------------------------------------------------------
// | Copyright (c) 2017~2021 https://catchadmin.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( https://github.com/JaguarJack/catchadmin-laravel/blob/master/LICENSE.md )
// +----------------------------------------------------------------------
// | Author: JaguarJack [ njphper@gmail.com ]
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace Catch\Support\Zip;
use Exception;
use ZipArchive;
class ZipRepository
{
private mixed $archive;
/**
* Construct with a given path
*
* @param $filePath
* @param bool $create
* @param $archive
*
* @return void
* @throws Exception
*
*/
public function __construct($filePath, bool $create, $archive = null)
{
//Check if ZipArchive is available
if (! class_exists('ZipArchive')) {
throw new Exception('Error: Your PHP version is not compiled with zip support');
}
$this->archive = $archive ? $archive : new ZipArchive();
$res = $this->archive->open($filePath, ($create ? ZipArchive::CREATE : 0));
if ($res !== true) {
throw new Exception("Error: Failed to open $filePath! Error: ".$this->getErrorMessage($res));
}
}
/**
* Add a file to the opened Archive
*
* @param $pathToFile
* @param $pathInArchive
*/
public function addFile($pathToFile, $pathInArchive): void
{
$this->archive->addFile($pathToFile, $pathInArchive);
}
/**
* Add an empty directory
*
* @param $dirName
*/
public function addEmptyDir($dirName): void
{
$this->archive->addEmptyDir($dirName);
}
/**
* Add a file to the opened Archive using its contents
*
* @param string $name
* @param $content
*/
public function addFromString(string $name, $content): void
{
$this->archive->addFromString($name, $content);
}
/**
* Remove a file permanently from the Archive
*
* @param string $pathInArchive
*/
public function removeFile(string $pathInArchive): void
{
$this->archive->deleteName($pathInArchive);
}
/**
* Get the content of a file
*
* @param string $pathInArchive
*
* @return string
*/
public function getFileContent(string $pathInArchive): string
{
return $this->archive->getFromName($pathInArchive);
}
/**
* Get the stream of a file
*
* @param string $pathInArchive
*
* @return bool
*/
public function getFileStream(string $pathInArchive): bool
{
return $this->archive->getStream($pathInArchive);
}
/**
* Will loop over every item in the archive and will execute the callback on them
* Will provide the filename for every item
*
* @param $callback
*/
public function each($callback): void
{
for ($i = 0; $i < $this->archive->numFiles; ++$i) {
//skip if folder
$stats = $this->archive->statIndex($i);
if ($stats['size'] === 0 && $stats['crc'] === 0) {
continue;
}
call_user_func_array($callback, [
'file' => $this->archive->getNameIndex($i),
'stats' => $this->archive->statIndex($i)
]);
}
}
/**
* Checks whether the file is in the archive
*
* @param $fileInArchive
*
* @return bool
*/
public function fileExists($fileInArchive): bool
{
return $this->archive->locateName($fileInArchive) !== false;
}
/**
* Sets the password to be used for decompressing
* function named usePassword for clarity
*
* @param $password
*
* @return bool
*/
public function usePassword($password): bool
{
return $this->archive->setPassword($password);
}
/**
* Returns the status of the archive as a string
*
* @return string
*/
public function getStatus(): string
{
return $this->archive->getStatusString();
}
/**
* Closes the archive and saves it
*/
public function close(): void
{
@$this->archive->close();
}
/**
* get error message
*
* @param $resultCode
* @return string
*/
private function getErrorMessage($resultCode): string
{
return match ($resultCode) {
ZipArchive::ER_EXISTS => 'ZipArchive::ER_EXISTS - File already exists.',
ZipArchive::ER_INCONS => 'ZipArchive::ER_INCONS - Zip archive inconsistent.',
ZipArchive::ER_MEMORY => 'ZipArchive::ER_MEMORY - Malloc failure.',
ZipArchive::ER_NOENT => 'ZipArchive::ER_NOENT - No such file.',
ZipArchive::ER_NOZIP => 'ZipArchive::ER_NOZIP - Not a zip archive.',
ZipArchive::ER_OPEN => 'ZipArchive::ER_OPEN - Can\'t open file.',
ZipArchive::ER_READ => 'ZipArchive::ER_READ - Read error.',
ZipArchive::ER_SEEK => 'ZipArchive::ER_SEEK - Seek error.',
default => "An unknown error [$resultCode] has occurred.",
};
}
}

View File

@@ -0,0 +1,626 @@
<?php
// +----------------------------------------------------------------------
// | CatchAdmin [Just Like ]
// +----------------------------------------------------------------------
// | Copyright (c) 2017~2021 https://catchadmin.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( https://github.com/JaguarJack/catchadmin-laravel/blob/master/LICENSE.md )
// +----------------------------------------------------------------------
// | Author: JaguarJack [ njphper@gmail.com ]
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace Catch\Support\Zip;
use Exception;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
use InvalidArgumentException;
use RuntimeException;
/**
* This Zipper class is a wrapper around the ZipArchive methods with some handy functions
*
* Class Zipper
*
*/
class Zipper
{
/**
* Constant for extracting
*/
public const WHITELIST = 1;
/**
* Constant for extracting
*/
public const BLACKLIST = 2;
/**
* Constant for matching only strictly equal file names
*/
public const EXACT_MATCH = 4;
/**
* @var string Represents the current location in the archive
*/
private string $currentFolder = '';
/**
* @var Filesystem Handler to the file system
*/
private Filesystem $file;
/**
* @var ZipRepository|null Handler to the archive
*/
private ?ZipRepository $repository;
/**
* @var string The path to the current zip file
*/
private string $filePath;
/**
* Constructor
*
* @param Filesystem|null $fs
*/
public function __construct(Filesystem $fs = null)
{
$this->file = $fs ? $fs : new Filesystem();
}
/**
* Destructor
*/
public function __destruct()
{
if (is_object($this->repository)) {
$this->repository->close();
}
}
/**
* Create a new zip Archive if the file does not exists
* opens a zip archive if the file exists
*
* @param $pathToFile string The file to open
* @return $this Zipper instance
* @throws Exception
*/
public function make(string $pathToFile): Zipper
{
$new = $this->createArchiveFile($pathToFile);
$this->repository = new ZipRepository($pathToFile, $new);
$this->filePath = $pathToFile;
return $this;
}
/**
* Create a new zip archive or open an existing one
*
* @param $pathToFile
*
* @return $this
* @throws Exception
*
*/
public function zip($pathToFile): Zipper
{
$this->make($pathToFile);
return $this;
}
/**
* Create a new phar file or open one
*
* @param $pathToFile
*
* @return $this
* @throws Exception
*
*/
public function phar($pathToFile): Zipper
{
$this->make($pathToFile, 'phar');
return $this;
}
/**
* Create a new rar file or open one
*
* @param $pathToFile
*
* @return $this
* @throws Exception
*
*/
public function rar($pathToFile): Zipper
{
$this->make($pathToFile, 'rar');
return $this;
}
/**
* Extracts the opened zip archive to the specified location <br/>
* you can provide an array of files and folders and define if they should be a white list
* or a black list to extract. By default this method compares file names using "string starts with" logic
*
* @param $path string The path to extract to
* @param array $files An array of files
* @param int $methodFlags The Method the files should be treated
*
* @throws Exception
*/
public function extractTo(string $path, array $files = [], int $methodFlags = self::BLACKLIST): void
{
if (! $this->file->exists($path) && ! $this->file->makeDirectory($path, 0755, true)) {
throw new RuntimeException('Failed to create folder');
}
if ($methodFlags & self::EXACT_MATCH) {
$matchingMethod = function ($haystack) use ($files) {
return in_array($haystack, $files, true);
};
} else {
$matchingMethod = function ($haystack) use ($files) {
return Str::startsWith($haystack, $files);
};
}
if ($methodFlags & self::WHITELIST) {
$this->extractFilesInternal($path, $matchingMethod);
} else {
// blacklist - extract files that do not match with $matchingMethod
$this->extractFilesInternal($path, function ($filename) use ($matchingMethod) {
return ! $matchingMethod($filename);
});
}
}
/**
* Extracts matching files/folders from the opened zip archive to the specified location.
*
* @param string $extractToPath The path to extract to
* @param string $regex regular expression used to match files. See @link http://php.net/manual/en/reference.pcre.pattern.syntax.php
*
* @throws InvalidArgumentException
* @throws RuntimeException
*/
public function extractMatchingRegex(string $extractToPath, string $regex): void
{
if (empty($regex)) {
throw new InvalidArgumentException('Missing pass valid regex parameter');
}
$this->extractFilesInternal($extractToPath, function ($filename) use ($regex) {
$match = preg_match($regex, $filename);
if ($match === 1) {
return true;
} elseif ($match === false) {
//invalid pattern for preg_match raises E_WARNING and returns FALSE
//so if you have custom error_handler set to catch and throw E_WARNINGs you never end up here
//but if you have not - this will throw exception
throw new RuntimeException("regular expression match on '$filename' failed with error. Please check if pattern is valid regular expression.");
}
return false;
});
}
/**
* Gets the content of a single file if available
*
* @param $filePath string The full path (including all folders) of the file in the zip
*
* @return string returns the content or throws an exception
* @throws Exception
*
*/
public function getFileContent(string $filePath): string
{
if ($this->repository->fileExists($filePath) === false) {
throw new Exception(sprintf('The file "%s" cannot be found', $filePath));
}
return $this->repository->getFileContent($filePath);
}
/**
* Add one or multiple files to the zip.
*
* @param $pathToAdd array|string An array or string of files and folders to add
* @param mixed|null $fileName
*
* @return $this Zipper instance
*/
public function add(array|string $pathToAdd, mixed $fileName = null): Zipper
{
if (is_array($pathToAdd)) {
foreach ($pathToAdd as $key => $dir) {
if (! is_int($key)) {
$this->add($dir, $key);
} else {
$this->add($dir);
}
}
} elseif ($this->file->isFile($pathToAdd)) {
if ($fileName) {
$this->addFile($pathToAdd, $fileName);
} else {
$this->addFile($pathToAdd);
}
} else {
$this->addDir($pathToAdd);
}
return $this;
}
/**
* Add an empty directory
*
* @param $dirName
*
* @return Zipper
*/
public function addEmptyDir($dirName): Zipper
{
$this->repository->addEmptyDir($dirName);
return $this;
}
/**
* Add a file to the zip using its contents
*
* @param $filename string The name of the file to create
* @param $content string The file contents
*
* @return $this Zipper instance
*/
public function addString(string $filename, string $content): Zipper
{
$this->addFromString($filename, $content);
return $this;
}
/**
* Gets the status of the zip.
*
* @return string The status of the internal zip file
*/
public function getStatus(): string
{
return $this->repository->getStatus();
}
/**
* Remove a file or array of files and folders from the zip archive
*
* @param $fileToRemove array|string The path/array to the files in the zip
*
* @return $this Zipper instance
*/
public function remove(array|string $fileToRemove): Zipper
{
if (is_array($fileToRemove)) {
$self = $this;
$this->repository->each(function ($file) use ($fileToRemove, $self) {
if (Str::startsWith($file, $fileToRemove)) {
$self->getRepository()->removeFile($file);
}
});
} else {
$this->repository->removeFile($fileToRemove);
}
return $this;
}
/**
* Returns the path of the current zip file if there is one.
*
* @return string The path to the file
*/
public function getFilePath(): string
{
return $this->filePath;
}
/**
* Sets the password to be used for decompressing
*
* @param $password
*
* @return bool
*/
public function usePassword($password): bool
{
return $this->repository->usePassword($password);
}
/**
* Closes the zip file and frees all handles
*/
public function close(): void
{
if (null !== $this->repository) {
$this->repository->close();
}
$this->filePath = '';
}
/**
* Sets the internal folder to the given path.<br/>
* Useful for extracting only a segment of a zip file.
*
* @param string $path
*
* @return $this
*/
public function folder(string $path): Zipper
{
$this->currentFolder = $path;
return $this;
}
/**
* Resets the internal folder to the root of the zip file.
*
* @return $this
*/
public function home(): Zipper
{
$this->currentFolder = '';
return $this;
}
/**
* Deletes the archive file
*/
public function delete(): void
{
if (null !== $this->repository) {
$this->repository->close();
}
$this->file->delete($this->filePath);
$this->filePath = '';
}
/**
* Get the type of the Archive
*
* @return string
*/
public function getArchiveType(): string
{
return get_class($this->repository);
}
/**
* Get the current internal folder pointer
*
* @return string
*/
public function getCurrentFolderPath(): string
{
return $this->currentFolder;
}
/**
* Checks if a file is present in the archive
*
* @param $fileInArchive
*
* @return bool
*/
public function contains($fileInArchive): bool
{
return $this->repository->fileExists($fileInArchive);
}
/**
* @return ZipRepository
*/
public function getRepository(): ZipRepository
{
return $this->repository;
}
/**
* @return Filesystem
*/
public function getFileHandler(): Filesystem
{
return $this->file;
}
/**
* Gets the path to the internal folder
*
* @return string
*/
public function getInternalPath(): string
{
return empty($this->currentFolder) ? '' : $this->currentFolder.'/';
}
/**
* List all files that are within the archive
*
* @param string|null $regexFilter regular expression to filter returned files/folders. See @link http://php.net/manual/en/reference.pcre.pattern.syntax.php
*
* @throws RuntimeException
*
* @return array
*/
public function listFiles(string $regexFilter = null): array
{
$filesList = [];
if ($regexFilter) {
$filter = function ($file) use (&$filesList, $regexFilter) {
// push/pop an error handler here to to make sure no error/exception thrown if $expected is not a regex
set_error_handler(function () {
});
$match = preg_match($regexFilter, $file);
restore_error_handler();
if ($match === 1) {
$filesList[] = $file;
} elseif ($match === false) {
throw new RuntimeException("regular expression match on '$file' failed with error. Please check if pattern is valid regular expression.");
}
};
} else {
$filter = function ($file) use (&$filesList) {
$filesList[] = $file;
};
}
$this->repository->each($filter);
return $filesList;
}
private function getCurrentFolderWithTrailingSlash(): string
{
if (empty($this->currentFolder)) {
return '';
}
$lastChar = mb_substr($this->currentFolder, -1);
if ($lastChar !== '/' || $lastChar !== '\\') {
return $this->currentFolder.'/';
}
return $this->currentFolder;
}
//---------------------PRIVATE FUNCTIONS-------------
/**
* @param $pathToZip
*
* @return bool
* @throws Exception
*
*/
private function createArchiveFile($pathToZip): bool
{
if (! $this->file->exists($pathToZip)) {
$dirname = dirname($pathToZip);
if (! $this->file->exists($dirname) && ! $this->file->makeDirectory($dirname, 0755, true)) {
throw new RuntimeException('Failed to create folder');
} elseif (! $this->file->isWritable($dirname)) {
throw new Exception(sprintf('The path "%s" is not writeable', $pathToZip));
}
return true;
}
return false;
}
/**
* @param $pathToDir
*/
private function addDir($pathToDir): void
{
// First go over the files in this directory and add them to the repository.
foreach ($this->file->files($pathToDir) as $file) {
$this->addFile($pathToDir.'/'.basename($file));
}
// Now let's visit the subdirectories and add them, too.
foreach ($this->file->directories($pathToDir) as $dir) {
$old_folder = $this->currentFolder;
$this->currentFolder = empty($this->currentFolder) ? basename($dir) : $this->currentFolder.'/'.basename($dir);
$this->addDir($pathToDir.'/'.basename($dir));
$this->currentFolder = $old_folder;
}
}
/**
* Add the file to the zip
*
* @param string $pathToAdd
* @param string|null $fileName
*/
private function addFile(string $pathToAdd, string $fileName = null): void
{
if (! $fileName) {
$info = pathinfo($pathToAdd);
$fileName = isset($info['extension']) ?
$info['filename'].'.'.$info['extension'] :
$info['filename'];
}
$this->repository->addFile($pathToAdd, $this->getInternalPath().$fileName);
}
/**
* Add the file to the zip from content
*
* @param $filename
* @param $content
*/
private function addFromString($filename, $content): void
{
$this->repository->addFromString($this->getInternalPath().$filename, $content);
}
private function extractFilesInternal($path, callable $matchingMethod): void
{
$self = $this;
$this->repository->each(function ($fileName) use ($path, $matchingMethod, $self) {
$currentPath = $self->getCurrentFolderWithTrailingSlash();
if (! empty($currentPath) && ! Str::startsWith($fileName, $currentPath)) {
return;
}
$filename = str_replace($self->getInternalPath(), '', $fileName);
if ($matchingMethod($filename)) {
$self->extractOneFileInternal($fileName, $path);
}
});
}
/**
* @param $fileName
* @param $path
*
* @throws RuntimeException
*/
private function extractOneFileInternal($fileName, $path): void
{
$tmpPath = str_replace($this->getInternalPath(), '', $fileName);
//Prevent Zip traversal attacks
if (str_contains($fileName, '../') || str_contains($fileName, '..\\')) {
throw new RuntimeException('Special characters found within filenames');
}
// We need to create the directory first in case it doesn't exist
$dir = pathinfo($path.DIRECTORY_SEPARATOR.$tmpPath, PATHINFO_DIRNAME);
if (! $this->file->exists($dir) && ! $this->file->makeDirectory($dir, 0755, true, true)) {
throw new RuntimeException('Failed to create folders');
}
$toPath = $path.DIRECTORY_SEPARATOR.$tmpPath;
$fileStream = $this->getRepository()->getFileStream($fileName);
$this->getFileHandler()->put($toPath, $fileStream);
}
}

View File

@@ -0,0 +1,122 @@
<?php
// +----------------------------------------------------------------------
// | CatchAdmin [Just Like ]
// +----------------------------------------------------------------------
// | Copyright (c) 2017~2021 https://catchadmin.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( https://github.com/JaguarJack/catchadmin-laravel/blob/master/LICENSE.md )
// +----------------------------------------------------------------------
// | Author: JaguarJack [ njphper@gmail.com ]
// +----------------------------------------------------------------------
declare(strict_types=1);
use Illuminate\Console\Application as Artisan;
use Illuminate\Console\Command;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Symfony\Component\Finder\Finder;
use Symfony\Component\VarDumper\VarDumper;
/**
* load commands
*/
if (! function_exists('loadCommands')) {
/**
* @throws ReflectionException
*/
function loadCommands($paths, $namespace, $searchPath = null): void
{
if (! $searchPath) {
$searchPath = dirname($paths).DIRECTORY_SEPARATOR;
}
$paths = Collection::make(Arr::wrap($paths))->unique()->filter(function ($path) {
return is_dir($path);
});
if ($paths->isEmpty()) {
return;
}
foreach ((new Finder())->in($paths->toArray())->files() as $command) {
$command = $namespace.str_replace(['/', '.php'], ['\\', ''], Str::after($command->getRealPath(), $searchPath));
if (is_subclass_of($command, Command::class) &&
! (new ReflectionClass($command))->isAbstract()) {
Artisan::starting(function ($artisan) use ($command) {
$artisan->resolve($command);
});
}
}
}
}
/**
* table prefix
*/
if (! function_exists('withTablePrefix')) {
function withTablePrefix(string $table): string
{
return DB::connection()->getTablePrefix().$table;
}
}
/**
* get guard name
*/
if (! function_exists('getGuardName')) {
function getGuardName(): string
{
$guardKeys = array_keys(config('catch.auth.guards'));
if (count($guardKeys)) {
return $guardKeys[0];
}
return 'admin';
}
}
/**
* get table columns
*/
if (! function_exists('getTableColumns')) {
function getTableColumns(string $table): array
{
$SQL = 'desc '.withTablePrefix($table);
$columns = [];
foreach (DB::select($SQL) as $column) {
$columns[] = $column->Field;
}
return $columns;
}
}
if (! function_exists('dd_')) {
/**
* @param mixed ...$vars
* @return never
*/
function dd_(...$vars): never
{
if (! in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && ! headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: *');
header('Access-Control-Allow-Headers: *');
foreach ($vars as $v) {
VarDumper::dump($v);
}
exit(1);
}
}