لینک‌های مفید برای شرکت در مسابقه:

در زمان مسابقه می‌توانید سوال‌های خود را از قسمت "سوال بپرسید" مطرح کنید.

دو سری راهنمایی برای سوالات اضافه شده است.

حالت تعمیر سفارشی


محمدرضا در شرکت عدالتخانه به‌عنوان توسعه‌دهنده‌ی بک‌اند مشغول به کار است. او اخیراً با یک چالش جدید مواجه شده است. همان‌طور که مطلع هستید، در Laravel 8 قابلیت‌های جدیدی به حالت تعمیر (maintenance mode) اضافه شده است. یکی از این قابلیت‌ها، امکان تعریف کلید secret برای حالت تعمیر است که با استفاده از آن می‌توان سایت را خارج از حالت تعمیر مشاهده کرد. این قابلیت در واقع یک کوکی با نام laravel_maintenance در مرورگر ذخیره کرده و با استفاده از آن، حالت تعمیر را برای کاربر فعلی غیرفعال می‌کند.

زمان اعتبار این کوکی ۱۲ ساعت است، اما تیم فنی شرکت عدالتخانه قصد دارد یک آرگومان به دستور down موجود در Artisan اضافه کند که در صورت مقداردهی آن، کوکی laravel_maintenance با زمان اعتبار واردشده در این آرگومان ست شود.

محمدرضا این task را برعهده گرفته، اما از پس آن برنیامده است. از شما می‌خواهیم این task را برای او انجام دهید.

جزئیات پروژه🔗

پروژه‌ی اولیه را از این لینک دانلود کنید. ساختار فایل‌های این پروژه به‌صورت زیر است:

maintenance_initial
├── app
│   ├── Console
│   │   └── Kernel.php
│   ├── Exceptions
│   │   └── Handler.php
│   ├── Http
│   │   ├── Controllers
│   │   │   └── Controller.php
│   │   ├── Middleware
│   │   │   ├── Authenticate.php
│   │   │   ├── EncryptCookies.php
│   │   │   ├── PreventRequestsDuringMaintenance.php
│   │   │   ├── RedirectIfAuthenticated.php
│   │   │   ├── TrimStrings.php
│   │   │   ├── TrustHosts.php
│   │   │   ├── TrustProxies.php
│   │   │   └── VerifyCsrfToken.php
│   │   └── Kernel.php
│   ├── Models
│   │   └── User.php
│   └── Providers
│       ├── AppServiceProvider.php
│       ├── AuthServiceProvider.php
│       ├── BroadcastServiceProvider.php
│       ├── EventServiceProvider.php
│       └── RouteServiceProvider.php
├── bootstrap
│   ├── cache
│   │   ├── packages.php
│   │   └── services.php
│   └── app.php
├── config
│   ├── app.php
│   ├── auth.php
│   ├── broadcasting.php
│   ├── cache.php
│   ├── cors.php
│   ├── database.php
│   ├── filesystems.php
│   ├── hashing.php
│   ├── logging.php
│   ├── mail.php
│   ├── queue.php
│   ├── services.php
│   ├── session.php
│   └── view.php
├── database
│   ├── factories
│   │   └── UserFactory.php
│   ├── migrations
│   │   ├── 2014_10_12_000000_create_users_table.php
│   │   ├── 2014_10_12_100000_create_password_resets_table.php
│   │   └── 2019_08_19_000000_create_failed_jobs_table.php
│   └── seeders
│       └── DatabaseSeeder.php
├── public
│   ├── favicon.ico
│   ├── index.php
│   ├── robots.txt
│   └── web.config
├── resources
│   ├── css
│   │   └── app.css
│   ├── js
│   │   ├── app.js
│   │   └── bootstrap.js
│   ├── lang
│   │   └── en
│   │       ├── auth.php
│   │       ├── pagination.php
│   │       ├── passwords.php
│   │       └── validation.php
│   └── views
│       └── welcome.blade.php
├── routes
│   ├── api.php
│   ├── channels.php
│   ├── console.php
│   └── web.php
├── storage
│   ├── app
│   │   └── public
│   ├── framework
│   │   ├── cache
│   │   │   └── data
│   │   ├── sessions
│   │   ├── testing
│   │   └── views
│   └── logs
├── tests
│   ├── Feature
│   │   └── ExampleTest.php
│   ├── Unit
│   │   └── ExampleTest.php
│   ├── CreatesApplication.php
│   └── TestCase.php
├── README.md
├── artisan
├── composer.json
├── composer.lock
├── package.json
├── phpunit.xml
├── server.php
└── webpack.mix.js
Plain text
راه‌اندازی پروژه

برای اجرای پروژه، باید php و composer را از قبل نصب کرده باشید.

  • ابتدا پروژه‌ی اولیه را دانلود و از حالت فشرده خارج کنید.
  • دستور composer install را در پوشه‌ی اصلی پروژه برای نصب نیازمندی‌ها اجرا کنید.

آرگومانی با نام time و مقدار پیش‌فرض 12 به دستور down در Artisan اضافه کنید که در صورت مقداردهی شدن، زمان کوکی laravel-maintenance برابر با مقدار آرگومان time باشد. مقدار آرگومان time یک عدد صحیح بوده و بیانگر زمان کوکی laravel-maintenance برحسب ساعت است. تضمین می‌شود که عدد صفر و اعداد منفی به این آرگومان داده نمی‌شوند.

دستور down به‌صورت زیر اجرا خواهد شد:

php artisan down --secret=MvYgEH651d3X4JRcys --time=24
Plain text

در این‌صورت، کاربر با ارسال درخواست به آدرس /MvYgEH651d3X4JRcys می‌تواند سایت را به مدت ۲۴ ساعت خارج از حالت تعمیر مشاهده کند.

نکات🔗

  • برای پیاده‌سازی این قابلیت، نیازمند جست‌وجو در سورس‌کد Laravel خواهید بود.
  • شما تنها مجاز به ایجاد تغییرات در پوشه‌های app و config هستید.

آن‌چه باید آپلود کنید🔗

پس از اعمال تغییرات، کل پروژه به غیر از پوشه‌ی vendor را Zip کرده و آپلود کنید. نام فایل Zip اهمیتی ندارد.

قسمت آموزشی🔗

در این قسمت راهنمایی‌های سوال، به مرور اضافه می‌شود. مشکلات‌تان در راستای حل سوال را می‌توانید از بخش "سوال بپرسید" مطرح کنید.

راهنمایی ۱

برای تغییر رفتار دستور down، باید کلاس جدیدی نظیر App\Console\Commands\ExtendedDownCommand تعریف کرد. این کلاس می‌تواند از کلاس Illuminate\Foundation\Console\DownCommand ارث‌بری کند. تنها تفاوت این کلاس با کلاس DownCommand در signature و بدنه‌ی متد getDownFilePayload است.

class ExtendedDownCommand extends DownCommand
{
    protected $signature = 'down {--redirect= : The path that users should be redirected to}
                                 {--render= : The view that should be prerendered for display during maintenance mode}
                                 {--retry= : The number of seconds after which the request may be retried}
                                 {--secret= : The secret phrase that may be used to bypass maintenance mode}
                                 {--time=12 : Bypass cookie expiration time (hours)}
                                 {--status=503 : The status code that should be used when returning the maintenance mode response}';

    protected function getDownFilePayload()
    {
        return [
            'redirect' => $this->redirectPath(),
            'retry' => $this->getRetryTime(),
            'secret' => $this->option('secret'),
            'time' => (int) $this->option('time'),
            'status' => (int) $this->option('status', 503),
            'template' => $this->option('render') ? $this->prerenderView() : null,
        ];
    }
}
PHP

برای پیاده‌سازی متدی جهت ساخت کوکی bypass، می‌توان کلاسی مشابه کلاس Illuminate\Foundation\Http\MaintenanceModeBypassCookie تعریف کرد، با این تفاوت که متد create آن یک آرگومان جدید با نام time نیز می‌پذیرد:

<?php

namespace App\Http;

use Illuminate\Support\Carbon;
use Symfony\Component\HttpFoundation\Cookie;

class MaintenanceModeBypassCookie
{
    public static function create(string $key, int $time = 12)
    {
        if ($time < 1) {
            $time = 1;
        }
        $expiresAt = Carbon::now()->addHours($time);

        return new Cookie('laravel_maintenance', base64_encode(json_encode([
            'expires_at' => $expiresAt->getTimestamp(),
            'mac' => hash_hmac('SHA256', $expiresAt->getTimestamp(), $key),
        ])), $expiresAt);
    }

    public static function isValid(string $cookie, string $key)
    {
        $payload = json_decode(base64_decode($cookie), true);

        return is_array($payload) &&
            is_numeric($payload['expires_at'] ?? null) &&
            isset($payload['mac']) &&
            hash_equals(hash_hmac('SHA256', $payload['expires_at'], $key), $payload['mac']) &&
            (int) $payload['expires_at'] >= Carbon::now()->getTimestamp();
    }
}
PHP
راهنمایی ۲

برای تغییر رفتار secret bypass route باید یک middleware مشابه Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance تعریف کرد و بخش ساخت کوکی آن را تغییر داد:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Contracts\Foundation\Application;
use \App\Http\MaintenanceModeBypassCookie;
use Symfony\Component\HttpKernel\Exception\HttpException;

class PreventRequestsDuringMaintenance
{
    protected $app;

    protected $except = [];

    public function __construct(Application $app)
    {
        $this->app = $app;
    }

    public function handle($request, Closure $next)
    {
        if ($this->app->isDownForMaintenance()) {
            $data = json_decode(file_get_contents($this->app->storagePath().'/framework/down'), true);

            if (isset($data['secret']) && isset($data['time']) && $request->path() === $data['secret']) {
                return $this->bypassResponse($data['secret'], $data['time']);
            }

            if ($this->hasValidBypassCookie($request, $data) ||
                $this->inExceptArray($request)) {
                return $next($request);
            }

            if (isset($data['redirect'])) {
                $path = $data['redirect'] === '/'
                            ? $data['redirect']
                            : trim($data['redirect'], '/');

                if ($request->path() !== $path) {
                    return redirect($path);
                }
            }

            if (isset($data['template'])) {
                return response(
                    $data['template'],
                    $data['status'] ?? 503,
                    isset($data['retry']) ? ['Retry-After' => $data['retry']] : []
                );
            }

            throw new HttpException(
                $data['status'] ?? 503,
                'Service Unavailable',
                null,
                isset($data['retry']) ? ['Retry-After' => $data['retry']] : []
            );
        }

        return $next($request);
    }

    protected function hasValidBypassCookie($request, array $data)
    {
        return isset($data['secret']) &&
                $request->cookie('laravel_maintenance') &&
                MaintenanceModeBypassCookie::isValid(
                    $request->cookie('laravel_maintenance'),
                    $data['secret']
                );
    }

    protected function inExceptArray($request)
    {
        foreach ($this->except as $except) {
            if ($except !== '/') {
                $except = trim($except, '/');
            }

            if ($request->fullUrlIs($except) || $request->is($except)) {
                return true;
            }
        }

        return false;
    }

    protected function bypassResponse(string $secret, int $time)
    {
        return redirect('/')->withCookie(
            MaintenanceModeBypassCookie::create($secret, $time)
        );
    }
}
PHP

در نهایت، باید یک service provider برای override کردن دستور پیش‌فرض down ایجاد کرد. بدنه‌ی متد register این کلاس به‌صورت زیر خواهد بود:

public function register()
{
    $this->commands([
        ExtendedDownCommand::class
    ]);
}
PHP

با افزودن این service provider به فایل app/config.php، رفتار دستور پیش‌فرض down تغییر می‌کند.

ارسال پاسخ برای این سؤال
در حال حاضر شما دسترسی ندارید.