پلاگین سیستم‌فایل – پایه

مقدمه

قبل از نسخه ۴ جوملا، برای نمایش تصاویر و سایر انواع رسانه‌ها در سایت جوملایی خود، لازم بود فایل‌ها را در یک پوشه فرعی از نصب جوملا، به طور پیش‌فرض پوشه `/images`، ذخیره کنید.

با معرفی مدیریت رسانه در جوملا ۴، تیم توسعه مفهوم «سیستم‌فایل» را به «آداپتور سیستم‌فایل» تبدیل کرد – یعنی یک کلاس PHP که امکاناتی شبیه به فایل ارائه می‌دهد، مانند ایجاد فایل، تغییر نام، حذف و غیره. این قابلیت امکان ذخیره رسانه‌ها را در مکان‌هایی به غیر از سیستم‌فایل محلی فراهم کرد، مثلاً روی یک سرور در «ابر» (cloud).

طراحی جوملا همچنین اجازه می‌دهد پلاگین‌ها نحوه ذخیره فایل‌های رسانه‌ای در سیستم‌فایل را سفارشی کنند، مثلاً با محدود کردن نوع عملیات موجود برای گروه‌های کاربری مختلف.

این بخش نشان می‌دهد چگونه یک پلاگین سیستم‌فایل پایه توسعه دهیم. این پلاگین مشابه عملکرد استاندارد جوملا است که رسانه‌ها را در پوشه `/images` ذخیره می‌کند. پلاگین ما به جای آن از پوشه `/restricted` استفاده خواهد کرد، و طوری تعریف می‌شود که فقط سوپریوزرها بتوانند فایل‌ها یا زیرپوشه‌ها را در این پوشه حذف کنند. در سایر موارد، عملکرد آن مشابه سیستم‌فایل استاندارد جوملا خواهد بود.

در بخش بعد، پلاگین سیستم‌فایل FTP نشان خواهد داد چگونه می‌توان فایل‌های رسانه را روی یک سرور FTP ذخیره کرد.

ارائه‌دهندگان و آداپتورهای سیستم‌فایل «Filesystem Providers and Adapters»

Media Manager Screenshot

تصویر صفحه مدیریت رسانه، یک نمونه جوملا را نشان می‌دهد که دارای ۲ ارائه‌دهنده سیستم‌فایل است:

- ارائه‌دهنده محلی (Local) که دسترسی از طریق ذخیره‌سازی فایل محلی را فراهم می‌کند — این همان چیزی است که به صورت پیش‌فرض با جوملا عرضه می‌شود.

- ارائه‌دهنده محدود (Restricted) که دسترسی به فایل‌های ذخیره شده در یک دایرکتوری محدود را فراهم می‌کند — این قابلیت توسط پلاگین سیستم‌فایل پایه توصیف شده در این بخش فعال شده است.

علاوه بر این، ارائه‌دهنده محلی دارای ۲ آداپتور سیستم‌فایل است:

- images— که فایل‌های ذخیره شده در پوشه `/images` را نمایش می‌دهد.

- videos— که فایل‌های ذخیره شده در پوشه `/videos` را نمایش می‌دهد.

این پلاگین فقط از طریق تنظیمات فعال شده است:

- ایجاد پوشه `/videos` در زیر شاخه نصب جوملا

- پیکربندی پلاگین "FileSystem - Local" جوملا برای اضافه کردن یک دایرکتوری دوم به نام videos.

ارائه‌دهنده محدود تنها یک آداپتور سیستم‌فایل به نام "restricted" دارد. این آداپتور نمایانگر ذخیره‌سازی در داخل پوشه `/restricted` است، و برای کار کردن این قابلیت باید این دایرکتوری را در مسیر ریشه نصب جوملا ایجاد کنید.

ترتیب نمایش ارائه‌دهندگان، بر اساس ترتیب پلاگین‌های سیستم‌فایل است که از طریق صفحه مدیریت System / Plugins تنظیم می‌شود.

چگونه مدیریت رسانه جوملا کار می‌کند

برای بررسی نحوه کار مدیریت رسانه، در نظر می‌گیریم وقتی کاربر روی دکمه Content / Media در بخش مدیریت (administrator back-end) کلیک می‌کند چه اتفاقی می‌افتد. مراحل نشان داده شده در نمودار آمده است. این توضیح کمی ساده شده است؛ نمودار توالی (sequence diagram) که پس از آن می‌آید دقیق‌تر است.

Media Manager Overview

- کاربر روی دکمه Content / Media در بخش مدیریت (در منوی کناری، با رنگ آبی تیره) کلیک می‌کند.

- این کلیک یک درخواست HTTP GET به `com_media` روی سرور ارسال می‌کند.

- در پاسخ به این درخواست، `com_media` پلاگین‌های «filesystem» را ایمپورت کرده و رویداد `onSetupProviders` را فراخوانی می‌کند.

- نمودار دو ارائه‌دهنده سیستم‌فایل را نشان می‌دهد: ارائه‌دهنده Local که با جوملا عرضه شده و ارائه‌دهنده Restricted که توسط پلاگین ما افزوده شده است. هر ارائه‌دهنده به `com_media` اعلام می‌کند که «من یک ارائه‌دهنده سیستم‌فایل هستم و این آداپتورهای سیستم‌فایل من هستند.»

- در پاسخ HTTP، `com_media` صفحه HTML را به مرورگر ارسال می‌کند که شامل بخش پنجره رسانه (رنگ خاکستری) برای نمایش همه فایل‌های رسانه‌ای است. در پنجره بخش سمت چپ، ارائه‌دهندگان سیستم‌فایل و آداپتورهایشان نمایش داده می‌شوند. همچنین مرورگر دستور دانلود و اجرای کد `media-manager.js` را دریافت می‌کند.

- کد `media-manager.js` یک درخواست Ajax به سرور ارسال می‌کند تا اطلاعات فایل‌های ارائه‌دهنده و آداپتور اول در پنجره سمت چپ را دریافت کند. (در واقع اینطور نیست که همیشه اولین انتخاب باشد؛ کد وضعیت جلسه و پوشه‌ای که کاربر قبلاً مشاهده می‌کرده را به یاد می‌آورد و اگر مشخص باشد، اطلاعات فایل‌های آن پوشه را درخواست می‌کند.)

- به دلیل ماهیت بدون حالت (stateless) پروتکل HTTP، `com_media` باید دوباره پلاگین‌های سیستم‌فایل را بارگیری و رویداد `onSetupProviders` را فراخوانی کند.

- ارائه‌دهندگان نیز همین کار را تکرار می‌کنند.

- بر اساس آداپتور و پوشه‌ای که در درخواست Ajax آمده، `com_media` تابع `getFiles` آن آداپتور را صدا می‌زند و نام پوشه را می‌فرستد.

- تابع `getFiles` آداپتور اطلاعات فایل‌ها (شامل زیرپوشه‌ها) در آن پوشه را برمی‌گرداند.

- `com_media` این اطلاعات فایل‌ها را در پاسخ JSON به `media-manager.js` ارسال می‌کند.

- کد `media-manager.js` پنجره رسانه را با جزئیات فایل‌ها و زیرپوشه‌ها به‌روزرسانی می‌کند.

بعد از آن، به محض اینکه کاربر عملیاتی روی فایل‌ها یا پوشه‌ها در پنجره رسانه انجام دهد، `media-manager.js` درخواست‌های Ajax مربوط به عملیات را به `com_media` ارسال می‌کند. 

مثالی در نمودار توالی زیر نشان داده شده است که کاربر درخواست حذف یک فایل را ارسال می‌کند. نمودار کمی دقیق‌تر از توضیح بالا است و تعامل با کلاس ProviderManager در `com_media` را نمایش می‌دهد.

نوشتن یک پلاگین سیستم‌فایل

برای نوشتن یک پلاگین سیستم‌فایل باید دو کلاس اصلی را ارائه دهید:

- کلاس Provider، که رابط `ProviderInterface` در مسیر زیر را پیاده سازی می کند.

`administrator/components/com_media/src/Provider/ProviderInterface.php`

این قسمت ساده است و می‌توانید روش پلاگین سیستم‌فایل Local را کپی کنید که در مسیر زیر قرار دارد.

`plugins/filesystem/local/src/Extension/Local.php`

کلاس اصلی پلاگین (Extension) همان کلاس Provider است.

- کلاس Adapter، که رابط `AdapterInterface` در مسیر زیر است را پیاده سازی می کند.

`administrator/components/com_media/src/Adapter/AdapterInterface.php`

بخش عمده کار در این کلاس است، چون باید بر اساس نوع فضای ذخیره فایل خود، عملیات‌های مختلف مربوط به فایل را پیاده‌سازی کنید.

در مثال پایه ما، قصد داریم از فضای ذخیره فایل محلی جوملا استفاده کنیم، بنابراین کلاس Adapter ما از کلاس `LocalAdapter` ارث‌بری می‌کند و فقط تابع حذف (`delete`) را بازنویسی می‌کنیم، که هنگام حذف فایل یا پوشه فراخوانی می‌شود:

 
public function delete(string $path)
{
    // اگر کاربر دارای امتیاز سوپریوزر باشد اجازه حذف داده می‌شود، وگرنه استثناء پرتاب می‌شود
    $user = Factory::getApplication()->getIdentity();
    if ($user->authorise('core.admin'))
    {
        parent::delete($path);
    } 
    else 
    {
        throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'));
    }
}
 

کد منبع پلاگین

می‌توانید کد زیر را در پوشه‌ای به نام `plg_filesystem_restricted` کپی کنید، یا پلاگین کامل را از لینک download restricted filesystem plugin دانلود کنید.

بعد از نصب، فراموش نکنید پلاگین را فعال کنید! همچنین باید پوشه `/restricted` را در مسیر ریشه نصب جوملای خود ایجاد نمایید.

فایل Manifest (تعریف پلاگین) 

`plg_filesystem_restricted/restricted.xml`

 
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="filesystem" method="upgrade">
    <name>plg_filesystem_restricted</name>
    <author>me</author>
    <creationDate>today</creationDate>
    <version>1.0.0</version>
    <description>My restricted filesystem</description>
    <namespace path="src">My\Plugin\Filesystem\Restricted</namespace>
    <files>
        <folder plugin="restricted">services</folder>
        <folder>src</folder>
    </files>
</extension>
 

فایل Service Provider

این کد قالب (boilerplate) برای نمونه‌سازی (instantiate) پلاگین از طریق کانتینر تزریق وابستگی جوملا (Joomla Dependency Injection Container) است. 

`plg_filesystem_restricted/services/provider.php`

 
<?php
defined('_JEXEC') or die;

use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use My\Plugin\Filesystem\Restricted\Extension\Restricted;

return new class () implements ServiceProviderInterface {

    public function register(Container $container)
    {
        $container->set(
            PluginInterface::class,
            function (Container $container) {
                $dispatcher = $container->get(DispatcherInterface::class);
                $plugin = new Restricted(
                    $dispatcher,
                    (array) PluginHelper::getPlugin('filesystem', 'restricted')
                );
                $plugin->setApplication(Factory::getApplication());

                return $plugin;
            }
        );
    }
};
 

کلاس پلاگین / ارائه‌دهنده (Plugin / Provider class)

این کلاس بر اساس کلاس معادل در پلاگین سیستم‌فایل Local جوملا ساخته شده است. 

`plg_filesystem_restricted/src/Extension/Restricted.php`


<?php
namespace My\Plugin\Filesystem\Restricted\Extension;

use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Component\Media\Administrator\Event\MediaProviderEvent;
use Joomla\Component\Media\Administrator\Provider\ProviderInterface;
use My\Plugin\Filesystem\Restricted\Adapter\RestrictedAdapter;

\defined('_JEXEC') or die;

final class Restricted extends CMSPlugin implements ProviderInterface
{
    public static function getSubscribedEvents(): array {
        return [
            'onSetupProviders' => 'onSetupProviders',
        ];
    }
    
    /**
     * راه‌اندازی ارائه‌دهندگان برای آداپتور Restricted
     */
    public function onSetupProviders(MediaProviderEvent $event)
    {
        $event->getProviderManager()->registerProvider($this);
    }

    /**
     * شناسه ارائه‌دهنده را بازمی‌گرداند
     */
    public function getID()
    {
        return $this->_name; // از فیلد "element" رکورد پلاگین در جدول extensions
    }

    /**
     * نام نمایشی ارائه‌دهنده را بازمی‌گرداند
     */
    public function getDisplayName()
    {
        return 'Restricted';
    }

    /**
     * آرایه‌ای از آداپتورها را بازمی‌گرداند
     */
    public function getAdapters()
    {
        $adapters = [];
        $adapter = new RestrictedAdapter(JPATH_ROOT . '/restricted', 'restricted');
        $adapters[$adapter->getAdapterName()] = $adapter;
        return $adapters;
    }
}
 

کلاس آداپتور (Adapter Class)

کلاس آداپتور ما از کلاس `LocalAdapter` جوملا ارث‌بری می‌کند و متد حذف `delete` را برای محدود کردن دسترسی بازنویسی می‌کند. 

`plg_filesystem_restricted/src/Adapter/RestrictedAdapter.php`

 
<?php

namespace My\Plugin\Filesystem\Restricted\Adapter;

use Joomla\CMS\Language\Text;
use Joomla\CMS\Factory;

\defined('_JEXEC') or die;

class RestrictedAdapter extends \Joomla\Plugin\Filesystem\Local\Adapter\LocalAdapter
{
    /**
     * سازنده آداپتور - بر اساس LocalAdapter جوملا
     */
    public function __construct(string $rootPath, string $filePath, bool $thumbnails = false, array $thumbnailSize = [200, 200])
    {
        parent::__construct($rootPath, $filePath);
    }

    /**
     * حذف فایل یا پوشه در مسیر داده شده
     */
    public function delete(string $path)
    {
        // اگر کاربر امتیاز سوپریوزر داشته باشد، اجازه حذف می‌دهد، در غیر این صورت استثناء پرتاب می‌کند
        $user = Factory::getApplication()->getIdentity();
        if ($user->authorise('core.admin'))
        {
            parent::delete($path);
        } 
        else 
        {
            throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'));
        }
    }
    /**
    * نام این آداپتور را برمی‌گرداند.
    *
    * @ مقدار بازگشتی از نوع رشته
    * ورژن 4
    */
    public function getAdapterName(): string
    {
        return "Restricted"; 
    }
}