feat: Add comprehensive documentation suite and reorganize project structure
- Created complete documentation in docs/ directory - Added PROJECT_OVERVIEW.md with feature highlights and getting started guide - Added ARCHITECTURE.md with system design and technical details - Added SECURITY.md with comprehensive security implementation guide - Added DEVELOPMENT.md with development workflows and best practices - Added DEPLOYMENT.md with production deployment instructions - Added API.md with complete REST API documentation - Added CONTRIBUTING.md with contribution guidelines - Added CHANGELOG.md with version history and migration notes - Reorganized all documentation files into docs/ directory for better organization - Updated README.md with proper documentation links and quick navigation - Enhanced project structure with professional documentation standards
This commit is contained in:
110
f_modules/m_frontend/m_donations/src/AnalyticsHandler.php
Normal file
110
f_modules/m_frontend/m_donations/src/AnalyticsHandler.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
namespace Donations;
|
||||
|
||||
class AnalyticsHandler {
|
||||
private $db;
|
||||
|
||||
public function __construct() {
|
||||
$this->db = db();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update analytics for a new donation
|
||||
*/
|
||||
public function updateAnalytics($streamer_id, $amount, $donor_id) {
|
||||
$date = date('Y-m-d');
|
||||
|
||||
// Get or create analytics record for today
|
||||
$sql = "INSERT INTO donation_analytics
|
||||
(streamer_id, date, total_donations, total_amount, unique_donors)
|
||||
VALUES (?, ?, 1, ?, 1)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
total_donations = total_donations + 1,
|
||||
total_amount = total_amount + ?,
|
||||
unique_donors = (
|
||||
SELECT COUNT(DISTINCT donor_id)
|
||||
FROM donations
|
||||
WHERE streamer_id = ? AND DATE(created_at) = ?
|
||||
)";
|
||||
|
||||
$this->db->query($sql, [$streamer_id, $date, $amount, $amount, $streamer_id, $date]);
|
||||
|
||||
// Update average donation
|
||||
$sql = "UPDATE donation_analytics
|
||||
SET average_donation = total_amount / total_donations
|
||||
WHERE streamer_id = ? AND date = ?";
|
||||
|
||||
$this->db->query($sql, [$streamer_id, $date]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get analytics for a specific period
|
||||
*/
|
||||
public function getAnalytics($streamer_id, $start_date, $end_date) {
|
||||
$sql = "SELECT
|
||||
date,
|
||||
total_donations,
|
||||
total_amount,
|
||||
average_donation,
|
||||
unique_donors
|
||||
FROM donation_analytics
|
||||
WHERE streamer_id = ?
|
||||
AND date BETWEEN ? AND ?
|
||||
ORDER BY date DESC";
|
||||
|
||||
return $this->db->getRows($sql, [$streamer_id, $start_date, $end_date]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get summary statistics
|
||||
*/
|
||||
public function getSummary($streamer_id) {
|
||||
$sql = "SELECT
|
||||
COUNT(*) as total_donations,
|
||||
SUM(amount) as total_amount,
|
||||
AVG(amount) as average_donation,
|
||||
COUNT(DISTINCT donor_id) as unique_donors,
|
||||
MAX(amount) as largest_donation,
|
||||
MIN(amount) as smallest_donation
|
||||
FROM donations
|
||||
WHERE streamer_id = ?";
|
||||
|
||||
return $this->db->getRow($sql, [$streamer_id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get top donors
|
||||
*/
|
||||
public function getTopDonors($streamer_id, $limit = 10) {
|
||||
$sql = "SELECT
|
||||
u.username,
|
||||
u.display_name,
|
||||
COUNT(*) as donation_count,
|
||||
SUM(d.amount) as total_amount
|
||||
FROM donations d
|
||||
JOIN users u ON d.donor_id = u.user_id
|
||||
WHERE d.streamer_id = ?
|
||||
GROUP BY d.donor_id
|
||||
ORDER BY total_amount DESC
|
||||
LIMIT ?";
|
||||
|
||||
return $this->db->getRows($sql, [$streamer_id, $limit]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get donation trends
|
||||
*/
|
||||
public function getTrends($streamer_id, $days = 30) {
|
||||
$sql = "SELECT
|
||||
DATE(created_at) as date,
|
||||
COUNT(*) as count,
|
||||
SUM(amount) as total
|
||||
FROM donations
|
||||
WHERE streamer_id = ?
|
||||
AND created_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY)
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date";
|
||||
|
||||
return $this->db->getRows($sql, [$streamer_id, $days]);
|
||||
}
|
||||
}
|
||||
160
f_modules/m_frontend/m_donations/src/ApiKeyManager.php
Normal file
160
f_modules/m_frontend/m_donations/src/ApiKeyManager.php
Normal file
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
namespace Donations;
|
||||
|
||||
class ApiKeyManager {
|
||||
private $db;
|
||||
private $rate_limit = 100; // requests per minute
|
||||
private $rate_window = 60; // seconds
|
||||
|
||||
public function __construct() {
|
||||
$this->db = db();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new API key
|
||||
*/
|
||||
public function generateKey($user_id, $name, $description = '') {
|
||||
$api_key = bin2hex(random_bytes(32));
|
||||
|
||||
$sql = "INSERT INTO api_keys (user_id, api_key, name, description)
|
||||
VALUES (?, ?, ?, ?)";
|
||||
|
||||
$this->db->query($sql, [$user_id, $api_key, $name, $description]);
|
||||
|
||||
return $api_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an API key
|
||||
*/
|
||||
public function validateKey($api_key) {
|
||||
$sql = "SELECT user_id FROM api_keys
|
||||
WHERE api_key = ? AND is_active = 1";
|
||||
|
||||
$result = $this->db->getRow($sql, [$api_key]);
|
||||
|
||||
if ($result) {
|
||||
$this->updateLastUsed($api_key);
|
||||
return $result['user_id'];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update last used timestamp
|
||||
*/
|
||||
private function updateLastUsed($api_key) {
|
||||
$sql = "UPDATE api_keys
|
||||
SET last_used = CURRENT_TIMESTAMP
|
||||
WHERE api_key = ?";
|
||||
|
||||
$this->db->query($sql, [$api_key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check rate limit
|
||||
*/
|
||||
public function checkRateLimit($api_key) {
|
||||
$sql = "SELECT request_count, window_start
|
||||
FROM api_rate_limits
|
||||
WHERE api_key = ?
|
||||
ORDER BY window_start DESC
|
||||
LIMIT 1";
|
||||
|
||||
$result = $this->db->getRow($sql, [$api_key]);
|
||||
|
||||
if (!$result) {
|
||||
$this->createRateLimitWindow($api_key);
|
||||
return true;
|
||||
}
|
||||
|
||||
$window_start = strtotime($result['window_start']);
|
||||
$current_time = time();
|
||||
|
||||
// If window has expired, create new window
|
||||
if ($current_time - $window_start >= $this->rate_window) {
|
||||
$this->createRateLimitWindow($api_key);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if rate limit exceeded
|
||||
if ($result['request_count'] >= $this->rate_limit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Increment request count
|
||||
$sql = "UPDATE api_rate_limits
|
||||
SET request_count = request_count + 1
|
||||
WHERE api_key = ? AND window_start = ?";
|
||||
|
||||
$this->db->query($sql, [$api_key, $result['window_start']]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new rate limit window
|
||||
*/
|
||||
private function createRateLimitWindow($api_key) {
|
||||
$sql = "INSERT INTO api_rate_limits (api_key, request_count, window_start)
|
||||
VALUES (?, 1, CURRENT_TIMESTAMP)";
|
||||
|
||||
$this->db->query($sql, [$api_key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's API keys
|
||||
*/
|
||||
public function getUserKeys($user_id) {
|
||||
$sql = "SELECT key_id, api_key, name, description, is_active,
|
||||
last_used, created_at
|
||||
FROM api_keys
|
||||
WHERE user_id = ?
|
||||
ORDER BY created_at DESC";
|
||||
|
||||
return $this->db->getRows($sql, [$user_id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate API key
|
||||
*/
|
||||
public function deactivateKey($key_id, $user_id) {
|
||||
$sql = "UPDATE api_keys
|
||||
SET is_active = 0
|
||||
WHERE key_id = ? AND user_id = ?";
|
||||
|
||||
return $this->db->query($sql, [$key_id, $user_id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reactivate API key
|
||||
*/
|
||||
public function reactivateKey($key_id, $user_id) {
|
||||
$sql = "UPDATE api_keys
|
||||
SET is_active = 1
|
||||
WHERE key_id = ? AND user_id = ?";
|
||||
|
||||
return $this->db->query($sql, [$key_id, $user_id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete API key
|
||||
*/
|
||||
public function deleteKey($key_id, $user_id) {
|
||||
$sql = "DELETE FROM api_keys
|
||||
WHERE key_id = ? AND user_id = ?";
|
||||
|
||||
return $this->db->query($sql, [$key_id, $user_id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old rate limit records
|
||||
*/
|
||||
public function cleanupRateLimits() {
|
||||
$sql = "DELETE FROM api_rate_limits
|
||||
WHERE window_start < DATE_SUB(NOW(), INTERVAL 1 HOUR)";
|
||||
|
||||
return $this->db->query($sql);
|
||||
}
|
||||
}
|
||||
146
f_modules/m_frontend/m_donations/src/Core/Api.php
Normal file
146
f_modules/m_frontend/m_donations/src/Core/Api.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
namespace Donations\Core;
|
||||
|
||||
class Api {
|
||||
protected $config;
|
||||
protected $db;
|
||||
protected $logger;
|
||||
protected $request;
|
||||
protected $response;
|
||||
|
||||
public function __construct() {
|
||||
$this->config = require DONATIONS_PATH . '/config/config.php';
|
||||
$this->db = db();
|
||||
$this->logger = new Logger();
|
||||
$this->request = $this->parseRequest();
|
||||
$this->response = [
|
||||
'success' => false,
|
||||
'message' => '',
|
||||
'data' => null
|
||||
];
|
||||
}
|
||||
|
||||
protected function parseRequest() {
|
||||
$request = [
|
||||
'method' => $_SERVER['REQUEST_METHOD'],
|
||||
'path' => parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH),
|
||||
'query' => $_GET,
|
||||
'body' => json_decode(file_get_contents('php://input'), true) ?? [],
|
||||
'headers' => getallheaders()
|
||||
];
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
protected function validateApiKey() {
|
||||
$apiKey = $this->request['headers']['X-API-Key'] ?? null;
|
||||
|
||||
if (!$apiKey) {
|
||||
$this->error('API key is required', 401);
|
||||
return false;
|
||||
}
|
||||
|
||||
$sql = "SELECT * FROM api_keys WHERE api_key = ? AND is_active = 1";
|
||||
$key = $this->db->fetch($sql, [$apiKey]);
|
||||
|
||||
if (!$key) {
|
||||
$this->error('Invalid API key', 401);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->checkRateLimit($key['id'])) {
|
||||
$this->error('Rate limit exceeded', 429);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function checkRateLimit($apiKeyId) {
|
||||
$timeframe = $this->config['api']['rate_limit']['timeframe'];
|
||||
$maxRequests = $this->config['api']['rate_limit']['max_requests'];
|
||||
|
||||
$sql = "SELECT COUNT(*) as count FROM api_rate_limits
|
||||
WHERE api_key_id = ? AND created_at >= DATE_SUB(NOW(), INTERVAL ? SECOND)";
|
||||
|
||||
$result = $this->db->fetch($sql, [$apiKeyId, $timeframe]);
|
||||
|
||||
if ($result['count'] >= $maxRequests) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$sql = "INSERT INTO api_rate_limits (api_key_id) VALUES (?)";
|
||||
$this->db->execute($sql, [$apiKeyId]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function success($message, $data = null) {
|
||||
$this->response = [
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
'data' => $data
|
||||
];
|
||||
$this->sendResponse();
|
||||
}
|
||||
|
||||
protected function error($message, $code = 400) {
|
||||
$this->response = [
|
||||
'success' => false,
|
||||
'message' => $message,
|
||||
'data' => null
|
||||
];
|
||||
$this->sendResponse($code);
|
||||
}
|
||||
|
||||
protected function sendResponse($code = 200) {
|
||||
http_response_code($code);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($this->response);
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function validateInput($data, $rules) {
|
||||
$errors = [];
|
||||
|
||||
foreach ($rules as $field => $rule) {
|
||||
if (!isset($data[$field]) && strpos($rule, 'required') !== false) {
|
||||
$errors[$field] = "The $field field is required.";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($data[$field])) {
|
||||
if (strpos($rule, 'numeric') !== false && !is_numeric($data[$field])) {
|
||||
$errors[$field] = "The $field must be a number.";
|
||||
}
|
||||
|
||||
if (strpos($rule, 'min:') !== false) {
|
||||
$min = substr($rule, strpos($rule, 'min:') + 4);
|
||||
if ($data[$field] < $min) {
|
||||
$errors[$field] = "The $field must be at least $min.";
|
||||
}
|
||||
}
|
||||
|
||||
if (strpos($rule, 'max:') !== false) {
|
||||
$max = substr($rule, strpos($rule, 'max:') + 4);
|
||||
if ($data[$field] > $max) {
|
||||
$errors[$field] = "The $field must not be greater than $max.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
$this->error('Validation failed', 422);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function sanitizeInput($input) {
|
||||
if (is_array($input)) {
|
||||
return array_map([$this, 'sanitizeInput'], $input);
|
||||
}
|
||||
return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
}
|
||||
208
f_modules/m_frontend/m_donations/src/Core/Cache.php
Normal file
208
f_modules/m_frontend/m_donations/src/Core/Cache.php
Normal file
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
namespace Donations\Core;
|
||||
|
||||
class Cache {
|
||||
private static $instance = null;
|
||||
private $config;
|
||||
private $cache;
|
||||
|
||||
private function __construct() {
|
||||
$this->config = require DONATIONS_PATH . '/config/config.php';
|
||||
$this->connect();
|
||||
}
|
||||
|
||||
public static function getInstance() {
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function connect() {
|
||||
try {
|
||||
$this->cache = new \Memcached();
|
||||
$this->cache->addServer(
|
||||
$this->config['cache']['host'],
|
||||
$this->config['cache']['port']
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
throw new \Exception("Cache connection failed: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function get($key) {
|
||||
$value = $this->cache->get($key);
|
||||
return $value !== false ? $value : null;
|
||||
}
|
||||
|
||||
public function set($key, $value, $ttl = 3600) {
|
||||
return $this->cache->set($key, $value, time() + $ttl);
|
||||
}
|
||||
|
||||
public function delete($key) {
|
||||
return $this->cache->delete($key);
|
||||
}
|
||||
|
||||
public function increment($key, $value = 1) {
|
||||
return $this->cache->increment($key, $value);
|
||||
}
|
||||
|
||||
public function decrement($key, $value = 1) {
|
||||
return $this->cache->decrement($key, $value);
|
||||
}
|
||||
|
||||
public function flush() {
|
||||
return $this->cache->flush();
|
||||
}
|
||||
|
||||
public function getMulti($keys) {
|
||||
return $this->cache->getMulti($keys);
|
||||
}
|
||||
|
||||
public function setMulti($items, $ttl = 3600) {
|
||||
return $this->cache->setMulti($items, time() + $ttl);
|
||||
}
|
||||
|
||||
public function deleteMulti($keys) {
|
||||
return $this->cache->deleteMulti($keys);
|
||||
}
|
||||
|
||||
public function remember($key, $ttl, $callback) {
|
||||
$value = $this->get($key);
|
||||
|
||||
if ($value !== null) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$value = $callback();
|
||||
$this->set($key, $value, $ttl);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function rememberForever($key, $callback) {
|
||||
return $this->remember($key, 0, $callback);
|
||||
}
|
||||
|
||||
public function has($key) {
|
||||
return $this->get($key) !== null;
|
||||
}
|
||||
|
||||
public function missing($key) {
|
||||
return !$this->has($key);
|
||||
}
|
||||
|
||||
public function pull($key) {
|
||||
$value = $this->get($key);
|
||||
$this->delete($key);
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function put($key, $value, $ttl = 3600) {
|
||||
return $this->set($key, $value, $ttl);
|
||||
}
|
||||
|
||||
public function add($key, $value, $ttl = 3600) {
|
||||
return $this->cache->add($key, $value, time() + $ttl);
|
||||
}
|
||||
|
||||
public function forever($key, $value) {
|
||||
return $this->set($key, $value, 0);
|
||||
}
|
||||
|
||||
public function forget($key) {
|
||||
return $this->delete($key);
|
||||
}
|
||||
|
||||
public function tags($names) {
|
||||
return new TaggedCache($this, (array) $names);
|
||||
}
|
||||
|
||||
public function flushTags($names) {
|
||||
foreach ((array) $names as $name) {
|
||||
$this->delete("tag:{$name}:keys");
|
||||
}
|
||||
}
|
||||
|
||||
public function getConnection() {
|
||||
return $this->cache;
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
$this->cache = null;
|
||||
}
|
||||
|
||||
private function __clone() {}
|
||||
private function __wakeup() {}
|
||||
}
|
||||
|
||||
class TaggedCache {
|
||||
private $cache;
|
||||
private $names;
|
||||
|
||||
public function __construct($cache, $names) {
|
||||
$this->cache = $cache;
|
||||
$this->names = $names;
|
||||
}
|
||||
|
||||
public function get($key) {
|
||||
return $this->cache->get($this->taggedItemKey($key));
|
||||
}
|
||||
|
||||
public function put($key, $value, $ttl = 3600) {
|
||||
$this->cache->set($this->taggedItemKey($key), $value, $ttl);
|
||||
$this->pushKey($key);
|
||||
}
|
||||
|
||||
public function remember($key, $ttl, $callback) {
|
||||
$value = $this->get($key);
|
||||
|
||||
if ($value !== null) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$value = $callback();
|
||||
$this->put($key, $value, $ttl);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function rememberForever($key, $callback) {
|
||||
return $this->remember($key, 0, $callback);
|
||||
}
|
||||
|
||||
public function forget($key) {
|
||||
$this->cache->delete($this->taggedItemKey($key));
|
||||
$this->pullKey($key);
|
||||
}
|
||||
|
||||
public function flush() {
|
||||
foreach ($this->names as $name) {
|
||||
$this->cache->flushTags($name);
|
||||
}
|
||||
}
|
||||
|
||||
private function taggedItemKey($key) {
|
||||
return implode(':', array_merge($this->names, [$key]));
|
||||
}
|
||||
|
||||
private function pushKey($key) {
|
||||
foreach ($this->names as $name) {
|
||||
$keys = $this->cache->get("tag:{$name}:keys") ?: [];
|
||||
if (!in_array($key, $keys)) {
|
||||
$keys[] = $key;
|
||||
$this->cache->forever("tag:{$name}:keys", $keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function pullKey($key) {
|
||||
foreach ($this->names as $name) {
|
||||
$keys = $this->cache->get("tag:{$name}:keys") ?: [];
|
||||
if (($index = array_search($key, $keys)) !== false) {
|
||||
unset($keys[$index]);
|
||||
$this->cache->forever("tag:{$name}:keys", array_values($keys));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
149
f_modules/m_frontend/m_donations/src/Core/Controller.php
Normal file
149
f_modules/m_frontend/m_donations/src/Core/Controller.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
namespace Donations\Core;
|
||||
|
||||
class Controller {
|
||||
protected $config;
|
||||
protected $db;
|
||||
protected $logger;
|
||||
protected $view;
|
||||
protected $request;
|
||||
protected $response;
|
||||
|
||||
public function __construct() {
|
||||
$this->config = require DONATIONS_PATH . '/config/config.php';
|
||||
$this->db = db();
|
||||
$this->logger = new Logger();
|
||||
$this->view = new View();
|
||||
$this->request = $this->parseRequest();
|
||||
$this->response = [
|
||||
'success' => false,
|
||||
'message' => '',
|
||||
'data' => null
|
||||
];
|
||||
}
|
||||
|
||||
protected function parseRequest() {
|
||||
$request = [
|
||||
'method' => $_SERVER['REQUEST_METHOD'],
|
||||
'path' => parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH),
|
||||
'query' => $_GET,
|
||||
'body' => json_decode(file_get_contents('php://input'), true) ?? [],
|
||||
'headers' => getallheaders()
|
||||
];
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
protected function validateCsrf() {
|
||||
if ($this->request['method'] === 'POST') {
|
||||
$token = $_POST['csrf_token'] ?? null;
|
||||
if (!$token || $token !== $_SESSION['csrf_token']) {
|
||||
$this->error('Invalid CSRF token', 403);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function requireAuth() {
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
$this->redirect('/login');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function requireAdmin() {
|
||||
if (!$this->requireAuth()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$sql = "SELECT is_admin FROM users WHERE id = ?";
|
||||
$user = $this->db->fetch($sql, [$_SESSION['user_id']]);
|
||||
|
||||
if (!$user['is_admin']) {
|
||||
$this->error('Unauthorized access', 403);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function redirect($path) {
|
||||
header('Location: ' . $this->config['base_url'] . $path);
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function json($data, $code = 200) {
|
||||
http_response_code($code);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function success($message, $data = null) {
|
||||
$this->response = [
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
'data' => $data
|
||||
];
|
||||
$this->json($this->response);
|
||||
}
|
||||
|
||||
protected function error($message, $code = 400) {
|
||||
$this->response = [
|
||||
'success' => false,
|
||||
'message' => $message,
|
||||
'data' => null
|
||||
];
|
||||
$this->json($this->response, $code);
|
||||
}
|
||||
|
||||
protected function validateInput($data, $rules) {
|
||||
$errors = [];
|
||||
|
||||
foreach ($rules as $field => $rule) {
|
||||
if (!isset($data[$field]) && strpos($rule, 'required') !== false) {
|
||||
$errors[$field] = "The $field field is required.";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($data[$field])) {
|
||||
if (strpos($rule, 'numeric') !== false && !is_numeric($data[$field])) {
|
||||
$errors[$field] = "The $field must be a number.";
|
||||
}
|
||||
|
||||
if (strpos($rule, 'min:') !== false) {
|
||||
$min = substr($rule, strpos($rule, 'min:') + 4);
|
||||
if ($data[$field] < $min) {
|
||||
$errors[$field] = "The $field must be at least $min.";
|
||||
}
|
||||
}
|
||||
|
||||
if (strpos($rule, 'max:') !== false) {
|
||||
$max = substr($rule, strpos($rule, 'max:') + 4);
|
||||
if ($data[$field] > $max) {
|
||||
$errors[$field] = "The $field must not be greater than $max.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
$this->error('Validation failed', 422);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function sanitizeInput($input) {
|
||||
if (is_array($input)) {
|
||||
return array_map([$this, 'sanitizeInput'], $input);
|
||||
}
|
||||
return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
protected function log($message, $level = 'info') {
|
||||
$this->logger->log($message, $level);
|
||||
}
|
||||
}
|
||||
96
f_modules/m_frontend/m_donations/src/Core/Database.php
Normal file
96
f_modules/m_frontend/m_donations/src/Core/Database.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
namespace Donations\Core;
|
||||
|
||||
class Database {
|
||||
private static $instance = null;
|
||||
private $connection;
|
||||
private $config;
|
||||
|
||||
private function __construct() {
|
||||
$this->config = require DONATIONS_PATH . '/config/config.php';
|
||||
$this->connect();
|
||||
}
|
||||
|
||||
public static function getInstance() {
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function connect() {
|
||||
try {
|
||||
$dsn = "mysql:host={$this->config['db']['host']};dbname={$this->config['db']['name']};charset=utf8mb4";
|
||||
$options = [
|
||||
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
|
||||
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
|
||||
\PDO::ATTR_EMULATE_PREPARES => false
|
||||
];
|
||||
|
||||
$this->connection = new \PDO(
|
||||
$dsn,
|
||||
$this->config['db']['user'],
|
||||
$this->config['db']['pass'],
|
||||
$options
|
||||
);
|
||||
} catch (\PDOException $e) {
|
||||
throw new DatabaseException("Connection failed: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function query($sql, $params = []) {
|
||||
try {
|
||||
$stmt = $this->connection->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
return $stmt;
|
||||
} catch (\PDOException $e) {
|
||||
throw new DatabaseException("Query failed: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function fetch($sql, $params = []) {
|
||||
$stmt = $this->query($sql, $params);
|
||||
return $stmt->fetch();
|
||||
}
|
||||
|
||||
public function fetchAll($sql, $params = []) {
|
||||
$stmt = $this->query($sql, $params);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public function execute($sql, $params = []) {
|
||||
$stmt = $this->query($sql, $params);
|
||||
return $stmt->rowCount();
|
||||
}
|
||||
|
||||
public function lastInsertId() {
|
||||
return $this->connection->lastInsertId();
|
||||
}
|
||||
|
||||
public function beginTransaction() {
|
||||
return $this->connection->beginTransaction();
|
||||
}
|
||||
|
||||
public function commit() {
|
||||
return $this->connection->commit();
|
||||
}
|
||||
|
||||
public function rollback() {
|
||||
return $this->connection->rollBack();
|
||||
}
|
||||
|
||||
public function quote($value) {
|
||||
return $this->connection->quote($value);
|
||||
}
|
||||
|
||||
public function getConnection() {
|
||||
return $this->connection;
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
$this->connection = null;
|
||||
}
|
||||
|
||||
private function __clone() {}
|
||||
private function __wakeup() {}
|
||||
}
|
||||
115
f_modules/m_frontend/m_donations/src/Core/Event.php
Normal file
115
f_modules/m_frontend/m_donations/src/Core/Event.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
namespace Donations\Core;
|
||||
|
||||
class Event {
|
||||
private static $instance = null;
|
||||
private $listeners = [];
|
||||
private $queue;
|
||||
|
||||
private function __construct() {
|
||||
$this->queue = Queue::getInstance();
|
||||
}
|
||||
|
||||
public static function getInstance() {
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function listen($event, $listener) {
|
||||
if (!isset($this->listeners[$event])) {
|
||||
$this->listeners[$event] = [];
|
||||
}
|
||||
$this->listeners[$event][] = $listener;
|
||||
}
|
||||
|
||||
public function fire($event, $data = []) {
|
||||
if (!isset($this->listeners[$event])) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->listeners[$event] as $listener) {
|
||||
if (is_string($listener)) {
|
||||
$listener = new $listener();
|
||||
}
|
||||
|
||||
if (method_exists($listener, 'handle')) {
|
||||
$listener->handle($data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function dispatch($event, $data = []) {
|
||||
$this->fire($event, $data);
|
||||
}
|
||||
|
||||
public function dispatchAsync($event, $data = []) {
|
||||
$this->queue->push('EventJob', [
|
||||
'event' => $event,
|
||||
'data' => $data
|
||||
]);
|
||||
}
|
||||
|
||||
public function dispatchDelayed($delay, $event, $data = []) {
|
||||
$this->queue->later($delay, 'EventJob', [
|
||||
'event' => $event,
|
||||
'data' => $data
|
||||
]);
|
||||
}
|
||||
|
||||
public function forget($event) {
|
||||
unset($this->listeners[$event]);
|
||||
}
|
||||
|
||||
public function flush() {
|
||||
$this->listeners = [];
|
||||
}
|
||||
|
||||
public function hasListeners($event) {
|
||||
return isset($this->listeners[$event]) && !empty($this->listeners[$event]);
|
||||
}
|
||||
|
||||
public function getListeners($event) {
|
||||
return $this->listeners[$event] ?? [];
|
||||
}
|
||||
|
||||
public function getEvents() {
|
||||
return array_keys($this->listeners);
|
||||
}
|
||||
|
||||
public function subscribe($subscriber) {
|
||||
if (method_exists($subscriber, 'subscribe')) {
|
||||
$subscriber->subscribe($this);
|
||||
}
|
||||
}
|
||||
|
||||
public function unsubscribe($subscriber) {
|
||||
foreach ($this->listeners as $event => $listeners) {
|
||||
$this->listeners[$event] = array_filter($listeners, function($listener) use ($subscriber) {
|
||||
return $listener !== $subscriber;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class EventJob {
|
||||
private $event;
|
||||
|
||||
public function handle($data) {
|
||||
$this->event = Event::getInstance();
|
||||
$this->event->fire($data['event'], $data['data']);
|
||||
}
|
||||
}
|
||||
|
||||
class EventSubscriber {
|
||||
public function subscribe($events) {
|
||||
// Override this method to subscribe to events
|
||||
}
|
||||
}
|
||||
|
||||
class EventListener {
|
||||
public function handle($data) {
|
||||
// Override this method to handle events
|
||||
}
|
||||
}
|
||||
82
f_modules/m_frontend/m_donations/src/Core/Exception.php
Normal file
82
f_modules/m_frontend/m_donations/src/Core/Exception.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
namespace Donations\Core;
|
||||
|
||||
class Exception extends \Exception {
|
||||
protected $data;
|
||||
|
||||
public function __construct($message = "", $code = 0, $data = null) {
|
||||
parent::__construct($message, $code);
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
public function getData() {
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public function toArray() {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $this->getMessage(),
|
||||
'code' => $this->getCode(),
|
||||
'data' => $this->getData()
|
||||
];
|
||||
}
|
||||
|
||||
public function toJson() {
|
||||
return json_encode($this->toArray());
|
||||
}
|
||||
}
|
||||
|
||||
class ValidationException extends Exception {
|
||||
public function __construct($errors) {
|
||||
parent::__construct('Validation failed', 422, $errors);
|
||||
}
|
||||
}
|
||||
|
||||
class AuthenticationException extends Exception {
|
||||
public function __construct($message = 'Authentication required') {
|
||||
parent::__construct($message, 401);
|
||||
}
|
||||
}
|
||||
|
||||
class AuthorizationException extends Exception {
|
||||
public function __construct($message = 'Unauthorized access') {
|
||||
parent::__construct($message, 403);
|
||||
}
|
||||
}
|
||||
|
||||
class NotFoundException extends Exception {
|
||||
public function __construct($message = 'Resource not found') {
|
||||
parent::__construct($message, 404);
|
||||
}
|
||||
}
|
||||
|
||||
class RateLimitException extends Exception {
|
||||
public function __construct($message = 'Rate limit exceeded') {
|
||||
parent::__construct($message, 429);
|
||||
}
|
||||
}
|
||||
|
||||
class PaymentException extends Exception {
|
||||
public function __construct($message, $data = null) {
|
||||
parent::__construct($message, 402, $data);
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseException extends Exception {
|
||||
public function __construct($message = 'Database error occurred') {
|
||||
parent::__construct($message, 500);
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigurationException extends Exception {
|
||||
public function __construct($message = 'Configuration error occurred') {
|
||||
parent::__construct($message, 500);
|
||||
}
|
||||
}
|
||||
|
||||
class WebhookException extends Exception {
|
||||
public function __construct($message = 'Webhook error occurred') {
|
||||
parent::__construct($message, 500);
|
||||
}
|
||||
}
|
||||
97
f_modules/m_frontend/m_donations/src/Core/Handler.php
Normal file
97
f_modules/m_frontend/m_donations/src/Core/Handler.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
namespace Donations\Core;
|
||||
|
||||
abstract class Handler {
|
||||
protected $config;
|
||||
protected $db;
|
||||
protected $logger;
|
||||
|
||||
public function __construct() {
|
||||
$this->config = require DONATIONS_PATH . '/config/config.php';
|
||||
$this->db = db();
|
||||
$this->logger = new Logger();
|
||||
}
|
||||
|
||||
protected function validate($data, $rules) {
|
||||
$errors = [];
|
||||
|
||||
foreach ($rules as $field => $rule) {
|
||||
if (!isset($data[$field]) && strpos($rule, 'required') !== false) {
|
||||
$errors[$field] = "The $field field is required.";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($data[$field])) {
|
||||
if (strpos($rule, 'numeric') !== false && !is_numeric($data[$field])) {
|
||||
$errors[$field] = "The $field must be a number.";
|
||||
}
|
||||
|
||||
if (strpos($rule, 'min:') !== false) {
|
||||
$min = substr($rule, strpos($rule, 'min:') + 4);
|
||||
if ($data[$field] < $min) {
|
||||
$errors[$field] = "The $field must be at least $min.";
|
||||
}
|
||||
}
|
||||
|
||||
if (strpos($rule, 'max:') !== false) {
|
||||
$max = substr($rule, strpos($rule, 'max:') + 4);
|
||||
if ($data[$field] > $max) {
|
||||
$errors[$field] = "The $field must not be greater than $max.";
|
||||
}
|
||||
}
|
||||
|
||||
if (strpos($rule, 'email') !== false && !filter_var($data[$field], FILTER_VALIDATE_EMAIL)) {
|
||||
$errors[$field] = "The $field must be a valid email address.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
protected function log($message, $level = 'info') {
|
||||
$this->logger->log($message, $level);
|
||||
}
|
||||
|
||||
protected function error($message, $code = 500) {
|
||||
$this->log($message, 'error');
|
||||
throw new \Exception($message, $code);
|
||||
}
|
||||
|
||||
protected function success($message, $data = null) {
|
||||
$this->log($message, 'info');
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
'data' => $data
|
||||
];
|
||||
}
|
||||
|
||||
protected function formatAmount($amount) {
|
||||
return number_format($amount, 2, '.', '');
|
||||
}
|
||||
|
||||
protected function sanitizeInput($input) {
|
||||
if (is_array($input)) {
|
||||
return array_map([$this, 'sanitizeInput'], $input);
|
||||
}
|
||||
return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
protected function generateUniqueId($prefix = '') {
|
||||
return $prefix . uniqid() . bin2hex(random_bytes(8));
|
||||
}
|
||||
|
||||
protected function getClientIp() {
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
|
||||
}
|
||||
return $ip;
|
||||
}
|
||||
|
||||
protected function isAllowedIp($ip) {
|
||||
return empty($this->config['api']['allowed_ips']) ||
|
||||
in_array($ip, $this->config['api']['allowed_ips']);
|
||||
}
|
||||
}
|
||||
65
f_modules/m_frontend/m_donations/src/Core/Logger.php
Normal file
65
f_modules/m_frontend/m_donations/src/Core/Logger.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
namespace Donations\Core;
|
||||
|
||||
class Logger {
|
||||
private $logFile;
|
||||
private $logLevels = ['debug', 'info', 'warning', 'error', 'critical'];
|
||||
|
||||
public function __construct() {
|
||||
$this->logFile = DONATIONS_PATH . '/logs/donations.log';
|
||||
$this->ensureLogDirectory();
|
||||
}
|
||||
|
||||
public function log($message, $level = 'info') {
|
||||
if (!in_array($level, $this->logLevels)) {
|
||||
$level = 'info';
|
||||
}
|
||||
|
||||
$timestamp = date('Y-m-d H:i:s');
|
||||
$logMessage = "[$timestamp] [$level] $message" . PHP_EOL;
|
||||
|
||||
file_put_contents($this->logFile, $logMessage, FILE_APPEND);
|
||||
}
|
||||
|
||||
private function ensureLogDirectory() {
|
||||
$logDir = dirname($this->logFile);
|
||||
if (!is_dir($logDir)) {
|
||||
mkdir($logDir, 0755, true);
|
||||
}
|
||||
}
|
||||
|
||||
public function debug($message) {
|
||||
$this->log($message, 'debug');
|
||||
}
|
||||
|
||||
public function info($message) {
|
||||
$this->log($message, 'info');
|
||||
}
|
||||
|
||||
public function warning($message) {
|
||||
$this->log($message, 'warning');
|
||||
}
|
||||
|
||||
public function error($message) {
|
||||
$this->log($message, 'error');
|
||||
}
|
||||
|
||||
public function critical($message) {
|
||||
$this->log($message, 'critical');
|
||||
}
|
||||
|
||||
public function getLogContents($lines = 100) {
|
||||
if (!file_exists($this->logFile)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$logs = file($this->logFile);
|
||||
return array_slice($logs, -$lines);
|
||||
}
|
||||
|
||||
public function clearLog() {
|
||||
if (file_exists($this->logFile)) {
|
||||
unlink($this->logFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
190
f_modules/m_frontend/m_donations/src/Core/Mail.php
Normal file
190
f_modules/m_frontend/m_donations/src/Core/Mail.php
Normal file
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
namespace Donations\Core;
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
|
||||
class Mail {
|
||||
private static $instance = null;
|
||||
private $config;
|
||||
private $mailer;
|
||||
private $queue;
|
||||
|
||||
private function __construct() {
|
||||
$this->config = require DONATIONS_PATH . '/config/config.php';
|
||||
$this->queue = Queue::getInstance();
|
||||
$this->setupMailer();
|
||||
}
|
||||
|
||||
public static function getInstance() {
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function setupMailer() {
|
||||
$this->mailer = new PHPMailer(true);
|
||||
|
||||
try {
|
||||
$this->mailer->isSMTP();
|
||||
$this->mailer->Host = $this->config['mail']['host'];
|
||||
$this->mailer->SMTPAuth = true;
|
||||
$this->mailer->Username = $this->config['mail']['username'];
|
||||
$this->mailer->Password = $this->config['mail']['password'];
|
||||
$this->mailer->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
|
||||
$this->mailer->Port = $this->config['mail']['port'];
|
||||
$this->mailer->CharSet = 'UTF-8';
|
||||
|
||||
$this->mailer->setFrom(
|
||||
$this->config['mail']['from']['address'],
|
||||
$this->config['mail']['from']['name']
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
throw new \Exception("Mail setup failed: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function to($address, $name = '') {
|
||||
$this->mailer->addAddress($address, $name);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function cc($address, $name = '') {
|
||||
$this->mailer->addCC($address, $name);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function bcc($address, $name = '') {
|
||||
$this->mailer->addBCC($address, $name);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function subject($subject) {
|
||||
$this->mailer->Subject = $subject;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function body($body) {
|
||||
$this->mailer->Body = $body;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function html($html) {
|
||||
$this->mailer->isHTML(true);
|
||||
$this->mailer->Body = $html;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function text($text) {
|
||||
$this->mailer->AltBody = $text;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function attach($path, $name = '') {
|
||||
$this->mailer->addAttachment($path, $name);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function embed($path, $cid) {
|
||||
$this->mailer->addEmbeddedImage($path, $cid);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function send() {
|
||||
try {
|
||||
return $this->mailer->send();
|
||||
} catch (Exception $e) {
|
||||
throw new \Exception("Mail send failed: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function queue($delay = 0) {
|
||||
$data = [
|
||||
'to' => $this->mailer->getAllRecipientAddresses(),
|
||||
'subject' => $this->mailer->Subject,
|
||||
'body' => $this->mailer->Body,
|
||||
'html' => $this->mailer->isHTML(),
|
||||
'text' => $this->mailer->AltBody,
|
||||
'attachments' => $this->getAttachments(),
|
||||
'embedded' => $this->getEmbedded()
|
||||
];
|
||||
|
||||
if ($delay > 0) {
|
||||
return $this->queue->later($delay, 'MailJob', $data);
|
||||
}
|
||||
|
||||
return $this->queue->push('MailJob', $data);
|
||||
}
|
||||
|
||||
protected function getAttachments() {
|
||||
$attachments = [];
|
||||
foreach ($this->mailer->getAttachments() as $attachment) {
|
||||
$attachments[] = [
|
||||
'path' => $attachment[0],
|
||||
'name' => $attachment[1]
|
||||
];
|
||||
}
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
protected function getEmbedded() {
|
||||
$embedded = [];
|
||||
foreach ($this->mailer->getEmbeddedImages() as $image) {
|
||||
$embedded[] = [
|
||||
'path' => $image[0],
|
||||
'cid' => $image[1]
|
||||
];
|
||||
}
|
||||
return $embedded;
|
||||
}
|
||||
|
||||
public function reset() {
|
||||
$this->mailer->clearAllRecipients();
|
||||
$this->mailer->clearAttachments();
|
||||
$this->mailer->clearCustomHeaders();
|
||||
$this->mailer->clearReplyTos();
|
||||
$this->mailer->Subject = '';
|
||||
$this->mailer->Body = '';
|
||||
$this->mailer->AltBody = '';
|
||||
$this->mailer->isHTML(false);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMailer() {
|
||||
return $this->mailer;
|
||||
}
|
||||
}
|
||||
|
||||
class MailJob {
|
||||
private $mail;
|
||||
|
||||
public function handle($data) {
|
||||
$this->mail = Mail::getInstance();
|
||||
|
||||
foreach ($data['to'] as $address => $name) {
|
||||
$this->mail->to($address, $name);
|
||||
}
|
||||
|
||||
$this->mail->subject($data['subject'])
|
||||
->body($data['body']);
|
||||
|
||||
if ($data['html']) {
|
||||
$this->mail->html($data['body']);
|
||||
}
|
||||
|
||||
if ($data['text']) {
|
||||
$this->mail->text($data['text']);
|
||||
}
|
||||
|
||||
foreach ($data['attachments'] as $attachment) {
|
||||
$this->mail->attach($attachment['path'], $attachment['name']);
|
||||
}
|
||||
|
||||
foreach ($data['embedded'] as $image) {
|
||||
$this->mail->embed($image['path'], $image['cid']);
|
||||
}
|
||||
|
||||
$this->mail->send();
|
||||
}
|
||||
}
|
||||
105
f_modules/m_frontend/m_donations/src/Core/Model.php
Normal file
105
f_modules/m_frontend/m_donations/src/Core/Model.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
namespace Donations\Core;
|
||||
|
||||
abstract class Model {
|
||||
protected $db;
|
||||
protected $table;
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
public function __construct() {
|
||||
$this->db = db();
|
||||
}
|
||||
|
||||
public function find($id) {
|
||||
$sql = "SELECT * FROM {$this->table} WHERE {$this->primaryKey} = ?";
|
||||
return $this->db->fetch($sql, [$id]);
|
||||
}
|
||||
|
||||
public function all($conditions = [], $orderBy = null, $limit = null) {
|
||||
$sql = "SELECT * FROM {$this->table}";
|
||||
$params = [];
|
||||
|
||||
if (!empty($conditions)) {
|
||||
$sql .= " WHERE " . $this->buildWhereClause($conditions, $params);
|
||||
}
|
||||
|
||||
if ($orderBy) {
|
||||
$sql .= " ORDER BY " . $orderBy;
|
||||
}
|
||||
|
||||
if ($limit) {
|
||||
$sql .= " LIMIT " . $limit;
|
||||
}
|
||||
|
||||
return $this->db->fetchAll($sql, $params);
|
||||
}
|
||||
|
||||
public function create($data) {
|
||||
$fields = array_keys($data);
|
||||
$values = array_values($data);
|
||||
$placeholders = str_repeat('?,', count($fields) - 1) . '?';
|
||||
|
||||
$sql = "INSERT INTO {$this->table} (" . implode(',', $fields) . ")
|
||||
VALUES ($placeholders)";
|
||||
|
||||
$this->db->execute($sql, $values);
|
||||
return $this->db->lastInsertId();
|
||||
}
|
||||
|
||||
public function update($id, $data) {
|
||||
$fields = array_keys($data);
|
||||
$values = array_values($data);
|
||||
$set = implode('=?,', $fields) . '=?';
|
||||
$values[] = $id;
|
||||
|
||||
$sql = "UPDATE {$this->table} SET $set
|
||||
WHERE {$this->primaryKey} = ?";
|
||||
|
||||
return $this->db->execute($sql, $values);
|
||||
}
|
||||
|
||||
public function delete($id) {
|
||||
$sql = "DELETE FROM {$this->table} WHERE {$this->primaryKey} = ?";
|
||||
return $this->db->execute($sql, [$id]);
|
||||
}
|
||||
|
||||
public function count($conditions = []) {
|
||||
$sql = "SELECT COUNT(*) as count FROM {$this->table}";
|
||||
$params = [];
|
||||
|
||||
if (!empty($conditions)) {
|
||||
$sql .= " WHERE " . $this->buildWhereClause($conditions, $params);
|
||||
}
|
||||
|
||||
$result = $this->db->fetch($sql, $params);
|
||||
return $result['count'];
|
||||
}
|
||||
|
||||
protected function buildWhereClause($conditions, &$params) {
|
||||
$clauses = [];
|
||||
foreach ($conditions as $field => $value) {
|
||||
if (is_array($value)) {
|
||||
$operator = $value[0];
|
||||
$val = $value[1];
|
||||
} else {
|
||||
$operator = '=';
|
||||
$val = $value;
|
||||
}
|
||||
$clauses[] = "$field $operator ?";
|
||||
$params[] = $val;
|
||||
}
|
||||
return implode(' AND ', $clauses);
|
||||
}
|
||||
|
||||
public function beginTransaction() {
|
||||
return $this->db->beginTransaction();
|
||||
}
|
||||
|
||||
public function commit() {
|
||||
return $this->db->commit();
|
||||
}
|
||||
|
||||
public function rollback() {
|
||||
return $this->db->rollback();
|
||||
}
|
||||
}
|
||||
170
f_modules/m_frontend/m_donations/src/Core/Queue.php
Normal file
170
f_modules/m_frontend/m_donations/src/Core/Queue.php
Normal file
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
namespace Donations\Core;
|
||||
|
||||
class Queue {
|
||||
private static $instance = null;
|
||||
private $config;
|
||||
private $connection;
|
||||
private $channel;
|
||||
private $queue;
|
||||
|
||||
private function __construct() {
|
||||
$this->config = require DONATIONS_PATH . '/config/config.php';
|
||||
$this->connect();
|
||||
}
|
||||
|
||||
public static function getInstance() {
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function connect() {
|
||||
try {
|
||||
$this->connection = new \AMQPConnection([
|
||||
'host' => $this->config['queue']['host'],
|
||||
'port' => $this->config['queue']['port'],
|
||||
'username' => $this->config['queue']['user'],
|
||||
'password' => $this->config['queue']['pass'],
|
||||
'vhost' => $this->config['queue']['vhost']
|
||||
]);
|
||||
|
||||
$this->channel = $this->connection->channel();
|
||||
$this->queue = $this->config['queue']['name'];
|
||||
|
||||
$this->channel->queue_declare($this->queue, false, true, false, false);
|
||||
} catch (\Exception $e) {
|
||||
throw new \Exception("Queue connection failed: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function push($job, $data = [], $delay = 0) {
|
||||
$message = [
|
||||
'job' => $job,
|
||||
'data' => $data,
|
||||
'attempts' => 0,
|
||||
'created_at' => time()
|
||||
];
|
||||
|
||||
$msg = new \AMQPMessage(
|
||||
json_encode($message),
|
||||
['delivery_mode' => \AMQPMessage::DELIVERY_MODE_PERSISTENT]
|
||||
);
|
||||
|
||||
if ($delay > 0) {
|
||||
$this->channel->basic_publish($msg, '', $this->queue . '_delayed');
|
||||
} else {
|
||||
$this->channel->basic_publish($msg, '', $this->queue);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function later($delay, $job, $data = []) {
|
||||
return $this->push($job, $data, $delay);
|
||||
}
|
||||
|
||||
public function pop() {
|
||||
$msg = $this->channel->basic_get($this->queue);
|
||||
|
||||
if ($msg) {
|
||||
$message = json_decode($msg->getBody(), true);
|
||||
$message['attempts']++;
|
||||
|
||||
$this->channel->basic_ack($msg->getDeliveryTag());
|
||||
return $message;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function process() {
|
||||
$this->channel->basic_qos(null, 1, null);
|
||||
|
||||
$callback = function($msg) {
|
||||
try {
|
||||
$message = json_decode($msg->getBody(), true);
|
||||
$job = $message['job'];
|
||||
$data = $message['data'];
|
||||
|
||||
$class = "Donations\\Jobs\\{$job}";
|
||||
$instance = new $class();
|
||||
$instance->handle($data);
|
||||
|
||||
$this->channel->basic_ack($msg->getDeliveryTag());
|
||||
} catch (\Exception $e) {
|
||||
$this->handleFailedJob($msg, $e);
|
||||
}
|
||||
};
|
||||
|
||||
$this->channel->basic_consume($this->queue, '', false, false, false, false, $callback);
|
||||
|
||||
while ($this->channel->is_consuming()) {
|
||||
$this->channel->wait();
|
||||
}
|
||||
}
|
||||
|
||||
protected function handleFailedJob($msg, $exception) {
|
||||
$message = json_decode($msg->getBody(), true);
|
||||
$message['attempts']++;
|
||||
|
||||
if ($message['attempts'] >= $this->config['queue']['max_attempts']) {
|
||||
$this->channel->basic_nack($msg->getDeliveryTag(), false, false);
|
||||
$this->logFailedJob($message, $exception);
|
||||
} else {
|
||||
$this->channel->basic_nack($msg->getDeliveryTag(), false, true);
|
||||
}
|
||||
}
|
||||
|
||||
protected function logFailedJob($message, $exception) {
|
||||
$sql = "INSERT INTO failed_jobs (job, data, error, failed_at) VALUES (?, ?, ?, NOW())";
|
||||
$this->db->execute($sql, [
|
||||
$message['job'],
|
||||
json_encode($message['data']),
|
||||
$exception->getMessage()
|
||||
]);
|
||||
}
|
||||
|
||||
public function size() {
|
||||
$queueInfo = $this->channel->queue_declare($this->queue, false, true, false, false);
|
||||
return $queueInfo[1];
|
||||
}
|
||||
|
||||
public function flush() {
|
||||
$this->channel->queue_purge($this->queue);
|
||||
}
|
||||
|
||||
public function retry($id) {
|
||||
$sql = "SELECT * FROM failed_jobs WHERE id = ?";
|
||||
$job = $this->db->fetch($sql, [$id]);
|
||||
|
||||
if ($job) {
|
||||
$this->push($job['job'], json_decode($job['data'], true));
|
||||
$this->db->execute("DELETE FROM failed_jobs WHERE id = ?", [$id]);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function forget($id) {
|
||||
return $this->db->execute("DELETE FROM failed_jobs WHERE id = ?", [$id]);
|
||||
}
|
||||
|
||||
public function getConnection() {
|
||||
return $this->connection;
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
if ($this->channel) {
|
||||
$this->channel->close();
|
||||
}
|
||||
if ($this->connection) {
|
||||
$this->connection->close();
|
||||
}
|
||||
}
|
||||
|
||||
private function __clone() {}
|
||||
private function __wakeup() {}
|
||||
}
|
||||
108
f_modules/m_frontend/m_donations/src/Core/Router.php
Normal file
108
f_modules/m_frontend/m_donations/src/Core/Router.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
namespace Donations\Core;
|
||||
|
||||
class Router {
|
||||
protected $routes = [];
|
||||
protected $config;
|
||||
|
||||
public function __construct() {
|
||||
$this->config = require DONATIONS_PATH . '/config/config.php';
|
||||
}
|
||||
|
||||
public function add($method, $path, $handler) {
|
||||
$this->routes[] = [
|
||||
'method' => strtoupper($method),
|
||||
'path' => $path,
|
||||
'handler' => $handler
|
||||
];
|
||||
}
|
||||
|
||||
public function get($path, $handler) {
|
||||
$this->add('GET', $path, $handler);
|
||||
}
|
||||
|
||||
public function post($path, $handler) {
|
||||
$this->add('POST', $path, $handler);
|
||||
}
|
||||
|
||||
public function put($path, $handler) {
|
||||
$this->add('PUT', $path, $handler);
|
||||
}
|
||||
|
||||
public function delete($path, $handler) {
|
||||
$this->add('DELETE', $path, $handler);
|
||||
}
|
||||
|
||||
public function dispatch() {
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
||||
$path = str_replace($this->config['base_url'], '', $path);
|
||||
|
||||
foreach ($this->routes as $route) {
|
||||
if ($route['method'] !== $method) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pattern = $this->convertPathToRegex($route['path']);
|
||||
if (preg_match($pattern, $path, $matches)) {
|
||||
array_shift($matches); // Remove the full match
|
||||
return $this->executeHandler($route['handler'], $matches);
|
||||
}
|
||||
}
|
||||
|
||||
// No route found
|
||||
http_response_code(404);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Route not found',
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
|
||||
protected function convertPathToRegex($path) {
|
||||
return '#^' . preg_replace('#\{([a-zA-Z0-9_]+)\}#', '([^/]+)', $path) . '$#';
|
||||
}
|
||||
|
||||
protected function executeHandler($handler, $params) {
|
||||
if (is_callable($handler)) {
|
||||
return call_user_func_array($handler, $params);
|
||||
}
|
||||
|
||||
if (is_string($handler)) {
|
||||
list($controller, $method) = explode('@', $handler);
|
||||
$controllerClass = "Donations\\Controllers\\{$controller}";
|
||||
$controllerInstance = new $controllerClass();
|
||||
return call_user_func_array([$controllerInstance, $method], $params);
|
||||
}
|
||||
|
||||
throw new \Exception('Invalid handler');
|
||||
}
|
||||
|
||||
public function group($prefix, $routes) {
|
||||
foreach ($routes as $route) {
|
||||
$this->add(
|
||||
$route['method'],
|
||||
$prefix . $route['path'],
|
||||
$route['handler']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function resource($name, $controller) {
|
||||
$this->get("/{$name}", "{$controller}@index");
|
||||
$this->get("/{$name}/create", "{$controller}@create");
|
||||
$this->post("/{$name}", "{$controller}@store");
|
||||
$this->get("/{$name}/{id}", "{$controller}@show");
|
||||
$this->get("/{$name}/{id}/edit", "{$controller}@edit");
|
||||
$this->put("/{$name}/{id}", "{$controller}@update");
|
||||
$this->delete("/{$name}/{id}", "{$controller}@destroy");
|
||||
}
|
||||
|
||||
public function apiResource($name, $controller) {
|
||||
$this->get("/api/{$name}", "{$controller}@index");
|
||||
$this->post("/api/{$name}", "{$controller}@store");
|
||||
$this->get("/api/{$name}/{id}", "{$controller}@show");
|
||||
$this->put("/api/{$name}/{id}", "{$controller}@update");
|
||||
$this->delete("/api/{$name}/{id}", "{$controller}@destroy");
|
||||
}
|
||||
}
|
||||
114
f_modules/m_frontend/m_donations/src/Core/Service.php
Normal file
114
f_modules/m_frontend/m_donations/src/Core/Service.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
namespace Donations\Core;
|
||||
|
||||
abstract class Service {
|
||||
protected $config;
|
||||
protected $db;
|
||||
protected $logger;
|
||||
|
||||
public function __construct() {
|
||||
$this->config = require DONATIONS_PATH . '/config/config.php';
|
||||
$this->db = db();
|
||||
$this->logger = new Logger();
|
||||
}
|
||||
|
||||
protected function beginTransaction() {
|
||||
return $this->db->beginTransaction();
|
||||
}
|
||||
|
||||
protected function commit() {
|
||||
return $this->db->commit();
|
||||
}
|
||||
|
||||
protected function rollback() {
|
||||
return $this->db->rollback();
|
||||
}
|
||||
|
||||
protected function validate($data, $rules) {
|
||||
$errors = [];
|
||||
|
||||
foreach ($rules as $field => $rule) {
|
||||
if (!isset($data[$field]) && strpos($rule, 'required') !== false) {
|
||||
$errors[$field] = "The $field field is required.";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($data[$field])) {
|
||||
if (strpos($rule, 'numeric') !== false && !is_numeric($data[$field])) {
|
||||
$errors[$field] = "The $field must be a number.";
|
||||
}
|
||||
|
||||
if (strpos($rule, 'min:') !== false) {
|
||||
$min = substr($rule, strpos($rule, 'min:') + 4);
|
||||
if ($data[$field] < $min) {
|
||||
$errors[$field] = "The $field must be at least $min.";
|
||||
}
|
||||
}
|
||||
|
||||
if (strpos($rule, 'max:') !== false) {
|
||||
$max = substr($rule, strpos($rule, 'max:') + 4);
|
||||
if ($data[$field] > $max) {
|
||||
$errors[$field] = "The $field must not be greater than $max.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
throw new \Exception(json_encode($errors));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function sanitizeInput($input) {
|
||||
if (is_array($input)) {
|
||||
return array_map([$this, 'sanitizeInput'], $input);
|
||||
}
|
||||
return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
protected function log($message, $level = 'info') {
|
||||
$this->logger->log($message, $level);
|
||||
}
|
||||
|
||||
protected function formatAmount($amount) {
|
||||
return number_format($amount, 2, '.', '');
|
||||
}
|
||||
|
||||
protected function generateUniqueId($prefix = '') {
|
||||
return $prefix . uniqid() . bin2hex(random_bytes(8));
|
||||
}
|
||||
|
||||
protected function getClientIp() {
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
|
||||
}
|
||||
return $ip;
|
||||
}
|
||||
|
||||
protected function isAllowedIp($ip) {
|
||||
return empty($this->config['api']['allowed_ips']) ||
|
||||
in_array($ip, $this->config['api']['allowed_ips']);
|
||||
}
|
||||
|
||||
protected function formatDate($date, $format = 'Y-m-d H:i:s') {
|
||||
return date($format, strtotime($date));
|
||||
}
|
||||
|
||||
protected function truncate($text, $length = 100) {
|
||||
if (strlen($text) <= $length) {
|
||||
return $text;
|
||||
}
|
||||
return substr($text, 0, $length) . '...';
|
||||
}
|
||||
|
||||
protected function escape($text) {
|
||||
return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
protected function isActive($path) {
|
||||
return strpos($_SERVER['REQUEST_URI'], $path) !== false ? 'active' : '';
|
||||
}
|
||||
}
|
||||
209
f_modules/m_frontend/m_donations/src/Core/Session.php
Normal file
209
f_modules/m_frontend/m_donations/src/Core/Session.php
Normal file
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
namespace Donations\Core;
|
||||
|
||||
class Session {
|
||||
private static $instance = null;
|
||||
private $config;
|
||||
private $started = false;
|
||||
|
||||
private function __construct() {
|
||||
$this->config = require DONATIONS_PATH . '/config/config.php';
|
||||
$this->start();
|
||||
}
|
||||
|
||||
public static function getInstance() {
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function start() {
|
||||
if (!$this->started) {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start([
|
||||
'cookie_httponly' => true,
|
||||
'cookie_secure' => $this->config['session']['secure'],
|
||||
'cookie_samesite' => 'Lax',
|
||||
'gc_maxlifetime' => $this->config['session']['lifetime'] * 60
|
||||
]);
|
||||
}
|
||||
$this->started = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function get($key, $default = null) {
|
||||
return $_SESSION[$key] ?? $default;
|
||||
}
|
||||
|
||||
public function set($key, $value) {
|
||||
$_SESSION[$key] = $value;
|
||||
}
|
||||
|
||||
public function has($key) {
|
||||
return isset($_SESSION[$key]);
|
||||
}
|
||||
|
||||
public function remove($key) {
|
||||
if (isset($_SESSION[$key])) {
|
||||
unset($_SESSION[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
public function all() {
|
||||
return $_SESSION;
|
||||
}
|
||||
|
||||
public function flush() {
|
||||
$_SESSION = [];
|
||||
}
|
||||
|
||||
public function regenerate($destroy = false) {
|
||||
session_regenerate_id($destroy);
|
||||
}
|
||||
|
||||
public function destroy() {
|
||||
session_destroy();
|
||||
$this->started = false;
|
||||
}
|
||||
|
||||
public function flash($key, $value = null) {
|
||||
if ($value !== null) {
|
||||
$_SESSION['_flash'][$key] = $value;
|
||||
} else {
|
||||
$value = $_SESSION['_flash'][$key] ?? null;
|
||||
unset($_SESSION['_flash'][$key]);
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
public function reflash() {
|
||||
if (isset($_SESSION['_flash'])) {
|
||||
$_SESSION['_flash'] = array_merge($_SESSION['_flash'], $_SESSION['_flash']);
|
||||
}
|
||||
}
|
||||
|
||||
public function keep($keys = null) {
|
||||
if ($keys === null) {
|
||||
$_SESSION['_flash'] = [];
|
||||
} else {
|
||||
foreach ((array) $keys as $key) {
|
||||
if (isset($_SESSION['_flash'][$key])) {
|
||||
$_SESSION['_flash'][$key] = $_SESSION['_flash'][$key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function token() {
|
||||
if (!isset($_SESSION['_token'])) {
|
||||
$_SESSION['_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
return $_SESSION['_token'];
|
||||
}
|
||||
|
||||
public function regenerateToken() {
|
||||
$_SESSION['_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
public function previousUrl() {
|
||||
return $_SESSION['_previous_url'] ?? null;
|
||||
}
|
||||
|
||||
public function intended($default = null) {
|
||||
return $_SESSION['_intended_url'] ?? $default;
|
||||
}
|
||||
|
||||
public function setIntendedUrl($url) {
|
||||
$_SESSION['_intended_url'] = $url;
|
||||
}
|
||||
|
||||
public function pull($key, $default = null) {
|
||||
$value = $this->get($key, $default);
|
||||
$this->remove($key);
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function increment($key, $value = 1) {
|
||||
if (!isset($_SESSION[$key])) {
|
||||
$_SESSION[$key] = 0;
|
||||
}
|
||||
$_SESSION[$key] += $value;
|
||||
return $_SESSION[$key];
|
||||
}
|
||||
|
||||
public function decrement($key, $value = 1) {
|
||||
return $this->increment($key, -$value);
|
||||
}
|
||||
|
||||
public function put($key, $value) {
|
||||
return $this->set($key, $value);
|
||||
}
|
||||
|
||||
public function forget($key) {
|
||||
return $this->remove($key);
|
||||
}
|
||||
|
||||
public function exists($key) {
|
||||
return $this->has($key);
|
||||
}
|
||||
|
||||
public function missing($key) {
|
||||
return !$this->has($key);
|
||||
}
|
||||
|
||||
public function save() {
|
||||
if ($this->started) {
|
||||
session_write_close();
|
||||
$this->started = false;
|
||||
}
|
||||
}
|
||||
|
||||
public function getId() {
|
||||
return session_id();
|
||||
}
|
||||
|
||||
public function setId($id) {
|
||||
if ($this->started) {
|
||||
session_write_close();
|
||||
}
|
||||
session_id($id);
|
||||
$this->start();
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
return session_name();
|
||||
}
|
||||
|
||||
public function setName($name) {
|
||||
if ($this->started) {
|
||||
session_write_close();
|
||||
}
|
||||
session_name($name);
|
||||
$this->start();
|
||||
}
|
||||
|
||||
public function getHandler() {
|
||||
return session_get_handler();
|
||||
}
|
||||
|
||||
public function setHandler($handler) {
|
||||
if ($this->started) {
|
||||
session_write_close();
|
||||
}
|
||||
session_set_save_handler($handler);
|
||||
$this->start();
|
||||
}
|
||||
|
||||
public function getCookieParams() {
|
||||
return session_get_cookie_params();
|
||||
}
|
||||
|
||||
public function setCookieParams($lifetime, $path = '/', $domain = '', $secure = false, $httponly = true) {
|
||||
if ($this->started) {
|
||||
session_write_close();
|
||||
}
|
||||
session_set_cookie_params($lifetime, $path, $domain, $secure, $httponly);
|
||||
$this->start();
|
||||
}
|
||||
}
|
||||
101
f_modules/m_frontend/m_donations/src/Core/View.php
Normal file
101
f_modules/m_frontend/m_donations/src/Core/View.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
namespace Donations\Core;
|
||||
|
||||
class View {
|
||||
protected $config;
|
||||
protected $data = [];
|
||||
protected $layout = 'default';
|
||||
protected $view;
|
||||
|
||||
public function __construct() {
|
||||
$this->config = require DONATIONS_PATH . '/config/config.php';
|
||||
}
|
||||
|
||||
public function setLayout($layout) {
|
||||
$this->layout = $layout;
|
||||
}
|
||||
|
||||
public function setView($view) {
|
||||
$this->view = $view;
|
||||
}
|
||||
|
||||
public function setData($data) {
|
||||
$this->data = array_merge($this->data, $data);
|
||||
}
|
||||
|
||||
public function render() {
|
||||
if (!$this->view) {
|
||||
throw new \Exception('View not set');
|
||||
}
|
||||
|
||||
$viewFile = DONATIONS_PATH . '/views/' . $this->view . '.php';
|
||||
if (!file_exists($viewFile)) {
|
||||
throw new \Exception("View file not found: {$this->view}");
|
||||
}
|
||||
|
||||
$layoutFile = DONATIONS_PATH . '/views/layouts/' . $this->layout . '.php';
|
||||
if (!file_exists($layoutFile)) {
|
||||
throw new \Exception("Layout file not found: {$this->layout}");
|
||||
}
|
||||
|
||||
extract($this->data);
|
||||
|
||||
ob_start();
|
||||
include $viewFile;
|
||||
$content = ob_get_clean();
|
||||
|
||||
include $layoutFile;
|
||||
}
|
||||
|
||||
public function partial($name, $data = []) {
|
||||
$partialFile = DONATIONS_PATH . '/views/partials/' . $name . '.php';
|
||||
if (!file_exists($partialFile)) {
|
||||
throw new \Exception("Partial file not found: {$name}");
|
||||
}
|
||||
|
||||
extract(array_merge($this->data, $data));
|
||||
include $partialFile;
|
||||
}
|
||||
|
||||
public function asset($path) {
|
||||
return $this->config['base_url'] . '/assets/' . $path;
|
||||
}
|
||||
|
||||
public function url($path) {
|
||||
return $this->config['base_url'] . '/' . $path;
|
||||
}
|
||||
|
||||
public function csrfToken() {
|
||||
if (!isset($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
return $_SESSION['csrf_token'];
|
||||
}
|
||||
|
||||
public function csrfField() {
|
||||
return '<input type="hidden" name="csrf_token" value="' . $this->csrfToken() . '">';
|
||||
}
|
||||
|
||||
public function formatAmount($amount) {
|
||||
return number_format($amount, 2, '.', ',');
|
||||
}
|
||||
|
||||
public function formatDate($date, $format = 'Y-m-d H:i:s') {
|
||||
return date($format, strtotime($date));
|
||||
}
|
||||
|
||||
public function truncate($text, $length = 100) {
|
||||
if (strlen($text) <= $length) {
|
||||
return $text;
|
||||
}
|
||||
return substr($text, 0, $length) . '...';
|
||||
}
|
||||
|
||||
public function escape($text) {
|
||||
return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
public function isActive($path) {
|
||||
return strpos($_SERVER['REQUEST_URI'], $path) !== false ? 'active' : '';
|
||||
}
|
||||
}
|
||||
175
f_modules/m_frontend/m_donations/src/DonationHandler.php
Normal file
175
f_modules/m_frontend/m_donations/src/DonationHandler.php
Normal file
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
namespace Donations;
|
||||
|
||||
use Square\SquareClient;
|
||||
use Square\Environment;
|
||||
use Square\Models\CreatePaymentRequest;
|
||||
use Square\Models\Money;
|
||||
|
||||
class DonationHandler {
|
||||
private $square_client;
|
||||
private $config;
|
||||
|
||||
public function __construct() {
|
||||
global $square_config;
|
||||
$this->config = $square_config;
|
||||
|
||||
// Initialize Square client
|
||||
$this->square_client = new SquareClient([
|
||||
'accessToken' => $this->config['square']['access_token'],
|
||||
'environment' => $this->config['square']['environment'] === 'production' ? Environment::PRODUCTION : Environment::SANDBOX
|
||||
]);
|
||||
}
|
||||
|
||||
public function createDonation($streamer_id, $amount, $donor_name = '', $message = '') {
|
||||
try {
|
||||
$this->validateAmount($amount);
|
||||
|
||||
$payment = $this->createSquarePayment($streamer_id, $amount, $donor_name, $message);
|
||||
|
||||
if ($payment->isSuccess()) {
|
||||
$this->recordDonation($streamer_id, $amount, $donor_name, $message, $payment->getResult()->getPayment()->getId());
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'payment_id' => $payment->getResult()->getPayment()->getId(),
|
||||
'message' => 'Donation processed successfully'
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to process donation: ' . $payment->getErrors()[0]->getDetail()
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Error processing donation: ' . $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function validateAmount($amount) {
|
||||
if ($amount < $this->config['square']['min_donation'] ||
|
||||
$amount > $this->config['square']['max_donation']) {
|
||||
throw new \Exception('Invalid donation amount');
|
||||
}
|
||||
}
|
||||
|
||||
private function createSquarePayment($streamer_id, $amount, $donor_name, $message) {
|
||||
$money = new Money();
|
||||
$money->setAmount($amount * 100); // Convert to cents
|
||||
$money->setCurrency($this->config['square']['currency']);
|
||||
|
||||
$payment_request = new CreatePaymentRequest();
|
||||
$payment_request->setSourceId('EXTERNAL');
|
||||
$payment_request->setAmountMoney($money);
|
||||
$payment_request->setLocationId($this->config['square']['location_id']);
|
||||
|
||||
// Add metadata
|
||||
$payment_request->setMetadata([
|
||||
'streamer_id' => $streamer_id,
|
||||
'donor_name' => $donor_name,
|
||||
'message' => $message
|
||||
]);
|
||||
|
||||
return $this->square_client->getPaymentsApi()->createPayment($payment_request);
|
||||
}
|
||||
|
||||
private function recordDonation($streamer_id, $amount, $donor_name, $message, $payment_id) {
|
||||
$sql = "INSERT INTO donations (
|
||||
streamer_id, amount, donor_name, message, payment_id, status, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, 'completed', NOW())";
|
||||
|
||||
$params = [$streamer_id, $amount, $donor_name, $message, $payment_id];
|
||||
db()->executeQuery($sql, $params);
|
||||
|
||||
// Update streamer's balance
|
||||
$sql = "UPDATE users SET donation_balance = donation_balance + ? WHERE user_id = ?";
|
||||
db()->executeQuery($sql, [$amount, $streamer_id]);
|
||||
}
|
||||
|
||||
public function getStreamerBalance($streamer_id) {
|
||||
$sql = "SELECT donation_balance FROM users WHERE user_id = ?";
|
||||
$result = db()->getRow($sql, [$streamer_id]);
|
||||
return $result['donation_balance'] ?? 0;
|
||||
}
|
||||
|
||||
public function requestPayout($streamer_id) {
|
||||
try {
|
||||
$balance = $this->getStreamerBalance($streamer_id);
|
||||
|
||||
if ($balance < $this->config['streamer']['min_balance']) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Insufficient balance for payout'
|
||||
];
|
||||
}
|
||||
|
||||
$payout = $this->createSquarePayout($streamer_id, $balance);
|
||||
|
||||
if ($payout->isSuccess()) {
|
||||
$this->recordPayout($streamer_id, $balance, $payout->getResult()->getPayout()->getId());
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'Payout processed successfully',
|
||||
'amount' => $balance
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to process payout: ' . $payout->getErrors()[0]->getDetail()
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Error processing payout: ' . $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function createSquarePayout($streamer_id, $amount) {
|
||||
// Calculate fees
|
||||
$fee_amount = ($amount * ($this->config['streamer']['payout_fee'] / 100)) + $this->config['streamer']['payout_fee_fixed'];
|
||||
$payout_amount = $amount - $fee_amount;
|
||||
|
||||
return $this->square_client->getPayoutsApi()->createPayout([
|
||||
'amount_money' => [
|
||||
'amount' => $payout_amount * 100, // Convert to cents
|
||||
'currency' => $this->config['square']['currency']
|
||||
],
|
||||
'location_id' => $this->config['square']['location_id']
|
||||
]);
|
||||
}
|
||||
|
||||
private function recordPayout($streamer_id, $amount, $payout_id) {
|
||||
// Calculate fees
|
||||
$fee_amount = ($amount * ($this->config['streamer']['payout_fee'] / 100)) + $this->config['streamer']['payout_fee_fixed'];
|
||||
$payout_amount = $amount - $fee_amount;
|
||||
|
||||
// Record payout
|
||||
$sql = "INSERT INTO payouts (
|
||||
streamer_id, amount, fee, payout_id, status, created_at
|
||||
) VALUES (?, ?, ?, ?, 'completed', NOW())";
|
||||
|
||||
$params = [$streamer_id, $payout_amount, $fee_amount, $payout_id];
|
||||
db()->executeQuery($sql, $params);
|
||||
|
||||
// Reset streamer's balance
|
||||
$sql = "UPDATE users SET donation_balance = 0 WHERE user_id = ?";
|
||||
db()->executeQuery($sql, [$streamer_id]);
|
||||
}
|
||||
|
||||
public function getDonationHistory($streamer_id, $limit = 10) {
|
||||
$sql = "SELECT d.*, u.username as streamer_name
|
||||
FROM donations d
|
||||
JOIN users u ON d.streamer_id = u.user_id
|
||||
WHERE d.streamer_id = ?
|
||||
ORDER BY d.created_at DESC
|
||||
LIMIT ?";
|
||||
|
||||
return db()->getRows($sql, [$streamer_id, $limit]);
|
||||
}
|
||||
}
|
||||
173
f_modules/m_frontend/m_donations/src/GoalHandler.php
Normal file
173
f_modules/m_frontend/m_donations/src/GoalHandler.php
Normal file
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
namespace Donations;
|
||||
|
||||
class GoalHandler {
|
||||
private $db;
|
||||
private $notification_handler;
|
||||
|
||||
public function __construct() {
|
||||
$this->db = db();
|
||||
$this->notification_handler = new NotificationHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new donation goal
|
||||
*/
|
||||
public function createGoal($streamer_id, $title, $description, $target_amount, $end_date = null) {
|
||||
$sql = "INSERT INTO donation_goals
|
||||
(streamer_id, title, description, target_amount, end_date)
|
||||
VALUES (?, ?, ?, ?, ?)";
|
||||
|
||||
$goal_id = $this->db->insert($sql, [
|
||||
$streamer_id,
|
||||
$title,
|
||||
$description,
|
||||
$target_amount,
|
||||
$end_date
|
||||
]);
|
||||
|
||||
// Create notification
|
||||
$this->notification_handler->createNotification(
|
||||
$streamer_id,
|
||||
'goal_created',
|
||||
'New Donation Goal Created',
|
||||
"A new donation goal '{$title}' has been created with a target of $" . number_format($target_amount, 2)
|
||||
);
|
||||
|
||||
return $goal_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a milestone to a goal
|
||||
*/
|
||||
public function addMilestone($goal_id, $title, $description, $target_amount, $reward_description) {
|
||||
$sql = "INSERT INTO donation_milestones
|
||||
(goal_id, title, description, target_amount, reward_description)
|
||||
VALUES (?, ?, ?, ?, ?)";
|
||||
|
||||
return $this->db->insert($sql, [
|
||||
$goal_id,
|
||||
$title,
|
||||
$description,
|
||||
$target_amount,
|
||||
$reward_description
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update goal progress
|
||||
*/
|
||||
public function updateProgress($goal_id, $amount) {
|
||||
// Update goal progress
|
||||
$sql = "UPDATE donation_goals
|
||||
SET current_amount = current_amount + ?,
|
||||
status = CASE
|
||||
WHEN current_amount + ? >= target_amount THEN 'completed'
|
||||
ELSE status
|
||||
END
|
||||
WHERE goal_id = ?";
|
||||
|
||||
$this->db->query($sql, [$amount, $amount, $goal_id]);
|
||||
|
||||
// Get goal details
|
||||
$goal = $this->getGoal($goal_id);
|
||||
|
||||
// Check and update milestones
|
||||
$this->checkMilestones($goal_id, $goal['current_amount']);
|
||||
|
||||
// If goal is completed, create notification
|
||||
if ($goal['current_amount'] >= $goal['target_amount']) {
|
||||
$this->notification_handler->createNotification(
|
||||
$goal['streamer_id'],
|
||||
'goal_completed',
|
||||
'Donation Goal Completed!',
|
||||
"Congratulations! The goal '{$goal['title']}' has been completed!"
|
||||
);
|
||||
}
|
||||
|
||||
return $goal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and update milestones
|
||||
*/
|
||||
private function checkMilestones($goal_id, $current_amount) {
|
||||
$sql = "SELECT * FROM donation_milestones
|
||||
WHERE goal_id = ?
|
||||
AND is_achieved = 0
|
||||
AND target_amount <= ?";
|
||||
|
||||
$achieved_milestones = $this->db->getRows($sql, [$goal_id, $current_amount]);
|
||||
|
||||
foreach ($achieved_milestones as $milestone) {
|
||||
$this->markMilestoneAchieved($milestone['milestone_id']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a milestone as achieved
|
||||
*/
|
||||
private function markMilestoneAchieved($milestone_id) {
|
||||
$sql = "UPDATE donation_milestones
|
||||
SET is_achieved = 1,
|
||||
achieved_at = CURRENT_TIMESTAMP
|
||||
WHERE milestone_id = ?";
|
||||
|
||||
$this->db->query($sql, [$milestone_id]);
|
||||
|
||||
// Get milestone and goal details for notification
|
||||
$sql = "SELECT m.*, g.streamer_id, g.title as goal_title
|
||||
FROM donation_milestones m
|
||||
JOIN donation_goals g ON m.goal_id = g.goal_id
|
||||
WHERE m.milestone_id = ?";
|
||||
|
||||
$milestone = $this->db->getRow($sql, [$milestone_id]);
|
||||
|
||||
// Create notification
|
||||
$this->notification_handler->createNotification(
|
||||
$milestone['streamer_id'],
|
||||
'milestone_achieved',
|
||||
'Milestone Achieved!',
|
||||
"Milestone '{$milestone['title']}' has been achieved for goal '{$milestone['goal_title']}'!"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get goal details
|
||||
*/
|
||||
public function getGoal($goal_id) {
|
||||
$sql = "SELECT * FROM donation_goals WHERE goal_id = ?";
|
||||
return $this->db->getRow($sql, [$goal_id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all goals for a streamer
|
||||
*/
|
||||
public function getStreamerGoals($streamer_id) {
|
||||
$sql = "SELECT * FROM donation_goals
|
||||
WHERE streamer_id = ?
|
||||
ORDER BY created_at DESC";
|
||||
return $this->db->getRows($sql, [$streamer_id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get milestones for a goal
|
||||
*/
|
||||
public function getGoalMilestones($goal_id) {
|
||||
$sql = "SELECT * FROM donation_milestones
|
||||
WHERE goal_id = ?
|
||||
ORDER BY target_amount ASC";
|
||||
return $this->db->getRows($sql, [$goal_id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active goals for a streamer
|
||||
*/
|
||||
public function getActiveGoals($streamer_id) {
|
||||
$sql = "SELECT * FROM donation_goals
|
||||
WHERE streamer_id = ?
|
||||
AND status = 'active'
|
||||
ORDER BY created_at DESC";
|
||||
return $this->db->getRows($sql, [$streamer_id]);
|
||||
}
|
||||
}
|
||||
110
f_modules/m_frontend/m_donations/src/NotificationHandler.php
Normal file
110
f_modules/m_frontend/m_donations/src/NotificationHandler.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
namespace Donations;
|
||||
|
||||
class NotificationHandler {
|
||||
private $db;
|
||||
|
||||
public function __construct() {
|
||||
$this->db = db();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new notification
|
||||
*/
|
||||
public function createNotification($streamer_id, $type, $title, $message, $donor_id = null) {
|
||||
$sql = "INSERT INTO donation_notifications
|
||||
(streamer_id, donor_id, type, title, message)
|
||||
VALUES (?, ?, ?, ?, ?)";
|
||||
|
||||
return $this->db->insert($sql, [
|
||||
$streamer_id,
|
||||
$donor_id,
|
||||
$type,
|
||||
$title,
|
||||
$message
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unread notifications for a streamer
|
||||
*/
|
||||
public function getUnreadNotifications($streamer_id, $limit = 10) {
|
||||
$sql = "SELECT * FROM donation_notifications
|
||||
WHERE streamer_id = ?
|
||||
AND is_read = 0
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?";
|
||||
|
||||
return $this->db->getRows($sql, [$streamer_id, $limit]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all notifications for a streamer
|
||||
*/
|
||||
public function getAllNotifications($streamer_id, $limit = 20) {
|
||||
$sql = "SELECT * FROM donation_notifications
|
||||
WHERE streamer_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?";
|
||||
|
||||
return $this->db->getRows($sql, [$streamer_id, $limit]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark notifications as read
|
||||
*/
|
||||
public function markAsRead($notification_ids) {
|
||||
if (empty($notification_ids)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$placeholders = str_repeat('?,', count($notification_ids) - 1) . '?';
|
||||
$sql = "UPDATE donation_notifications
|
||||
SET is_read = 1
|
||||
WHERE notification_id IN ($placeholders)";
|
||||
|
||||
return $this->db->query($sql, $notification_ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get notification count
|
||||
*/
|
||||
public function getUnreadCount($streamer_id) {
|
||||
$sql = "SELECT COUNT(*) as count
|
||||
FROM donation_notifications
|
||||
WHERE streamer_id = ?
|
||||
AND is_read = 0";
|
||||
|
||||
$result = $this->db->getRow($sql, [$streamer_id]);
|
||||
return $result['count'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete old notifications
|
||||
*/
|
||||
public function cleanupOldNotifications($days = 30) {
|
||||
$sql = "DELETE FROM donation_notifications
|
||||
WHERE created_at < DATE_SUB(NOW(), INTERVAL ? DAY)";
|
||||
|
||||
return $this->db->query($sql, [$days]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get notification by ID
|
||||
*/
|
||||
public function getNotification($notification_id) {
|
||||
$sql = "SELECT * FROM donation_notifications WHERE notification_id = ?";
|
||||
return $this->db->getRow($sql, [$notification_id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all notifications as read
|
||||
*/
|
||||
public function markAllAsRead($streamer_id) {
|
||||
$sql = "UPDATE donation_notifications
|
||||
SET is_read = 1
|
||||
WHERE streamer_id = ?";
|
||||
|
||||
return $this->db->query($sql, [$streamer_id]);
|
||||
}
|
||||
}
|
||||
120
f_modules/m_frontend/m_donations/src/WebhookHandler.php
Normal file
120
f_modules/m_frontend/m_donations/src/WebhookHandler.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
namespace Donations;
|
||||
|
||||
class WebhookHandler {
|
||||
private $config;
|
||||
|
||||
public function __construct() {
|
||||
global $square_config;
|
||||
$this->config = $square_config;
|
||||
}
|
||||
|
||||
public function handle() {
|
||||
$payload = file_get_contents('php://input');
|
||||
|
||||
if (!$this->verifySignature($payload)) {
|
||||
http_response_code(401);
|
||||
die('Invalid signature');
|
||||
}
|
||||
|
||||
$event = json_decode($payload, true);
|
||||
|
||||
switch ($event['type']) {
|
||||
case 'payment.created':
|
||||
$this->handlePaymentCreated($event['data']['object']);
|
||||
break;
|
||||
case 'payment.updated':
|
||||
$this->handlePaymentUpdated($event['data']['object']);
|
||||
break;
|
||||
case 'payout.created':
|
||||
$this->handlePayoutCreated($event['data']['object']);
|
||||
break;
|
||||
case 'payout.updated':
|
||||
$this->handlePayoutUpdated($event['data']['object']);
|
||||
break;
|
||||
}
|
||||
|
||||
http_response_code(200);
|
||||
echo 'OK';
|
||||
}
|
||||
|
||||
private function verifySignature($payload) {
|
||||
$signature = $_SERVER['HTTP_X-SQUARE-SIGNATURE'] ?? '';
|
||||
if (!$signature) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$computed_signature = hash_hmac('sha256', $payload, $this->config['square']['webhook_secret']);
|
||||
return hash_equals($computed_signature, $signature);
|
||||
}
|
||||
|
||||
private function handlePaymentCreated($payment) {
|
||||
$metadata = $payment['metadata'] ?? [];
|
||||
$streamer_id = $metadata['streamer_id'] ?? null;
|
||||
|
||||
if (!$streamer_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sql = "UPDATE donations SET
|
||||
status = 'completed',
|
||||
updated_at = NOW()
|
||||
WHERE payment_id = ? AND streamer_id = ?";
|
||||
|
||||
db()->executeQuery($sql, [$payment['id'], $streamer_id]);
|
||||
|
||||
$this->logWebhookEvent('payment.created', $payment['id'], $streamer_id);
|
||||
}
|
||||
|
||||
private function handlePaymentUpdated($payment) {
|
||||
$metadata = $payment['metadata'] ?? [];
|
||||
$streamer_id = $metadata['streamer_id'] ?? null;
|
||||
|
||||
if (!$streamer_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$status = $payment['status'] === 'COMPLETED' ? 'completed' : 'failed';
|
||||
|
||||
$sql = "UPDATE donations SET
|
||||
status = ?,
|
||||
updated_at = NOW()
|
||||
WHERE payment_id = ? AND streamer_id = ?";
|
||||
|
||||
db()->executeQuery($sql, [$status, $payment['id'], $streamer_id]);
|
||||
|
||||
$this->logWebhookEvent('payment.updated', $payment['id'], $streamer_id);
|
||||
}
|
||||
|
||||
private function handlePayoutCreated($payout) {
|
||||
$sql = "UPDATE payouts SET
|
||||
status = 'processing',
|
||||
updated_at = NOW()
|
||||
WHERE payout_id = ?";
|
||||
|
||||
db()->executeQuery($sql, [$payout['id']]);
|
||||
|
||||
$this->logWebhookEvent('payout.created', $payout['id']);
|
||||
}
|
||||
|
||||
private function handlePayoutUpdated($payout) {
|
||||
$status = $payout['status'] === 'COMPLETED' ? 'completed' : 'failed';
|
||||
|
||||
$sql = "UPDATE payouts SET
|
||||
status = ?,
|
||||
updated_at = NOW()
|
||||
WHERE payout_id = ?";
|
||||
|
||||
db()->executeQuery($sql, [$status, $payout['id']]);
|
||||
|
||||
$this->logWebhookEvent('payout.updated', $payout['id']);
|
||||
}
|
||||
|
||||
private function logWebhookEvent($event_type, $resource_id, $streamer_id = null) {
|
||||
$sql = "INSERT INTO webhook_logs (
|
||||
event_type, resource_id, streamer_id, created_at
|
||||
) VALUES (?, ?, ?, NOW())";
|
||||
|
||||
db()->executeQuery($sql, [$event_type, $resource_id, $streamer_id]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user