محمدرضا در شرکت عدالتخانه بهعنوان توسعهدهندهی بکاند مشغول به کار است. او اخیراً با یک چالش جدید مواجه شده است. همانطور که مطلع هستید، در *Laravel 8* قابلیتهای جدیدی به حالت تعمیر (*maintenance mode*) اضافه شده است. یکی از این قابلیتها، امکان تعریف کلید *secret* برای حالت تعمیر است که با استفاده از آن میتوان سایت را خارج از حالت تعمیر مشاهده کرد. این قابلیت در واقع یک کوکی با نام `laravel_maintenance` در مرورگر ذخیره کرده و با استفاده از آن، حالت تعمیر را برای کاربر فعلی غیرفعال میکند.
زمان اعتبار این کوکی ۱۲ ساعت است، اما تیم فنی شرکت عدالتخانه قصد دارد یک آرگومان به دستور `down` موجود در *Artisan* اضافه کند که در صورت مقداردهی آن، کوکی `laravel_maintenance` با زمان اعتبار واردشده در این آرگومان ست شود.
محمدرضا این *task* را برعهده گرفته، اما از پس آن برنیامده است. از شما میخواهیم این *task* را برای او انجام دهید.
# جزئیات پروژه
پروژهی اولیه را از  [این لینک](/contest/assignments/20699/download_problem_initial_project/67892/)  دانلود کنید. ساختار فایلهای این پروژه بهصورت زیر است:
```
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
```
<details class="brown">
<summary>راهاندازی پروژه</summary>
**برای اجرای پروژه، باید `php` و `composer` را از قبل نصب کرده باشید.**
+ ابتدا پروژهی اولیه را دانلود و از حالت فشرده خارج کنید.
+ دستور `composer install` را در پوشهی اصلی پروژه برای نصب نیازمندیها اجرا کنید.
</details>
آرگومانی با نام `time` و مقدار پیشفرض `12` به دستور `down` در *Artisan* اضافه کنید که در صورت مقداردهی شدن، زمان کوکی `laravel-maintenance` برابر با مقدار آرگومان `time` باشد. مقدار آرگومان `time` یک عدد صحیح بوده و بیانگر زمان کوکی `laravel-maintenance` برحسب ساعت است. **تضمین میشود** که عدد صفر و اعداد منفی به این آرگومان داده نمیشوند.
دستور `down` بهصورت زیر اجرا خواهد شد:
```
php artisan down --secret=MvYgEH651d3X4JRcys --time=24
```
در اینصورت، کاربر با ارسال درخواست به آدرس `/MvYgEH651d3X4JRcys` میتواند سایت را به مدت ۲۴ ساعت خارج از حالت تعمیر مشاهده کند.
# نکات
+ برای پیادهسازی این قابلیت، نیازمند جستوجو در سورسکد *Laravel* خواهید بود.
+ شما تنها مجاز به ایجاد تغییرات در پوشههای `app` و `config` هستید.
# آنچه باید آپلود کنید
پس از اعمال تغییرات، کل پروژه به غیر از پوشهی `vendor` را *Zip* کرده و آپلود کنید. نام فایل *Zip* اهمیتی ندارد.
# قسمت آموزشی
در این قسمت راهنماییهای سوال، به مرور اضافه میشود. مشکلاتتان در راستای حل سوال را میتوانید از بخش ["سوال بپرسید"](https://quera.ir/contest/clarification/20699/) مطرح کنید.
<details class="blue">
<summary>راهنمایی ۱</summary>
برای تغییر رفتار دستور `down`، باید کلاس جدیدی نظیر `App\Console\Commands\ExtendedDownCommand` تعریف کرد. این کلاس میتواند از کلاس `Illuminate\Foundation\Console\DownCommand` ارثبری کند. تنها تفاوت این کلاس با کلاس `DownCommand` در *signature* و بدنهی متد `getDownFilePayload` است.
```php
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,
        ];
    }
}
```
برای پیادهسازی متدی جهت ساخت کوکی *bypass*، میتوان کلاسی مشابه کلاس `Illuminate\Foundation\Http\MaintenanceModeBypassCookie` تعریف کرد، با این تفاوت که متد `create` آن یک آرگومان جدید با نام `time` نیز میپذیرد:
```php
<?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();
    }
}
```
</details>
<details class="blue">
<summary>راهنمایی ۲</summary>
برای تغییر رفتار *secret bypass route* باید یک *middleware* مشابه `Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance` تعریف کرد و بخش ساخت کوکی آن را تغییر داد:
```php
<?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)
        );
    }
}
```
در نهایت، باید یک *service provider* برای *override* کردن دستور پیشفرض `down` ایجاد کرد. بدنهی متد `register` این کلاس بهصورت زیر خواهد بود:
```php
public function register()
{
    $this->commands([
        ExtendedDownCommand::class
    ]);
}
```
با افزودن این *service provider* به فایل `app/config.php`، رفتار دستور پیشفرض `down` تغییر میکند.
</details>
  
    
      ارسال پاسخ برای این سؤال
    
    
  
  
    
      در حال حاضر شما دسترسی ندارید.