Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
P
phphr
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
phphr
Commits
1446ef10
Commit
1446ef10
authored
Apr 08, 2026
by
Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update 8 files via Son of Anton
parent
a31c9498
Pipeline
#32
failed with stage
Changes
8
Pipelines
1
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
214 additions
and
208 deletions
+214
-208
app.php
bootstrap/app.php
+13
-7
autoload.php
bootstrap/autoload.php
+4
-1
captain-definition
captain-definition
+2
-2
php-custom.ini
docker/php-custom.ini
+7
-10
Request.php
engine/Core/Request.php
+12
-1
WebhookController.php
modules/Webhooks/Controllers/WebhookController.php
+63
-61
WebhookDispatcher.php
modules/Webhooks/Services/WebhookDispatcher.php
+41
-80
index.php
public/index.php
+72
-46
No files found.
bootstrap/app.php
View file @
1446ef10
<?php
<?php
declare
(
strict_types
=
1
);
declare
(
strict_types
=
1
);
require_once
__DIR__
.
'/autoload.php'
;
// Autoload is already loaded by public/index.php
// ROOT_PATH is already defined by public/index.php
if
(
!
defined
(
'ROOT_PATH'
))
{
define
(
'ROOT_PATH'
,
dirname
(
__DIR__
));
require_once
__DIR__
.
'/autoload.php'
;
}
use
Engine\Core
\
{
Container
,
Config
,
Router
};
use
Engine\Core
\
{
Container
,
Config
,
Router
};
use
Engine\Database\Connection
;
use
Engine\Database\Connection
;
...
@@ -23,7 +28,6 @@ date_default_timezone_set('Africa/Cairo');
...
@@ -23,7 +28,6 @@ date_default_timezone_set('Africa/Cairo');
// Error handling — log everything
// Error handling — log everything
set_error_handler
(
function
(
int
$errno
,
string
$errstr
,
string
$errfile
,
int
$errline
)
{
set_error_handler
(
function
(
int
$errno
,
string
$errstr
,
string
$errfile
,
int
$errline
)
{
// Don't throw on suppressed errors
if
(
!
(
error_reporting
()
&
$errno
))
return
false
;
if
(
!
(
error_reporting
()
&
$errno
))
return
false
;
throw
new
\ErrorException
(
$errstr
,
0
,
$errno
,
$errfile
,
$errline
);
throw
new
\ErrorException
(
$errstr
,
0
,
$errno
,
$errfile
,
$errline
);
});
});
...
@@ -31,10 +35,10 @@ set_error_handler(function (int $errno, string $errstr, string $errfile, int $er
...
@@ -31,10 +35,10 @@ set_error_handler(function (int $errno, string $errstr, string $errfile, int $er
// ─── CONTAINER REGISTRATION ───
// ─── CONTAINER REGISTRATION ───
$container
=
Container
::
getInstance
();
$container
=
Container
::
getInstance
();
// Config
first — everything depends on it
// Config
$container
->
singleton
(
Config
::
class
,
fn
()
=>
new
Config
());
$container
->
singleton
(
Config
::
class
,
fn
()
=>
new
Config
());
// Database
— everything depends on it
// Database
$container
->
singleton
(
Connection
::
class
,
fn
()
=>
new
Connection
());
$container
->
singleton
(
Connection
::
class
,
fn
()
=>
new
Connection
());
// Auth layer
// Auth layer
...
@@ -62,9 +66,11 @@ $calcEngine = new CalculationEngine();
...
@@ -62,9 +66,11 @@ $calcEngine = new CalculationEngine();
$calculatorsFile
=
ROOT_PATH
.
'/config/calculators.php'
;
$calculatorsFile
=
ROOT_PATH
.
'/config/calculators.php'
;
if
(
file_exists
(
$calculatorsFile
))
{
if
(
file_exists
(
$calculatorsFile
))
{
$calculators
=
require
$calculatorsFile
;
$calculators
=
require
$calculatorsFile
;
if
(
is_array
(
$calculators
))
{
foreach
(
$calculators
as
$name
=>
$class
)
{
foreach
(
$calculators
as
$name
=>
$class
)
{
$calcEngine
->
register
(
$name
,
$class
);
$calcEngine
->
register
(
$name
,
$class
);
}
}
}
}
}
$container
->
instance
(
CalculationEngine
::
class
,
$calcEngine
);
$container
->
instance
(
CalculationEngine
::
class
,
$calcEngine
);
...
@@ -76,7 +82,7 @@ if ($routeFiles) {
...
@@ -76,7 +82,7 @@ if ($routeFiles) {
require_once
$routeFile
;
require_once
$routeFile
;
}
catch
(
\Throwable
$e
)
{
}
catch
(
\Throwable
$e
)
{
error_log
(
"ROUTE LOAD ERROR [
{
$routeFile
}
]: "
.
$e
->
getMessage
());
error_log
(
"ROUTE LOAD ERROR [
{
$routeFile
}
]: "
.
$e
->
getMessage
());
// Don't die — skip broken route files
and continue
// Don't die — skip broken route files
}
}
}
}
}
}
...
...
bootstrap/autoload.php
View file @
1446ef10
<?php
<?php
declare
(
strict_types
=
1
);
declare
(
strict_types
=
1
);
define
(
'ROOT_PATH'
,
dirname
(
__DIR__
));
// ROOT_PATH is defined in public/index.php BEFORE this file loads
if
(
!
defined
(
'ROOT_PATH'
))
{
define
(
'ROOT_PATH'
,
dirname
(
__DIR__
));
}
spl_autoload_register
(
function
(
string
$class
)
:
void
{
spl_autoload_register
(
function
(
string
$class
)
:
void
{
// Namespace → directory mappings
// Namespace → directory mappings
...
...
captain-definition
View file @
1446ef10
docker/php-custom.ini
View file @
1446ef10
; AL-ARCADE HR Platform PHP Configuration
; AL-ARCADE HR Platform PHP Configuration
display_errors
=
Off
; ── SHOW ERRORS (set to Off when stable) ──
display_startup_errors
=
Off
display_errors
=
On
display_startup_errors
=
On
log_errors
=
On
log_errors
=
On
error_log
=
/var/www/html/storage/logs/php-error.log
error_log
=
/var/www/html/storage/logs/php-error.log
error_reporting
=
E_ALL
& ~E_DEPRECATED & ~E_STRICT
error_reporting
=
E_ALL
; Upload limits
; Upload limits
upload_max_filesize
=
25M
upload_max_filesize
=
25M
...
@@ -22,16 +23,12 @@ session.cookie_httponly = 1
...
@@ -22,16 +23,12 @@ session.cookie_httponly = 1
session.cookie_samesite
=
Lax
session.cookie_samesite
=
Lax
session.use_strict_mode
=
1
session.use_strict_mode
=
1
; OPcache
; OPcache — disabled during debugging
opcache.enable
=
1
opcache.enable
=
0
opcache.memory_consumption
=
128
opcache.max_accelerated_files
=
10000
opcache.revalidate_freq
=
60
opcache.validate_timestamps
=
1
; Timezone
; Timezone
date.timezone
=
Africa/Cairo
date.timezone
=
Africa/Cairo
; Output buffering
(SSE needs this off for streaming)
; Output buffering
output_buffering
=
4096
output_buffering
=
4096
zlib.output_compression
=
Off
zlib.output_compression
=
Off
\ No newline at end of file
engine/Core/Request.php
View file @
1446ef10
...
@@ -23,6 +23,14 @@ final class Request
...
@@ -23,6 +23,14 @@ final class Request
$this
->
headers
=
$this
->
parseHeaders
();
$this
->
headers
=
$this
->
parseHeaders
();
}
}
/**
* Static factory — creates a Request from PHP superglobals.
*/
public
static
function
capture
()
:
self
{
return
new
self
();
}
private
function
parseHeaders
()
:
array
private
function
parseHeaders
()
:
array
{
{
$headers
=
[];
$headers
=
[];
...
@@ -35,6 +43,9 @@ final class Request
...
@@ -35,6 +43,9 @@ final class Request
if
(
isset
(
$this
->
server
[
'CONTENT_TYPE'
]))
{
if
(
isset
(
$this
->
server
[
'CONTENT_TYPE'
]))
{
$headers
[
'content-type'
]
=
$this
->
server
[
'CONTENT_TYPE'
];
$headers
[
'content-type'
]
=
$this
->
server
[
'CONTENT_TYPE'
];
}
}
if
(
isset
(
$this
->
server
[
'CONTENT_LENGTH'
]))
{
$headers
[
'content-length'
]
=
$this
->
server
[
'CONTENT_LENGTH'
];
}
return
$headers
;
return
$headers
;
}
}
...
@@ -42,7 +53,7 @@ final class Request
...
@@ -42,7 +53,7 @@ final class Request
{
{
$method
=
strtoupper
(
$this
->
server
[
'REQUEST_METHOD'
]
??
'GET'
);
$method
=
strtoupper
(
$this
->
server
[
'REQUEST_METHOD'
]
??
'GET'
);
if
(
$method
===
'POST'
)
{
if
(
$method
===
'POST'
)
{
$override
=
$this
->
input
(
'_method'
)
??
$this
->
header
(
'x-http-method-override'
);
$override
=
$this
->
post
[
'_method'
]
??
$this
->
header
(
'x-http-method-override'
);
if
(
$override
)
{
if
(
$override
)
{
$method
=
strtoupper
(
$override
);
$method
=
strtoupper
(
$override
);
}
}
...
...
modules/Webhooks/Controllers/WebhookController.php
View file @
1446ef10
...
@@ -9,13 +9,14 @@ use Engine\Core\Response;
...
@@ -9,13 +9,14 @@ use Engine\Core\Response;
use
Engine\Database\Connection
;
use
Engine\Database\Connection
;
use
Engine\Auth\PermissionEngine
;
use
Engine\Auth\PermissionEngine
;
use
Engine\Audit\AuditLogger
;
use
Engine\Audit\AuditLogger
;
use
Modules\Webhooks\Services\WebhookDispatcher
;
use
Engine\Template\TemplateEngine
;
final
class
WebhookController
final
class
WebhookController
{
{
private
Connection
$db
;
private
Connection
$db
;
private
PermissionEngine
$perms
;
private
PermissionEngine
$perms
;
private
AuditLogger
$audit
;
private
AuditLogger
$audit
;
private
TemplateEngine
$templates
;
public
function
__construct
()
public
function
__construct
()
{
{
...
@@ -23,6 +24,7 @@ final class WebhookController
...
@@ -23,6 +24,7 @@ final class WebhookController
$this
->
db
=
$c
->
resolve
(
Connection
::
class
);
$this
->
db
=
$c
->
resolve
(
Connection
::
class
);
$this
->
perms
=
$c
->
resolve
(
PermissionEngine
::
class
);
$this
->
perms
=
$c
->
resolve
(
PermissionEngine
::
class
);
$this
->
audit
=
$c
->
resolve
(
AuditLogger
::
class
);
$this
->
audit
=
$c
->
resolve
(
AuditLogger
::
class
);
$this
->
templates
=
$c
->
resolve
(
TemplateEngine
::
class
);
}
}
public
function
index
(
Request
$request
)
:
Response
public
function
index
(
Request
$request
)
:
Response
...
@@ -31,28 +33,23 @@ final class WebhookController
...
@@ -31,28 +33,23 @@ final class WebhookController
$this
->
perms
->
denyUnlessAllowed
(
$user
,
'webhooks.manage'
);
$this
->
perms
->
denyUnlessAllowed
(
$user
,
'webhooks.manage'
);
$webhooks
=
$this
->
db
->
fetchAll
(
$webhooks
=
$this
->
db
->
fetchAll
(
"SELECT w.*,
u.full_name_en as created_by_name,
"SELECT w.*,
(SELECT COUNT(*) FROM webhook_deliveries WHERE webhook_id = w.id) as total_deliveries,
(SELECT COUNT(*) FROM webhook_deliveries WHERE webhook_id = w.id) as total_deliveries,
(SELECT COUNT(*) FROM webhook_deliveries WHERE webhook_id = w.id AND status = 'failed') as failed_deliveries,
(SELECT COUNT(*) FROM webhook_deliveries WHERE webhook_id = w.id AND status = 'failed') as failed_deliveries,
(SELECT MAX(created_at) FROM webhook_deliveries WHERE webhook_id = w.id) as last_triggered_at
(SELECT MAX(created_at) FROM webhook_deliveries WHERE webhook_id = w.id) as last_triggered_at
FROM webhooks w
FROM webhooks w ORDER BY w.created_at DESC"
JOIN users u ON u.id = w.created_by_id
ORDER BY w.created_at DESC"
);
);
foreach
(
$webhooks
as
&
$w
)
{
foreach
(
$webhooks
as
&
$w
h
)
{
$w
[
'subscribed_events'
]
=
json_decode
(
$w
[
'subscribed_events_json'
],
true
)
;
$w
h
[
'subscribed_events'
]
=
json_decode
(
$wh
[
'subscribed_events_json'
]
??
'[]'
,
true
)
?:
[]
;
}
}
if
(
$request
->
wantsJson
())
{
return
Response
::
json
([
'webhooks'
=>
$webhooks
]);
}
$templates
=
Container
::
getInstance
()
->
resolve
(
\Engine\Template\TemplateEngine
::
class
);
$availableEvents
=
require
ROOT_PATH
.
'/config/webhook_events.php'
;
$availableEvents
=
require
ROOT_PATH
.
'/config/webhook_events.php'
;
return
Response
::
html
(
$templates
->
render
(
'webhooks/index'
,
[
'user'
=>
$user
,
'webhooks'
=>
$webhooks
,
'available_events'
=>
$availableEvents
,
$data
=
[
'user'
=>
$user
,
'webhooks'
=>
$webhooks
,
'available_events'
=>
$availableEvents
];
]));
if
(
$request
->
wantsJson
())
return
Response
::
json
(
$data
);
return
Response
::
html
(
$this
->
templates
->
render
(
'webhooks/index'
,
$data
));
}
}
public
function
create
(
Request
$request
)
:
Response
public
function
create
(
Request
$request
)
:
Response
...
@@ -62,33 +59,24 @@ final class WebhookController
...
@@ -62,33 +59,24 @@ final class WebhookController
$url
=
$request
->
input
(
'url'
);
$url
=
$request
->
input
(
'url'
);
if
(
!
$url
||
!
filter_var
(
$url
,
FILTER_VALIDATE_URL
))
{
if
(
!
$url
||
!
filter_var
(
$url
,
FILTER_VALIDATE_URL
))
{
return
Response
::
json
([
'error'
=>
'Valid URL is required.'
],
422
);
return
Response
::
json
([
'error'
=>
'Valid URL required'
],
422
);
}
$events
=
$request
->
input
(
'subscribed_events'
,
[]);
if
(
empty
(
$events
))
{
return
Response
::
json
([
'error'
=>
'At least one event must be subscribed.'
],
422
);
}
}
$secret
=
bin2hex
(
random_bytes
(
32
));
$secret
=
bin2hex
(
random_bytes
(
32
));
$events
=
$request
->
input
(
'events'
,
[]);
$id
=
$this
->
db
->
insert
(
'webhooks'
,
[
$id
=
$this
->
db
->
insert
(
'webhooks'
,
[
'url'
=>
$url
,
'url'
=>
$url
,
'secret'
=>
$secret
,
'secret'
=>
$secret
,
'is_active'
=>
1
,
'subscribed_events_json'
=>
json_encode
(
$events
),
'subscribed_events_json'
=>
json_encode
(
$events
),
'created_by_id'
=>
$user
[
'id'
],
'created_by_id'
=>
$user
[
'id'
],
]);
]);
$this
->
audit
->
log
(
$user
,
'WEBHOOK_CREATED'
,
'webhook'
,
$id
,
'webhooks'
,
'/webhooks'
,
$this
->
audit
->
log
(
$user
,
'WEBHOOK_CREATED'
,
'webhook'
,
$id
,
'webhooks'
,
'/webhooks'
,
null
,
[
'url'
=>
$url
,
'events'
=>
$events
],
$request
->
ip
(),
$request
->
userAgent
());
null
,
[
'url'
=>
$url
],
$request
->
ip
(),
$request
->
userAgent
());
return
Response
::
json
([
return
Response
::
json
([
'success'
=>
true
,
'id'
=>
$id
,
'secret'
=>
$secret
,
'success'
=>
true
,
'warning'
=>
'Save this secret now. It cannot be retrieved later.'
]);
'id'
=>
$id
,
'secret'
=>
$secret
,
'warning'
=>
'This secret is shown ONCE. Store it securely for signature verification.'
,
]);
}
}
public
function
update
(
Request
$request
,
string
$webhookId
)
:
Response
public
function
update
(
Request
$request
,
string
$webhookId
)
:
Response
...
@@ -96,20 +84,14 @@ final class WebhookController
...
@@ -96,20 +84,14 @@ final class WebhookController
$user
=
$request
->
user
();
$user
=
$request
->
user
();
$this
->
perms
->
denyUnlessAllowed
(
$user
,
'webhooks.manage'
);
$this
->
perms
->
denyUnlessAllowed
(
$user
,
'webhooks.manage'
);
$webhook
=
$this
->
db
->
fetchOne
(
"SELECT * FROM webhooks WHERE id = ?"
,
[(
int
)
$webhookId
]);
if
(
!
$webhook
)
return
Response
::
json
([
'error'
=>
'Not found'
],
404
);
$data
=
[];
$data
=
[];
if
(
$request
->
input
(
'url'
)
!==
null
)
$data
[
'url'
]
=
$request
->
input
(
'url'
);
if
(
$request
->
input
(
'url'
)
!==
null
)
$data
[
'url'
]
=
$request
->
input
(
'url'
);
if
(
$request
->
input
(
'is_active'
)
!==
null
)
$data
[
'is_active'
]
=
(
int
)
$request
->
input
(
'is_active'
);
if
(
$request
->
input
(
'is_active'
)
!==
null
)
$data
[
'is_active'
]
=
(
int
)
$request
->
input
(
'is_active'
);
if
(
$request
->
input
(
'subscribed_events'
)
!==
null
)
{
if
(
$request
->
input
(
'events'
)
!==
null
)
$data
[
'subscribed_events_json'
]
=
json_encode
(
$request
->
input
(
'events'
));
$data
[
'subscribed_events_json'
]
=
json_encode
(
$request
->
input
(
'subscribed_events'
));
}
if
(
!
empty
(
$data
))
{
if
(
empty
(
$data
))
return
Response
::
json
([
'error'
=>
'Nothing to update'
],
422
);
$this
->
db
->
update
(
'webhooks'
,
$data
,
'id = ?'
,
[(
int
)
$webhookId
]);
}
$this
->
db
->
update
(
'webhooks'
,
$data
,
'id = ?'
,
[(
int
)
$webhookId
]);
return
Response
::
json
([
'success'
=>
true
]);
return
Response
::
json
([
'success'
=>
true
]);
}
}
...
@@ -121,14 +103,50 @@ final class WebhookController
...
@@ -121,14 +103,50 @@ final class WebhookController
$webhook
=
$this
->
db
->
fetchOne
(
"SELECT * FROM webhooks WHERE id = ?"
,
[(
int
)
$webhookId
]);
$webhook
=
$this
->
db
->
fetchOne
(
"SELECT * FROM webhooks WHERE id = ?"
,
[(
int
)
$webhookId
]);
if
(
!
$webhook
)
return
Response
::
json
([
'error'
=>
'Not found'
],
404
);
if
(
!
$webhook
)
return
Response
::
json
([
'error'
=>
'Not found'
],
404
);
$dispatcher
=
new
WebhookDispatcher
(
$this
->
db
);
$payload
=
json_encode
([
$result
=
$dispatcher
->
dispatchSingle
(
$webhook
,
'test.ping'
,
[
'event'
=>
'test.ping'
,
'message'
=>
'Webhook test from AL-ARCADE HR Platform'
,
'timestamp'
=>
date
(
'c'
),
'timestamp'
=>
date
(
'c'
),
'data'
=>
[
'message'
=>
'This is a test webhook delivery.'
],
]);
$signature
=
hash_hmac
(
'sha256'
,
$payload
,
$webhook
[
'secret'
]);
$ch
=
curl_init
(
$webhook
[
'url'
]);
curl_setopt_array
(
$ch
,
[
CURLOPT_POST
=>
true
,
CURLOPT_POSTFIELDS
=>
$payload
,
CURLOPT_HTTPHEADER
=>
[
'Content-Type: application/json'
,
'X-Webhook-Signature: sha256='
.
$signature
,
'X-Webhook-Event: test.ping'
,
],
CURLOPT_RETURNTRANSFER
=>
true
,
CURLOPT_TIMEOUT
=>
10
,
CURLOPT_FOLLOWLOCATION
=>
false
,
]);
$responseBody
=
curl_exec
(
$ch
);
$httpCode
=
curl_getinfo
(
$ch
,
CURLINFO_HTTP_CODE
);
$error
=
curl_error
(
$ch
);
curl_close
(
$ch
);
$status
=
(
$httpCode
>=
200
&&
$httpCode
<
300
)
?
'success'
:
'failed'
;
$this
->
db
->
insert
(
'webhook_deliveries'
,
[
'webhook_id'
=>
(
int
)
$webhookId
,
'webhook_id'
=>
(
int
)
$webhookId
,
'event'
=>
'test.ping'
,
'payload_json'
=>
$payload
,
'attempt_number'
=>
1
,
'response_code'
=>
$httpCode
?:
null
,
'response_body'
=>
$responseBody
?:
$error
,
'status'
=>
$status
,
]);
]);
return
Response
::
json
([
'success'
=>
true
,
'delivery'
=>
$result
]);
return
Response
::
json
([
'success'
=>
$status
===
'success'
,
'http_code'
=>
$httpCode
,
'response'
=>
substr
(
$responseBody
?:
$error
,
0
,
500
),
]);
}
}
public
function
deliveries
(
Request
$request
,
string
$webhookId
)
:
Response
public
function
deliveries
(
Request
$request
,
string
$webhookId
)
:
Response
...
@@ -136,34 +154,18 @@ final class WebhookController
...
@@ -136,34 +154,18 @@ final class WebhookController
$user
=
$request
->
user
();
$user
=
$request
->
user
();
$this
->
perms
->
denyUnlessAllowed
(
$user
,
'webhooks.manage'
);
$this
->
perms
->
denyUnlessAllowed
(
$user
,
'webhooks.manage'
);
$page
=
max
(
1
,
(
int
)(
$request
->
query
(
'page'
,
1
)));
$perPage
=
50
;
$offset
=
(
$page
-
1
)
*
$perPage
;
$deliveries
=
$this
->
db
->
fetchAll
(
$deliveries
=
$this
->
db
->
fetchAll
(
"SELECT * FROM webhook_deliveries WHERE webhook_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?"
,
"SELECT id, event, attempt_number, response_code, status, created_at
[(
int
)
$webhookId
,
$perPage
,
$offset
]
FROM webhook_deliveries WHERE webhook_id = ? ORDER BY created_at DESC LIMIT 100"
,
);
$total
=
(
int
)
$this
->
db
->
fetchColumn
(
"SELECT COUNT(*) FROM webhook_deliveries WHERE webhook_id = ?"
,
[(
int
)
$webhookId
]
[(
int
)
$webhookId
]
);
);
return
Response
::
json
([
'deliveries'
=>
$deliveries
]);
return
Response
::
json
([
'deliveries'
=>
$deliveries
,
'total'
=>
$total
,
'page'
=>
$page
,
'last_page'
=>
(
int
)
ceil
(
$total
/
$perPage
),
]);
}
}
public
function
delete
(
Request
$request
,
string
$webhookId
)
:
Response
public
function
delete
(
Request
$request
,
string
$webhookId
)
:
Response
{
{
$user
=
$request
->
user
();
$user
=
$request
->
user
();
$this
->
perms
->
denyUnlessAllowed
(
$user
,
'webhooks.manage'
);
$this
->
perms
->
denyUnlessAllowed
(
$user
,
'webhooks.manage'
);
$this
->
db
->
delete
(
'webhook_deliveries'
,
'webhook_id = ?'
,
[(
int
)
$webhookId
]);
$this
->
db
->
delete
(
'webhooks'
,
'id = ?'
,
[(
int
)
$webhookId
]);
$this
->
db
->
delete
(
'webhooks'
,
'id = ?'
,
[(
int
)
$webhookId
]);
$this
->
audit
->
log
(
$user
,
'WEBHOOK_DELETED'
,
'webhook'
,
(
int
)
$webhookId
,
'webhooks'
,
'/webhooks'
,
$this
->
audit
->
log
(
$user
,
'WEBHOOK_DELETED'
,
'webhook'
,
(
int
)
$webhookId
,
'webhooks'
,
'/webhooks'
,
...
...
modules/Webhooks/Services/WebhookDispatcher.php
View file @
1446ef10
...
@@ -20,114 +20,75 @@ final class WebhookDispatcher
...
@@ -20,114 +20,75 @@ final class WebhookDispatcher
"SELECT * FROM webhooks WHERE is_active = 1"
"SELECT * FROM webhooks WHERE is_active = 1"
);
);
foreach
(
$webhooks
as
$webhook
)
{
foreach
(
$webhooks
as
$wh
)
{
$subscribedEvents
=
json_decode
(
$webhook
[
'subscribed_events_json'
],
true
)
?:
[];
$events
=
json_decode
(
$wh
[
'subscribed_events_json'
]
??
'[]'
,
true
)
?:
[];
if
(
!
in_array
(
$event
,
$subscribedEvents
))
{
if
(
!
in_array
(
$event
,
$events
)
&&
!
in_array
(
'*'
,
$events
))
continue
;
continue
;
}
$this
->
dispatchSingle
(
$webhook
,
$event
,
$payload
);
}
}
public
function
dispatchSingle
(
array
$webhook
,
string
$event
,
array
$payload
)
:
array
$body
=
json_encode
([
{
$fullPayload
=
[
'event'
=>
$event
,
'event'
=>
$event
,
'timestamp'
=>
date
(
'c'
),
'timestamp'
=>
date
(
'c'
),
'data'
=>
$payload
,
'data'
=>
$payload
,
];
$jsonPayload
=
json_encode
(
$fullPayload
);
$signature
=
hash_hmac
(
'sha256'
,
$jsonPayload
,
$webhook
[
'secret'
]);
$deliveryId
=
$this
->
db
->
insert
(
'webhook_deliveries'
,
[
'webhook_id'
=>
$webhook
[
'id'
],
'event'
=>
$event
,
'payload_json'
=>
$jsonPayload
,
'attempt_number'
=>
1
,
'status'
=>
'pending'
,
]);
]);
$result
=
$this
->
sendRequest
(
$webhook
[
'url'
],
$jsonPayload
,
$signature
,
$event
);
$signature
=
hash_hmac
(
'sha256'
,
$body
,
$wh
[
'secret'
]);
$this
->
db
->
update
(
'webhook_deliveries'
,
[
'response_code'
=>
$result
[
'http_code'
],
'response_body'
=>
substr
(
$result
[
'body'
]
??
''
,
0
,
5000
),
'status'
=>
$result
[
'success'
]
?
'success'
:
'failed'
,
],
'id = ?'
,
[
$deliveryId
]);
return
[
$this
->
send
(
$wh
,
$event
,
$body
,
$signature
,
1
);
'delivery_id'
=>
$deliveryId
,
}
'http_code'
=>
$result
[
'http_code'
],
'success'
=>
$result
[
'success'
],
];
}
}
public
function
retryFailed
()
:
int
public
function
retryFailed
()
:
void
{
{
$failed
Deliveries
=
$this
->
db
->
fetchAll
(
$failed
=
$this
->
db
->
fetchAll
(
"SELECT wd.*, w.url, w.secret FROM webhook_deliveries wd
"SELECT wd.*, w.url, w.secret FROM webhook_deliveries wd
JOIN webhooks w ON w.id = wd.webhook_id
JOIN webhooks w ON w.id = wd.webhook_id
WHERE wd.status = 'failed' AND wd.attempt_number < 3
WHERE wd.status = 'failed' AND wd.attempt_number < 3
AND wd.created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)
AND wd.created_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)
AND w.is_active = 1
ORDER BY wd.created_at ASC LIMIT 50"
ORDER BY wd.created_at ASC LIMIT 50"
);
);
$retried
=
0
;
foreach
(
$failed
as
$delivery
)
{
foreach
(
$failedDeliveries
as
$delivery
)
{
$signature
=
hash_hmac
(
'sha256'
,
$delivery
[
'payload_json'
],
$delivery
[
'secret'
]);
$signature
=
hash_hmac
(
'sha256'
,
$delivery
[
'payload_json'
],
$delivery
[
'secret'
]);
$result
=
$this
->
sendRequest
(
$delivery
[
'url'
],
$delivery
[
'payload_json'
],
$signature
,
$delivery
[
'event'
]);
$this
->
send
(
[
'id'
=>
$delivery
[
'webhook_id'
],
'url'
=>
$delivery
[
'url'
],
'secret'
=>
$delivery
[
'secret'
]],
$newAttempt
=
$delivery
[
'attempt_number'
]
+
1
;
$delivery
[
'event'
],
$delivery
[
'payload_json'
],
$this
->
db
->
insert
(
'webhook_deliveries'
,
[
$signature
,
'webhook_id'
=>
$delivery
[
'webhook_id'
],
$delivery
[
'attempt_number'
]
+
1
'event'
=>
$delivery
[
'event'
],
);
'payload_json'
=>
$delivery
[
'payload_json'
],
'attempt_number'
=>
$newAttempt
,
'response_code'
=>
$result
[
'http_code'
],
'response_body'
=>
substr
(
$result
[
'body'
]
??
''
,
0
,
5000
),
'status'
=>
$result
[
'success'
]
?
'success'
:
'failed'
,
]);
if
(
$result
[
'success'
])
{
$this
->
db
->
update
(
'webhook_deliveries'
,
[
'status'
=>
'success'
],
'id = ?'
,
[
$delivery
[
'id'
]]);
}
}
$retried
++
;
}
}
return
$retried
;
private
function
send
(
array
$webhook
,
string
$event
,
string
$body
,
string
$signature
,
int
$attempt
)
:
void
}
private
function
sendRequest
(
string
$url
,
string
$jsonPayload
,
string
$signature
,
string
$event
)
:
array
{
{
$ch
=
curl_init
(
$
url
);
$ch
=
curl_init
(
$
webhook
[
'url'
]
);
curl_setopt_array
(
$ch
,
[
curl_setopt_array
(
$ch
,
[
CURLOPT_POST
=>
true
,
CURLOPT_POST
=>
true
,
CURLOPT_POSTFIELDS
=>
$jsonPayload
,
CURLOPT_POSTFIELDS
=>
$body
,
CURLOPT_RETURNTRANSFER
=>
true
,
CURLOPT_TIMEOUT
=>
10
,
CURLOPT_CONNECTTIMEOUT
=>
5
,
CURLOPT_HTTPHEADER
=>
[
CURLOPT_HTTPHEADER
=>
[
'Content-Type: application/json'
,
'Content-Type: application/json'
,
'X-Webhook-Signature: sha256='
.
$signature
,
'X-Webhook-Signature: sha256='
.
$signature
,
'X-Webhook-Event: '
.
$event
,
'X-Webhook-Event: '
.
$event
,
'User-Agent: AL-ARCADE-HR/3.0'
,
],
],
CURLOPT_RETURNTRANSFER
=>
true
,
CURLOPT_TIMEOUT
=>
10
,
CURLOPT_FOLLOWLOCATION
=>
false
,
]);
]);
$
body
=
curl_exec
(
$ch
);
$
response
=
curl_exec
(
$ch
);
$httpCode
=
(
int
)
curl_getinfo
(
$ch
,
CURLINFO_HTTP_CODE
);
$httpCode
=
(
int
)
curl_getinfo
(
$ch
,
CURLINFO_HTTP_CODE
);
$error
=
curl_error
(
$ch
);
$error
=
curl_error
(
$ch
);
curl_close
(
$ch
);
curl_close
(
$ch
);
return
[
$status
=
(
$httpCode
>=
200
&&
$httpCode
<
300
)
?
'success'
:
'failed'
;
'http_code'
=>
$httpCode
,
'body'
=>
$body
?:
$error
,
$this
->
db
->
insert
(
'webhook_deliveries'
,
[
'success'
=>
$httpCode
>=
200
&&
$httpCode
<
300
,
'webhook_id'
=>
$webhook
[
'id'
],
];
'event'
=>
$event
,
'payload_json'
=>
$body
,
'attempt_number'
=>
$attempt
,
'response_code'
=>
$httpCode
?:
null
,
'response_body'
=>
substr
(
$response
?:
$error
,
0
,
2000
),
'status'
=>
$status
,
]);
}
}
}
}
\ No newline at end of file
public/index.php
View file @
1446ef10
<?php
<?php
declare
(
strict_types
=
1
);
/**
* AL-ARCADE HR Platform v3.0 — Front Controller
*
* This file is the ONLY entry point for all HTTP requests.
* It must NEVER fail silently. If something breaks, SHOW IT.
*/
// ───
SHOW ERRORS IN OUTPUT (until production-stable
) ───
// ───
FORCE ERROR DISPLAY (overrides ini
) ───
error_reporting
(
E_ALL
);
error_reporting
(
E_ALL
);
ini_set
(
'display_errors'
,
'1'
);
ini_set
(
'display_errors'
,
'1'
);
ini_set
(
'display_startup_errors'
,
'1'
);
ini_set
(
'log_errors'
,
'1'
);
ini_set
(
'log_errors'
,
'1'
);
try
{
// ─── Define ROOT_PATH early so error handler can use it ───
// ─── BOOTSTRAP ───
define
(
'ROOT_PATH'
,
dirname
(
__DIR__
));
$container
=
require
__DIR__
.
'/../bootstrap/app.php'
;
use
Engine\Core
\
{
Router
,
Request
,
Response
};
// ─── GLOBAL CRASH HANDLER ───
// This catches EVERYTHING — even errors during bootstrap
set_exception_handler
(
function
(
\Throwable
$e
)
{
if
(
!
headers_sent
())
{
http_response_code
(
500
);
header
(
'Content-Type: text/html; charset=utf-8'
);
}
$msg
=
htmlspecialchars
(
$e
->
getMessage
());
$file
=
htmlspecialchars
(
$e
->
getFile
());
$line
=
$e
->
getLine
();
$trace
=
htmlspecialchars
(
$e
->
getTraceAsString
());
echo
"<!DOCTYPE html><html><head><title>Fatal Error</title>
<style>body
{
font-family:monospace;background:#0f172a;color:#e2e8f0;padding:40px;margin:0
}
h1
{
color:#ef4444}pre{background:#1e293b;padding:16px;border-radius:8px;overflow-x:auto;font-size:13px
}
.msg{color:#fbbf24;font-size:1.2em;background:#1e293b;padding:12px;border-radius:8px;border-left:4px solid #ef4444}
.loc{color:#818cf8;margin:8px 0}</style></head><body>
<h1>💀 FATAL ERROR</h1>
<div class='msg'>
{
$msg
}
</div>
<div class='loc'>
{
$file
}
:
{
$line
}
</div>
<h3 style='color:#94a3b8;margin-top:20px'>Stack Trace:</h3>
<pre>
{
$trace
}
</pre></body></html>"
;
exit
(
1
);
});
// Also catch fatal errors (out of memory, parse errors in included files, etc)
register_shutdown_function
(
function
()
{
$error
=
error_get_last
();
if
(
$error
&&
in_array
(
$error
[
'type'
],
[
E_ERROR
,
E_PARSE
,
E_CORE_ERROR
,
E_COMPILE_ERROR
]))
{
if
(
!
headers_sent
())
{
http_response_code
(
500
);
header
(
'Content-Type: text/html; charset=utf-8'
);
}
$msg
=
htmlspecialchars
(
$error
[
'message'
]);
$file
=
htmlspecialchars
(
$error
[
'file'
]);
$line
=
$error
[
'line'
];
echo
"<!DOCTYPE html><html><head><title>Fatal Error</title>
<style>body
{
font-family:monospace;background:#0f172a;color:#e2e8f0;padding:40px;margin:0
}
h1
{
color:#ef4444}pre{background:#1e293b;padding:16px;border-radius:8px;overflow-x:auto
}
.msg
{
color:#fbbf24;font-size:1.2em
}
</style></head><body>
<h1>💀 PHP FATAL ERROR</h1>
<p class='msg'>
{
$msg
}
</p>
<p style='color:#818cf8'>
{
$file
}
:
{
$line
}
</p></body></html>"
;
}
});
// ─── HANDLE REQUEST ───
// ─── BOOTSTRAP ───
$request
=
new
Request
();
try
{
$router
=
$container
->
resolve
(
Router
::
class
);
require_once
ROOT_PATH
.
'/bootstrap/autoload.php'
;
}
catch
(
\Throwable
$e
)
{
throw
new
\RuntimeException
(
"Autoloader failed: "
.
$e
->
getMessage
(),
0
,
$e
);
}
try
{
$container
=
require
ROOT_PATH
.
'/bootstrap/app.php'
;
}
catch
(
\Throwable
$e
)
{
throw
new
\RuntimeException
(
"Bootstrap failed: "
.
$e
->
getMessage
(),
0
,
$e
);
}
// ─── DISPATCH REQUEST ───
try
{
$request
=
\Engine\Core\Request
::
capture
();
$router
=
$container
->
resolve
(
\Engine\Core\Router
::
class
);
$response
=
$router
->
dispatch
(
$request
);
$response
=
$router
->
dispatch
(
$request
);
$response
->
send
();
$response
->
send
();
}
catch
(
\Engine\Auth\ForbiddenException
$e
)
{
}
catch
(
\Engine\Auth\ForbiddenException
$e
)
{
http_response_code
(
403
);
http_response_code
(
403
);
echo
'<h1>403 Forbidden</h1><p>'
.
htmlspecialchars
(
$e
->
getMessage
())
.
'</p>'
;
echo
'<h1>403 Forbidden</h1><p>'
.
htmlspecialchars
(
$e
->
getMessage
())
.
'</p>'
;
}
catch
(
\Throwable
$e
)
{
}
catch
(
\Throwable
$e
)
{
// ─── SHOW THE ACTUAL FUCKING ERROR ───
// Re-throw to the global handler
http_response_code
(
500
);
throw
$e
;
$msg
=
$e
->
getMessage
();
$file
=
$e
->
getFile
();
$line
=
$e
->
getLine
();
$trace
=
$e
->
getTraceAsString
();
error_log
(
"FATAL:
{
$msg
}
in
{
$file
}
:
{
$line
}
\n
{
$trace
}
"
);
echo
<<<HTML
<!DOCTYPE html>
<html>
<head><title>500 — Server Error</title>
<style>
body{font-family:-apple-system,sans-serif;background:#0f172a;color:#e2e8f0;margin:0;padding:40px;min-height:100vh}
h1{color:#ef4444;font-size:2.5em;margin-bottom:10px}
.box{max-width:900px;margin:0 auto}
.error-msg{background:#1e293b;padding:20px;border-radius:8px;border-left:4px solid #ef4444;margin:16px 0;font-size:1.1em;color:#fbbf24;word-wrap:break-word}
.trace{background:#1e293b;padding:16px;border-radius:8px;font-family:monospace;font-size:0.8em;overflow-x:auto;white-space:pre-wrap;color:#94a3b8;margin:16px 0;max-height:500px;overflow-y:auto}
.file{color:#818cf8;font-size:0.9em}
a{color:#6366f1}
</style>
</head>
<body>
<div class="box">
<h1>500 — Application Error</h1>
<div class="error-msg">{$msg}</div>
<div class="file">{$file}:{$line}</div>
<h3 style="margin-top:20px;color:#94a3b8">Stack Trace:</h3>
<div class="trace">{$trace}</div>
<p style="margin-top:20px"><a href="/login">← Try Login</a> · <a href="/dashboard">Dashboard</a></p>
</div>
</body>
</html>
HTML;
}
}
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment