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:
SamiAhmed7777
2025-10-21 00:39:45 -07:00
commit 0b7e2d0a5b
6080 changed files with 1332936 additions and 0 deletions

View 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]);
}
}

View 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);
}
}

View 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');
}
}

View 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));
}
}
}
}

View 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);
}
}

View 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() {}
}

View 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
}
}

View 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);
}
}

View 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']);
}
}

View 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);
}
}
}

View 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();
}
}

View 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();
}
}

View 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() {}
}

View 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");
}
}

View 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' : '';
}
}

View 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();
}
}

View 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' : '';
}
}

View 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]);
}
}

View 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]);
}
}

View 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]);
}
}

View 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]);
}
}