create-prisma-php-app 4.0.0-alpha.53 → 4.0.0-alpha.54
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/src/Lib/Websocket/websocket-server.php +2 -1
- package/package.json +1 -1
- package/dist/src/Lib/AI/ChatGPTClient.php +0 -147
- package/dist/src/Lib/FileManager/UploadFile.php +0 -383
- package/dist/src/Lib/Headers/Boom.php +0 -192
- package/dist/src/Lib/PHPMailer/Mailer.php +0 -169
- package/dist/src/Lib/Security/RateLimiter.php +0 -33
|
@@ -13,6 +13,7 @@ use Ratchet\Server\IoServer;
|
|
|
13
13
|
use Ratchet\Http\HttpServer;
|
|
14
14
|
use Ratchet\WebSocket\WsServer;
|
|
15
15
|
use Lib\Websocket\ConnectionManager;
|
|
16
|
+
use React\EventLoop\LoopInterface;
|
|
16
17
|
use Throwable;
|
|
17
18
|
|
|
18
19
|
// ── Load .env (optional) and timezone ─────────────────────────────────────────
|
|
@@ -74,7 +75,7 @@ echo " Started: {$ts}" . PHP_EOL;
|
|
|
74
75
|
|
|
75
76
|
// ── Graceful shutdown & periodic logs (if loop available) ────────────────────
|
|
76
77
|
$loop = property_exists($server, 'loop') ? $server->loop : null;
|
|
77
|
-
if ($loop instanceof
|
|
78
|
+
if ($loop instanceof LoopInterface) {
|
|
78
79
|
// Periodic stats every 60s
|
|
79
80
|
$loop->addPeriodicTimer(60, function () use ($ok) {
|
|
80
81
|
$mem = function_exists('memory_get_usage') ? number_format(memory_get_usage(true) / 1048576, 2) . ' MB' : 'n/a';
|
package/package.json
CHANGED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
namespace Lib\AI;
|
|
6
|
-
|
|
7
|
-
use GuzzleHttp\Client;
|
|
8
|
-
use GuzzleHttp\Exception\RequestException;
|
|
9
|
-
use PPHP\Validator;
|
|
10
|
-
use RuntimeException;
|
|
11
|
-
|
|
12
|
-
class ChatGPTClient
|
|
13
|
-
{
|
|
14
|
-
private Client $client;
|
|
15
|
-
private string $apiUrl = '';
|
|
16
|
-
private string $apiKey = '';
|
|
17
|
-
private array $cache = [];
|
|
18
|
-
|
|
19
|
-
public function __construct(?Client $client = null)
|
|
20
|
-
{
|
|
21
|
-
// Initialize the Guzzle HTTP client, allowing for dependency injection
|
|
22
|
-
$this->client = $client ?: new Client();
|
|
23
|
-
|
|
24
|
-
// API URL for chat completions
|
|
25
|
-
$this->apiUrl = 'https://api.openai.com/v1/chat/completions';
|
|
26
|
-
|
|
27
|
-
// Get the API key from environment variables (keep this private and secure)
|
|
28
|
-
$this->apiKey = $_ENV['CHATGPT_API_KEY'];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Determines the appropriate model based on internal logic.
|
|
33
|
-
*
|
|
34
|
-
* @param array $conversationHistory The conversation history array.
|
|
35
|
-
* @return string The model name to be used.
|
|
36
|
-
*/
|
|
37
|
-
protected function determineModel(array $conversationHistory): string
|
|
38
|
-
{
|
|
39
|
-
$messageCount = count($conversationHistory);
|
|
40
|
-
$totalTokens = array_reduce(
|
|
41
|
-
$conversationHistory,
|
|
42
|
-
fn($carry, $item) => $carry + str_word_count($item['content'] ?? ''),
|
|
43
|
-
0
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
// If the conversation is long or complex, use a model with more tokens
|
|
47
|
-
if ($totalTokens > 4000 || $messageCount > 10) {
|
|
48
|
-
return 'gpt-3.5-turbo-16k'; // Use the model with a larger token limit
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Default to the standard model for shorter conversations
|
|
52
|
-
return 'gpt-3.5-turbo';
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Formats the conversation history to ensure it is valid.
|
|
57
|
-
*
|
|
58
|
-
* @param array $conversationHistory The conversation history array.
|
|
59
|
-
* @return array The formatted conversation history.
|
|
60
|
-
*/
|
|
61
|
-
protected function formatConversationHistory(array $conversationHistory): array
|
|
62
|
-
{
|
|
63
|
-
$formattedHistory = [];
|
|
64
|
-
foreach ($conversationHistory as $message) {
|
|
65
|
-
if (is_array($message) && isset($message['role'], $message['content']) && Validator::string($message['content'])) {
|
|
66
|
-
$formattedHistory[] = $message;
|
|
67
|
-
} else {
|
|
68
|
-
$formattedHistory[] = ['role' => 'user', 'content' => (string) $message];
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return $formattedHistory;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Sends a message to the OpenAI API and returns the AI's response as HTML.
|
|
76
|
-
*
|
|
77
|
-
* @param array $conversationHistory The conversation history array containing previous messages.
|
|
78
|
-
* @param string $userMessage The new user message to add to the conversation.
|
|
79
|
-
* @return string The AI-generated HTML response.
|
|
80
|
-
*
|
|
81
|
-
* @throws \InvalidArgumentException If a message in the conversation history is not valid.
|
|
82
|
-
* @throws RuntimeException If the API request fails or returns an unexpected format.
|
|
83
|
-
*/
|
|
84
|
-
public function sendMessage(array $conversationHistory, string $userMessage): string
|
|
85
|
-
{
|
|
86
|
-
if (!Validator::string($userMessage)) {
|
|
87
|
-
throw new \InvalidArgumentException("Invalid user message: must be a string.");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Optional: Convert emojis or special patterns in the message
|
|
91
|
-
$userMessage = Validator::emojis($userMessage);
|
|
92
|
-
|
|
93
|
-
// Prepare the conversation, including a system-level instruction to return valid HTML
|
|
94
|
-
$systemInstruction = [
|
|
95
|
-
'role' => 'system',
|
|
96
|
-
'content' => 'You are ChatGPT. Please provide your response in valid HTML format.'
|
|
97
|
-
];
|
|
98
|
-
|
|
99
|
-
// Format existing history, then prepend the system message
|
|
100
|
-
$formattedHistory = $this->formatConversationHistory($conversationHistory);
|
|
101
|
-
array_unshift($formattedHistory, $systemInstruction);
|
|
102
|
-
|
|
103
|
-
// Append the new user message
|
|
104
|
-
$formattedHistory[] = ['role' => 'user', 'content' => $userMessage];
|
|
105
|
-
|
|
106
|
-
// Check cache
|
|
107
|
-
$cacheKey = md5(serialize($formattedHistory));
|
|
108
|
-
if (isset($this->cache[$cacheKey])) {
|
|
109
|
-
return $this->cache[$cacheKey];
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Determine the appropriate model to use
|
|
113
|
-
$model = $this->determineModel($formattedHistory);
|
|
114
|
-
|
|
115
|
-
try {
|
|
116
|
-
// Sending a POST request to the AI API
|
|
117
|
-
$response = $this->client->request('POST', $this->apiUrl, [
|
|
118
|
-
'headers' => [
|
|
119
|
-
'Authorization' => 'Bearer ' . $this->apiKey,
|
|
120
|
-
'Content-Type' => 'application/json',
|
|
121
|
-
],
|
|
122
|
-
'json' => [
|
|
123
|
-
'model' => $model,
|
|
124
|
-
'messages' => $formattedHistory,
|
|
125
|
-
'max_tokens' => 500,
|
|
126
|
-
],
|
|
127
|
-
]);
|
|
128
|
-
|
|
129
|
-
$responseBody = $response->getBody();
|
|
130
|
-
$responseContent = json_decode((string) $responseBody, true);
|
|
131
|
-
|
|
132
|
-
// Check if response is in expected format
|
|
133
|
-
if (isset($responseContent['choices'][0]['message']['content'])) {
|
|
134
|
-
$aiMessage = $responseContent['choices'][0]['message']['content'];
|
|
135
|
-
|
|
136
|
-
// Cache the result
|
|
137
|
-
$this->cache[$cacheKey] = $aiMessage;
|
|
138
|
-
|
|
139
|
-
return $aiMessage;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
throw new RuntimeException('Unexpected API response format.');
|
|
143
|
-
} catch (RequestException $e) {
|
|
144
|
-
throw new RuntimeException("API request failed: " . $e->getMessage(), 0, $e);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
@@ -1,383 +0,0 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
namespace Lib\FileManager;
|
|
6
|
-
|
|
7
|
-
class UploadFile
|
|
8
|
-
{
|
|
9
|
-
protected string $destination = '';
|
|
10
|
-
protected array $messages = [];
|
|
11
|
-
protected array $errorCode = [];
|
|
12
|
-
protected array $successfulUploads = [];
|
|
13
|
-
protected int $maxSize = 51200; // 50KB default
|
|
14
|
-
protected array $permittedTypes = [
|
|
15
|
-
'image/jpeg',
|
|
16
|
-
'image/pjpeg',
|
|
17
|
-
'image/gif',
|
|
18
|
-
'image/png',
|
|
19
|
-
'image/webp'
|
|
20
|
-
];
|
|
21
|
-
protected string $newName = '';
|
|
22
|
-
protected bool $typeCheckingOn = true;
|
|
23
|
-
protected array $notTrusted = ['bin', 'cgi', 'exe', 'js', 'pl', 'php', 'py', 'sh'];
|
|
24
|
-
protected string $suffix = '.upload';
|
|
25
|
-
protected bool $renameDuplicates = true;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Constructor for the UploadFile class.
|
|
29
|
-
*
|
|
30
|
-
* @param string $uploadFolder The folder to which uploaded files will be moved.
|
|
31
|
-
* @throws \Exception If the upload folder is not a valid, writable folder.
|
|
32
|
-
*/
|
|
33
|
-
public function __construct(string $uploadFolder)
|
|
34
|
-
{
|
|
35
|
-
if (!is_dir($uploadFolder) || !is_writable($uploadFolder)) {
|
|
36
|
-
throw new \Exception("$uploadFolder must be a valid, writable folder.");
|
|
37
|
-
}
|
|
38
|
-
// Ensure the folder ends with a '/'
|
|
39
|
-
$this->destination = rtrim($uploadFolder, '/') . '/';
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Sets the maximum size for uploaded files.
|
|
44
|
-
*
|
|
45
|
-
* @param int $bytes The maximum size in bytes.
|
|
46
|
-
* @return void
|
|
47
|
-
*/
|
|
48
|
-
public function setMaxSize(int $bytes): void
|
|
49
|
-
{
|
|
50
|
-
$serverMax = self::convertToBytes(ini_get('upload_max_filesize'));
|
|
51
|
-
if ($bytes > $serverMax) {
|
|
52
|
-
throw new \Exception('Maximum size cannot exceed server limit for individual files: ' . self::convertFromBytes($serverMax));
|
|
53
|
-
}
|
|
54
|
-
if ($bytes > 0) {
|
|
55
|
-
$this->maxSize = $bytes;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Converts a string value representing a file size to bytes.
|
|
61
|
-
*
|
|
62
|
-
* @param string $val The string value representing the file size.
|
|
63
|
-
* @return int The file size in bytes.
|
|
64
|
-
*/
|
|
65
|
-
public static function convertToBytes(string $val): int
|
|
66
|
-
{
|
|
67
|
-
$val = trim($val);
|
|
68
|
-
$last = strtolower($val[strlen($val) - 1]);
|
|
69
|
-
$multiplier = match ($last) {
|
|
70
|
-
'g' => 1024 * 1024 * 1024,
|
|
71
|
-
'm' => 1024 * 1024,
|
|
72
|
-
'k' => 1024,
|
|
73
|
-
default => 1,
|
|
74
|
-
};
|
|
75
|
-
return (int) $val * $multiplier;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Converts the given number of bytes to a human-readable string representation.
|
|
80
|
-
*
|
|
81
|
-
* @param int $bytes The number of bytes to convert.
|
|
82
|
-
* @return string The human-readable string representation of the converted bytes.
|
|
83
|
-
*/
|
|
84
|
-
public static function convertFromBytes(int $bytes): string
|
|
85
|
-
{
|
|
86
|
-
return $bytes >= 1024 * 1024
|
|
87
|
-
? number_format($bytes / (1024 * 1024), 1) . ' MB'
|
|
88
|
-
: number_format($bytes / 1024, 1) . ' KB';
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Disable type checking and set the allowed file types.
|
|
93
|
-
*
|
|
94
|
-
* @param string|null $suffix The file suffix to allow. If null, the current suffix will be used.
|
|
95
|
-
* @return void
|
|
96
|
-
*/
|
|
97
|
-
public function allowAllTypes(?string $suffix = null): void
|
|
98
|
-
{
|
|
99
|
-
$this->typeCheckingOn = false;
|
|
100
|
-
$this->suffix = $suffix ? (strpos($suffix, '.') === 0 ? $suffix : ".$suffix") : $this->suffix;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Uploads file(s) to the server.
|
|
105
|
-
*
|
|
106
|
-
* @param bool $renameDuplicates (optional) Whether to rename duplicate files. Default is true.
|
|
107
|
-
* @return void
|
|
108
|
-
*/
|
|
109
|
-
public function upload(bool $renameDuplicates = true): void
|
|
110
|
-
{
|
|
111
|
-
$this->renameDuplicates = $renameDuplicates;
|
|
112
|
-
if (empty($_FILES) || !is_array(current($_FILES))) {
|
|
113
|
-
// No file was uploaded or the structure is invalid, handle this as an error
|
|
114
|
-
$this->messages[] = "No files were uploaded.";
|
|
115
|
-
$this->errorCode[] = UPLOAD_ERR_NO_FILE;
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
$uploaded = current($_FILES);
|
|
120
|
-
|
|
121
|
-
// Handle single and multiple file uploads using a unified approach
|
|
122
|
-
$files = is_array($uploaded['name']) ? $this->rearrangeFilesArray($uploaded) : [$uploaded];
|
|
123
|
-
|
|
124
|
-
foreach ($files as $file) {
|
|
125
|
-
if ($this->checkFile($file)) {
|
|
126
|
-
$this->moveFile($file);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Updates an existing file by deleting the old file and uploading the new one, using the old file's name.
|
|
133
|
-
*
|
|
134
|
-
* @param array $file The new file information from $_FILES.
|
|
135
|
-
* @param string $oldFilename The name of the file to be replaced.
|
|
136
|
-
* @return bool True if the update was successful, false otherwise.
|
|
137
|
-
*/
|
|
138
|
-
public function update(array $file, string $oldFilename): bool
|
|
139
|
-
{
|
|
140
|
-
// First, delete the old file
|
|
141
|
-
if (!$this->delete($oldFilename)) {
|
|
142
|
-
$this->messages[] = "Failed to delete the old file $oldFilename. Update aborted.";
|
|
143
|
-
$this->errorCode[] = UPLOAD_ERR_CANT_WRITE; // Error code for failure to update
|
|
144
|
-
return false;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Now proceed to upload the new file with the old filename
|
|
148
|
-
if ($this->checkFile($file)) {
|
|
149
|
-
// Set the new file name to match the old file's name
|
|
150
|
-
$this->newName = $oldFilename;
|
|
151
|
-
$this->moveFile($file);
|
|
152
|
-
return true;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return false;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Renames a file in the destination folder.
|
|
160
|
-
*
|
|
161
|
-
* @param string $oldName The current name of the file.
|
|
162
|
-
* @param string $newName The new name for the file.
|
|
163
|
-
* @return bool True if the rename was successful, false otherwise.
|
|
164
|
-
*/
|
|
165
|
-
public function rename(string $oldName, string $newName): bool
|
|
166
|
-
{
|
|
167
|
-
$oldPath = $this->destination . $oldName;
|
|
168
|
-
|
|
169
|
-
// Extract the file extension from the old file
|
|
170
|
-
$extension = pathinfo($oldName, PATHINFO_EXTENSION);
|
|
171
|
-
|
|
172
|
-
// Add the extension to the new name
|
|
173
|
-
$newNameWithExtension = str_replace(' ', '_', $newName) . '.' . $extension;
|
|
174
|
-
$newPath = $this->destination . $newNameWithExtension;
|
|
175
|
-
|
|
176
|
-
// Check if the file exists
|
|
177
|
-
if (!file_exists($oldPath)) {
|
|
178
|
-
$this->messages[] = "File $oldName does not exist.";
|
|
179
|
-
$this->errorCode[] = UPLOAD_ERR_NO_FILE; // Error code for file not found
|
|
180
|
-
return false;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Validate that the new name doesn't already exist
|
|
184
|
-
if (file_exists($newPath)) {
|
|
185
|
-
$this->messages[] = "A file with the name $newNameWithExtension already exists.";
|
|
186
|
-
$this->errorCode[] = UPLOAD_ERR_CANT_WRITE; // Error code for name conflict
|
|
187
|
-
return false;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Attempt to rename the file
|
|
191
|
-
if (rename($oldPath, $newPath)) {
|
|
192
|
-
$this->messages[] = "File $oldName renamed successfully to $newNameWithExtension";
|
|
193
|
-
$this->errorCode[] = 0; // Success code
|
|
194
|
-
return true;
|
|
195
|
-
} else {
|
|
196
|
-
$this->messages[] = "Failed to rename $oldName to $newNameWithExtension";
|
|
197
|
-
$this->errorCode[] = UPLOAD_ERR_CANT_WRITE; // Error code for rename failure
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Deletes a file from the destination folder.
|
|
204
|
-
*
|
|
205
|
-
* @param string $filename The name of the file to delete.
|
|
206
|
-
* @return bool True if the file was deleted, false otherwise.
|
|
207
|
-
*/
|
|
208
|
-
public function delete(string $filename): bool
|
|
209
|
-
{
|
|
210
|
-
$filePath = $this->destination . $filename;
|
|
211
|
-
|
|
212
|
-
if (!file_exists($filePath)) {
|
|
213
|
-
$this->messages[] = "File $filename does not exist.";
|
|
214
|
-
$this->errorCode[] = UPLOAD_ERR_NO_FILE; // Error code for file not found
|
|
215
|
-
return false;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (unlink($filePath)) {
|
|
219
|
-
$this->messages[] = "File $filename deleted successfully.";
|
|
220
|
-
$this->errorCode[] = 0; // Success code
|
|
221
|
-
return true;
|
|
222
|
-
} else {
|
|
223
|
-
$this->messages[] = "Failed to delete $filename.";
|
|
224
|
-
$this->errorCode[] = UPLOAD_ERR_CANT_WRITE; // Error code for failure to delete
|
|
225
|
-
return false;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Retrieves the messages associated with the file upload.
|
|
231
|
-
*
|
|
232
|
-
* @return array The array of messages.
|
|
233
|
-
*/
|
|
234
|
-
public function getMessages(): array
|
|
235
|
-
{
|
|
236
|
-
return $this->messages;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Retrieves the error codes associated with the file upload.
|
|
241
|
-
*
|
|
242
|
-
* @return array The array of error codes.
|
|
243
|
-
*/
|
|
244
|
-
public function getErrorCode(): array
|
|
245
|
-
{
|
|
246
|
-
return $this->errorCode;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
protected function checkFile(array $file): bool
|
|
250
|
-
{
|
|
251
|
-
if ($file['error'] !== UPLOAD_ERR_OK) {
|
|
252
|
-
$this->getErrorMessage($file);
|
|
253
|
-
return false;
|
|
254
|
-
}
|
|
255
|
-
if (!$this->checkSize($file) || ($this->typeCheckingOn && !$this->checkType($file))) {
|
|
256
|
-
return false;
|
|
257
|
-
}
|
|
258
|
-
$this->checkName($file);
|
|
259
|
-
return true;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
protected function getErrorMessage(array $file): void
|
|
263
|
-
{
|
|
264
|
-
$errorMessages = [
|
|
265
|
-
UPLOAD_ERR_INI_SIZE => $file['name'] . ' exceeds the maximum size: ' . self::convertFromBytes($this->maxSize),
|
|
266
|
-
UPLOAD_ERR_FORM_SIZE => $file['name'] . ' exceeds the form limit.',
|
|
267
|
-
UPLOAD_ERR_PARTIAL => $file['name'] . ' was only partially uploaded.',
|
|
268
|
-
UPLOAD_ERR_NO_FILE => 'No file submitted.',
|
|
269
|
-
];
|
|
270
|
-
|
|
271
|
-
$this->errorCode[] = $file['error'];
|
|
272
|
-
$this->messages[] = $errorMessages[$file['error']] ?? 'Problem uploading ' . $file['name'];
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
protected function checkSize(array $file): bool
|
|
276
|
-
{
|
|
277
|
-
if ($file['size'] == 0) {
|
|
278
|
-
$this->messages[] = $file['name'] . ' is empty.';
|
|
279
|
-
$this->errorCode[] = UPLOAD_ERR_NO_FILE; // Log an error code for empty files
|
|
280
|
-
return false;
|
|
281
|
-
}
|
|
282
|
-
if ($file['size'] > $this->maxSize) {
|
|
283
|
-
$this->messages[] = $file['name'] . ' exceeds the maximum size: ' . self::convertFromBytes($this->maxSize);
|
|
284
|
-
$this->errorCode[] = UPLOAD_ERR_INI_SIZE; // Log an error code for exceeding size
|
|
285
|
-
return false;
|
|
286
|
-
}
|
|
287
|
-
return true;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
protected function checkType(array $file): bool
|
|
291
|
-
{
|
|
292
|
-
if (!in_array($file['type'], $this->permittedTypes)) {
|
|
293
|
-
$this->messages[] = $file['name'] . ' is not a permitted type.';
|
|
294
|
-
$this->errorCode[] = UPLOAD_ERR_EXTENSION; // Log an error code for invalid file type
|
|
295
|
-
return false;
|
|
296
|
-
}
|
|
297
|
-
return true;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
protected function checkName(array $file): void
|
|
301
|
-
{
|
|
302
|
-
$this->newName = '';
|
|
303
|
-
$noSpaces = str_replace(' ', '_', $file['name']);
|
|
304
|
-
if ($noSpaces != $file['name']) {
|
|
305
|
-
$this->newName = $noSpaces;
|
|
306
|
-
}
|
|
307
|
-
$nameParts = pathinfo($noSpaces);
|
|
308
|
-
$extension = $nameParts['extension'] ?? '';
|
|
309
|
-
if (!$this->typeCheckingOn && !empty($this->suffix)) {
|
|
310
|
-
if (in_array($extension, $this->notTrusted) || empty($extension)) {
|
|
311
|
-
$this->newName = $noSpaces . $this->suffix;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
if ($this->renameDuplicates) {
|
|
315
|
-
$name = isset($this->newName) ? $this->newName : $file['name'];
|
|
316
|
-
$existing = scandir($this->destination);
|
|
317
|
-
if (in_array($name, $existing)) {
|
|
318
|
-
$i = 1;
|
|
319
|
-
do {
|
|
320
|
-
$this->newName = $nameParts['filename'] . '_' . $i++;
|
|
321
|
-
if (!empty($extension)) {
|
|
322
|
-
$this->newName .= ".$extension";
|
|
323
|
-
}
|
|
324
|
-
if (in_array($extension, $this->notTrusted)) {
|
|
325
|
-
$this->newName .= $this->suffix;
|
|
326
|
-
}
|
|
327
|
-
} while (in_array($this->newName, $existing));
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
protected function moveFile(array $file): void
|
|
333
|
-
{
|
|
334
|
-
// Ensure the newName is set or fallback to the original file name
|
|
335
|
-
$filename = $this->newName ?: $file['name'];
|
|
336
|
-
$destination = $this->destination . $filename;
|
|
337
|
-
$success = move_uploaded_file($file['tmp_name'], $destination);
|
|
338
|
-
|
|
339
|
-
if ($success) {
|
|
340
|
-
$message = "{$file['name']} uploaded successfully.";
|
|
341
|
-
if ($this->newName && $this->newName !== $file['name']) {
|
|
342
|
-
$message .= " Renamed to {$this->newName}";
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
$this->successfulUploads[] = [
|
|
346
|
-
'original' => $file['name'],
|
|
347
|
-
'final' => $filename
|
|
348
|
-
];
|
|
349
|
-
} else {
|
|
350
|
-
$message = "Failed to upload {$file['name']}.";
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
$this->messages[] = $message;
|
|
354
|
-
// Add a success/error code for file move
|
|
355
|
-
$this->errorCode[] = $success ? 0 : UPLOAD_ERR_CANT_WRITE; // 0 for success, error code for failure
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Utility function to restructure the $_FILES array for multiple uploads
|
|
359
|
-
protected function rearrangeFilesArray(array $files): array
|
|
360
|
-
{
|
|
361
|
-
$rearranged = [];
|
|
362
|
-
foreach ($files['name'] as $key => $name) {
|
|
363
|
-
$rearranged[] = [
|
|
364
|
-
'name' => $files['name'][$key],
|
|
365
|
-
'type' => $files['type'][$key],
|
|
366
|
-
'tmp_name' => $files['tmp_name'][$key],
|
|
367
|
-
'error' => $files['error'][$key],
|
|
368
|
-
'size' => $files['size'][$key],
|
|
369
|
-
];
|
|
370
|
-
}
|
|
371
|
-
return $rearranged;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Retrieves the successfully uploaded file names.
|
|
376
|
-
*
|
|
377
|
-
* @return array An array of arrays containing 'original' and 'final' file names.
|
|
378
|
-
*/
|
|
379
|
-
public function getSuccessfulUploads(): array
|
|
380
|
-
{
|
|
381
|
-
return $this->successfulUploads;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
namespace Lib\Headers;
|
|
6
|
-
|
|
7
|
-
use InvalidArgumentException;
|
|
8
|
-
use JsonException;
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* HTTP‑error helper.
|
|
12
|
-
*
|
|
13
|
-
* @method static self badRequest(string $message = 'Bad Request', array $details = [])
|
|
14
|
-
* @method static self unauthorized(string $message = 'Unauthorized', array $details = [])
|
|
15
|
-
* @method static self paymentRequired(string $message = 'Payment Required', array $details = [])
|
|
16
|
-
* @method static self forbidden(string $message = 'Forbidden', array $details = [])
|
|
17
|
-
* @method static self notFound(string $message = 'Not Found', array $details = [])
|
|
18
|
-
* @method static self methodNotAllowed(string $message = 'Method Not Allowed', array $details = [])
|
|
19
|
-
* @method static self notAcceptable(string $message = 'Not Acceptable', array $details = [])
|
|
20
|
-
* @method static self conflict(string $message = 'Conflict', array $details = [])
|
|
21
|
-
* @method static self gone(string $message = 'Gone', array $details = [])
|
|
22
|
-
* @method static self lengthRequired(string $message = 'Length Required', array $details = [])
|
|
23
|
-
* @method static self preconditionFailed(string $message = 'Precondition Failed', array $details = [])
|
|
24
|
-
* @method static self payloadTooLarge(string $message = 'Payload Too Large', array $details = [])
|
|
25
|
-
* @method static self uriTooLarge(string $message = 'URI Too Large', array $details = [])
|
|
26
|
-
* @method static self unsupportedMediaType(string $message = 'Unsupported Media Type', array $details = [])
|
|
27
|
-
* @method static self rangeNotSatisfiable(string $message = 'Range Not Satisfiable', array $details = [])
|
|
28
|
-
* @method static self expectationFailed(string $message = 'Expectation Failed', array $details = [])
|
|
29
|
-
* @method static self iAmATeapot(string $message = "I'm a teapot", array $details = [])
|
|
30
|
-
* @method static self misdirectedRequest(string $message = 'Misdirected Request', array $details = [])
|
|
31
|
-
* @method static self unprocessableEntity(string $message = 'Unprocessable Entity', array $details = [])
|
|
32
|
-
* @method static self locked(string $message = 'Locked', array $details = [])
|
|
33
|
-
* @method static self failedDependency(string $message = 'Failed Dependency', array $details = [])
|
|
34
|
-
* @method static self tooEarly(string $message = 'Too Early', array $details = [])
|
|
35
|
-
* @method static self upgradeRequired(string $message = 'Upgrade Required', array $details = [])
|
|
36
|
-
* @method static self preconditionRequired(string $message = 'Precondition Required', array $details = [])
|
|
37
|
-
* @method static self tooManyRequests(string $message = 'Too Many Requests', array $details = [])
|
|
38
|
-
* @method static self requestHeaderFieldsTooLarge(string $message = 'Request Header Fields Too Large', array $details = [])
|
|
39
|
-
* @method static self unavailableForLegalReasons(string $message = 'Unavailable for Legal Reasons', array $details = [])
|
|
40
|
-
* @method static self internal(string $message = 'Internal Server Error', array $details = [])
|
|
41
|
-
* @method static self notImplemented(string $message = 'Not Implemented', array $details = [])
|
|
42
|
-
* @method static self badGateway(string $message = 'Bad Gateway', array $details = [])
|
|
43
|
-
* @method static self serviceUnavailable(string $message = 'Service Unavailable', array $details = [])
|
|
44
|
-
* @method static self gatewayTimeout(string $message = 'Gateway Timeout', array $details = [])
|
|
45
|
-
* @method static self httpVersionNotSupported(string $message = 'HTTP Version Not Supported', array $details = [])
|
|
46
|
-
* @method static self insufficientStorage(string $message = 'Insufficient Storage', array $details = [])
|
|
47
|
-
* @method static self loopDetected(string $message = 'Loop Detected', array $details = [])
|
|
48
|
-
* @method static self notExtended(string $message = 'Not Extended', array $details = [])
|
|
49
|
-
* @method static self networkAuthenticationRequired(string $message = 'Network Authentication Required', array $details = [])
|
|
50
|
-
* @method static self networkConnectTimeoutError(string $message = 'Network Connect Timeout Error', array $details = [])
|
|
51
|
-
*/
|
|
52
|
-
class Boom
|
|
53
|
-
{
|
|
54
|
-
private const PHRASES = [
|
|
55
|
-
/* 4XX Client error */
|
|
56
|
-
400 => 'Bad Request',
|
|
57
|
-
401 => 'Unauthorized',
|
|
58
|
-
402 => 'Payment Required',
|
|
59
|
-
403 => 'Forbidden',
|
|
60
|
-
404 => 'Not Found',
|
|
61
|
-
405 => 'Method Not Allowed',
|
|
62
|
-
406 => 'Not Acceptable',
|
|
63
|
-
407 => 'Proxy Authentication Required',
|
|
64
|
-
408 => 'Request Timeout',
|
|
65
|
-
409 => 'Conflict',
|
|
66
|
-
410 => 'Gone',
|
|
67
|
-
411 => 'Length Required',
|
|
68
|
-
412 => 'Precondition Failed',
|
|
69
|
-
413 => 'Payload Too Large',
|
|
70
|
-
414 => 'URI Too Large',
|
|
71
|
-
415 => 'Unsupported Media Type',
|
|
72
|
-
416 => 'Range Not Satisfiable',
|
|
73
|
-
417 => 'Expectation Failed',
|
|
74
|
-
418 => "I'm a teapot",
|
|
75
|
-
421 => 'Misdirected Request',
|
|
76
|
-
422 => 'Unprocessable Entity',
|
|
77
|
-
423 => 'Locked',
|
|
78
|
-
424 => 'Failed Dependency',
|
|
79
|
-
425 => 'Too Early',
|
|
80
|
-
426 => 'Upgrade Required',
|
|
81
|
-
428 => 'Precondition Required',
|
|
82
|
-
429 => 'Too Many Requests',
|
|
83
|
-
431 => 'Request Header Fields Too Large',
|
|
84
|
-
451 => 'Unavailable for Legal Reasons',
|
|
85
|
-
499 => 'Client Closed Request',
|
|
86
|
-
|
|
87
|
-
/* 5XX Server error */
|
|
88
|
-
500 => 'Internal Server Error',
|
|
89
|
-
501 => 'Not Implemented',
|
|
90
|
-
502 => 'Bad Gateway',
|
|
91
|
-
503 => 'Service Unavailable',
|
|
92
|
-
504 => 'Gateway Timeout',
|
|
93
|
-
505 => 'HTTP Version Not Supported',
|
|
94
|
-
507 => 'Insufficient Storage',
|
|
95
|
-
508 => 'Loop Detected',
|
|
96
|
-
510 => 'Not Extended',
|
|
97
|
-
511 => 'Network Authentication Required',
|
|
98
|
-
599 => 'Network Connect Timeout Error',
|
|
99
|
-
];
|
|
100
|
-
|
|
101
|
-
/** @var int */
|
|
102
|
-
protected int $statusCode;
|
|
103
|
-
|
|
104
|
-
/** @var string */
|
|
105
|
-
protected string $errorMessage;
|
|
106
|
-
|
|
107
|
-
/** @var array<string,mixed> */
|
|
108
|
-
protected array $errorDetails;
|
|
109
|
-
|
|
110
|
-
public function __construct(
|
|
111
|
-
int $statusCode,
|
|
112
|
-
string $errorMessage = '',
|
|
113
|
-
array $errorDetails = [],
|
|
114
|
-
) {
|
|
115
|
-
if (!isset(self::PHRASES[$statusCode])) {
|
|
116
|
-
throw new InvalidArgumentException("Unsupported HTTP status code: $statusCode");
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
$this->statusCode = $statusCode;
|
|
120
|
-
$this->errorMessage = $errorMessage ?: self::PHRASES[$statusCode];
|
|
121
|
-
$this->errorDetails = $errorDetails;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
public static function create(int $code, ?string $msg = null, array $details = []): self
|
|
125
|
-
{
|
|
126
|
-
return new self($code, $msg ?? '', $details);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Dynamic factories: Boom::tooManyRequests(), Boom::badRequest(), …
|
|
131
|
-
*
|
|
132
|
-
* @param array{0?:string,1?:array<mixed>} $args
|
|
133
|
-
*/
|
|
134
|
-
public static function __callStatic(string $method, array $args): self
|
|
135
|
-
{
|
|
136
|
-
// Convert camelCase to Studly Caps → Reason‑Phrase → code
|
|
137
|
-
$normalized = strtolower(preg_replace('/([a-z])([A-Z])/', '$1 $2', $method) ?? '');
|
|
138
|
-
$code = array_search(
|
|
139
|
-
ucwords(str_replace(' ', ' ', $normalized)),
|
|
140
|
-
self::PHRASES,
|
|
141
|
-
true
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
if ($code === false) {
|
|
145
|
-
throw new InvalidArgumentException("Undefined Boom factory: $method()");
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
$msg = $args[0] ?? '';
|
|
149
|
-
$details = $args[1] ?? [];
|
|
150
|
-
|
|
151
|
-
return new self((int)$code, $msg, $details);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
public function toResponse(): void
|
|
155
|
-
{
|
|
156
|
-
http_response_code($this->statusCode);
|
|
157
|
-
header('Content-Type: application/json; charset=utf-8');
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
echo json_encode(
|
|
161
|
-
[
|
|
162
|
-
'statusCode' => $this->statusCode,
|
|
163
|
-
'error' => $this->errorMessage,
|
|
164
|
-
'details' => (object)$this->errorDetails,
|
|
165
|
-
],
|
|
166
|
-
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
|
|
167
|
-
);
|
|
168
|
-
} catch (JsonException $e) {
|
|
169
|
-
echo '{"statusCode":500,"error":"JSON encoding error"}';
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
exit; // Ensure no further output
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
public static function isBoom(mixed $err): bool
|
|
176
|
-
{
|
|
177
|
-
return $err instanceof self;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
public function getStatusCode(): int
|
|
181
|
-
{
|
|
182
|
-
return $this->statusCode;
|
|
183
|
-
}
|
|
184
|
-
public function getErrorMessage(): string
|
|
185
|
-
{
|
|
186
|
-
return $this->errorMessage;
|
|
187
|
-
}
|
|
188
|
-
public function getErrorDetails(): array
|
|
189
|
-
{
|
|
190
|
-
return $this->errorDetails;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
namespace Lib\PHPMailer;
|
|
6
|
-
|
|
7
|
-
use PHPMailer\PHPMailer\PHPMailer;
|
|
8
|
-
use PHPMailer\PHPMailer\Exception;
|
|
9
|
-
use PPHP\Validator;
|
|
10
|
-
|
|
11
|
-
class Mailer
|
|
12
|
-
{
|
|
13
|
-
private PHPMailer $mail;
|
|
14
|
-
|
|
15
|
-
public function __construct()
|
|
16
|
-
{
|
|
17
|
-
$this->mail = new PHPMailer(true);
|
|
18
|
-
$this->mail->CharSet = 'UTF-8';
|
|
19
|
-
$this->setup();
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
private function setup(): void
|
|
23
|
-
{
|
|
24
|
-
$this->mail->isSMTP();
|
|
25
|
-
$this->mail->SMTPDebug = 0;
|
|
26
|
-
$this->mail->Host = $_ENV['SMTP_HOST'];
|
|
27
|
-
$this->mail->SMTPAuth = true;
|
|
28
|
-
$this->mail->Username = $_ENV['SMTP_USERNAME'];
|
|
29
|
-
$this->mail->Password = $_ENV['SMTP_PASSWORD'];
|
|
30
|
-
$this->mail->SMTPSecure = $_ENV['SMTP_ENCRYPTION'];
|
|
31
|
-
$this->mail->Port = (int) $_ENV['SMTP_PORT'];
|
|
32
|
-
$this->mail->setFrom($_ENV['MAIL_FROM'], $_ENV['MAIL_FROM_NAME']);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Send an email.
|
|
37
|
-
*
|
|
38
|
-
* @param string $to The recipient's email address.
|
|
39
|
-
* @param string $subject The subject of the email.
|
|
40
|
-
* @param string $body The HTML body of the email.
|
|
41
|
-
* @param array $options (optional) Additional email options like name, altBody, CC, BCC, and attachments.
|
|
42
|
-
* - attachments: A string or an array of file paths, or an array of associative arrays with keys 'path' and 'name'.
|
|
43
|
-
*
|
|
44
|
-
* @return bool Returns true if the email is sent successfully, false otherwise.
|
|
45
|
-
*
|
|
46
|
-
* @throws Exception Throws an exception if the email could not be sent.
|
|
47
|
-
*/
|
|
48
|
-
public function send(string $to, string $subject, string $body, array $options = []): bool
|
|
49
|
-
{
|
|
50
|
-
try {
|
|
51
|
-
// Validate and sanitize inputs
|
|
52
|
-
$to = Validator::email($to);
|
|
53
|
-
if (!$to) {
|
|
54
|
-
throw new Exception('Invalid email address for the main recipient');
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
$subject = Validator::string($subject);
|
|
58
|
-
$body = Validator::html($body);
|
|
59
|
-
$altBody = $this->convertToPlainText($body);
|
|
60
|
-
|
|
61
|
-
$name = $options['name'] ?? '';
|
|
62
|
-
$addCC = $options['addCC'] ?? [];
|
|
63
|
-
$addBCC = $options['addBCC'] ?? [];
|
|
64
|
-
$attachments = $options['attachments'] ?? [];
|
|
65
|
-
|
|
66
|
-
$name = Validator::string($name);
|
|
67
|
-
|
|
68
|
-
// Handle CC recipients
|
|
69
|
-
$this->handleRecipients($addCC, 'CC');
|
|
70
|
-
// Handle BCC recipients
|
|
71
|
-
$this->handleRecipients($addBCC, 'BCC');
|
|
72
|
-
// Handle file attachments if provided
|
|
73
|
-
if (!empty($attachments)) {
|
|
74
|
-
$this->handleAttachments($attachments);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Set the main recipient and other email properties
|
|
78
|
-
$this->mail->addAddress($to, $name);
|
|
79
|
-
$this->mail->isHTML(true);
|
|
80
|
-
$this->mail->Subject = $subject;
|
|
81
|
-
$this->mail->Body = $body;
|
|
82
|
-
$this->mail->AltBody = $altBody;
|
|
83
|
-
|
|
84
|
-
// Send the email
|
|
85
|
-
return $this->mail->send();
|
|
86
|
-
} catch (Exception $e) {
|
|
87
|
-
throw new Exception($e->getMessage());
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Handle adding CC or BCC recipients.
|
|
93
|
-
*
|
|
94
|
-
* @param string|array $recipients Email addresses to add.
|
|
95
|
-
* @param string $type Type of recipient ('CC' or 'BCC').
|
|
96
|
-
*
|
|
97
|
-
* @throws Exception Throws an exception if any email address is invalid.
|
|
98
|
-
*/
|
|
99
|
-
private function handleRecipients(string|array $recipients, string $type): void
|
|
100
|
-
{
|
|
101
|
-
if (!empty($recipients)) {
|
|
102
|
-
$method = $type === 'CC' ? 'addCC' : 'addBCC';
|
|
103
|
-
|
|
104
|
-
if (is_array($recipients)) {
|
|
105
|
-
foreach ($recipients as $recipient) {
|
|
106
|
-
$recipient = Validator::email($recipient);
|
|
107
|
-
if ($recipient) {
|
|
108
|
-
$this->mail->{$method}($recipient);
|
|
109
|
-
} else {
|
|
110
|
-
throw new Exception("Invalid email address in $type");
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
} else {
|
|
114
|
-
$recipient = Validator::email($recipients);
|
|
115
|
-
if ($recipient) {
|
|
116
|
-
$this->mail->{$method}($recipient);
|
|
117
|
-
} else {
|
|
118
|
-
throw new Exception("Invalid email address in $type");
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Handle adding file attachments.
|
|
126
|
-
*
|
|
127
|
-
* @param string|array $attachments File path(s) to attach.
|
|
128
|
-
* You can pass a string for a single file or an array of file paths.
|
|
129
|
-
* Alternatively, each attachment can be an array with keys 'path' and 'name' for custom naming.
|
|
130
|
-
*
|
|
131
|
-
* @throws Exception Throws an exception if any attachment file is not found.
|
|
132
|
-
*/
|
|
133
|
-
private function handleAttachments(string|array $attachments): void
|
|
134
|
-
{
|
|
135
|
-
if (is_array($attachments)) {
|
|
136
|
-
foreach ($attachments as $attachment) {
|
|
137
|
-
if (is_array($attachment)) {
|
|
138
|
-
$file = $attachment['path'] ?? null;
|
|
139
|
-
$name = $attachment['name'] ?? '';
|
|
140
|
-
if (!$file || !file_exists($file)) {
|
|
141
|
-
throw new Exception("Attachment file does not exist: " . ($file ?? 'unknown'));
|
|
142
|
-
}
|
|
143
|
-
$this->mail->addAttachment($file, $name);
|
|
144
|
-
} else {
|
|
145
|
-
if (!file_exists($attachment)) {
|
|
146
|
-
throw new Exception("Attachment file does not exist: $attachment");
|
|
147
|
-
}
|
|
148
|
-
$this->mail->addAttachment($attachment);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
} else {
|
|
152
|
-
if (!file_exists($attachments)) {
|
|
153
|
-
throw new Exception("Attachment file does not exist: $attachments");
|
|
154
|
-
}
|
|
155
|
-
$this->mail->addAttachment($attachments);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Convert HTML content to plain text.
|
|
161
|
-
*
|
|
162
|
-
* @param string $html The HTML content to convert.
|
|
163
|
-
* @return string The plain text content.
|
|
164
|
-
*/
|
|
165
|
-
private function convertToPlainText(string $html): string
|
|
166
|
-
{
|
|
167
|
-
return strip_tags(str_replace(['<br>', '<br/>', '<br />', '</p>'], "\n", $html));
|
|
168
|
-
}
|
|
169
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
namespace Lib\Security;
|
|
3
|
-
|
|
4
|
-
use Lib\Headers\Boom;
|
|
5
|
-
|
|
6
|
-
final class RateLimiter
|
|
7
|
-
{
|
|
8
|
-
/**
|
|
9
|
-
* Lanza HTTP 429 si se supera el máximo de intentos en la ventana dada.
|
|
10
|
-
*
|
|
11
|
-
* @param string $key Identificador único (p. ej. IP o user‑id).
|
|
12
|
-
* @param int $maxAttempts Nº de peticiones permitidas.
|
|
13
|
-
* @param int $seconds Ventana de tiempo en segundos.
|
|
14
|
-
*/
|
|
15
|
-
public static function check(string $key, int $maxAttempts = 60, int $seconds = 60): void
|
|
16
|
-
{
|
|
17
|
-
if (!function_exists('apcu_fetch')) {
|
|
18
|
-
// APCu no instalado: conviene registrar un “fallback” o lanzar excepción.
|
|
19
|
-
Boom::internal("APCu extension missing for rate‑limit.")->toResponse();
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
$apcuKey = "ratelimit:{$key}";
|
|
23
|
-
$current = apcu_fetch($apcuKey) ?: 0;
|
|
24
|
-
|
|
25
|
-
if ($current >= $maxAttempts) {
|
|
26
|
-
// HTTP 429 Too Many Requests
|
|
27
|
-
Boom::tooManyRequests('Rate limit exceeded, try again later.')->toResponse();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Incrementa contador y refresca TTL
|
|
31
|
-
apcu_store($apcuKey, $current + 1, $seconds);
|
|
32
|
-
}
|
|
33
|
-
}
|