create-prisma-php-app 4.0.0-alpha.2 → 4.0.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.htaccess +54 -41
- package/dist/bootstrap.php +143 -98
- package/dist/index.js +264 -99
- package/dist/settings/auto-swagger-docs.ts +196 -95
- package/dist/settings/bs-config.ts +56 -58
- package/dist/settings/files-list.json +1 -1
- package/dist/settings/restart-mcp.ts +58 -0
- package/dist/settings/restart-websocket.ts +51 -45
- package/dist/settings/utils.ts +240 -0
- package/dist/src/Lib/AI/ChatGPTClient.php +147 -0
- package/dist/src/Lib/Auth/Auth.php +544 -0
- package/dist/src/Lib/Auth/AuthConfig.php +89 -0
- package/dist/src/Lib/CacheHandler.php +121 -0
- package/dist/src/Lib/ErrorHandler.php +322 -0
- package/dist/src/Lib/FileManager/UploadFile.php +383 -0
- package/dist/src/Lib/Headers/Boom.php +192 -0
- package/dist/src/Lib/IncludeTracker.php +59 -0
- package/dist/src/Lib/MCP/WeatherTools.php +104 -0
- package/dist/src/Lib/MCP/mcp-server.php +80 -0
- package/dist/src/Lib/MainLayout.php +230 -0
- package/dist/src/Lib/Middleware/AuthMiddleware.php +154 -0
- package/dist/src/Lib/Middleware/CorsMiddleware.php +145 -0
- package/dist/src/Lib/PHPMailer/Mailer.php +169 -0
- package/dist/src/Lib/PHPX/Exceptions/ComponentValidationException.php +49 -0
- package/dist/src/Lib/PHPX/Fragment.php +32 -0
- package/dist/src/Lib/PHPX/IPHPX.php +22 -0
- package/dist/src/Lib/PHPX/PHPX.php +287 -0
- package/dist/src/Lib/PHPX/TemplateCompiler.php +641 -0
- package/dist/src/Lib/PHPX/TwMerge.php +346 -0
- package/dist/src/Lib/PHPX/TypeCoercer.php +490 -0
- package/dist/src/Lib/PartialRenderer.php +40 -0
- package/dist/src/Lib/PrismaPHPSettings.php +181 -0
- package/dist/src/Lib/Request.php +479 -0
- package/dist/src/Lib/Security/RateLimiter.php +33 -0
- package/dist/src/Lib/Set.php +102 -0
- package/dist/src/Lib/StateManager.php +127 -0
- package/dist/src/Lib/Validator.php +752 -0
- package/dist/src/{Websocket → Lib/Websocket}/ConnectionManager.php +1 -1
- package/dist/src/Lib/Websocket/websocket-server.php +118 -0
- package/dist/src/app/error.php +1 -1
- package/dist/src/app/index.php +24 -5
- package/dist/src/app/js/index.js +1 -1
- package/dist/src/app/layout.php +2 -2
- package/package.json +1 -1
- package/dist/settings/restart-websocket.bat +0 -28
- package/dist/src/app/assets/images/prisma-php-black.svg +0 -6
- package/dist/websocket-server.php +0 -22
- package/vendor/autoload.php +0 -25
- package/vendor/composer/ClassLoader.php +0 -579
- package/vendor/composer/InstalledVersions.php +0 -359
- package/vendor/composer/LICENSE +0 -21
- package/vendor/composer/autoload_classmap.php +0 -10
- package/vendor/composer/autoload_namespaces.php +0 -9
- package/vendor/composer/autoload_psr4.php +0 -10
- package/vendor/composer/autoload_real.php +0 -38
- package/vendor/composer/autoload_static.php +0 -25
- package/vendor/composer/installed.json +0 -825
- package/vendor/composer/installed.php +0 -132
- package/vendor/composer/platform_check.php +0 -26
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Lib\MCP;
|
|
6
|
+
|
|
7
|
+
use GuzzleHttp\Client;
|
|
8
|
+
use GuzzleHttp\Exception\GuzzleException;
|
|
9
|
+
use PhpMcp\Server\Attributes\McpTool;
|
|
10
|
+
use PhpMcp\Server\Attributes\Schema;
|
|
11
|
+
use RuntimeException;
|
|
12
|
+
use JsonException;
|
|
13
|
+
|
|
14
|
+
final class WeatherTools
|
|
15
|
+
{
|
|
16
|
+
#[McpTool(
|
|
17
|
+
name: 'get-weathers',
|
|
18
|
+
description: 'Returns current temperature for a city'
|
|
19
|
+
)]
|
|
20
|
+
public function getWeather(
|
|
21
|
+
#[Schema(type: 'string', minLength: 1, description: 'City name')]
|
|
22
|
+
string $city
|
|
23
|
+
): string {
|
|
24
|
+
$http = new Client([
|
|
25
|
+
'timeout' => 10,
|
|
26
|
+
'connect_timeout' => 8,
|
|
27
|
+
'http_errors' => true,
|
|
28
|
+
'headers' => ['Accept' => 'application/json'],
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
$geoUrl = 'https://geocoding-api.open-meteo.com/v1/search'
|
|
33
|
+
. '?name=' . rawurlencode($city)
|
|
34
|
+
. '&count=1&language=en&format=json';
|
|
35
|
+
|
|
36
|
+
$geoRes = $http->get($geoUrl);
|
|
37
|
+
$geoJson = $geoRes->getBody()->getContents();
|
|
38
|
+
$geo = json_decode($geoJson, true, 512, JSON_THROW_ON_ERROR);
|
|
39
|
+
|
|
40
|
+
if (empty($geo['results'][0])) {
|
|
41
|
+
throw new RuntimeException("City \"$city\" not found (geocoding returned no results).");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
$lat = (float) $geo['results'][0]['latitude'];
|
|
45
|
+
$lon = (float) $geo['results'][0]['longitude'];
|
|
46
|
+
|
|
47
|
+
$wxUrl = "https://api.open-meteo.com/v1/forecast?latitude={$lat}&longitude={$lon}¤t_weather=true";
|
|
48
|
+
|
|
49
|
+
$wxRes = $http->get($wxUrl);
|
|
50
|
+
$wxJson = $wxRes->getBody()->getContents();
|
|
51
|
+
$wx = json_decode($wxJson, true, 512, JSON_THROW_ON_ERROR);
|
|
52
|
+
|
|
53
|
+
$cw = $wx['current_weather'] ?? null;
|
|
54
|
+
if (!is_array($cw)) {
|
|
55
|
+
throw new RuntimeException('No current weather data found in API response.');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
$map = [
|
|
59
|
+
0 => 'Clear sky',
|
|
60
|
+
1 => 'Mainly clear',
|
|
61
|
+
2 => 'Partly cloudy',
|
|
62
|
+
3 => 'Overcast',
|
|
63
|
+
45 => 'Fog',
|
|
64
|
+
48 => 'Depositing rime fog',
|
|
65
|
+
51 => 'Light drizzle',
|
|
66
|
+
53 => 'Moderate drizzle',
|
|
67
|
+
55 => 'Dense drizzle',
|
|
68
|
+
56 => 'Light freezing drizzle',
|
|
69
|
+
57 => 'Dense freezing drizzle',
|
|
70
|
+
61 => 'Slight rain',
|
|
71
|
+
63 => 'Moderate rain',
|
|
72
|
+
65 => 'Heavy rain',
|
|
73
|
+
66 => 'Light freezing rain',
|
|
74
|
+
67 => 'Heavy freezing rain',
|
|
75
|
+
71 => 'Slight snow fall',
|
|
76
|
+
73 => 'Moderate snow fall',
|
|
77
|
+
75 => 'Heavy snow fall',
|
|
78
|
+
77 => 'Snow grains',
|
|
79
|
+
80 => 'Slight rain showers',
|
|
80
|
+
81 => 'Moderate rain showers',
|
|
81
|
+
82 => 'Violent rain showers',
|
|
82
|
+
85 => 'Slight snow showers',
|
|
83
|
+
86 => 'Heavy snow showers',
|
|
84
|
+
95 => 'Thunderstorm',
|
|
85
|
+
96 => 'Thunderstorm with slight hail',
|
|
86
|
+
99 => 'Thunderstorm with heavy hail',
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
$code = (int)($cw['weathercode'] ?? -1);
|
|
90
|
+
$tempC = $cw['temperature'] ?? null;
|
|
91
|
+
|
|
92
|
+
if (!is_numeric($tempC)) {
|
|
93
|
+
throw new RuntimeException('Temperature missing or not numeric in current weather payload.');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
$desc = $map[$code] ?? 'Unknown';
|
|
97
|
+
return "🌡️ {$tempC} °C · {$desc} (code {$code})";
|
|
98
|
+
} catch (GuzzleException $e) {
|
|
99
|
+
throw new RuntimeException('Weather request failed: ' . $e->getMessage(), previous: $e);
|
|
100
|
+
} catch (JsonException $e) {
|
|
101
|
+
throw new RuntimeException('Weather JSON parsing failed: ' . $e->getMessage(), previous: $e);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Lib\MCP;
|
|
6
|
+
|
|
7
|
+
$root = dirname(__DIR__, 3);
|
|
8
|
+
require $root . '/vendor/autoload.php';
|
|
9
|
+
require $root . '/settings/paths.php';
|
|
10
|
+
|
|
11
|
+
use Dotenv\Dotenv;
|
|
12
|
+
use PhpMcp\Server\Server;
|
|
13
|
+
use PhpMcp\Server\Transports\StreamableHttpServerTransport;
|
|
14
|
+
use Throwable;
|
|
15
|
+
|
|
16
|
+
// ── Load .env (optional) and timezone ──────────────────────────────────────────
|
|
17
|
+
if (file_exists(DOCUMENT_PATH . '/.env')) {
|
|
18
|
+
Dotenv::createImmutable(DOCUMENT_PATH)->safeLoad();
|
|
19
|
+
}
|
|
20
|
+
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'UTC');
|
|
21
|
+
|
|
22
|
+
// ── Resolve settings (with sane defaults) ─────────────────────────────────────
|
|
23
|
+
$appName = $_ENV['MCP_NAME'] ?? 'prisma-php-mcp';
|
|
24
|
+
$appVersion = $_ENV['MCP_VERSION'] ?? '0.0.1';
|
|
25
|
+
$host = $_ENV['MCP_HOST'] ?? '127.0.0.1';
|
|
26
|
+
$port = (int)($_ENV['MCP_PORT'] ?? 4000);
|
|
27
|
+
$prefix = trim($_ENV['MCP_PATH_PREFIX'] ?? 'mcp', '/');
|
|
28
|
+
$enableJson = filter_var($_ENV['MCP_JSON_RESPONSE'] ?? 'false', FILTER_VALIDATE_BOOLEAN);
|
|
29
|
+
|
|
30
|
+
// ── Build server and discover tools ───────────────────────────────────────────
|
|
31
|
+
$server = Server::make()
|
|
32
|
+
->withServerInfo($appName, $appVersion)
|
|
33
|
+
->build();
|
|
34
|
+
|
|
35
|
+
// Scan your source tree for #[McpTool] classes
|
|
36
|
+
$server->discover(DOCUMENT_PATH, ['src']);
|
|
37
|
+
|
|
38
|
+
// ── Pretty console output ─────────────────────────────────────────────────────
|
|
39
|
+
$pid = getmypid() ?: 0;
|
|
40
|
+
$base = "http://{$host}:{$port}/{$prefix}";
|
|
41
|
+
$color = static fn(string $t, string $c) => "\033[{$c}m{$t}\033[0m";
|
|
42
|
+
|
|
43
|
+
echo PHP_EOL;
|
|
44
|
+
echo $color("⚙ {$appName} (v{$appVersion})", '1;36') . PHP_EOL;
|
|
45
|
+
echo $color("→ MCP server starting…", '33') . PHP_EOL;
|
|
46
|
+
echo " Host: {$host}" . PHP_EOL;
|
|
47
|
+
echo " Port: {$port}" . PHP_EOL;
|
|
48
|
+
echo " Path: /{$prefix}" . PHP_EOL;
|
|
49
|
+
echo " JSON resp: " . ($enableJson ? 'enabled' : 'disabled') . PHP_EOL;
|
|
50
|
+
echo " PID: {$pid}" . PHP_EOL;
|
|
51
|
+
echo " URL: {$base}" . PHP_EOL;
|
|
52
|
+
|
|
53
|
+
// ── Graceful shutdown (if pcntl is available) ─────────────────────────────────
|
|
54
|
+
if (function_exists('pcntl_signal')) {
|
|
55
|
+
$stop = function (int $sig) use ($color) {
|
|
56
|
+
echo PHP_EOL . $color("⏹ Caught signal {$sig}. Shutting down…", '33') . PHP_EOL;
|
|
57
|
+
exit(0);
|
|
58
|
+
};
|
|
59
|
+
pcntl_signal(SIGINT, $stop);
|
|
60
|
+
pcntl_signal(SIGTERM, $stop);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Listen ────────────────────────────────────────────────────────────────────
|
|
64
|
+
try {
|
|
65
|
+
$transport = new StreamableHttpServerTransport(
|
|
66
|
+
$host,
|
|
67
|
+
$port,
|
|
68
|
+
$prefix,
|
|
69
|
+
null, // sslContext
|
|
70
|
+
true, // logger
|
|
71
|
+
$enableJson // enableJsonResponse
|
|
72
|
+
// , false // (optional) stateless
|
|
73
|
+
);
|
|
74
|
+
echo $color("✓ Listening on {$base}", '32') . PHP_EOL;
|
|
75
|
+
$server->listen($transport);
|
|
76
|
+
echo PHP_EOL . $color('✔ Server stopped.', '32') . PHP_EOL;
|
|
77
|
+
} catch (Throwable $e) {
|
|
78
|
+
fwrite(STDERR, $color('✖ Server error: ', '31') . $e->getMessage() . PHP_EOL);
|
|
79
|
+
exit(1);
|
|
80
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Lib;
|
|
6
|
+
|
|
7
|
+
use Lib\Set;
|
|
8
|
+
|
|
9
|
+
class MainLayout
|
|
10
|
+
{
|
|
11
|
+
public static string $title = '';
|
|
12
|
+
public static string $description = '';
|
|
13
|
+
public static string $children = '';
|
|
14
|
+
public static string $childLayoutChildren = '';
|
|
15
|
+
public static string $html = '';
|
|
16
|
+
|
|
17
|
+
/** @var Set<string>|null */
|
|
18
|
+
private static ?Set $headScripts = null;
|
|
19
|
+
/** @var Set<string>|null */
|
|
20
|
+
private static ?Set $footerScripts = null;
|
|
21
|
+
private static array $customMetadata = [];
|
|
22
|
+
|
|
23
|
+
private static array $processedScripts = [];
|
|
24
|
+
|
|
25
|
+
public static function init(): void
|
|
26
|
+
{
|
|
27
|
+
if (self::$headScripts === null) {
|
|
28
|
+
self::$headScripts = new Set();
|
|
29
|
+
}
|
|
30
|
+
if (self::$footerScripts === null) {
|
|
31
|
+
self::$footerScripts = new Set();
|
|
32
|
+
}
|
|
33
|
+
self::$processedScripts = [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Adds one or more scripts to the head section if they are not already present.
|
|
38
|
+
*
|
|
39
|
+
* @param string ...$scripts The scripts to be added to the head section.
|
|
40
|
+
* @return void
|
|
41
|
+
*/
|
|
42
|
+
public static function addHeadScript(string ...$scripts): void
|
|
43
|
+
{
|
|
44
|
+
foreach ($scripts as $script) {
|
|
45
|
+
self::$headScripts->add($script);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Adds one or more scripts to the footer section if they are not already present.
|
|
51
|
+
*
|
|
52
|
+
* @param string ...$scripts One or more scripts to be added to the footer.
|
|
53
|
+
* @return void
|
|
54
|
+
*/
|
|
55
|
+
public static function addFooterScript(string ...$scripts): void
|
|
56
|
+
{
|
|
57
|
+
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
|
|
58
|
+
$callerClass = $trace[1]['class'] ?? 'Unknown';
|
|
59
|
+
|
|
60
|
+
foreach ($scripts as $script) {
|
|
61
|
+
$scriptKey = md5(trim($script));
|
|
62
|
+
|
|
63
|
+
if (strpos($script, '<script') !== false) {
|
|
64
|
+
$taggedScript = "<!-- class:" . $callerClass . " -->\n" . $script;
|
|
65
|
+
if (!isset(self::$processedScripts[$scriptKey])) {
|
|
66
|
+
self::$footerScripts->add($taggedScript);
|
|
67
|
+
self::$processedScripts[$scriptKey] = true;
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
if (!isset(self::$processedScripts[$scriptKey])) {
|
|
71
|
+
self::$footerScripts->add($script);
|
|
72
|
+
self::$processedScripts[$scriptKey] = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Generates all the head scripts with dynamic attributes.
|
|
80
|
+
*
|
|
81
|
+
* This method iterates over all registered head scripts and adds a custom dynamic attribute
|
|
82
|
+
* based on the tag type (script, link, or style).
|
|
83
|
+
*
|
|
84
|
+
* @return string The concatenated head scripts with dynamic attributes.
|
|
85
|
+
*/
|
|
86
|
+
public static function outputHeadScripts(): string
|
|
87
|
+
{
|
|
88
|
+
$headScriptsArray = self::$headScripts->values();
|
|
89
|
+
$headScriptsWithAttributes = array_map(function ($tag) {
|
|
90
|
+
if (strpos($tag, '<script') !== false) {
|
|
91
|
+
return str_replace('<script', '<script pp-dynamic-script="81D7D"', $tag);
|
|
92
|
+
} elseif (strpos($tag, '<link') !== false) {
|
|
93
|
+
return str_replace('<link', '<link pp-dynamic-link="81D7D"', $tag);
|
|
94
|
+
} elseif (strpos($tag, '<style') !== false) {
|
|
95
|
+
return str_replace('<style', '<style pp-dynamic-style="81D7D"', $tag);
|
|
96
|
+
}
|
|
97
|
+
return $tag;
|
|
98
|
+
}, $headScriptsArray);
|
|
99
|
+
|
|
100
|
+
return implode("\n", $headScriptsWithAttributes);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Generates all the footer scripts.
|
|
105
|
+
*
|
|
106
|
+
* @return string The concatenated footer scripts.
|
|
107
|
+
*/
|
|
108
|
+
public static function outputFooterScripts(): string
|
|
109
|
+
{
|
|
110
|
+
$processed = [];
|
|
111
|
+
$componentCounter = 0;
|
|
112
|
+
|
|
113
|
+
foreach (self::$footerScripts->values() as $script) {
|
|
114
|
+
if (preg_match('/<!-- class:([^\s]+) -->/', $script, $matches)) {
|
|
115
|
+
$rawClassName = $matches[1];
|
|
116
|
+
$script = preg_replace('/<!-- class:[^\s]+ -->\s*/', '', $script, 1);
|
|
117
|
+
|
|
118
|
+
if (str_starts_with(trim($script), '<script')) {
|
|
119
|
+
$script = preg_replace_callback(
|
|
120
|
+
'/<script\b([^>]*)>/i',
|
|
121
|
+
function ($m) use ($rawClassName, &$componentCounter) {
|
|
122
|
+
$attrs = $m[1];
|
|
123
|
+
$scriptHash = substr(md5($m[0]), 0, 8);
|
|
124
|
+
$encodedClass = 's' . base_convert(sprintf('%u', crc32($rawClassName . $componentCounter . $scriptHash)), 10, 36);
|
|
125
|
+
$componentCounter++;
|
|
126
|
+
|
|
127
|
+
if (!str_contains($attrs, 'pp-component=')) {
|
|
128
|
+
$attrs .= " pp-component=\"{$encodedClass}\"";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!preg_match('/\btype\s*=\s*(["\'])[^\1]*\1|\btype\s*=\s*\S+/i', $attrs)) {
|
|
132
|
+
$attrs .= ' type="text/php"';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return "<script{$attrs}>";
|
|
136
|
+
},
|
|
137
|
+
$script,
|
|
138
|
+
1
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
$processed[] = $script;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return implode("\n", $processed);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Clears all head scripts.
|
|
151
|
+
*
|
|
152
|
+
* @return void
|
|
153
|
+
*/
|
|
154
|
+
public static function clearHeadScripts(): void
|
|
155
|
+
{
|
|
156
|
+
self::$headScripts->clear();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Clears all footer scripts.
|
|
161
|
+
*
|
|
162
|
+
* @return void
|
|
163
|
+
*/
|
|
164
|
+
public static function clearFooterScripts(): void
|
|
165
|
+
{
|
|
166
|
+
self::$footerScripts->clear();
|
|
167
|
+
self::$processedScripts = [];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Adds custom metadata.
|
|
172
|
+
*
|
|
173
|
+
* @param string $key The metadata key.
|
|
174
|
+
* @param string $value The metadata value.
|
|
175
|
+
* @return void
|
|
176
|
+
*/
|
|
177
|
+
public static function addCustomMetadata(string $key, string $value): void
|
|
178
|
+
{
|
|
179
|
+
self::$customMetadata[$key] = $value;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Retrieves custom metadata by key.
|
|
184
|
+
*
|
|
185
|
+
* @param string $key The metadata key.
|
|
186
|
+
* @return string|null The metadata value or null if the key does not exist.
|
|
187
|
+
*/
|
|
188
|
+
public static function getCustomMetadata(string $key): ?string
|
|
189
|
+
{
|
|
190
|
+
return self::$customMetadata[$key] ?? null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Generates the metadata as meta tags for the head section.
|
|
195
|
+
*
|
|
196
|
+
* This method includes default tags for charset and viewport, a title tag,
|
|
197
|
+
* and additional metadata. If a description is not already set in the custom metadata,
|
|
198
|
+
* it will use the class's description property.
|
|
199
|
+
*
|
|
200
|
+
* @return string The concatenated meta tags.
|
|
201
|
+
*/
|
|
202
|
+
public static function outputMetadata(): string
|
|
203
|
+
{
|
|
204
|
+
$metadataContent = [
|
|
205
|
+
'<meta charset="UTF-8">',
|
|
206
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1.0">',
|
|
207
|
+
];
|
|
208
|
+
$metadataContent[] = '<title>' . htmlspecialchars(self::$title) . '</title>';
|
|
209
|
+
|
|
210
|
+
if (!isset(self::$customMetadata['description'])) {
|
|
211
|
+
self::$customMetadata['description'] = self::$description;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
foreach (self::$customMetadata as $key => $value) {
|
|
215
|
+
$metadataContent[] = '<meta name="' . htmlspecialchars($key) . '" content="' . htmlspecialchars($value) . '" pp-dynamic-meta="81D7D">';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return implode("\n", $metadataContent);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Clears all custom metadata.
|
|
223
|
+
*
|
|
224
|
+
* @return void
|
|
225
|
+
*/
|
|
226
|
+
public static function clearCustomMetadata(): void
|
|
227
|
+
{
|
|
228
|
+
self::$customMetadata = [];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Lib\Middleware;
|
|
6
|
+
|
|
7
|
+
use Lib\Auth\Auth;
|
|
8
|
+
use Lib\Auth\AuthConfig;
|
|
9
|
+
use Lib\Request;
|
|
10
|
+
|
|
11
|
+
final class AuthMiddleware
|
|
12
|
+
{
|
|
13
|
+
public static function handle($requestPathname)
|
|
14
|
+
{
|
|
15
|
+
if (AuthConfig::IS_ALL_ROUTES_PRIVATE) {
|
|
16
|
+
$isLogin = Auth::getInstance()->isAuthenticated();
|
|
17
|
+
$isApiAuthRoute = stripos($requestPathname, AuthConfig::API_AUTH_PREFIX) === 0;
|
|
18
|
+
$isPublicRoute = self::matches($requestPathname, AuthConfig::$publicRoutes);
|
|
19
|
+
$isAuthRoute = self::matches($requestPathname, AuthConfig::$authRoutes);
|
|
20
|
+
|
|
21
|
+
// Check if the user is authenticated and refresh the token if necessary
|
|
22
|
+
if (AuthConfig::IS_TOKEN_AUTO_REFRESH) {
|
|
23
|
+
$auth = Auth::getInstance();
|
|
24
|
+
if (isset($_COOKIE[Auth::$cookieName])) {
|
|
25
|
+
$jwt = $_COOKIE[Auth::$cookieName];
|
|
26
|
+
$jwt = $auth->refreshToken($jwt);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Skip the middleware if the route is api auth route
|
|
31
|
+
if ($isApiAuthRoute) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Redirect to the default sign in route if the user is already authenticated
|
|
36
|
+
if ($isAuthRoute) {
|
|
37
|
+
if ($isLogin) {
|
|
38
|
+
Request::redirect(AuthConfig::DEFAULT_SIGNIN_REDIRECT);
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Redirect to the default home route if the user is already authenticated
|
|
44
|
+
if (!$isLogin && !$isPublicRoute) {
|
|
45
|
+
Request::redirect("/signin");
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
// Skip the middleware if the route is public
|
|
49
|
+
if (!self::matches($requestPathname, AuthConfig::$privateRoutes)) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check if the user is authorized to access the route or redirect to login
|
|
54
|
+
if (!self::isAuthorized()) {
|
|
55
|
+
Request::redirect('/signin');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check if the user has the required role to access the route or redirect to denied
|
|
60
|
+
if (AuthConfig::IS_ROLE_BASE) {
|
|
61
|
+
$matchValue = self::hasRequiredRole($requestPathname);
|
|
62
|
+
if ($matchValue === "Route not in array") {
|
|
63
|
+
// echo "No validation needed for this route.";
|
|
64
|
+
} elseif ($matchValue === "Match") {
|
|
65
|
+
// echo "You are authorized to access this route";
|
|
66
|
+
} elseif ($matchValue === "Role mismatch") {
|
|
67
|
+
// echo "You are not authorized to access this route";
|
|
68
|
+
Request::redirect('/denied');
|
|
69
|
+
} else {
|
|
70
|
+
// echo "Unexpected error encountered";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
protected static function matches(string $requestPathname, array $routes): bool
|
|
76
|
+
{
|
|
77
|
+
foreach ($routes ?? [] as $pattern) {
|
|
78
|
+
$getUriRegexValue = self::getUriRegex($pattern, $requestPathname);
|
|
79
|
+
if ($getUriRegexValue) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
protected static function isAuthorized(): bool
|
|
87
|
+
{
|
|
88
|
+
$auth = Auth::getInstance();
|
|
89
|
+
if (!isset($_COOKIE[Auth::$cookieName])) {
|
|
90
|
+
unset($_SESSION[Auth::PAYLOAD_SESSION_KEY]);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
$jwt = $_COOKIE[Auth::$cookieName];
|
|
95
|
+
|
|
96
|
+
if (AuthConfig::IS_TOKEN_AUTO_REFRESH) {
|
|
97
|
+
$jwt = $auth->refreshToken($jwt);
|
|
98
|
+
$verifyToken = $auth->verifyToken($jwt);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
$verifyToken = $auth->verifyToken($jwt);
|
|
102
|
+
if ($verifyToken === false) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Access the PAYLOAD_NAME property using the -> operator instead of array syntax
|
|
107
|
+
if (isset($verifyToken->{Auth::PAYLOAD_NAME})) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
protected static function hasRequiredRole(string $requestPathname): string
|
|
115
|
+
{
|
|
116
|
+
$auth = Auth::getInstance();
|
|
117
|
+
$roleBasedRoutes = AuthConfig::$roleBasedRoutes ?? [];
|
|
118
|
+
|
|
119
|
+
// Normalize the request path for matching
|
|
120
|
+
$requestPathnameValue = trim($requestPathname, '/');
|
|
121
|
+
|
|
122
|
+
foreach ($roleBasedRoutes as $pattern => $data) {
|
|
123
|
+
$patternValue = trim($pattern, '/');
|
|
124
|
+
if ($patternValue === $requestPathnameValue) {
|
|
125
|
+
// Route is found in array, check permissions
|
|
126
|
+
$userRole = Auth::ROLE_NAME ? $auth->getPayload()[Auth::ROLE_NAME] : $auth->getPayload();
|
|
127
|
+
return ($userRole !== null && AuthConfig::checkAuthRole($userRole, $data[AuthConfig::ROLE_IDENTIFIER]))
|
|
128
|
+
? "Match"
|
|
129
|
+
: "Role mismatch";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Route not found in role-based routes array
|
|
134
|
+
return "Route not in array";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private static function getUriRegex(string $pattern, string $requestPathname): int|bool
|
|
138
|
+
{
|
|
139
|
+
// Normalize both the pattern and the request path
|
|
140
|
+
$pattern = strtolower(trim($pattern, '/'));
|
|
141
|
+
$requestPathname = strtolower(trim($requestPathname, '/'));
|
|
142
|
+
|
|
143
|
+
// Handle the case where the requestPathname is empty, which means home or "/"
|
|
144
|
+
if (empty($requestPathname)) {
|
|
145
|
+
$requestPathname = '/';
|
|
146
|
+
} else {
|
|
147
|
+
$requestPathname = "/$requestPathname";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Construct the regex pattern
|
|
151
|
+
$regex = "#^/?" . preg_quote("/$pattern", '#') . "(/.*)?$#";
|
|
152
|
+
return preg_match($regex, $requestPathname);
|
|
153
|
+
}
|
|
154
|
+
}
|