محمدرضا در شرکت عدالتخانه بهعنوان توسعهدهندهی بکاند مشغول به کار است. او اخیراً با یک چالش جدید مواجه شده است. همانطور که مطلع هستید، در *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>
ارسال پاسخ برای این سؤال
در حال حاضر شما دسترسی ندارید.