نموذج متكامل: WordPress/WooCommerce/AI — إضافة تسجيل ملفات إنتاجية

📊 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. خريطة الطبقات النهائية

#الطبقةالملفملاحظات
1Bootstrapai-file-logger.phpPHP 8.1+، autoload
2ContainerSupport/Container.phpsingleton + scoped
3ConfigSupport/Config.phpenv + filter
4Multi-tenancyTenancy/*مسار + مفتاح معزول
5Path guardSecurity/PathGuard.phptraversal + symlink
6Size guardSecurity/SizeGuard.phpentry/file/total
7ContentSecurity/ContentInspector.phpMIME + sanitize
8MalwareSecurity/MalwareScanner.phpINSTREAM
9RedactorSecurity/Redactor.php14 قاعدة + Luhn
10SigningSigning/Ed25519Signer.phpsodium
11HMACSigning/HmacSigner.phpبديل
12VerifyChainSigning/VerifyChain.phpسلسلة
13OpenTelemetryTelemetry/Tracer.phpOTLP/HTTP
14WriterStorage/Writer.phpatomic + fsync + lock
15VerifierStorage/Verifier.phphash + سلسلة
16RotatorStorage/Rotator.phpحجم + prune
17RetentionRetention/RetentionManager.phpTTL tags
18RESTRest/AdminController.phpverify/sweep/logs/rotate
19CLICli/*wp-cli
20IntegrationsIntegrations/*Woo/AI/WP
21Helmhelm/HPA/PDB/NetPol

15. ملاحظات WordPress/WooCommerce الخاصة

  1. أذونات الملفات: WordPress يعمل عادةً بـ user www-data. تأكد أن AIFL_STORAGE_ROOT خارج wp-content/uploads العام، أو استخدم .htaccess مع Require all denied.
  2. Multisite: tenancy.mode = blog_id يعزل تلقائياً لكل مدونة.
  3. WooCommerce HPOS: استخدم wc_get_orders() لا get_posts() لتوافق HPOS.
  4. AI plugins: لا تخزّن الـ prompts/الردود كاملة — استخدم redactor + preview 200 حرف.
  5. GDPR: وسوم pii بـ TTL 90 يوم افتراضي، مع توفير wp aifl sweep دوري.
  6. الاتصال بـ ClamAV: استخدم stream_socket_client مع Unix socket داخل نفس Pod إن أمكن (أسرع).
  7. التوقيع: مفاتيح Ed25519 لكل مدونة تُخزّن في .keys/ بأذونات 0600 — لا تُعرض عبر الويب أبداً.
  8. OTEL: wp_remote_post غير حاجب (blocking=false) لمنع تأخير الطلبات.
  9. REST auth: current_user_can('manage_options') + HTTPS إلزامي.
  10. 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 للاستعلام عن السجلات والاحتفاظ؟
Scroll to Top

🤖 موصى به لك بناءً على اهتماماتك