diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f90bf2d..d55dbecb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +# v1.0.8 +## 02/05/2016 + +1. [](#new) + * Added a logout button when not authorized to access a page in Admin + * Added the option to hide a tab from an extended blueprint (https://github.com/getgrav/grav/issues/620) + * Many new languages and updates to existing languages from the Translation team. +1. [](#improved) + * Check frontmatter for validity prior to saving + * Add noindex, nofollow across the entire admin theme if no other robots headers are set on a page + * Allow to hide a configuration blueprint section / tab and still save its values + * Allow to show user defined blueprints in configuration + * Updated FontAwesome to latest 4.5.0 version +1. [](#bugfix) + * Fixed an issue with user registration on Linux caused by `glob()` possibly returning false. + * Fixed an issue preventing Admin to work correctly in a multisite configuration + * Fixed preview and insertion of images with non-lowercase extension + * Fixed an incorrect number of pages being displayed in the sidebar in some cases + * [Security] Don't reveal Grav filesystem path when trying to delete non-existing images + * [Security] Fix PHP error happening when uploading file without extension if the JS dropzone uploader is configured to allow empty file extensions + * [Security] Ensure correct escaping in various Twig files + # v1.0.7 ## 01/15/2016 diff --git a/admin.php b/admin.php index 0ce10e88..366182e1 100644 --- a/admin.php +++ b/admin.php @@ -257,7 +257,7 @@ class AdminPlugin extends Plugin } // Replace themes service with admin. - $this->grav['themes'] = function ($c) { + $this->grav['themes'] = function () { require_once __DIR__ . '/classes/themes.php'; return new Themes($this->grav); }; @@ -411,9 +411,6 @@ class AdminPlugin extends Plugin { $twig = $this->grav['twig']; - // Dynamic type support - $format = $this->uri->extension(); - $twig->twig_vars['location'] = $this->template; $twig->twig_vars['base_url_relative_frontend'] = $twig->twig_vars['base_url_relative'] ?: '/'; $twig->twig_vars['admin_route'] = trim($this->config->get('plugins.admin.route'), '/'); @@ -436,7 +433,18 @@ class AdminPlugin extends Plugin break; case 'pages': - $page = $this->admin->page(true); + $path = $this->route; + + if (!$path) { + $path = '/'; + } + + if (!isset($this->pages[$path])) { + $page = null; + } else { + $page = $this->pages[$path]; + } + if ($page != null) { $twig->twig_vars['file'] = File::instance($page->filePath()); $twig->twig_vars['media_types'] = str_replace('defaults,', '', @@ -555,6 +563,7 @@ class AdminPlugin extends Plugin $this->admin = new Admin($this->grav, $this->base, $this->template, $this->route); + // And store the class into DI container. $this->grav['admin'] = $this->admin; diff --git a/blueprints.yaml b/blueprints.yaml index 90b8876a..7c70bfa1 100644 --- a/blueprints.yaml +++ b/blueprints.yaml @@ -1,5 +1,5 @@ name: Admin Panel -version: 1.0.7 +version: 1.0.8 description: Adds an advanced administration panel to manage your site icon: empire author: diff --git a/blueprints/config/streams.yaml b/blueprints/config/streams.yaml deleted file mode 100644 index 2a20cf51..00000000 --- a/blueprints/config/streams.yaml +++ /dev/null @@ -1,9 +0,0 @@ -title: PLUGIN_ADMIN.FILE_STREAMS - -form: - validation: loose - hidden: true - - fields: - schemes.xxx: - type: array diff --git a/classes/admin.php b/classes/admin.php index 8fb4e57a..0d4ec834 100644 --- a/classes/admin.php +++ b/classes/admin.php @@ -184,8 +184,7 @@ class Admin /** @var Grav $grav */ $grav = $this->grav; - $this->setMessage($this->translate('PLUGIN_ADMIN.LOGIN_LOGGED_IN', [$this->user->language]), 'info'); - + $this->setMessage($this->translate('PLUGIN_ADMIN.LOGIN_LOGGED_IN'), 'info'); $redirect_route = $this->uri->route(); $grav->redirect($redirect_route); } @@ -389,6 +388,8 @@ class Admin /** * Get all routes. * + * @param bool $unique + * * @return array */ public function routes($unique = false) @@ -455,6 +456,8 @@ class Admin /** * Get all plugins. * + * @param bool $local + * * @return array */ public function plugins($local = true) @@ -476,6 +479,8 @@ class Admin /** * Get all themes. * + * @param bool $local + * * @return array */ public function themes($local = true) @@ -749,6 +754,7 @@ class Admin $pages = Grav::instance()['pages']; $route = '/' . ltrim(Grav::instance()['admin']->route, '/'); + /** @var Page $page */ $page = $pages->dispatch($route); $parent_route = null; if ($page) { @@ -829,22 +835,11 @@ class Admin /** * Translate a string to the user-defined language * - * @param string $string the string to translate - * @return string - */ - public function translate($string) - { - return $this->_translate($string, [$this->grav['user']->authenticated ? $this->grav['user']->language : 'en']); - } - - /** * @param array|mixed $args - * @param array|null $languages - * @param bool $array_support - * @param bool $html_out + * * @return string */ - public function _translate($args, Array $languages = null, $array_support = false, $html_out = false) + public function translate($args) { if (is_array($args)) { $lookup = array_shift($args); @@ -853,6 +848,8 @@ class Admin $args = []; } + $languages = [$this->grav['user']->authenticated ? $this->grav['user']->language : 'en']; + if ($lookup) { if (empty($languages) || reset($languages) == null) { if ($this->grav['config']->get('system.languages.translations_fallback', true)) { @@ -861,22 +858,19 @@ class Admin $languages = (array)$this->grav['language']->getDefault(); } } - } else { - $languages = ['en']; } - foreach ((array)$languages as $lang) { - $translation = $this->grav['language']->getTranslation($lang, $lookup, $array_support); + $translation = $this->grav['language']->getTranslation($lang, $lookup); if (!$translation) { $language = $this->grav['language']->getDefault() ?: 'en'; - $translation = $this->grav['language']->getTranslation($language, $lookup, $array_support); + $translation = $this->grav['language']->getTranslation($language, $lookup); } if (!$translation) { $language = 'en'; - $translation = $this->grav['language']->getTranslation($language, $lookup, $array_support); + $translation = $this->grav['language']->getTranslation($language, $lookup); } if ($translation) { diff --git a/classes/controller.php b/classes/controller.php index 3982a843..185d42eb 100644 --- a/classes/controller.php +++ b/classes/controller.php @@ -1084,6 +1084,60 @@ class AdminController } /** + * Handles creating an empty page folder (without markdown file) + * + * @return bool True if the action was performed. + */ + public function taskSaveNewFolder() + { + if (!$this->authorizeTask('save', $this->dataPermissions())) { + return; + } + + $data = $this->post; + + if ($data['route'] == '/') { + $path = $this->grav['locator']->findResource('page://'); + } else { + $path = $page = $this->grav['page']->find($data['route'])->path(); + } + + $files = Folder::all($path, ['recursive' => false]); + + $highestOrder = 0; + foreach ($files as $file) { + preg_match(PAGE_ORDER_PREFIX_REGEX, $file, $order); + + if (isset($order[0])) { + $theOrder = intval(trim($order[0], '.')); + } else { + $theOrder = 0; + } + + if ($theOrder >= $highestOrder) { + $highestOrder = $theOrder; + } + } + + $orderOfNewFolder = $highestOrder + 1; + + if ($orderOfNewFolder < 10) { + $orderOfNewFolder = '0' . $orderOfNewFolder; + } + + Folder::mkdir($path . '/' . $orderOfNewFolder . '.' . $data['folder']); + + $this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.SUCCESSFULLY_SAVED'), 'info'); + + $multilang = $this->isMultilang(); + $admin_route = $this->grav['config']->get('plugins.admin.route'); + $redirect_url = '/' . ($multilang ? ($this->grav['session']->admin_lang) : '') . $admin_route . '/' . $this->view; + $this->setRedirect($redirect_url); + + return true; + } + + /* * @param string $frontmatter * @return bool */ @@ -1102,6 +1156,7 @@ class AdminController } catch (ParseException $e) { return false; } + return true; } @@ -1452,7 +1507,6 @@ class AdminController return false; } - // $reorder = false; $data = $this->post; $language = $data['lang']; diff --git a/classes/gpm.php b/classes/gpm.php index 17d86742..2b429e4b 100644 --- a/classes/gpm.php +++ b/classes/gpm.php @@ -1,7 +1,7 @@ findResource($package->package_type . '://' . $package->slug); + $location = Grav::instance()['locator']->findResource($package->package_type . '://' . $package->slug); // Check destination Installer::isValidDestination($location); @@ -168,7 +166,7 @@ class Gpm { $contents = Response::get($package->zipball_url, []); - $cache_dir = self::getGrav()['locator']->findResource('cache://', true); + $cache_dir = Grav::instance()['locator']->findResource('cache://', true); $cache_dir = $cache_dir . DS . 'tmp/Grav-' . uniqid(); Folder::mkdir($cache_dir); diff --git a/classes/popularity.php b/classes/popularity.php index f56887c1..b82b0b06 100644 --- a/classes/popularity.php +++ b/classes/popularity.php @@ -3,16 +3,15 @@ namespace Grav\Plugin; use Grav\Common\Config\Config; use Grav\Common\Grav; -use Grav\Common\Plugins; -use Grav\Common\Themes; use Grav\Common\Page\Page; use Grav\Common\Data; -use Grav\Common\GravTrait; +/** + * Class Popularity + * @package Grav\Plugin + */ class Popularity { - use GravTrait; - /** @var Config */ protected $config; protected $data_path; @@ -36,9 +35,9 @@ class Popularity public function __construct() { - $this->config = self::getGrav()['config']; + $this->config = Grav::instance()['config']; - $this->data_path = self::$grav['locator']->findResource('log://popularity', true, true); + $this->data_path = Grav::instance()['locator']->findResource('log://popularity', true, true); $this->daily_file = $this->data_path.'/'.self::DAILY_FILE; $this->monthly_file = $this->data_path.'/'.self::MONTHLY_FILE; $this->totals_file = $this->data_path.'/'.self::TOTALS_FILE; @@ -49,13 +48,13 @@ class Popularity public function trackHit() { // Don't track bot or crawler requests - if (!self::getGrav()['browser']->isHuman()) { + if (!Grav::instance()['browser']->isHuman()) { return; } /** @var Page $page */ - $page = self::getGrav()['page']; - $relative_url = str_replace(self::getGrav()['base_url_relative'], '', $page->url()); + $page = Grav::instance()['page']; + $relative_url = str_replace(Grav::instance()['base_url_relative'], '', $page->url()); // Don't track error pages or pages that have no route if ($page->template() == 'error' || !$page->route()) { @@ -79,7 +78,7 @@ class Popularity $this->updateDaily(); $this->updateMonthly(); $this->updateTotals($page->route()); - $this->updateVisitors(self::getGrav()['uri']->ip()); + $this->updateVisitors(Grav::instance()['uri']->ip()); } @@ -126,7 +125,7 @@ class Popularity $data = array(); foreach ($chart_data as $date => $count) { - $labels[] = self::getGrav()['grav']['admin']->translate(['PLUGIN_ADMIN.' . strtoupper(date('D', strtotime($date)))]); + $labels[] = Grav::instance()['grav']['admin']->translate(['PLUGIN_ADMIN.' . strtoupper(date('D', strtotime($date)))]); $data[] = $count; } diff --git a/languages/cs.yaml b/languages/cs.yaml new file mode 100644 index 00000000..bf226802 --- /dev/null +++ b/languages/cs.yaml @@ -0,0 +1,478 @@ +--- +PLUGIN_ADMIN: + ADMIN_BETA_MSG: Jedná se o beta verzi! V ostrém provozu používejte pouze na vlastní nebezpečí... + ADMIN_REPORT_ISSUE: Objevili jste problém? Nahlaste ho, prosím, na GitHub. + EMAIL_FOOTER: 'Powered by Grav - The Modern Flat File CMS' + LOGIN_BTN: Přihlásit + LOGIN_BTN_FORGOT: Obnovit heslo + LOGIN_BTN_RESET: Obnovit heslo + LOGIN_BTN_SEND_INSTRUCTIONS: Odeslány pokyny pro obnovu hesla + LOGIN_BTN_CLEAR: Vymazat formulář + LOGIN_BTN_CREATE_USER: Vytvořit uživatele + LOGIN_LOGGED_IN: Byli jste úspěšně přihlášeni + LOGIN_FAILED: Přihlášení se nezdařilo + LOGGED_OUT: Byli jste odhlášeni + RESET_LINK_EXPIRED: Odkaz pro obnovení vypršel, zkuste to, prosím, znovu + RESET_PASSWORD_RESET: Heslo bylo změněno + RESET_INVALID_LINK: Použit neplatný odkaz pro obnovu hesla, zkuste to, prosím, znovu + FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL: 'Pokyny pro obnovení hesla byly odeslány na e-mail %s' + FORGOT_FAILED_TO_EMAIL: Nepodařilo se odeslat instrukce pro obnovu hesla, zkuste to, prosím, později + FORGOT_CANNOT_RESET_EMAIL_NO_EMAIL: 'Nepodařilo se resetovat heslo pro %s, emailová adresa neexisuje' + FORGOT_USERNAME_DOES_NOT_EXIST: 'Neexistuje uživatel s uživatelským jménem %s' + FORGOT_EMAIL_NOT_CONFIGURED: Heslo nelze obnovit. Tento web není nastaven pro odesílání e-mailů + FORGOT_EMAIL_SUBJECT: '%s Požadavek na obnovení hesla' + FORGOT_EMAIL_BODY: '
Vážený %1$s,
Toto je požadavek z %4$s na obnovení Vašeho hesla.
Klikněte zde pro nastavení nového hesla
Případně, zkopírujte následujíci URL do Vašeho prohlížeče:
%2$s
Děkujeme,
%3$s
%1$s Annwyl,
Gwnaed cais ar %4$s i ailosod eich cyfrinair.
< br / > hyn i ailosod eich cyfrinair cliciwch < br / > < br / >
Fel arall, gopïo URL canlynol i'r bar cyfeiriad eich porwr:
%2$s
< br / > Cofion, < br / > < br / >%3$s
+ MANAGE_PAGES: Rheoli tudalennau + CONFIGURATION: Ffurfweddiad + PAGES: Tudalennau + PLUGINS: Ategion + PLUGIN: Ategyn + THEMES: Themâu + LOGOUT: Allgofnodi + BACK: Yn ôl + ADD_PAGE: Ychwanegu Tudalen + ADD_MODULAR: Ychwanegu modiwlaidd + MOVE: Symud + DELETE: Dileu + SAVE: Cadw + NORMAL: Arferol + EXPERT: Arbenigol + EXPAND_ALL: "Ehangu'r cyfan" + COLLAPSE_ALL: "Crebachu'r cyfan" + ERROR: Gwall + CLOSE: Cau + CANCEL: Canslo + CONTINUE: Yn parhau + MODAL_DELETE_PAGE_CONFIRMATION_REQUIRED_TITLE: Cadarnhad sydd yn ofynnol + MODAL_CHANGED_DETECTED_TITLE: Ganfod newidiadau + MODAL_CHANGED_DETECTED_DESC: "Wedi ichi golli'r newidiadau. Ydych chi'n siŵr eich bod am adael heb arbed?" + MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_TITLE: Cadarnhad sydd yn ofynnol + MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_DESC: "Ydych chi'n siŵr eich bod am ddileu'r ffeil hon? Ni ellir dad-wneud y cam gweithredu hwn." + ADD_FILTERS: Ychwanegu hidlydd + SEARCH_PAGES: Tudalennau chwilio + VERSION: Fersiwn + WAS_MADE_WITH: Wedi greu hefo + BY: Gan + AUTHOR: Awdur + HOMEPAGE: Hafan + KEYWORDS: Allweddeiriau + LICENSE: Trwydded + DESCRIPTION: Disgrifiad + THEME: Thema + BACK_TO_THEMES: Ôl i themâu + BACK_TO_PLUGINS: Ôl i ategion + CHECK_FOR_UPDATES: Chwilio am ddiweddariadau + ADD: Ychwanegu + CLEAR_CACHE: Clirio storfa + CLEAR_CACHE_ALL_CACHE: Holl storfa + CLEAR_CACHE_ASSETS_ONLY: Asedau yn unig + CLEAR_CACHE_IMAGES_ONLY: Llyniau yn unig + CLEAR_CACHE_CACHE_ONLY: Storfa yn unig + DASHBOARD: Dangosfwrdd + UPDATES_AVAILABLE: Diweddariadau ar gael + DAYS: Diwrnod + UPDATE: Diweddaru + STATISTICS: Ystadegau + TODAY: Heddiw + WEEK: Wythnos + MONTH: Mis + UPDATED: "Wedi'w ddiweddaru" + MON: Llu + TUE: Maw + WED: Mer + THU: Iau + FRI: Gwe + SAT: Sad + SUN: Sul + COPY: Copi + EDIT: Golygu + CREATE: Creu + AVAILABLE: Ar gael diff --git a/languages/de.yaml b/languages/de.yaml index b42d1e82..2ab2b991 100644 --- a/languages/de.yaml +++ b/languages/de.yaml @@ -1,417 +1,478 @@ +--- PLUGIN_ADMIN: - ADMIN_BETA_MSG: "Dies ist eine Beta-Version! Benutzung auf eigene Gefahr..." - ADMIN_REPORT_ISSUE: "Fehler gefunden? Bitte melden Sie ihn auf GitHub." - EMAIL_FOOTER: "Powered by Grav - The Modern Flat File CMS" - LOGIN_BTN: "Login" - LOGIN_BTN_FORGOT: "Passwort vergessen" - LOGIN_BTN_RESET: "Passwort zurücksetzen" - LOGIN_BTN_SEND_INSTRUCTIONS: "Neues Passwort anfordern" - LOGIN_LOGGED_IN: "Anmeldung erfolgreich" - LOGIN_FAILED: "Anmeldung fehlgeschlagen" - LOGGED_OUT: "Sie wurden abgemeldet" - RESET_LINK_EXPIRED: "Der Link zum Zurücksetzen Ihres Passwortes ist abgelaufen, bitte probieren Sie es erneut" - RESET_PASSWORD_RESET: "Das Passwort wurde zurückgesetzt" - RESET_INVALID_LINK: "Der Link zum Zurücksetzen Ihres Passwortes ist ungültig, bitte probieren Sie es erneut" - FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL: "Anweisungen zum Zurücksetzen des Passwortes wurden an %s gesendet" - FORGOT_FAILED_TO_EMAIL: "Anweisungen zum Zurücksetzen des Passwortes konnten nicht versendet werden, bitte probieren Sie es erneut" - FORGOT_CANNOT_RESET_EMAIL_NO_EMAIL: "Das Passwort für %s kann nicht geändert werden, da keine E-Mail-Adresse hinterlegt ist" - FORGOT_USERNAME_DOES_NOT_EXIST: "Es existiert kein Benutzer mit dem Namen %s" - FORGOT_EMAIL_NOT_CONFIGURED: "Passwort konnte nicht zurückgesetzt werden, da diese Seite nicht zum Versenden von E-Mails konfiguriert worden ist" - FORGOT_EMAIL_SUBJECT: "Zurücksetzen des Passwortes von %s" - FORGOT_EMAIL_BODY: "Hallo %1$s,
Auf %4$s wurde die Zurücksetzung Ihres Passwortes angefordert.
Klicken Sie hier um Ihr Passwort zurückzusetzen.
Alternativ können Sie auch die folgende Adresse in die Adresszeile Ihres Browsers kopieren:
%2$s
Viele Grüße,
%3$s
Hallo %1$s,
Auf %4$s wurde die Zurücksetzung Ihres Passwortes angefordert.
Klicken Sie hier um Ihr Passwort zurückzusetzen.
Alternativ können Sie auch die folgende Adresse in die Adresszeile Ihres Browsers kopieren:
%2$s
Viele Grüße,
%3$s
Estimado %1$s,
Se ha realizado una petición el %4$s para restablecer tu contraseña.
Pincha aquí para restablecer tu contraseña
Alternativamente, copia la siguiente URL en la barra de direcciones de tu navegador:
%2$s
Atentamente,
%3$s
Estimado %1$s,
Se ha realizado una petición el %4$s para restablecer tu contraseña.
Pincha aquí para restablecer tu contraseña
Alternativamente, copia la siguiente URL en la barra de direcciones de tu navegador:
%2$s
Atentamente,
%3$s
%1$s,
Une requête a été faite sur %4$s pour réinitialiser votre mot de passe.
Cliquez ici pour réinitialiser votre mot de passe
Alternativement, copiez l'URL suivante dans la barre d'adresse de votre navigateur:
%2$s
Cordialement,
%3$s
%1$s,
Une requête a été faite sur %4$s pour réinitialiser votre mot de passe.
Cliquez ici pour réinitialiser votre mot de passe
Alternativement, copiez l'URL suivante dans la barre d'adresse de votre navigateur:
%2$s
Cordialement,
%3$s
Dragi %1$s,
Podnesen je zahtjev %4$s za resetiranjem tvoje lozinke.
Klikni ovdje kako bi lozinka bila resetirana
Alternativno, kopiraj sljedeći link u svoj web preglednik:
%2$s
Lijep pozdrav,
%3$s
Dragi %1$s,
Podnesen je zahtjev %4$s za resetiranjem tvoje lozinke.
Klikni ovdje kako bi lozinka bila resetirana
Alternativno, kopiraj sljedeći link u svoj web preglednik:
%2$s
Lijep pozdrav,
%3$s
Kedves %1$s,
Az alábbi időpontban valaki a jelszavad cseréjét kezdeményezte: %4$s.
Kattints ide jelszavad cseréjéhez
Esetleg másold a következő URL-t a böngésződ címsávjába:
%2$s
Üdvözlettel,
%3$s
Kedves %1$s,
Az alábbi időpontban valaki a jelszavad cseréjét kezdeményezte: %4$s.
Kattints ide jelszavad cseréjéhez
Esetleg másold a következő URL-t a böngésződ címsávjába:
%2$s
Üdvözlettel,
%3$s
Caro %1$s,
Una richiesta di reset password è stata effettuata su %4$s.
Clicca qui per resettare la tua password
In alternativa, copia il seguente URL nella barra indirizzi del tuo browser:
%2$s
Cordiali saluti,
%3$s
Caro %1$s,
Una richiesta di reset password è stata effettuata su %4$s.
Clicca qui per resettare la tua password
In alternativa, copia il seguente URL nella barra indirizzi del tuo browser:
%2$s
Cordiali saluti,
%3$s
%1$s 様,
%4$s であなたのパスワードリセット要求がありました。
パスワードをリセットするには、このボタンをクリックしてください
または、次のURLをブラウザのアドレスバーにコピーしてください。:
%2$s
よろしくお願いします。
%3$s
%1$s さん
%4$s でパスワードをリセットするリクエストがありました。
パスワードをリセットするには、このリンクをクリックしてください。
あるいは、次の URL をブラウザのアドレスバーにコピーしてください:
%2$s
===========
%3$s
Dear %1$s,
A request was made on %4$s to reset your password.
Click this to reset your password
Alternatively, copy the following URL into your browser's address bar:
%2$s
Kind regards,
%3$s
Kjære %1$s,
En forespørsel ble gjort på %4$s for å tilbakestille passordet.
Klikk her for å tilbakestille passordet
Eventuelt, kopier over følgende URL til din nettlesers adresselinje:
%2$s
Vennligst hilsen,
%3$s
Caro %1$s,
Um pedido foi feito em%4$s pra restabelecer a sua senha.
Click aqui pra restabelecer a sua senha
Alternativamente, copia o seguinte URL na barra de endereços do seu navegador:
%2$s
Atenciosamente,
%3$s
Уважемый %1$s,
Было отправлен запрос на %4$s для сброса пароля.
Нажмите эту кнопку, чтобы сбросить свой пароль
Или скопируйте следующий URL в адресную строку вашего браузера:
%2$s
С уважением,
%3$s
Уважемый %1$s,
Был отправлен запрос на %4$s для сброса пароля.
Нажмите эту кнопку, чтобы сбросить свой пароль
Или скопируйте следующий URL в адресную строку вашего браузера:
%2$s
С уважением,
%3$s
Вітаємо, %1$s!
%4$s було здійснено запит на скидання паролю.
Клацніть тут, щоб скинути пароль
Також, ви можете скопіювати наступне посилання у поле адреси вашого браузера:
%2$s
З найкращими побажаннями,
%3$s