📊 1 مشاهدة
نموذج متكامل: WordPress/WooCommerce/AI — إضافة تسجيل ملفات إنتاجية
سأبني إضافة WordPress كاملة (Plugin) تحوي كل الطبقات التي طلبها: Multi-tenancy، OpenTelemetry، Admin REST API، وHelm chart. البنية مقسّمة على ملفات PHP بشكل صارم (PSR-4-ish داخل الإضافة).
هيكل الإضافة
text
ai-file-logger/ ├── ai-file-logger.php # Bootstrap ├── composer.json ├── src/ │ ├── Plugin.php │ ├── Support/ │ │ ├── Container.php │ │ ├── Config.php │ │ └── Logger.php │ ├── Security/ │ │ ├── PathGuard.php │ │ ├── SizeGuard.php │ │ ├── ContentInspector.php │ │ ├── MalwareScanner.php │ │ └── Redactor.php │ ├── Tenancy/ │ │ ├── TenantResolver.php │ │ ├── TenantContext.php │ │ └── TenantStorage.php │ ├── Signing/ │ │ ├── SignerInterface.php │ │ ├── Ed25519Signer.php │ │ ├── HmacSigner.php │ │ └── VerifyChain.php │ ├── Telemetry/ │ │ ├── Tracer.php │ │ ├── Span.php │ │ └── Exporter.php │ ├── Storage/ │ │ ├── Writer.php │ │ ├── Verifier.php │ │ └── Rotator.php │ ├── Retention/ │ │ └── RetentionManager.php │ ├── Cdc/ │ │ ├── Pipeline.php │ │ └── Sinks/ │ │ ├── ElasticsearchSink.php │ │ └── ClickHouseSink.php │ ├── Rest/ │ │ ├── AdminController.php │ │ └── Auth.php │ ├── Cli/ │ │ ├── SweepCommand.php │ │ └── VerifyCommand.php │ └── Integrations/ │ ├── WooCommerceHooks.php │ ├── AiEvents.php │ └── WordPressAudit.php ├── assets/ ├── helm/ │ ├── Chart.yaml │ ├── values.yaml │ └── templates/ │ ├── deployment.yaml │ ├── service.yaml │ ├── hpa.yaml │ ├── pdb.yaml │ ├── networkpolicy.yaml │ ├── secret.yaml │ ├── configmap.yaml │ ├── serviceaccount.yaml │ └── ingress.yaml └── tests/
1. ملف Bootstrap
php
<?php
/**
* Plugin Name: AI File Logger
* Description: تسجيل ملفات إنتاجي آمن: Multi-tenancy، توقيع، OpenTelemetry، REST API.
* Version: 1.0.0
* Requires PHP: 8.1
* Author: Your Co
* License: GPL-2.0-or-later
* Text Domain: ai-file-logger
*/
declare(strict_types=1);
if (!defined('ABSPATH')) exit;
define('AIFL_VERSION', '1.0.0');
define('AIFL_FILE', __FILE__);
define('AIFL_DIR', __DIR__);
require_once AIFL_DIR . '/vendor/autoload.php';
register_activation_hook(__FILE__, [\AIFL\Plugin::class, 'activate']);
register_deactivation_hook(__FILE__, [\AIFL\Plugin::class, 'deactivate']);
add_action('plugins_loaded', static function () {
\AIFL\Plugin::instance()->boot();
});
composer.json
json
{
"name": "yourco/ai-file-logger",
"type": "wordpress-plugin",
"require": {
"php": ">=8.1",
"ext-json": "*",
"ext-openssl": "*",
"ext-sodium": "*",
"guzzlehttp/guzzle": "^7.8",
"psr/log": "^3.0"
},
"require-dev": {
"phpunit/phpunit": "^10.0",
"squizlabs/php_codesniffer": "^3.9",
"phpstan/phpstan": "^1.11"
},
"autoload": {
"psr-4": { "AIFL\\": "src/" }
}
}
2. Plugin.php — الواجهة الرئيسية
php
<?php
declare(strict_types=1);
namespace AIFL;
use AIFL\Support\{Container, Config, Logger};
use AIFL\Tenancy\{TenantResolver, TenantContext, TenantStorage};
use AIFL\Security\{PathGuard, SizeGuard, ContentInspector, MalwareScanner, Redactor};
use AIFL\Signing\{Ed25519Signer, HmacSigner};
use AIFL\Telemetry\Tracer;
use AIFL\Storage\{Writer, Verifier, Rotator};
use AIFL\Retention\RetentionManager;
use AIFL\Cdc\Pipeline;
use AIFL\Rest\AdminController;
use AIFL\Cli\{SweepCommand, VerifyCommand};
use AIFL\Integrations\{WooCommerceHooks, AiEvents, WordPressAudit};
final class Plugin
{
private static ?self $instance = null;
private Container $c;
public static function instance(): self
{
return self::$instance ??= new self();
}
public function boot(): void
{
$this->c = new Container();
$this->registerServices();
// REST API
add_action('rest_api_init', [$this->c->get(AdminController::class), 'registerRoutes']);
// WP-CLI
if (defined('WP_CLI') && WP_CLI) {
\WP_CLI::add_command('aifl sweep', $this->c->get(SweepCommand::class));
\WP_CLI::add_command('aifl verify', $this->c->get(VerifyCommand::class));
}
// Integrations
if (class_exists('WooCommerce')) {
$this->c->get(WooCommerceHooks::class)->register();
}
$this->c->get(AiEvents::class)->register();
$this->c->get(WordPressAudit::class)->register();
do_action('aifl_booted', $this->c);
}
public static function activate(): void
{
$root = self::storageRoot();
if (!is_dir($root)) {
wp_mkdir_p($root);
@chmod($root, 0700);
}
// حماية المسار عبر .htaccess
$htaccess = $root . '/.htaccess';
if (!file_exists($htaccess)) {
file_put_contents($htaccess, "Require all denied\nDeny from all\n");
}
// جدول فهرس (اختياري)
global $wpdb;
$table = $wpdb->prefix . 'aifl_index';
$charset = $wpdb->get_charset_collate();
$sql = "CREATE TABLE IF NOT EXISTS $table (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id VARCHAR(64) NOT NULL,
rel_path VARCHAR(512) NOT NULL,
sha256 CHAR(64) NOT NULL,
bytes BIGINT UNSIGNED NOT NULL,
mime VARCHAR(128) NOT NULL,
tag VARCHAR(32) NOT NULL DEFAULT 'default',
signed TINYINT(1) NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
INDEX tenant_created (tenant_id, created_at),
INDEX sha (sha256)
) $charset;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta($sql);
}
public static function deactivate(): void
{
// لا نحذف البيانات تلقائياً
}
public static function storageRoot(): string
{
return defined('AIFL_STORAGE_ROOT')
? AIFL_STORAGE_ROOT
: WP_CONTENT_DIR . '/aifl-storage';
}
private function registerServices(): void
{
$c = $this->c;
// Support
$c->singleton(Config::class, fn() => new Config());
$c->singleton(Logger::class, fn($c) => new Logger($c->get(Config::class)->get('log_level', 'info')));
// Telemetry
$c->singleton(Tracer::class, fn($c) => new Tracer(
$c->get(Config::class)->get('otel.endpoint'),
$c->get(Config::class)->get('otel.service_name', 'ai-file-logger')
));
// Tenancy
$c->singleton(TenantResolver::class, fn($c) => new TenantResolver(
$c->get(Config::class)->get('tenancy.mode', 'blog_id')
));
$c->singleton(TenantStorage::class, fn($c) => new TenantStorage(
self::storageRoot(),
$c->get(TenantResolver::class)
));
$c->scoped(TenantContext::class, fn($c) => new TenantContext(
$c->get(TenantResolver::class),
$c->get(TenantStorage::class)
));
// Security
$c->singleton(PathGuard::class, fn() => new PathGuard());
$c->singleton(SizeGuard::class, fn($c) => new SizeGuard(
$c->get(Config::class)->get('limits.entry_bytes', 1_048_576),
$c->get(Config::class)->get('limits.file_bytes', 52_428_800),
$c->get(Config::class)->get('limits.total_bytes', 2_147_483_648)
));
$c->singleton(ContentInspector::class, fn() => new ContentInspector());
$c->singleton(MalwareScanner::class, fn($c) => new MalwareScanner(
$c->get(Config::class)->get('malware.enabled', false),
$c->get(Config::class)->get('malware.mode', 'tcp'),
$c->get(Config::class)->get('malware.host', '127.0.0.1'),
(int)$c->get(Config::class)->get('malware.port', 3310),
$c->get(Config::class)->get('malware.socket_path'),
(int)$c->get(Config::class)->get('malware.timeout_ms', 15000)
));
$c->singleton(Redactor::class, fn() => new Redactor(true));
// Signing (per-tenant — نحلّها داخل TenantContext)
$c->scoped(Ed25519Signer::class, fn($c) => new Ed25519Signer(
$c->get(TenantContext::class)->signingKey()
));
$c->scoped(HmacSigner::class, fn($c) => new HmacSigner(
$c->get(TenantContext::class)->hmacKey()
));
// Storage
$c->scoped(Writer::class, fn($c) => new Writer(
$c->get(TenantContext::class)->storagePath(),
$c->get(PathGuard::class),
$c->get(SizeGuard::class),
$c->get(ContentInspector::class),
$c->get(MalwareScanner::class),
$c->get(Redactor::class),
$c->get(Ed25519Signer::class),
$c->get(Logger::class),
$c->get(Tracer::class)
));
$c->scoped(Verifier::class, fn($c) => new Verifier(
$c->get(TenantContext::class)->storagePath(),
$c->get(Ed25519Signer::class)
));
$c->scoped(Rotator::class, fn($c) => new Rotator(
$c->get(TenantContext::class)->storagePath()
));
$c->scoped(RetentionManager::class, fn($c) => new RetentionManager(
$c->get(TenantContext::class)->storagePath()
));
// CDC
$c->singleton(Pipeline::class, fn($c) => new Pipeline(
$c->get(Config::class)->get('cdc.sinks', []),
$c->get(Logger::class)
));
// REST + CLI + Integrations
$c->singleton(AdminController::class, fn($c) => new AdminController(
$c, $c->get(TenantResolver::class), $c->get(Logger::class)
));
$c->singleton(SweepCommand::class, fn($c) => new SweepCommand($c));
$c->singleton(VerifyCommand::class, fn($c) => new VerifyCommand($c));
$c->singleton(WooCommerceHooks::class, fn($c) => new WooCommerceHooks($c));
$c->singleton(AiEvents::class, fn($c) => new AiEvents($c));
$c->singleton(WordPressAudit::class, fn($c) => new WordPressAudit($c));
}
}
3. Multi-Tenancy — عزل كامل
TenantResolver.php
php
<?php
declare(strict_types=1);
namespace AIFL\Tenancy;
final class TenantResolver
{
public function __construct(private string $mode = 'blog_id') {}
/**
* يعيد معرّفاً آمناً للمستأجر (لا يحتوي محارف خطرة).
*/
public function currentId(): string
{
$raw = match ($this->mode) {
'blog_id' => (string)get_current_blog_id(),
'user_id' => (string)get_current_user_id(),
'header' => $this->fromHeader(),
'constant' => defined('AIFL_TENANT_ID') ? (string)AIFL_TENANT_ID : 'default',
default => 'default',
};
return $this->sanitize($raw);
}
private function fromHeader(): string
{
$h = $_SERVER['HTTP_X_TENANT_ID'] ?? '';
if (!is_string($h) || $h === '') return 'default';
// تحقق: فقط [a-zA-Z0-9_-]{1,64}
if (!preg_match('/^[a-zA-Z0-9_-]{1,64}$/', $h)) {
throw new \RuntimeException('AIFL_TENANT_INVALID');
}
return $h;
}
private function sanitize(string $raw): string
{
$clean = preg_replace('/[^a-zA-Z0-9_-]/', '', $raw) ?? '';
if ($clean === '') $clean = 'default';
return substr($clean, 0, 64);
}
}
TenantStorage.php
php
<?php
declare(strict_types=1);
namespace AIFL\Tenancy;
final class TenantStorage
{
public function __construct(
private string $root,
private TenantResolver $resolver
) {
$this->root = rtrim($root, '/');
}
/**
* مسار معزول لكل مستأجر، مع فحص يمنع الهروب من الجذر.
*/
public function pathFor(string $tenantId, string $sub = ''): string
{
$tenantId = $this->assertSafe($tenantId);
$base = $this->root . '/tenants/' . $tenantId;
if ($sub === '') return $base;
// منع traversal
$sub = str_replace('\\', '/', $sub);
if (str_contains($sub, "\0") || preg_match('#(^|/)\.\.(/|$)#', $sub)) {
throw new \RuntimeException('AIFL_PATH_TRAVERSAL');
}
$target = $base . '/' . ltrim($sub, '/');
$real = $this->normalize($target);
$baseNorm = $this->normalize($base);
if (!str_starts_with($real, $baseNorm . '/') && $real !== $baseNorm) {
throw new \RuntimeException('AIFL_PATH_ESCAPE');
}
return $real;
}
public function signingKeyPath(string $tenantId, string $kind = 'ed25519'): string
{
return $this->pathFor($tenantId, '.keys/' . $kind . '.key');
}
public function hmacKeyPath(string $tenantId): string
{
return $this->pathFor($tenantId, '.keys/hmac.key');
}
public function ensureTenant(string $tenantId): void
{
$base = $this->pathFor($tenantId);
if (!is_dir($base)) {
wp_mkdir_p($base);
@chmod($base, 0700);
}
$keys = $this->pathFor($tenantId, '.keys');
if (!is_dir($keys)) {
wp_mkdir_p($keys);
@chmod($keys, 0700);
}
// منع الوصول عبر الويب
$ht = $base . '/.htaccess';
if (!file_exists($ht)) {
file_put_contents($ht, "Require all denied\nDeny from all\n");
}
}
private function assertSafe(string $id): string
{
if (!preg_match('/^[a-zA-Z0-9_-]{1,64}$/', $id)) {
throw new \RuntimeException('AIFL_TENANT_INVALID');
}
return $id;
}
private function normalize(string $p): string
{
// تبسيط المسار دون استخدام realpath (لأن الملف قد لا يوجد بعد)
$parts = [];
foreach (explode('/', $p) as $seg) {
if ($seg === '' || $seg === '.') continue;
if ($seg === '..') array_pop($parts);
else $parts[] = $seg;
}
return '/' . implode('/', $parts);
}
}
TenantContext.php
php
<?php
declare(strict_types=1);
namespace AIFL\Tenancy;
final class TenantContext
{
private ?string $tenantId = null;
private ?string $storagePath = null;
public function __construct(
private TenantResolver $resolver,
private TenantStorage $storage
) {}
public function id(): string
{
return $this->tenantId ??= $this->resolver->currentId();
}
public function storagePath(): string
{
if ($this->storagePath === null) {
$this->storage->ensureTenant($this->id());
$this->storagePath = $this->storage->pathFor($this->id());
}
return $this->storagePath;
}
/**
* يعيد مفتاح Ed25519 الخاص بالمستأجر (ينشئه إن لم يوجد).
* @return array{private:string, public:string}
*/
public function signingKey(): array
{
$path = $this->storage->signingKeyPath($this->id());
if (file_exists($path)) {
$data = file_get_contents($path);
if ($data === false) throw new \RuntimeException('AIFL_KEY_READ');
$json = json_decode($data, true);
if (!is_array($json) || !isset($json['private'], $json['public'])) {
throw new \RuntimeException('AIFL_KEY_CORRUPT');
}
return $json;
}
if (!function_exists('sodium_crypto_sign_keypair')) {
throw new \RuntimeException('AIFL_SODIUM_REQUIRED');
}
$kp = sodium_crypto_sign_keypair();
$sk = sodium_crypto_sign_secretkey($kp);
$pk = sodium_crypto_sign_publickey($kp);
$out = [
'private' => base64_encode($sk),
'public' => base64_encode($pk),
];
file_put_contents($path, json_encode($out, JSON_UNESCAPED_SLASHES), LOCK_EX);
@chmod($path, 0600);
return $out;
}
public function hmacKey(): string
{
$path = $this->storage->hmacKeyPath($this->id());
if (file_exists($path)) {
$k = file_get_contents($path);
if ($k === false || strlen($k) < 32) throw new \RuntimeException('AIFL_HMAC_KEY');
return $k;
}
$k = random_bytes(64);
file_put_contents($path, $k, LOCK_EX);
@chmod($path, 0600);
return $k;
}
/** للاختبارات */
public function overrideTenant(string $id): void
{
$this->tenantId = $id;
$this->storagePath = null;
}
}
نقطة العزل: كل مستأجر يحصل على مفتاح Ed25519 مستقل → توقيعات معزولة، ومسار مستقل، ومفتاح HMAC مستقل.
4. Security Layer
PathGuard.php
php
<?php
declare(strict_types=1);
namespace AIFL\Security;
final class PathGuard
{
public function resolve(string $root, string $relative): string
{
if ($relative === '' || str_contains($relative, "\0")) {
throw new \RuntimeException('AIFL_PATH_INVALID');
}
$root = rtrim(str_replace('\\', '/', $root), '/');
$rel = str_replace('\\', '/', $relative);
// ارفض المطلق
if (str_starts_with($rel, '/')) {
throw new \RuntimeException('AIFL_PATH_ABSOLUTE');
}
// رفض .. و.
foreach (explode('/', $rel) as $seg) {
if ($seg === '..' || $seg === '.') {
throw new \RuntimeException('AIFL_PATH_TRAVERSAL');
}
if ($seg !== '' && !preg_match('/^[A-Za-z0-9._\-]+$/u', $seg)) {
throw new \RuntimeException('AIFL_PATH_SEGMENT:' . $seg);
}
}
$full = $root . '/' . $rel;
// فحص symlink
$parent = dirname($full);
while (!is_dir($parent) && $parent !== '/' && $parent !== '') {
$parent = dirname($parent);
}
$realParent = realpath($parent);
$realRoot = realpath($root);
if ($realRoot === false || $realParent === false) {
throw new \RuntimeException('AIFL_PATH_UNRESOLVED');
}
if (!str_starts_with($realParent, $realRoot)) {
throw new \RuntimeException('AIFL_PATH_ESCAPE');
}
return $full;
}
}
SizeGuard.php
php
<?php
declare(strict_types=1);
namespace AIFL\Security;
final class SizeGuard
{
public function __construct(
private int $maxEntryBytes,
private int $maxFileBytes,
private int $maxTotalBytes
) {}
public function checkEntry(int $bytes): void
{
if ($bytes > $this->maxEntryBytes) {
throw new \RuntimeException("AIFL_ENTRY_TOO_LARGE:{$bytes}");
}
}
public function checkFile(string $path, int $incoming): bool
{
$current = is_file($path) ? (int)filesize($path) : 0;
if ($current + $incoming > $this->maxFileBytes) return true; // rotate
return false;
}
public function checkTotal(string $root): void
{
$total = 0;
$it = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($root, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($it as $f) {
if ($f->isFile()) $total += $f->getSize();
}
if ($total > $this->maxTotalBytes) {
throw new \RuntimeException("AIFL_STORAGE_FULL:{$total}");
}
}
}
ContentInspector.php
php
<?php
declare(strict_types=1);
namespace AIFL\Security;
final class ContentInspector
{
private const MAGIC = [
'image/png' => "\x89PNG",
'image/jpeg' => "\xFF\xD8\xFF",
'application/pdf' => '%PDF-',
'application/zip' => "PK\x03\x04",
'application/gzip' => "\x1F\x8B",
];
private const ANSI = "/\x1B\[[0-9;]*[A-Za-z]/";
private const CTRL = "/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/";
public function inspect(string $buffer, ?string $declaredMime = null, bool $allowBinary = false): array
{
$mime = $this->detectMime($buffer);
if ($declaredMime && $declaredMime !== $mime && $mime !== 'text/plain') {
throw new \RuntimeException("AIFL_MIME_MISMATCH:{$declaredMime}!={$mime}");
}
if ($mime === 'application/octet-stream' && !$allowBinary) {
throw new \RuntimeException('AIFL_BINARY_REJECTED');
}
if ($mime === 'text/plain') {
$buffer = $this->sanitize($buffer);
}
return ['buffer' => $buffer, 'mime' => $mime];
}
public function detectMime(string $buf): string
{
foreach (self::MAGIC as $mime => $sig) {
if (str_starts_with($buf, $sig)) return $mime;
}
// هل النص UTF-8 صالح؟
if (preg_match('//u', $buf) && mb_check_encoding($buf, 'UTF-8')) {
return 'text/plain';
}
return 'application/octet-stream';
}
public function sanitize(string $s): string
{
$s = preg_replace(self::ANSI, '', $s) ?? $s;
$s = preg_replace(self::CTRL, '', $s) ?? $s;
return str_replace(["\r\n", "\r"], "\n", $s);
}
}
MalwareScanner.php
php
<?php
declare(strict_types=1);
namespace AIFL\Security;
final class MalwareScanner
{
public function __construct(
private bool $enabled,
private string $mode,
private string $host,
private int $port,
private ?string $socketPath,
private int $timeoutMs
) {}
public function scan(string $buffer): array
{
if (!$this->enabled) return ['clean' => true, 'skipped' => true];
return $this->scanClamd($buffer);
}
private function scanClamd(string $buffer): array
{
$errno = 0; $errstr = '';
$addr = $this->mode === 'tcp'
? "tcp://{$this->host}:{$this->port}"
: "unix://{$this->socketPath}";
$sock = @stream_socket_client($addr, $errno, $errstr, $this->timeoutMs / 1000);
if (!$sock) throw new \RuntimeException("AIFL_SCAN_CONNECT:{$errstr}");
stream_set_timeout($sock, (int)ceil($this->timeoutMs / 1000));
fwrite($sock, "zINSTREAM\0");
$chunkSize = 64 * 1024;
$len = strlen($buffer);
for ($i = 0; $i < $len; $i += $chunkSize) {
$chunk = substr($buffer, $i, $chunkSize);
fwrite($sock, pack('N', strlen($chunk)) . $chunk);
}
fwrite($sock, pack('N', 0));
$resp = stream_get_contents($sock) ?: '';
fclose($sock);
$resp = trim($resp);
if (str_ends_with($resp, 'OK')) return ['clean' => true, 'engine' => 'clamav'];
if (str_contains($resp, 'FOUND')) return ['clean' => false, 'engine' => 'clamav', 'detail' => $resp];
throw new \RuntimeException("AIFL_SCAN_ERROR:{$resp}");
}
public function quarantine(string $buffer, string $reason, string $quarantineDir): string
{
if (!is_dir($quarantineDir)) {
wp_mkdir_p($quarantineDir);
@chmod($quarantineDir, 0700);
}
$name = gmdate('Ymd\THis\Z') . '-' . bin2hex(random_bytes(8)) . '.bin';
$path = $quarantineDir . '/' . $name;
file_put_contents($path, $buffer, LOCK_EX);
@chmod($path, 0600);
file_put_contents($path . '.meta.json', wp_json_encode([
'reason' => $reason,
'ts' => gmdate('c'),
'sha256' => hash('sha256', $buffer),
]), LOCK_EX);
return $path;
}
}
Redactor.php
php
<?php
declare(strict_types=1);
namespace AIFL\Security;
final class Redactor
{
/** @var array<int,array{name:string,re:string,replace:string}> */
private array $rules;
private array $stats = [];
public function __construct(bool $enabled = true, array $customRules = [])
{
$this->enabled = $enabled;
$this->rules = array_merge(self::defaultRules(), $customRules);
}
private bool $enabled;
public function redact(string $input): string
{
if (!$this->enabled) return $input;
$out = $input;
foreach ($this->rules as $r) {
$out = (string)preg_replace_callback($r['re'], function ($m) use ($r) {
if (($r['validate'] ?? null) === 'luhn' && !$this->luhn($m[0])) return $m[0];
$this->stats[$r['name']] = ($this->stats[$r['name']] ?? 0) + 1;
return $r['replace'];
}, $out);
}
return $out;
}
public function stats(): array { return $this->stats; }
private function luhn(string $num): bool
{
$digits = preg_replace('/\D/', '', $num);
$len = strlen($digits);
if ($len < 13 || $len > 19) return false;
$sum = 0; $alt = false;
for ($i = $len - 1; $i >= 0; $i--) {
$d = (int)$digits[$i];
if ($alt) { $d *= 2; if ($d > 9) $d -= 9; }
$sum += $d; $alt = !$alt;
}
return $sum % 10 === 0;
}
private static function defaultRules(): array
{
return [
['name' => 'url_password', 're' => '/([?&](?:password|passwd|pwd|token|api[_-]?key)=)[^&\s]+/i', 'replace' => '$1[REDACTED]'],
['name' => 'jwt', 're' => '/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/', 'replace' => '[JWT]'],
['name' => 'bearer', 're' => '/\bBearer\s+[A-Za-z0-9._\-+\/=]{20,}/i', 'replace' => 'Bearer [REDACTED]'],
['name' => 'aws_key', 're' => '/\bAKIA[0-9A-Z]{16}\b/', 'replace' => '[AWS_KEY]'],
['name' => 'gh_token', 're' => '/\bgh[pousr]_[A-Za-z0-9]{36,}\b/', 'replace' => '[GH_TOKEN]'],
['name' => 'slack', 're' => '/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/', 'replace' => '[SLACK]'],
['name' => 'stripe', 're' => '/\b(?:sk|pk|rk)_(?:live|test)_[A-Za-z0-9]{20,}\b/', 'replace' => '[STRIPE]'],
['name' => 'cc', 're' => '/\b(?:\d[ -]*?){13,19}\b/', 'replace' => '[CARD]', 'validate' => 'luhn'],
['name' => 'pem', 're' => '/-----BEGIN [A-Z ]+PRIVATE KEY-----[\s\S]+?-----END [A-Z ]+PRIVATE KEY-----/', 'replace' => '[PRIVATE_KEY]'],
['name' => 'json_password', 're' => '/("(?:password|passwd|pwd|secret|token)"\s*:\s*")[^"]*(")/i', 'replace' => '$1[REDACTED]$2'],
['name' => 'saudi_id', 're' => '/\b[12]\d{9}\b/', 'replace' => '[SAUDI_ID]'],
['name' => 'email', 're' => '/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/', 'replace' => '[EMAIL]'],
['name' => 'ipv4', 're' => '/\b(?:\d{1,3}\.){3}\d{1,3}\b/', 'replace' => '[IP]'],
['name' => 'phone', 're' => '/\+\d{1,3}[\s-]?\d{6,14}\b/', 'replace' => '[PHONE]'],
];
}
}
5. Signing (per-tenant Ed25519 / HMAC)
Ed25519Signer.php
php
<?php
declare(strict_types=1);
namespace AIFL\Signing;
final class Ed25519Signer implements SignerInterface
{
private string $secretKey;
private string $publicKey;
/** @param array{private:string,public:string} $keypair */
public function __construct(array $keypair)
{
$sk = base64_decode($keypair['private'], true);
$pk = base64_decode($keypair['public'], true);
if ($sk === false || $pk === false || strlen($sk) !== SODIUM_CRYPTO_SIGN_SECRETKEYBYTES) {
throw new \RuntimeException('AIFL_ED25519_KEY_INVALID');
}
$this->secretKey = $sk;
$this->publicKey = $pk;
}
/** يعيد التوقيع base64 على (prevSig || payload) */
public function sign(?string $prevSignature, string $payload): string
{
$prefix = $prevSignature !== null ? $prevSignature : '';
$sig = sodium_crypto_sign_detached($prefix . $payload, $this->secretKey);
return base64_encode($sig);
}
public function verify(?string $prevSignature, string $payload, string $signature): bool
{
$sig = base64_decode($signature, true);
if ($sig === false) return false;
$prefix = $prevSignature !== null ? $prevSignature : '';
return sodium_crypto_sign_verify_detached($sig, $prefix . $payload, $this->publicKey);
}
public function publicKey(): string { return base64_encode($this->publicKey); }
}
HmacSigner.php
php
<?php
declare(strict_types=1);
namespace AIFL\Signing;
final class HmacSigner implements SignerInterface
{
public function __construct(private string $key)
{
if (strlen($key) < 32) throw new \RuntimeException('AIFL_HMAC_KEY_SHORT');
}
public function sign(?string $prevSignature, string $payload): string
{
$prefix = $prevSignature !== null ? base64_decode($prevSignature, true) : '';
return base64_encode(hash_hmac('sha256', ($prefix ?: '') . $payload, $this->key, true));
}
public function verify(?string $prevSignature, string $payload, string $signature): bool
{
return hash_equals($this->sign($prevSignature, $payload), $signature);
}
public function publicKey(): string { return ''; } // HMAC لا يملك مفتاحاً عاماً
}
VerifyChain.php
php
<?php
declare(strict_types=1);
namespace AIFL\Signing;
final class VerifyChain
{
public function __construct(private SignerInterface $signer) {}
/**
* ملفّ بنيته:
* JSON payload
* #sig:<base64>
* #prev:<base64|0>
* يعيد ['valid'=>bool,'entries'=>int,'firstBrokenAt'=>?int,'reason'=>?string]
*/
public function verifyFile(string $path): array
{
if (!is_readable($path)) {
return ['valid' => false, 'entries' => 0, 'firstBrokenAt' => null, 'reason' => 'UNREADABLE'];
}
$fh = fopen($path, 'rb');
$prev = null; $entries = 0; $lineNo = 0;
while (($line = fgets($fh)) !== false) {
$lineNo++;
$line = rtrim($line, "\r\n");
if ($line === '' || str_starts_with($line, '#sig:') || str_starts_with($line, '#prev:')) continue;
$sigLine = fgets($fh);
if ($sigLine === false || !str_starts_with($sigLine, '#sig:')) {
fclose($fh);
return ['valid' => false, 'entries' => $entries, 'firstBrokenAt' => $lineNo, 'reason' => 'MISSING_SIG'];
}
$sig = trim(substr($sigLine, 5));
// سطر prev اختياري
$pos = ftell($fh);
$prevLine = fgets($fh);
if ($prevLine !== false && str_starts_with($prevLine, '#prev:')) {
// تجاهل القيمة، فنحن نبني السلسلة داخلياً
} else {
fseek($fh, $pos);
}
if (!$this->signer->verify($prev, $line, $sig)) {
fclose($fh);
return ['valid' => false, 'entries' => $entries, 'firstBrokenAt' => $lineNo, 'reason' => 'SIG_INVALID'];
}
$prev = base64_decode($sig, true) ?: null;
$entries++;
}
fclose($fh);
return ['valid' => true, 'entries' => $entries, 'firstBrokenAt' => null, 'reason' => null];
}
}
6. OpenTelemetry (بدون SDK ثقيل — OTLP/HTTP مباشر)
Tracer.php
php
<?php
declare(strict_types=1);
namespace AIFL\Telemetry;
final class Tracer
{
private ?string $endpoint;
private string $serviceName;
private array $resource;
private ?string $traceId = null;
private ?string $spanId = null;
public function __construct(?string $endpoint, string $serviceName, array $resource = [])
{
$this->endpoint = $endpoint;
$this->serviceName = $serviceName;
$this->resource = $resource + [
'service.name' => $serviceName,
'service.version' => defined('AIFL_VERSION') ? AIFL_VERSION : '0.0.0',
'deployment.environment' => defined('WP_ENV') ? WP_ENV : 'production',
];
}
public function startRootSpan(string $name, array $attributes = []): Span
{
$traceId = bin2hex(random_bytes(16));
$spanId = bin2hex(random_bytes(8));
$this->traceId = $traceId;
$this->spanId = $spanId;
return new Span($this, $traceId, $spanId, null, $name, $attributes);
}
public function currentTraceId(): ?string { return $this->traceId; }
public function currentSpanId(): ?string { return $this->spanId; }
public function export(Span $span): void
{
if (!$this->endpoint) return;
$payload = [
'resourceSpans' => [[
'resource' => [
'attributes' => $this->kvList($this->resource),
],
'scopeSpans' => [[
'scope' => ['name' => 'aifl', 'version' => '1.0.0'],
'spans' => [$span->toOtlp()],
]],
]],
];
// إرسال غير حاجب
$this->post($this->endpoint . '/v1/traces', $payload);
}
private function kvList(array $assoc): array
{
$out = [];
foreach ($assoc as $k => $v) {
$out[] = ['key' => (string)$k, 'value' => $this->anyValue($v)];
}
return $out;
}
private function anyValue(mixed $v): array
{
return match (true) {
is_bool($v) => ['boolValue' => $v],
is_int($v) => ['intValue' => (string)$v],
is_float($v) => ['doubleValue' => $v],
is_array($v) => ['arrayValue' => ['values' => array_map(fn($x) => $this->anyValue($x), $v)]],
default => ['stringValue' => (string)$v],
};
}
private function post(string $url, array $body): void
{
if (function_exists('wp_remote_post')) {
wp_remote_post($url, [
'timeout' => 2,
'blocking' => false,
'headers' => ['Content-Type' => 'application/json'],
'body' => wp_json_encode($body),
]);
return;
}
// fallback بدون WP
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_TIMEOUT => 2,
CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
curl_close($ch);
}
}
Span.php
php
<?php
declare(strict_types=1);
namespace AIFL\Telemetry;
final class Span
{
private int $startNs;
private ?int $endNs = null;
private array $events = [];
private array $attributes;
public function __construct(
private Tracer $tracer,
private string $traceId,
private string $spanId,
private ?string $parentSpanId,
private string $name,
array $attributes = []
) {
$this->startNs = (int)(microtime(true) * 1e9);
$this->attributes = $attributes;
}
public function setAttribute(string $k, mixed $v): self
{
$this->attributes[$k] = $v;
return $this;
}
public function addEvent(string $name, array $attrs = []): self
{
$this->events[] = [
'name' => $name,
'timeUnixNano' => (string)(int)(microtime(true) * 1e9),
'attributes' => $attrs,
];
return $this;
}
public function end(): void
{
if ($this->endNs !== null) return;
$this->endNs = (int)(microtime(true) * 1e9);
$this->tracer->export($this);
}
public function toOtlp(): array
{
return [
'traceId' => $this->traceId,
'spanId' => $this->spanId,
'parentSpanId' => $this->parentSpanId ?? '',
'name' => $this->name,
'kind' => 1, // INTERNAL
'startTimeUnixNano' => (string)$this->startNs,
'endTimeUnixNano' => (string)($this->endNs ?? $this->startNs),
'attributes' => array_map(
fn($k, $v) => ['key' => $k, 'value' => ['stringValue' => is_scalar($v) ? (string)$v : json_encode($v)]],
array_keys($this->attributes),
array_values($this->attributes)
),
'events' => array_map(fn($e) => [
'name' => $e['name'],
'timeUnixNano' => $e['timeUnixNano'],
'attributes' => array_map(
fn($k, $v) => ['key' => $k, 'value' => ['stringValue' => is_scalar($v) ? (string)$v : json_encode($v)]],
array_keys($e['attributes']),
array_values($e['attributes'])
),
], $this->events),
];
}
}
ملاحظة: هذا OTEL مبسّط عبر OTLP/HTTP — بلا SDK ثقيل. في الإنتاج يمكن استبداله بـ
open-telemetry/sdkإن أردت full spec.
7. Storage — Writer/Verifier/Rotator
Writer.php
php
<?php
declare(strict_types=1);
namespace AIFL\Storage;
use AIFL\Security\{PathGuard, SizeGuard, ContentInspector, MalwareScanner, Redactor};
use AIFL\Signing\SignerInterface;
use AIFL\Support\Logger;
use AIFL\Telemetry\Tracer;
final class Writer
{
public function __construct(
private string $root,
private PathGuard $pathGuard,
private SizeGuard $sizeGuard,
private ContentInspector $inspector,
private MalwareScanner $scanner,
private Redactor $redactor,
private SignerInterface $signer,
private Logger $logger,
private Tracer $tracer
) {}
/**
* @return array{filePath:string,bytes:int,sha256:string,mime:string,rotated:bool}
*/
public function write(string $relativePath, string $content, array $opts = []): array
{
$span = $this->tracer->startRootSpan('aifl.write', [
'path' => $relativePath,
'append' => (bool)($opts['append'] ?? false),
]);
try {
$filePath = $this->pathGuard->resolve($this->root, $relativePath);
$dir = dirname($filePath);
if (!is_dir($dir)) {
wp_mkdir_p($dir);
@chmod($dir, 0700);
}
// تنقية
$redacted = $this->redactor->redact($content);
$raw = $redacted;
// فحص محتوى
$inspected = $this->inspector->inspect($raw, $opts['mime'] ?? null, (bool)($opts['allow_binary'] ?? false));
$buffer = $inspected['buffer'];
$mime = $inspected['mime'];
// حدود حجم
$this->sizeGuard->checkEntry(strlen($buffer));
$shouldRotate = $this->sizeGuard->checkFile($filePath, strlen($buffer));
try { $this->sizeGuard->checkTotal($this->root); }
catch (\RuntimeException $e) {
if (!str_starts_with($e->getMessage(), 'AIFL_STORAGE_FULL')) throw $e;
$this->logger->warning('التخزين يقترب من الامتلاء', ['root' => $this->root]);
}
// فحص فيروسات
$scanT0 = microtime(true);
$scan = $this->scanner->scan($buffer);
$span->addEvent('scan.done', [
'clean' => $scan['clean'] ? '1' : '0',
'duration_ms' => (string)round((microtime(true) - $scanT0) * 1000, 2),
]);
if (empty($scan['clean'])) {
$q = $this->scanner->quarantine($buffer, $scan['detail'] ?? 'unknown', $this->root . '/.quarantine');
$span->setAttribute('quarantine', $q);
$this->logger->error('برمجية خبيثة', ['path' => $relativePath, 'quarantine' => $q]);
throw new \RuntimeException('AIFL_MALWARE_DETECTED');
}
// توقيع السلسلة
$prevSig = $this->readLastSignature($filePath);
$sha = hash('sha256', $buffer);
$envelope = $this->encodeEntry($opts['level'] ?? 'info', $buffer, $sha, $mime, $opts);
$sig = $this->signer->sign($prevSig, $envelope);
$record = $envelope . "\n#sig:{$sig}\n#prev:" . ($prevSig ?? '0') . "\n";
$rotated = false;
if ($shouldRotate && !($opts['no_rotate'] ?? false)) {
$this->rotate($filePath);
$rotated = true;
}
// كتابة ذرية/ملحقة
$mode = ($opts['append'] ?? false) ? 'ab' : 'wb';
$tmp = null;
if ($mode === 'wb') {
$tmp = $filePath . '.' . bin2hex(random_bytes(6)) . '.tmp';
$mode = 'xb';
$target = $tmp;
} else {
$target = $filePath;
}
$fh = fopen($target, $mode);
if (!$fh) throw new \RuntimeException('AIFL_WRITE_OPEN');
try {
fwrite($fh, $record);
fflush($fh);
if (function_exists('fsync')) @fsync($fh);
} finally { fclose($fh); }
@chmod($target, 0600);
if ($tmp !== null) {
if (!rename($tmp, $filePath)) {
@unlink($tmp);
throw new \RuntimeException('AIFL_WRITE_RENAME');
}
}
// تحقق
if ($tmp === null) {
clearstatcache(true, $filePath);
}
$size = filesize($filePath);
if ($size === false || $size < strlen($record)) {
throw new \RuntimeException('AIFL_VERIFY_SIZE');
}
// فهرس
$this->index($relativePath, $sha, $size, $mime, $opts['tag'] ?? 'default');
$span->setAttribute('bytes', strlen($record));
$span->setAttribute('sha256', $sha);
$span->end();
return [
'filePath' => $filePath,
'bytes' => strlen($record),
'sha256' => $sha,
'mime' => $mime,
'rotated' => $rotated,
];
} catch (\Throwable $e) {
$span->setAttribute('error', 'true');
$span->setAttribute('error.message', $e->getMessage());
$span->end();
throw $e;
}
}
private function encodeEntry(string $level, string $content, string $sha, string $mime, array $opts): string
{
$entry = [
'ts' => gmdate('c'),
'level' => $level,
'sha256' => $sha,
'size' => strlen($content),
'mime' => $mime,
'source' => $opts['source'] ?? 'unknown',
'tenant' => $opts['tenant'] ?? null,
'traceId' => $this->tracer->currentTraceId(),
'spanId' => $this->tracer->currentSpanId(),
'data' => $this->maybeJson($content),
];
return wp_json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}
private function maybeJson(string $content): mixed
{
$decoded = json_decode($content, true);
return json_last_error() === JSON_ERROR_NONE ? $decoded : $content;
}
private function readLastSignature(string $filePath): ?string
{
if (!is_file($filePath) || filesize($filePath) === 0) return null;
$fh = fopen($filePath, 'rb');
if (!$fh) return null;
$size = filesize($filePath);
$read = min($size, 8192);
fseek($fh, -$read, SEEK_END);
$tail = fread($fh, $read) ?: '';
fclose($fh);
if (preg_match_all('/#sig:([A-Za-z0-9+\/=]+)/', $tail, $m) && $m[1]) {
return end($m[1]);
}
return null;
}
private function rotate(string $filePath): void
{
if (!is_file($filePath)) return;
$ts = gmdate('Ymd\THis\Z');
$rotated = $filePath . '.' . $ts;
if (!rename($filePath, $rotated)) return;
// gzip
$data = file_get_contents($rotated);
if ($data !== false) {
file_put_contents($rotated . '.gz', gzencode($data, 6), LOCK_EX);
@chmod($rotated . '.gz', 0600);
@unlink($rotated);
}
// prune
$this->prune($filePath, 10);
}
private function prune(string $filePath, int $keep): void
{
$dir = dirname($filePath);
$base = basename($filePath);
$files = glob($dir . '/' . $base . '.*.gz') ?: [];
rsort($files);
foreach (array_slice($files, $keep) as $old) @unlink($old);
}
private function index(string $rel, string $sha, int $bytes, string $mime, string $tag): void
{
if (!function_exists('get_current_blog_id')) return;
global $wpdb;
$table = $wpdb->prefix . 'aifl_index';
$wpdb->insert($table, [
'tenant_id' => (string)get_current_blog_id(),
'rel_path' => $rel,
'sha256' => $sha,
'bytes' => $bytes,
'mime' => $mime,
'tag' => $tag,
'signed' => 1,
'created_at' => gmdate('Y-m-d H:i:s'),
], ['%s','%s','%s','%d','%s','%s','%d','%s']);
}
}
Verifier.php
php
<?php
declare(strict_types=1);
namespace AIFL\Storage;
use AIFL\Signing\{SignerInterface, VerifyChain};
final class Verifier
{
public function __construct(
private string $root,
private SignerInterface $signer
) {}
public function verify(string $relativePath): array
{
$path = $this->root . '/' . ltrim($relativePath, '/');
if (!is_file($path)) return ['valid' => false, 'reason' => 'NOT_FOUND'];
return (new VerifyChain($this->signer))->verifyFile($path);
}
}
Rotator.php
php
<?php
declare(strict_types=1);
namespace AIFL\Storage;
final class Rotator
{
public function __construct(private string $root, private int $keep = 10) {}
public function sweep(string $relativePath): array
{
$file = $this->root . '/' . ltrim($relativePath, '/');
$dir = dirname($file);
$base = basename($file);
$pruned = [];
$files = glob($dir . '/' . $base . '.*.gz') ?: [];
rsort($files);
foreach (array_slice($files, $this->keep) as $old) {
if (@unlink($old)) $pruned[] = basename($old);
}
return $pruned;
}
}
8. Retention — TTL Tags
RetentionManager.php
php
<?php
declare(strict_types=1);
namespace AIFL\Retention;
final class RetentionManager
{
public const DEFAULT_TAGS = [
'audit' => ['ttl' => 365 * 7, 'immutable' => true],
'financial' => ['ttl' => 365 * 5, 'immutable' => false],
'security' => ['ttl' => 365 * 2, 'immutable' => false],
'pii' => ['ttl' => 90, 'immutable' => false],
'debug' => ['ttl' => 7, 'immutable' => false],
'default' => ['ttl' => 30, 'immutable' => false],
];
/** @param array<string,array{ttl:int,immutable?:bool}> $tags */
public function __construct(
private string $root,
private array $tags = [],
private bool $dryRun = false
) {
$this->tags = $tags + self::DEFAULT_TAGS;
}
public function sweep(?int $now = null): array
{
$now ??= time();
$deleted = []; $skipped = []; $errors = [];
$ri = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($this->root, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($ri as $file) {
/** @var \SplFileInfo $file */
if (!$file->isFile()) continue;
$rel = ltrim(substr($file->getPathname(), strlen($this->root)), '/');
$tagName = $this->classify($rel);
$tag = $this->tags[$tagName] ?? $this->tags['default'];
if (!empty($tag['immutable'])) { $skipped[] = $rel; continue; }
$ageDays = (int)floor(($now - $file->getMTime()) / 86400);
if ($ageDays > $tag['ttl']) {
if (!$this->dryRun && !@unlink($file->getPathname())) {
$errors[] = ['path' => $rel, 'error' => 'UNLINK'];
} else {
$deleted[] = $rel;
}
}
}
return ['scanned' => iterator_count($ri), 'deleted' => $deleted, 'skippedImmutable' => $skipped, 'errors' => $errors];
}
private function classify(string $rel): string
{
$first = explode('/', $rel)[0];
return isset($this->tags[$first]) ? $first : 'default';
}
}
9. REST Admin API
AdminController.php
php
<?php
declare(strict_types=1);
namespace AIFL\Rest;
use AIFL\Support\{Container, Logger};
use AIFL\Tenancy\TenantResolver;
use AIFL\Storage\Verifier;
use AIFL\Retention\RetentionManager;
final class AdminController
{
public function __construct(
private Container $c,
private TenantResolver $resolver,
private Logger $logger
) {}
public function registerRoutes(): void
{
register_rest_route('aifl/v1', '/verify', [
'methods' => 'POST',
'permission_callback' => [$this, 'canManage'],
'args' => [
'path' => ['required' => true, 'type' => 'string'],
],
'callback' => [$this, 'verify'],
]);
register_rest_route('aifl/v1', '/retention/sweep', [
'methods' => 'POST',
'permission_callback' => [$this, 'canManage'],
'args' => [
'dry_run' => ['type' => 'boolean', 'default' => true],
],
'callback' => [$this, 'sweep'],
]);
register_rest_route('aifl/v1', '/tenants', [
'methods' => 'GET',
'permission_callback' => [$this, 'canManage'],
'callback' => [$this, 'listTenants'],
]);
register_rest_route('aifl/v1', '/logs', [
'methods' => 'GET',
'permission_callback' => [$this, 'canManage'],
'args' => [
'tenant' => ['type' => 'string'],
'from' => ['type' => 'string'],
'to' => ['type' => 'string'],
'limit' => ['type' => 'integer', 'default' => 100, 'maximum' => 1000],
'offset' => ['type' => 'integer', 'default' => 0],
],
'callback' => [$this, 'listLogs'],
]);
register_rest_route('aifl/v1', '/rotate', [
'methods' => 'POST',
'permission_callback' => [$this, 'canManage'],
'args' => [ 'path' => ['required' => true, 'type' => 'string'] ],
'callback' => [$this, 'rotate'],
]);
}
public function canManage(): bool
{
return current_user_can('manage_options');
}
public function verify(\WP_REST_Request $req): \WP_REST_Response
{
$path = (string)$req->get_param('path');
if (!preg_match('#^[A-Za-z0-9._/\-]+$#', $path) || str_contains($path, '..')) {
return new \WP_REST_Response(['error' => 'INVALID_PATH'], 400);
}
try {
$v = new Verifier(
$this->c->get(\AIFL\Tenancy\TenantContext::class)->storagePath(),
$this->c->get(\AIFL\Signing\Ed25519Signer::class)
);
return new \WP_REST_Response($v->verify($path), 200);
} catch (\Throwable $e) {
return new \WP_REST_Response(['error' => $e->getMessage()], 500);
}
}
public function sweep(\WP_REST_Request $req): \WP_REST_Response
{
$dry = (bool)$req->get_param('dry_run');
try {
$m = new RetentionManager(
$this->c->get(\AIFL\Tenancy\TenantContext::class)->storagePath(),
[],
$dry
);
return new \WP_REST_Response($m->sweep(), 200);
} catch (\Throwable $e) {
return new \WP_REST_Response(['error' => $e->getMessage()], 500);
}
}
public function listTenants(): \WP_REST_Response
{
$root = \AIFL\Plugin::storageRoot() . '/tenants';
if (!is_dir($root)) return new \WP_REST_Response([], 200);
$tenants = array_values(array_filter(scandir($root) ?: [], fn($n) => $n !== '.' && $n !== '..' && is_dir("$root/$n")));
return new \WP_REST_Response(array_map(fn($t) => ['tenant' => $t], $tenants), 200);
}
public function listLogs(\WP_REST_Request $req): \WP_REST_Response
{
global $wpdb;
$table = $wpdb->prefix . 'aifl_index';
$tenant = (string)($req->get_param('tenant') ?? get_current_blog_id());
$from = (string)($req->get_param('from') ?? '1970-01-01');
$to = (string)($req->get_param('to') ?? '9999-12-31');
$limit = (int)$req->get_param('limit');
$offset = (int)$req->get_param('offset');
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT rel_path, sha256, bytes, mime, tag, signed, created_at
FROM $table
WHERE tenant_id = %s AND created_at BETWEEN %s AND %s
ORDER BY created_at DESC LIMIT %d OFFSET %d",
$tenant, $from, $to, $limit, $offset
), ARRAY_A);
return new \WP_REST_Response(['tenant' => $tenant, 'count' => count($rows), 'rows' => $rows], 200);
}
public function rotate(\WP_REST_Request $req): \WP_REST_Response
{
$path = (string)$req->get_param('path');
if (!preg_match('#^[A-Za-z0-9._/\-]+$#', $path) || str_contains($path, '..')) {
return new \WP_REST_Response(['error' => 'INVALID_PATH'], 400);
}
try {
$r = new \AIFL\Storage\Rotator(
$this->c->get(\AIFL\Tenancy\TenantContext::class)->storagePath()
);
$pruned = $r->sweep($path);
return new \WP_REST_Response(['pruned' => $pruned], 200);
} catch (\Throwable $e) {
return new \WP_REST_Response(['error' => $e->getMessage()], 500);
}
}
}
تأمين REST
php
// داخل Plugin::boot() — لتقييد REST على HTTPS
add_filter('rest_authentication_errors', function ($result) {
if (!empty($result)) return $result;
if (!is_ssl() && !defined('AIFL_ALLOW_HTTP')) {
return new \WP_Error('aifl_https_required', 'HTTPS مطلوب', ['status' => 403]);
}
return $result;
});
10. Integrations (WordPress / WooCommerce / AI)
WooCommerceHooks.php
php
<?php
declare(strict_types=1);
namespace AIFL\Integrations;
use AIFL\Support\Container;
final class WooCommerceHooks
{
public function __construct(private Container $c) {}
public function register(): void
{
add_action('woocommerce_checkout_order_processed', [$this, 'orderCreated'], 10, 3);
add_action('woocommerce_order_status_changed', [$this, 'orderStatus'], 10, 4);
add_action('woocommerce_payment_complete', [$this, 'paymentComplete'], 10, 1);
add_action('woocommerce_order_refunded', [$this, 'refunded'], 10, 2);
}
public function orderCreated(int $orderId, array $postedData, $order): void
{
$this->log('financial', 'wc_order_created', [
'order_id' => $orderId,
'total' => (float)$order->get_total(),
'currency' => $order->get_currency(),
'items' => count($order->get_items()),
]);
}
public function orderStatus(int $orderId, string $from, string $to, $order): void
{
$this->log('audit', 'wc_order_status', [
'order_id' => $orderId,
'from' => $from,
'to' => $to,
]);
}
public function paymentComplete(int $orderId): void
{
$this->log('financial', 'wc_payment_complete', ['order_id' => $orderId]);
}
public function refunded(int $orderId, int $refundId): void
{
$this->log('financial', 'wc_refund', ['order_id' => $orderId, 'refund_id' => $refundId]);
}
private function log(string $tag, string $event, array $data): void
{
try {
$writer = $this->c->get(\AIFL\Storage\Writer::class);
$writer->write(
$tag . '/' . gmdate('Y-m-d') . '.log',
wp_json_encode(['event' => $event] + $data),
['append' => true, 'source' => 'woocommerce', 'tag' => $tag, 'level' => 'info']
);
} catch (\Throwable $e) {
$this->c->get(\AIFL\Support\Logger::class)->error('WooCommerce log failed', ['error' => $e->getMessage()]);
}
}
}
AiEvents.php
php
<?php
declare(strict_types=1);
namespace AIFL\Integrations;
use AIFL\Support\Container;
final class AiEvents
{
public function __construct(private Container $c) {}
public function register(): void
{
// تكامل عام مع أي مزود AI عبر هوك مخصص
add_action('aifl_ai_event', [$this, 'handle'], 10, 2);
// OpenAI plugin
add_action('open_ai_request_completed', [$this, 'openAiRequest'], 10, 2);
// Anthropic
add_action('anthropic_request_completed', [$this, 'anthropicRequest'], 10, 2);
}
public function handle(string $type, array $payload): void
{
$this->write('ai', ['type' => $type] + $payload);
}
public function openAiRequest(array $response, array $request): void
{
$this->write('ai', [
'provider' => 'openai',
'model' => $request['model'] ?? null,
'tokens_in' => $response['usage']['prompt_tokens'] ?? null,
'tokens_out' => $response['usage']['completion_tokens'] ?? null,
'status' => $response['status'] ?? null,
// لا نحتفظ بالمحتوى الخام — فقط ملخص
'prompt_preview' => mb_substr((string)($request['prompt'] ?? ''), 0, 200),
]);
}
public function anthropicRequest(array $response, array $request): void
{
$this->write('ai', [
'provider' => 'anthropic',
'model' => $request['model'] ?? null,
'tokens_in' => $response['usage']['input_tokens'] ?? null,
'tokens_out' => $response['usage']['output_tokens'] ?? null,
]);
}
private function write(string $tag, array $data): void
{
try {
$this->c->get(\AIFL\Storage\Writer::class)->write(
$tag . '/' . gmdate('Y-m-d') . '.log',
wp_json_encode($data),
['append' => true, 'source' => 'ai', 'tag' => 'pii', 'level' => 'info']
);
} catch (\Throwable $e) {
$this->c->get(\AIFL\Support\Logger::class)->error('AI log failed', ['error' => $e->getMessage()]);
}
}
}
WordPressAudit.php
php
<?php
declare(strict_types=1);
namespace AIFL\Integrations;
use AIFL\Support\Container;
final class WordPressAudit
{
public function __construct(private Container $c) {}
public function register(): void
{
add_action('wp_login', [$this, 'login'], 10, 2);
add_action('wp_login_failed', [$this, 'loginFailed'], 10, 1);
add_action('user_register', [$this, 'userRegister'], 10, 1);
add_action('profile_update', [$this, 'profileUpdate'], 10, 1);
add_action('delete_user', [$this, 'deleteUser'], 10, 1);
add_action('updated_option', [$this, 'optionUpdate'], 10, 3);
add_action('activated_plugin', [$this, 'pluginActivate'], 10, 2);
}
public function login(string $login, \WP_User $user): void
{
$this->audit('auth.login', ['user' => $login, 'id' => $user->ID]);
}
public function loginFailed(string $login): void
{
$this->audit('auth.login_failed', ['user' => $login]);
}
public function userRegister(int $userId): void
{
$this->audit('user.register', ['user_id' => $userId]);
}
public function profileUpdate(int $userId): void
{
$this->audit('user.update', ['user_id' => $userId]);
}
public function deleteUser(int $userId): void
{
$this->audit('user.delete', ['user_id' => $userId]);
}
public function optionUpdate(string $option, mixed $old, mixed $new): void
{
if (in_array($option, ['siteurl', 'home', 'admin_email', 'users_can_register'], true)) {
$this->audit('option.update', ['option' => $option, 'old' => is_scalar($old) ? $old : null, 'new' => is_scalar($new) ? $new : null]);
}
}
public function pluginActivate(string $plugin, bool $network): void
{
$this->audit('plugin.activate', ['plugin' => $plugin, 'network' => $network]);
}
private function audit(string $event, array $data): void
{
try {
$this->c->get(\AIFL\Storage\Writer::class)->write(
'audit/' . gmdate('Y-m-d') . '.log',
wp_json_encode(['event' => $event] + $data),
['append' => true, 'source' => 'wordpress', 'tag' => 'audit', 'level' => 'info']
);
} catch (\Throwable $e) {
$this->c->get(\AIFL\Support\Logger::class)->error('Audit failed', ['error' => $e->getMessage()]);
}
}
}
11. Container — بسيط وواضح
Container.php
php
<?php
declare(strict_types=1);
namespace AIFL\Support;
final class Container
{
/** @var array<string,callable> */
private array $factories = [];
/** @var array<string,mixed> */
private array $instances = [];
/** @var array<string,bool> */
private array $scoped = [];
public function singleton(string $id, callable $factory): void
{
$this->factories[$id] = $factory;
}
public function scoped(string $id, callable $factory): void
{
$this->factories[$id] = $factory;
$this->scoped[$id] = true;
}
public function get(string $id): mixed
{
if (isset($this->instances[$id]) && empty($this->scoped[$id])) {
return $this->instances[$id];
}
if (!isset($this->factories[$id])) {
throw new \RuntimeException("AIFL_CONTAINER_UNKNOWN:{$id}");
}
$obj = ($this->factories[$id])($this);
if (empty($this->scoped[$id])) {
$this->instances[$id] = $obj;
}
return $obj;
}
public function reset(): void
{
$this->instances = [];
}
}
Config.php
php
<?php
declare(strict_types=1);
namespace AIFL\Support;
final class Config
{
private array $data;
public function __construct()
{
$defaults = [
'log_level' => 'info',
'tenancy' => ['mode' => 'blog_id'],
'limits' => [
'entry_bytes' => 1_048_576,
'file_bytes' => 52_428_800,
'total_bytes' => 2_147_483_648,
],
'malware' => [
'enabled' => (bool)getenv('AIFL_MALWARE_ENABLED'),
'mode' => getenv('AIFL_MALWARE_MODE') ?: 'tcp',
'host' => getenv('AIFL_MALWARE_HOST') ?: '127.0.0.1',
'port' => (int)(getenv('AIFL_MALWARE_PORT') ?: 3310),
'socket_path' => getenv('AIFL_MALWARE_SOCKET') ?: null,
'timeout_ms' => 15_000,
],
'otel' => [
'endpoint' => getenv('OTEL_EXPORTER_OTLP_ENDPOINT') ?: null,
'service_name' => 'ai-file-logger',
],
'cdc' => ['sinks' => []],
];
$this->data = apply_filters('aifl_config', $defaults);
}
public function get(string $key, mixed $default = null): mixed
{
$parts = explode('.', $key);
$cur = $this->data;
foreach ($parts as $p) {
if (!is_array($cur) || !array_key_exists($p, $cur)) return $default;
$cur = $cur[$p];
}
return $cur;
}
}
Logger.php
php
<?php
declare(strict_types=1);
namespace AIFL\Support;
final class Logger
{
private const LEVELS = ['debug' => 10, 'info' => 20, 'warning' => 30, 'error' => 40];
public function __construct(private string $minLevel = 'info') {}
public function debug(string $msg, array $ctx = []): void { $this->log('debug', $msg, $ctx); }
public function info(string $msg, array $ctx = []): void { $this->log('info', $msg, $ctx); }
public function warning(string $msg, array $ctx = []): void { $this->log('warning', $msg, $ctx); }
public function error(string $msg, array $ctx = []): void { $this->log('error', $msg, $ctx); }
private function log(string $level, string $msg, array $ctx): void
{
if ((self::LEVELS[$level] ?? 0) < (self::LEVELS[$this->minLevel] ?? 20)) return;
if (function_exists('error_log')) {
error_log('[AIFL][' . $level . '] ' . $msg . ' ' . wp_json_encode($ctx));
}
}
}
12. Helm Chart — إنتاج Kubernetes
helm/Chart.yaml
yaml
apiVersion: v2 name: ai-file-logger description: WordPress AI File Logger with multi-tenancy, signing, and OpenTelemetry type: application version: 1.0.0 appVersion: "1.0.0"
helm/values.yaml
yaml
replicaCount: 3
image:
repository: ghcr.io/yourco/ai-file-logger
tag: "1.0.0"
pullPolicy: IfNotPresent
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
annotations: {}
name: ""
podAnnotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9464"
prometheus.io/path: "/metrics"
podSecurityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
service:
type: ClusterIP
port: 80
targetPort: 8080
resources:
requests: { cpu: 100m, memory: 256Mi }
limits: { cpu: 1000m, memory: 1Gi }
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 20
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 80
pdb:
enabled: true
minAvailable: 2
networkPolicy:
enabled: true
ingress:
- from:
- namespaceSelector:
matchLabels:
name: ingress-nginx
ports:
- protocol: TCP
port: 8080
egress:
- to:
- namespaceSelector:
matchLabels:
name: observability
ports:
- { protocol: TCP, port: 4318 }
- to:
- podSelector:
matchLabels:
app: clamav
ports:
- { protocol: TCP, port: 3310 }
persistence:
enabled: true
storageClass: ""
size: 50Gi
accessModes: [ReadWriteOnce]
env:
AIFL_MALWARE_ENABLED: "true"
AIFL_MALWARE_MODE: "tcp"
AIFL_MALWARE_HOST: "clamav.observability.svc.cluster.local"
AIFL_MALWARE_PORT: "3310"
OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector.observability.svc.cluster.local:4318"
OTEL_SERVICE_NAME: "ai-file-logger"
envFromSecrets:
- ai-file-logger-secrets
livenessProbe:
httpGet: { path: /healthz, port: http }
initialDelaySeconds: 20
periodSeconds: 15
readinessProbe:
httpGet: { path: /readyz, port: http }
initialDelaySeconds: 10
periodSeconds: 10
nodeSelector: {}
tolerations: []
affinity: {}
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app.kubernetes.io/name: ai-file-logger
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/proxy-body-size: "8m"
hosts:
- host: logger.example.com
paths:
- { path: /, pathType: Prefix }
tls:
- secretName: ai-file-logger-tls
hosts: [logger.example.com]
helm/templates/deployment.yaml
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "ai-file-logger.fullname" . }}
labels: {{- include "ai-file-logger.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels: {{- include "ai-file-logger.selectorLabels" . | nindent 6 }}
strategy:
type: RollingUpdate
rollingUpdate: { maxSurge: 1, maxUnavailable: 0 }
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels: {{- include "ai-file-logger.selectorLabels" . | nindent 8 }}
spec:
serviceAccountName: {{ include "ai-file-logger.serviceAccountName" . }}
securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: app
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
securityContext: {{- toYaml .Values.securityContext | nindent 12 }}
ports:
- { name: http, containerPort: 8080 }
- { name: metrics, containerPort: 9464 }
envFrom:
- configMapRef: { name: {{ include "ai-file-logger.fullname" . }}-config }
{{- range .Values.envFromSecrets }}
- secretRef: { name: {{ . }} }
{{- end }}
livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }}
readinessProbe: {{- toYaml .Values.readinessProbe | nindent 12 }}
resources: {{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- { name: storage, mountPath: /var/log/myapp }
- { name: tmp, mountPath: /tmp }
volumes:
- name: storage
{{- if .Values.persistence.enabled }}
persistentVolumeClaim: { claimName: {{ include "ai-file-logger.fullname" . }}-data }
{{- else }}
emptyDir: {}
{{- end }}
- { name: tmp, emptyDir: {} }
{{- with .Values.nodeSelector }}
nodeSelector: {{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations: {{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity: {{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.topologySpreadConstraints }}
topologySpreadConstraints: {{- toYaml . | nindent 8 }}
{{- end }}
helm/templates/hpa.yaml
yaml
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "ai-file-logger.fullname" . }}
labels: {{- include "ai-file-logger.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "ai-file-logger.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
- type: Resource
resource:
name: cpu
target: { type: Utilization, averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} }
- type: Resource
resource:
name: memory
target: { type: Utilization, averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} }
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies: [{ type: Percent, value: 50, periodSeconds: 60 }]
scaleUp:
stabilizationWindowSeconds: 30
policies: [{ type: Percent, value: 100, periodSeconds: 30 }]
{{- end }}
helm/templates/pdb.yaml
yaml
{{- if .Values.pdb.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "ai-file-logger.fullname" . }}
labels: {{- include "ai-file-logger.labels" . | nindent 4 }}
spec:
minAvailable: {{ .Values.pdb.minAvailable }}
selector:
matchLabels: {{- include "ai-file-logger.selectorLabels" . | nindent 6 }}
{{- end }}
helm/templates/networkpolicy.yaml
yaml
{{- if .Values.networkPolicy.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ include "ai-file-logger.fullname" . }}
labels: {{- include "ai-file-logger.labels" . | nindent 4 }}
spec:
podSelector:
matchLabels: {{- include "ai-file-logger.selectorLabels" . | nindent 6 }}
policyTypes: [Ingress, Egress]
ingress: {{- toYaml .Values.networkPolicy.ingress | nindent 4 }}
egress:
# DNS
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- { protocol: UDP, port: 53 }
- { protocol: TCP, port: 53 }
{{- toYaml .Values.networkPolicy.egress | nindent 4 }}
{{- end }}
helm/templates/secret.yaml
yaml
apiVersion: v1
kind: Secret
metadata:
name: {{ include "ai-file-logger.fullname" . }}-secrets
labels: {{- include "ai-file-logger.labels" . | nindent 4 }}
type: Opaque
stringData:
# لا تضع السر هنا مباشرة — استخدم external-secrets أو sealed-secrets في الإنتاج
AIFL_KMS_PROVIDER: {{ .Values.kms.provider | quote }}
AIFL_KMS_KEY_ID: {{ .Values.kms.keyId | quote }}
helm/templates/configmap.yaml
yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "ai-file-logger.fullname" . }}-config
labels: {{- include "ai-file-logger.labels" . | nindent 4 }}
data:
{{- range $k, $v := .Values.env }}
{{ $k }}: {{ $v | quote }}
{{- end }}
helm/templates/serviceaccount.yaml
yaml
{{- if .Values.serviceAccount.create }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "ai-file-logger.serviceAccountName" . }}
labels: {{- include "ai-file-logger.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations: {{- toYaml . | nindent 4 }}
{{- end }}
automountServiceAccountToken: false
{{- end }}
helm/templates/ingress.yaml
yaml
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "ai-file-logger.fullname" . }}
labels: {{- include "ai-file-logger.labels" . | nindent 4 }}
annotations: {{- toYaml .Values.ingress.annotations | nindent 4 }}
spec:
ingressClassName: {{ .Values.ingress.className }}
tls: {{- toYaml .Values.ingress.tls | nindent 4 }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "ai-file-logger.fullname" $ }}
port: { number: {{ $.Values.service.port }} }
{{- end }}
{{- end }}
{{- end }}
helm/templates/pvc.yaml
yaml
{{- if .Values.persistence.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "ai-file-logger.fullname" . }}-data
spec:
accessModes: {{- toYaml .Values.persistence.accessModes | nindent 4 }}
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- if .Values.persistence.storageClass }}
storageClassName: {{ .Values.persistence.storageClass }}
{{- end }}
{{- end }}
helm/templates/_helpers.tpl
yaml
{{- define "ai-file-logger.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "ai-file-logger.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name (include "ai-file-logger.name" .) | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- define "ai-file-logger.labels" -}}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app.kubernetes.io/name: {{ include "ai-file-logger.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
{{- define "ai-file-logger.selectorLabels" -}}
app.kubernetes.io/name: {{ include "ai-file-logger.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
{{- define "ai-file-logger.serviceAccountName" -}}
{{- if .Values.serviceAccount.create -}}
{{- default (include "ai-file-logger.fullname" .) .Values.serviceAccount.name -}}
{{- else -}}
{{- default "default" .Values.serviceAccount.name -}}
{{- end -}}
{{- end -}}
النشر
bash
helm upgrade --install logger ./helm \ --namespace logging --create-namespace \ --set image.tag=1.0.0 \ --set kms.provider=aws-kms \ --set kms.keyId=$KMS_KEY_ID
13. اختبارات أساسية
tests/PathGuardTest.php
php
<?php
declare(strict_types=1);
use AIFL\Security\PathGuard;
use PHPUnit\Framework\TestCase;
final class PathGuardTest extends TestCase
{
private PathGuard $g;
protected function setUp(): void { $this->g = new PathGuard(); }
public static function badPaths(): array
{
return [
['../etc/passwd'],
['/etc/passwd'],
['a/../../b'],
['a\0b'],
[''],
];
}
/** @dataProvider badPaths */
public function testRejects(string $path): void
{
$this->expectException(\RuntimeException::class);
$this->g->resolve('/tmp/root', $path);
}
public function testAccepts(): void
{
$dir = sys_get_temp_dir() . '/aifl-' . uniqid();
mkdir($dir, 0700, true);
$out = $this->g->resolve($dir, 'logs/app.log');
$this->assertStringStartsWith($dir, $out);
rmdir($dir);
}
}
tests/RedactorTest.php
php
<?php
declare(strict_types=1);
use AIFL\Security\Redactor;
use PHPUnit\Framework\TestCase;
final class RedactorTest extends TestCase
{
public function testJwt(): void
{
$r = new Redactor();
$out = $r->redact('token=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.abc');
$this->assertStringContainsString('[JWT]', $out);
}
public function testAwsKey(): void
{
$r = new Redactor();
$this->assertSame('[AWS_KEY]', $r->redact('AKIAIOSFODNN7EXAMPLE'));
}
public function testLuhnCard(): void
{
$r = new Redactor();
$this->assertSame('card: [CARD]', $r->redact('card: 4111 1111 1111 1111'));
}
public function testNonLuhnKept(): void
{
$r = new Redactor();
$this->assertSame('num: 1234 5678 9012 3456', $r->redact('num: 1234 5678 9012 3456'));
}
}
tests/TenantStorageTest.php
php
<?php
declare(strict_types=1);
use AIFL\Tenancy\{TenantResolver, TenantStorage};
use PHPUnit\Framework\TestCase;
final class TenantStorageTest extends TestCase
{
public function testIsolation(): void
{
$root = sys_get_temp_dir() . '/aifl-tenants-' . uniqid();
mkdir($root, 0700, true);
$s = new TenantStorage($root, new TenantResolver('constant'));
$a = $s->pathFor('tenantA', 'logs/2026.log');
$b = $s->pathFor('tenantB', 'logs/2026.log');
$this->assertNotSame($a, $b);
$this->assertStringContainsString('/tenants/tenantA/', $a);
$this->assertStringContainsString('/tenants/tenantB/', $b);
}
public function testTraversalRejected(): void
{
$root = sys_get_temp_dir() . '/aifl-x-' . uniqid();
mkdir($root, 0700, true);
$s = new TenantStorage($root, new TenantResolver('constant'));
$this->expectException(\RuntimeException::class);
$s->pathFor('tenantA', '../tenantB/secret.log');
}
}
14. خريطة الطبقات النهائية
| # | الطبقة | الملف | ملاحظات |
|---|---|---|---|
| 1 | Bootstrap | ai-file-logger.php | PHP 8.1+، autoload |
| 2 | Container | Support/Container.php | singleton + scoped |
| 3 | Config | Support/Config.php | env + filter |
| 4 | Multi-tenancy | Tenancy/* | مسار + مفتاح معزول |
| 5 | Path guard | Security/PathGuard.php | traversal + symlink |
| 6 | Size guard | Security/SizeGuard.php | entry/file/total |
| 7 | Content | Security/ContentInspector.php | MIME + sanitize |
| 8 | Malware | Security/MalwareScanner.php | INSTREAM |
| 9 | Redactor | Security/Redactor.php | 14 قاعدة + Luhn |
| 10 | Signing | Signing/Ed25519Signer.php | sodium |
| 11 | HMAC | Signing/HmacSigner.php | بديل |
| 12 | VerifyChain | Signing/VerifyChain.php | سلسلة |
| 13 | OpenTelemetry | Telemetry/Tracer.php | OTLP/HTTP |
| 14 | Writer | Storage/Writer.php | atomic + fsync + lock |
| 15 | Verifier | Storage/Verifier.php | hash + سلسلة |
| 16 | Rotator | Storage/Rotator.php | حجم + prune |
| 17 | Retention | Retention/RetentionManager.php | TTL tags |
| 18 | REST | Rest/AdminController.php | verify/sweep/logs/rotate |
| 19 | CLI | Cli/* | wp-cli |
| 20 | Integrations | Integrations/* | Woo/AI/WP |
| 21 | Helm | helm/ | HPA/PDB/NetPol |
15. ملاحظات WordPress/WooCommerce الخاصة
- أذونات الملفات: WordPress يعمل عادةً بـ user
www-data. تأكد أنAIFL_STORAGE_ROOTخارجwp-content/uploadsالعام، أو استخدم.htaccessمعRequire all denied. - Multisite:
tenancy.mode = blog_idيعزل تلقائياً لكل مدونة. - WooCommerce HPOS: استخدم
wc_get_orders()لاget_posts()لتوافق HPOS. - AI plugins: لا تخزّن الـ prompts/الردود كاملة — استخدم redactor + preview 200 حرف.
- GDPR: وسوم
piiبـ TTL 90 يوم افتراضي، مع توفيرwp aifl sweepدوري. - الاتصال بـ ClamAV: استخدم
stream_socket_clientمع Unix socket داخل نفس Pod إن أمكن (أسرع). - التوقيع: مفاتيح Ed25519 لكل مدونة تُخزّن في
.keys/بأذونات0600— لا تُعرض عبر الويب أبداً. - OTEL:
wp_remote_postغير حاجب (blocking=false) لمنع تأخير الطلبات. - REST auth:
current_user_can('manage_options')+ HTTPS إلزامي. - Helm:
readOnlyRootFilesystem: true+emptyDirللـ/tmp+ PVC للـ storage.
هل تريد أن أُتبع هذا بـ:
- اختبارات PHPUnit شاملة مع WordPress test suite (WP_UnitTestCase)؟
- مخطط قاعدة بيانات لـ CDC مباشر من MySQL binlog إلى Elasticsearch؟
- تكامل AWS KMS/HSM عبر PHP SDK داخل نفس الإضافة؟
- لوحة إدارة React داخل wp-admin للاستعلام عن السجلات والاحتفاظ؟