From 49da2ffbce11957102a7a6a24287cfa214048080 Mon Sep 17 00:00:00 2001 From: Keith Carangelo Date: Fri, 23 Oct 2020 09:49:40 -0400 Subject: [PATCH 001/155] Ignore plugins except for those installed by default --- .gitignore | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.gitignore b/.gitignore index b21d2118f..5b647aa1d 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,17 @@ phpmd.html phpdoc.xml # User plugin configuration +plugins/* +!addlink_toolbar +!archiveorg +!default_colors +!demo_plugin +!isso +!piwik +!playvideos +!pubsubhubbub +!qrcode +!wallabag plugins/*/config.php plugins/default_colors/default_colors.css From 6f9e0609f4c118142504234ebcc7d93456b5e588 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 12 Nov 2020 13:05:19 +0100 Subject: [PATCH 002/155] Update badge versions --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 46dda8d5e..711980329 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ _It is designed to be personal (single-user), fast and handy._ [![](https://img.shields.io/badge/stable-v0.11.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.11.1) [![](https://img.shields.io/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli) • -[![](https://img.shields.io/badge/latest-v0.12.0-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.12.0) +[![](https://img.shields.io/badge/latest-v0.12.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.12.1) [![](https://img.shields.io/travis/shaarli/Shaarli/latest.svg?label=latest)](https://travis-ci.org/shaarli/Shaarli) • [![](https://img.shields.io/badge/master-v0.12.x-blue.svg)](https://github.com/shaarli/Shaarli) From 831e974ea5fa93d689e65313f84d0c80999674c3 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 12 Nov 2020 13:16:20 +0100 Subject: [PATCH 003/155] Doc: fix missing merge on Release page --- doc/md/dev/Release-Shaarli.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/md/dev/Release-Shaarli.md b/doc/md/dev/Release-Shaarli.md index 2c7724067..d79be9ce6 100644 --- a/doc/md/dev/Release-Shaarli.md +++ b/doc/md/dev/Release-Shaarli.md @@ -64,6 +64,14 @@ git pull upstream master # If releasing a new minor version, create a release branch $ git checkout -b v0.x +# Otherwise just use the existing one +$ git checkout v0.x + +# Get the latest changes +$ git merge master + +# Check that everything went fine: +$ make test # Bump shaarli_version.php from dev to 0.x.0, **without the v** $ vim shaarli_version.php From 150f2a0f2443e1aa5d993dd2e7126a2db86fe591 Mon Sep 17 00:00:00 2001 From: prog-it Date: Sat, 14 Nov 2020 07:45:10 +0500 Subject: [PATCH 004/155] Add russian language selection --- application/Languages.php | 1 + 1 file changed, 1 insertion(+) diff --git a/application/Languages.php b/application/Languages.php index 60e916317..7177db2cc 100644 --- a/application/Languages.php +++ b/application/Languages.php @@ -186,6 +186,7 @@ public static function getAvailableLanguages() 'en' => t('English'), 'fr' => t('French'), 'jp' => t('Japanese'), + 'ru' => t('Russian'), ]; } } From 1595d0e2b3b399bf248148fecf0ecc76e3da5d62 Mon Sep 17 00:00:00 2001 From: prog-it Date: Sun, 15 Nov 2020 06:16:55 +0500 Subject: [PATCH 005/155] Add russian language file --- inc/languages/ru/LC_MESSAGES/shaarli.po | 1944 +++++++++++++++++++++++ 1 file changed, 1944 insertions(+) create mode 100755 inc/languages/ru/LC_MESSAGES/shaarli.po diff --git a/inc/languages/ru/LC_MESSAGES/shaarli.po b/inc/languages/ru/LC_MESSAGES/shaarli.po new file mode 100755 index 000000000..98e704252 --- /dev/null +++ b/inc/languages/ru/LC_MESSAGES/shaarli.po @@ -0,0 +1,1944 @@ +msgid "" +msgstr "" +"Project-Id-Version: Shaarli\n" +"POT-Creation-Date: 2020-11-14 07:47+0500\n" +"PO-Revision-Date: 2020-11-15 06:16+0500\n" +"Last-Translator: progit \n" +"Language-Team: Shaarli\n" +"Language: ru_RU\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.0.1\n" +"X-Poedit-Basepath: ../../../..\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Poedit-SourceCharset: UTF-8\n" +"X-Poedit-KeywordsList: t:1,2;t\n" +"X-Poedit-SearchPath-0: application\n" +"X-Poedit-SearchPath-1: tmp\n" +"X-Poedit-SearchPath-2: index.php\n" +"X-Poedit-SearchPath-3: init.php\n" +"X-Poedit-SearchPath-4: plugins\n" + +#: application/History.php:181 +msgid "History file isn't readable or writable" +msgstr "Файл истории не доступен для чтения или записи" + +#: application/History.php:192 +msgid "Could not parse history file" +msgstr "Не удалось разобрать файл истории" + +#: application/Languages.php:184 +msgid "Automatic" +msgstr "Автоматический" + +#: application/Languages.php:185 +msgid "German" +msgstr "Немецкий" + +#: application/Languages.php:186 +msgid "English" +msgstr "Английский" + +#: application/Languages.php:187 +msgid "French" +msgstr "Французский" + +#: application/Languages.php:188 +msgid "Japanese" +msgstr "Японский" + +#: application/Languages.php:189 +msgid "Russian" +msgstr "Русский" + +#: application/Thumbnailer.php:62 +msgid "" +"php-gd extension must be loaded to use thumbnails. Thumbnails are now " +"disabled. Please reload the page." +msgstr "" +"для использования миниатюр необходимо загрузить расширение php-gd. Миниатюры " +"сейчас отключены. Перезагрузите страницу." + +#: application/Utils.php:405 +msgid "Setting not set" +msgstr "Настройка не задана" + +#: application/Utils.php:412 +msgid "Unlimited" +msgstr "Неограниченно" + +#: application/Utils.php:415 +msgid "B" +msgstr "Б" + +#: application/Utils.php:415 +msgid "kiB" +msgstr "КБ" + +#: application/Utils.php:415 +msgid "MiB" +msgstr "МБ" + +#: application/Utils.php:415 +msgid "GiB" +msgstr "ГБ" + +#: application/bookmark/BookmarkFileService.php:185 +#: application/bookmark/BookmarkFileService.php:207 +#: application/bookmark/BookmarkFileService.php:229 +#: application/bookmark/BookmarkFileService.php:243 +msgid "You're not authorized to alter the datastore" +msgstr "У вас нет прав на изменение хранилища данных" + +#: application/bookmark/BookmarkFileService.php:210 +msgid "This bookmarks already exists" +msgstr "Эта закладка уже существует" + +#: application/bookmark/BookmarkInitializer.php:42 +msgid "(private bookmark with thumbnail demo)" +msgstr "(личная закладка с показом миниатюр)" + +#: application/bookmark/BookmarkInitializer.php:45 +msgid "" +"Shaarli will automatically pick up the thumbnail for links to a variety of " +"websites.\n" +"\n" +"Explore your new Shaarli instance by trying out controls and menus.\n" +"Visit the project on [Github](https://github.com/shaarli/Shaarli) or [the " +"documentation](https://shaarli.readthedocs.io/en/master/) to learn more " +"about Shaarli.\n" +"\n" +"Now you can edit or delete the default shaares.\n" +msgstr "" +"Shaarli автоматически подберет миниатюру для ссылок на различные сайты.\n" +"\n" +"Изучите Shaarli, попробовав элементы управления и меню.\n" +"Посетите проект [Github](https://github.com/shaarli/Shaarli) или " +"[документацию](https://shaarli.readthedocs.io/en/master/),чтобы узнать " +"больше о Shaarli.\n" +"\n" +"Теперь вы можете редактировать или удалять шаары по умолчанию.\n" + +#: application/bookmark/BookmarkInitializer.php:58 +msgid "Note: Shaare descriptions" +msgstr "Примечание: описания Шаар" + +#: application/bookmark/BookmarkInitializer.php:60 +msgid "" +"Adding a shaare without entering a URL creates a text-only \"note\" post " +"such as this one.\n" +"This note is private, so you are the only one able to see it while logged " +"in.\n" +"\n" +"You can use this to keep notes, post articles, code snippets, and much " +"more.\n" +"\n" +"The Markdown formatting setting allows you to format your notes and bookmark " +"description:\n" +"\n" +"### Title headings\n" +"\n" +"#### Multiple headings levels\n" +" * bullet lists\n" +" * _italic_ text\n" +" * **bold** text\n" +" * ~~strike through~~ text\n" +" * `code` blocks\n" +" * images\n" +" * [links](https://en.wikipedia.org/wiki/Markdown)\n" +"\n" +"Markdown also supports tables:\n" +"\n" +"| Name | Type | Color | Qty |\n" +"| ------- | --------- | ------ | ----- |\n" +"| Orange | Fruit | Orange | 126 |\n" +"| Apple | Fruit | Any | 62 |\n" +"| Lemon | Fruit | Yellow | 30 |\n" +"| Carrot | Vegetable | Red | 14 |\n" +msgstr "" +"При добавлении закладки без ввода URL адреса создается текстовая \"заметка" +"\", такая как эта.\n" +"Эта заметка является личной, поэтому вы единственный, кто может ее увидеть, " +"находясь в системе.\n" +"\n" +"Вы можете использовать это для хранения заметок, публикации статей, " +"фрагментов кода и многого другого.\n" +"\n" +"Параметр форматирования Markdown позволяет форматировать заметки и описание " +"закладок:\n" +"\n" +"### Заголовок заголовков\n" +"\n" +"#### Multiple headings levels\n" +" * маркированные списки\n" +" * _наклонный_ текст\n" +" * **жирный** текст\n" +" * ~~зачеркнутый~~ текст\n" +" * блоки `кода`\n" +" * изображения\n" +" * [ссылки](https://en.wikipedia.org/wiki/Markdown)\n" +"\n" +"Markdown также поддерживает таблицы:\n" +"\n" +"| Имя | Тип | Цвет | Количество |\n" +"| ------- | --------- | ------ | ----- |\n" +"| Апельсин | Фрукт | Оранжевый | 126 |\n" +"| Яблоко | Фрукт | Любой | 62 |\n" +"| Лимон | Фрукт | Желтый | 30 |\n" +"| Морковь | Овощ | Красный | 14 |\n" + +#: application/bookmark/BookmarkInitializer.php:94 +#: application/legacy/LegacyLinkDB.php:246 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48 +msgid "" +"The personal, minimalist, super-fast, database free, bookmarking service" +msgstr "Личный, минималистичный, сверхбыстрый сервис закладок без баз данных" + +#: application/bookmark/BookmarkInitializer.php:97 +msgid "" +"Welcome to Shaarli!\n" +"\n" +"Shaarli allows you to bookmark your favorite pages, and share them with " +"others or store them privately.\n" +"You can add a description to your bookmarks, such as this one, and tag " +"them.\n" +"\n" +"Create a new shaare by clicking the `+Shaare` button, or using any of the " +"recommended tools (browser extension, mobile app, bookmarklet, REST API, " +"etc.).\n" +"\n" +"You can easily retrieve your links, even with thousands of them, using the " +"internal search engine, or search through tags (e.g. this Shaare is tagged " +"with `shaarli` and `help`).\n" +"Hashtags such as #shaarli #help are also supported.\n" +"You can also filter the available [RSS feed](/feed/atom) and picture wall by " +"tag or plaintext search.\n" +"\n" +"We hope that you will enjoy using Shaarli, maintained with ❤️ by the " +"community!\n" +"Feel free to open [an issue](https://github.com/shaarli/Shaarli/issues) if " +"you have a suggestion or encounter an issue.\n" +msgstr "" +"Добро пожаловать в Shaarli!\n" +"\n" +"Shaarli позволяет добавлять в закладки свои любимые страницы и делиться ими " +"с другими или хранить их в частном порядке.\n" +"Вы можете добавить описание к своим закладкам, например этой, и пометить " +"их.\n" +"\n" +"Создайте новую закладку, нажав кнопку `+Поделиться`, или используя любой из " +"рекомендуемых инструментов (расширение для браузера, мобильное приложение, " +"букмарклет, REST API и т.д.).\n" +"\n" +"Вы можете легко получить свои ссылки, даже если их тысячи, с помощью " +"внутренней поисковой системы или поиска по тегам (например, эта заметка " +"помечена тегами `shaarli` and `help`).\n" +"Также поддерживаются хэштеги, такие как #shaarli #help.\n" +"Вы можете также фильтровать доступный [RSS канал](/feed/atom) и галерею по " +"тегу или по поиску текста.\n" +"\n" +"Мы надеемся, что вам понравится использовать Shaarli, с ❤️ поддерживаемый " +"сообществом!\n" +"Не стесняйтесь открывать [запрос](https://github.com/shaarli/Shaarli/" +"issues), если у вас есть предложение или возникла проблема.\n" + +#: application/bookmark/exception/BookmarkNotFoundException.php:14 +msgid "The link you are trying to reach does not exist or has been deleted." +msgstr "" +"Ссылка, по которой вы пытаетесь перейти, не существует или была удалена." + +#: application/config/ConfigJson.php:52 application/config/ConfigPhp.php:131 +msgid "" +"Shaarli could not create the config file. Please make sure Shaarli has the " +"right to write in the folder is it installed in." +msgstr "" +"Shaarli не удалось создать файл конфигурации. Убедитесь, что у Shaarli есть " +"право на запись в папку, в которой он установлен." + +#: application/config/ConfigManager.php:137 +#: application/config/ConfigManager.php:164 +msgid "Invalid setting key parameter. String expected, got: " +msgstr "Неверная настройка ключевого параметра. Ожидалась строка, получено: " + +#: application/config/exception/MissingFieldConfigException.php:20 +#, php-format +msgid "Configuration value is required for %s" +msgstr "Значение конфигурации требуется для %s" + +#: application/config/exception/PluginConfigOrderException.php:15 +msgid "An error occurred while trying to save plugins loading order." +msgstr "Произошла ошибка при попытке сохранить порядок загрузки плагинов." + +#: application/config/exception/UnauthorizedConfigException.php:15 +msgid "You are not authorized to alter config." +msgstr "Вы не авторизованы для изменения конфигурации." + +#: application/exceptions/IOException.php:23 +msgid "Error accessing" +msgstr "Ошибка доступа" + +#: application/feed/FeedBuilder.php:180 +msgid "Direct link" +msgstr "Прямая ссылка" + +#: application/feed/FeedBuilder.php:182 +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103 +#: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179 +msgid "Permalink" +msgstr "Постоянная ссылка" + +#: application/front/controller/admin/ConfigureController.php:56 +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 +msgid "Configure" +msgstr "Настройка" + +#: application/front/controller/admin/ConfigureController.php:106 +#: application/legacy/LegacyUpdater.php:539 +msgid "You have enabled or changed thumbnails mode." +msgstr "Вы включили или изменили режим миниатюр." + +#: application/front/controller/admin/ConfigureController.php:108 +#: application/front/controller/admin/ServerController.php:76 +#: application/legacy/LegacyUpdater.php:540 +msgid "Please synchronize them." +msgstr "Пожалуйста, синхронизируйте их." + +#: application/front/controller/admin/ConfigureController.php:119 +#: application/front/controller/visitor/InstallController.php:149 +msgid "Error while writing config file after configuration update." +msgstr "Ошибка при записи файла конфигурации после обновления конфигурации." + +#: application/front/controller/admin/ConfigureController.php:128 +msgid "Configuration was saved." +msgstr "Конфигурация сохранена." + +#: application/front/controller/admin/ExportController.php:26 +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:64 +msgid "Export" +msgstr "Экспорт" + +#: application/front/controller/admin/ExportController.php:42 +msgid "Please select an export mode." +msgstr "Выберите режим экспорта." + +#: application/front/controller/admin/ImportController.php:41 +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 +msgid "Import" +msgstr "Импорт" + +#: application/front/controller/admin/ImportController.php:55 +msgid "No import file provided." +msgstr "Файл импорта не предоставлен." + +#: application/front/controller/admin/ImportController.php:66 +#, php-format +msgid "" +"The file you are trying to upload is probably bigger than what this " +"webserver can accept (%s). Please upload in smaller chunks." +msgstr "" +"Файл, который вы пытаетесь загрузить, вероятно, больше, чем может принять " +"этот сервер (%s). Пожалуйста, загружайте небольшими частями." + +#: application/front/controller/admin/ManageTagController.php:30 +msgid "whitespace" +msgstr "пробел" + +#: application/front/controller/admin/ManageTagController.php:35 +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 +msgid "Manage tags" +msgstr "Управление тегами" + +#: application/front/controller/admin/ManageTagController.php:54 +msgid "Invalid tags provided." +msgstr "Предоставлены недействительные теги." + +#: application/front/controller/admin/ManageTagController.php:78 +#, php-format +msgid "The tag was removed from %d bookmark." +msgid_plural "The tag was removed from %d bookmarks." +msgstr[0] "Тег был удален из %d закладки." +msgstr[1] "Тег был удален из %d закладок." +msgstr[2] "Тег был удален из %d закладок." + +#: application/front/controller/admin/ManageTagController.php:83 +#, php-format +msgid "The tag was renamed in %d bookmark." +msgid_plural "The tag was renamed in %d bookmarks." +msgstr[0] "Тег был переименован в %d закладке." +msgstr[1] "Тег был переименован в %d закладках." +msgstr[2] "Тег был переименован в %d закладках." + +#: application/front/controller/admin/ManageTagController.php:105 +msgid "Tags separator must be a single character." +msgstr "Разделитель тегов должен состоять из одного символа." + +#: application/front/controller/admin/ManageTagController.php:111 +msgid "These characters are reserved and can't be used as tags separator: " +msgstr "" +"Эти символы зарезервированы и не могут использоваться в качестве разделителя " +"тегов: " + +#: application/front/controller/admin/PasswordController.php:28 +#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 +msgid "Change password" +msgstr "Изменить пароль" + +#: application/front/controller/admin/PasswordController.php:55 +msgid "You must provide the current and new password to change it." +msgstr "Вы должны предоставить текущий и новый пароль, чтобы изменить его." + +#: application/front/controller/admin/PasswordController.php:71 +msgid "The old password is not correct." +msgstr "Старый пароль неверен." + +#: application/front/controller/admin/PasswordController.php:97 +msgid "Your password has been changed" +msgstr "Пароль изменен" + +#: application/front/controller/admin/PluginsController.php:45 +msgid "Plugin Administration" +msgstr "Управление плагинами" + +#: application/front/controller/admin/PluginsController.php:76 +msgid "Setting successfully saved." +msgstr "Настройка успешно сохранена." + +#: application/front/controller/admin/PluginsController.php:79 +msgid "Error while saving plugin configuration: " +msgstr "Ошибка при сохранении конфигурации плагина: " + +#: application/front/controller/admin/ServerController.php:35 +msgid "Check disabled" +msgstr "Проверка отключена" + +#: application/front/controller/admin/ServerController.php:57 +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +msgid "Server administration" +msgstr "Администрирование сервера" + +#: application/front/controller/admin/ServerController.php:74 +msgid "Thumbnails cache has been cleared." +msgstr "Кэш миниатюр очищен." + +#: application/front/controller/admin/ServerController.php:85 +msgid "Shaarli's cache folder has been cleared!" +msgstr "Папка с кэшем Shaarli очищена!" + +#: application/front/controller/admin/ShaareAddController.php:26 +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 +msgid "Shaare a new link" +msgstr "Поделиться новой ссылкой" + +#: application/front/controller/admin/ShaareManageController.php:35 +#: application/front/controller/admin/ShaareManageController.php:93 +msgid "Invalid bookmark ID provided." +msgstr "Указан неверный идентификатор закладки." + +#: application/front/controller/admin/ShaareManageController.php:47 +#: application/front/controller/admin/ShaareManageController.php:116 +#: application/front/controller/admin/ShaareManageController.php:156 +#: application/front/controller/admin/ShaarePublishController.php:82 +#, php-format +msgid "Bookmark with identifier %s could not be found." +msgstr "Закладка с идентификатором %s не найдена." + +#: application/front/controller/admin/ShaareManageController.php:101 +msgid "Invalid visibility provided." +msgstr "Предоставлена недопустимая видимость." + +#: application/front/controller/admin/ShaarePublishController.php:173 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171 +msgid "Edit" +msgstr "Редактировать" + +#: application/front/controller/admin/ShaarePublishController.php:176 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28 +msgid "Shaare" +msgstr "Поделиться" + +#: application/front/controller/admin/ShaarePublishController.php:208 +msgid "Note: " +msgstr "Заметка: " + +#: application/front/controller/admin/ThumbnailsController.php:37 +#: tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 +msgid "Thumbnails update" +msgstr "Обновление миниатюр" + +#: application/front/controller/admin/ToolsController.php:31 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:33 +msgid "Tools" +msgstr "Инструменты" + +#: application/front/controller/visitor/BookmarkListController.php:121 +msgid "Search: " +msgstr "Поиск: " + +#: application/front/controller/visitor/DailyController.php:200 +msgid "day" +msgstr "день" + +#: application/front/controller/visitor/DailyController.php:200 +#: application/front/controller/visitor/DailyController.php:203 +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:48 +msgid "Daily" +msgstr "За день" + +#: application/front/controller/visitor/DailyController.php:201 +msgid "week" +msgstr "неделя" + +#: application/front/controller/visitor/DailyController.php:201 +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 +msgid "Weekly" +msgstr "За неделю" + +#: application/front/controller/visitor/DailyController.php:202 +msgid "month" +msgstr "месяц" + +#: application/front/controller/visitor/DailyController.php:202 +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 +msgid "Monthly" +msgstr "За месяц" + +#: application/front/controller/visitor/ErrorController.php:30 +msgid "Error: " +msgstr "Ошибка: " + +#: application/front/controller/visitor/ErrorController.php:34 +msgid "Please report it on Github." +msgstr "Пожалуйста, сообщите об этом на Github." + +#: application/front/controller/visitor/ErrorController.php:39 +msgid "An unexpected error occurred." +msgstr "Произошла непредвиденная ошибка." + +#: application/front/controller/visitor/ErrorNotFoundController.php:25 +msgid "Requested page could not be found." +msgstr "Запрошенная страница не может быть найдена." + +#: application/front/controller/visitor/InstallController.php:65 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 +msgid "Install Shaarli" +msgstr "Установить Shaarli" + +#: application/front/controller/visitor/InstallController.php:85 +#, php-format +msgid "" +"
Sessions do not seem to work correctly on your server.
Make sure the " +"variable \"session.save_path\" is set correctly in your PHP config, and that " +"you have write access to it.
It currently points to %s.
On some " +"browsers, accessing your server via a hostname like 'localhost' or any " +"custom hostname without a dot causes cookie storage to fail. We recommend " +"accessing your server via it's IP address or Fully Qualified Domain Name.
" +msgstr "" +"
Сессии на вашем сервере работают некорректно.
Убедитесь, что " +"переменная \"session.save_path\" правильно установлена в вашей конфигурации " +"PHP и что у вас есть доступ к ней на запись.
В настоящее время она " +"указывает на %s.
В некоторых браузерах доступ к вашему серверу через имя " +"хоста, например localhost или любое другое имя хоста без точки, приводит к " +"сбою хранилища файлов cookie. Мы рекомендуем получить доступ к вашему " +"серверу через его IP адрес или полное доменное имя.
" + +#: application/front/controller/visitor/InstallController.php:157 +msgid "" +"Shaarli is now configured. Please login and start shaaring your bookmarks!" +msgstr "Shaarli настроен. Войдите и начните делиться своими закладками!" + +#: application/front/controller/visitor/InstallController.php:171 +msgid "Insufficient permissions:" +msgstr "Недостаточно разрешений:" + +#: application/front/controller/visitor/LoginController.php:46 +#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 +#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:77 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:101 +msgid "Login" +msgstr "Вход" + +#: application/front/controller/visitor/LoginController.php:78 +msgid "Wrong login/password." +msgstr "Неверный логин или пароль." + +#: application/front/controller/visitor/PictureWallController.php:29 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:43 +msgid "Picture wall" +msgstr "Галерея" + +#: application/front/controller/visitor/TagCloudController.php:90 +msgid "Tag " +msgstr "Тег " + +#: application/front/exceptions/AlreadyInstalledException.php:11 +msgid "Shaarli has already been installed. Login to edit the configuration." +msgstr "Shaarli уже установлен. Войдите, чтобы изменить конфигурацию." + +#: application/front/exceptions/LoginBannedException.php:11 +msgid "" +"You have been banned after too many failed login attempts. Try again later." +msgstr "" +"Вы были заблокированы из-за большого количества неудачных попыток входа в " +"систему. Попробуйте позже." + +#: application/front/exceptions/OpenShaarliPasswordException.php:16 +msgid "You are not supposed to change a password on an Open Shaarli." +msgstr "Вы не должны менять пароль на Open Shaarli." + +#: application/front/exceptions/ThumbnailsDisabledException.php:11 +msgid "Picture wall unavailable (thumbnails are disabled)." +msgstr "Галерея недоступна (миниатюры отключены)." + +#: application/front/exceptions/WrongTokenException.php:16 +msgid "Wrong token." +msgstr "Неправильный токен." + +#: application/helper/ApplicationUtils.php:163 +#, php-format +msgid "" +"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus " +"cannot run. Your PHP version has known security vulnerabilities and should " +"be updated as soon as possible." +msgstr "" +"Ваша версия PHP устарела! Shaarli требует как минимум PHP %s, и поэтому не " +"может работать. В вашей версии PHP есть известные уязвимости в системе " +"безопасности, и ее следует обновить как можно скорее." + +#: application/helper/ApplicationUtils.php:198 +#: application/helper/ApplicationUtils.php:218 +msgid "directory is not readable" +msgstr "папка не доступна для чтения" + +#: application/helper/ApplicationUtils.php:221 +msgid "directory is not writable" +msgstr "папка не доступна для записи" + +#: application/helper/ApplicationUtils.php:245 +msgid "file is not readable" +msgstr "файл не доступен для чтения" + +#: application/helper/ApplicationUtils.php:248 +msgid "file is not writable" +msgstr "файл не доступен для записи" + +#: application/helper/ApplicationUtils.php:282 +msgid "Configuration parsing" +msgstr "Разбор конфигурации" + +#: application/helper/ApplicationUtils.php:283 +msgid "Slim Framework (routing, etc.)" +msgstr "Slim Framework (маршрутизация и т. д.)" + +#: application/helper/ApplicationUtils.php:284 +msgid "Multibyte (Unicode) string support" +msgstr "Поддержка многобайтовых (Unicode) строк" + +#: application/helper/ApplicationUtils.php:285 +msgid "Required to use thumbnails" +msgstr "Обязательно использование миниатюр" + +#: application/helper/ApplicationUtils.php:286 +msgid "Localized text sorting (e.g. e->è->f)" +msgstr "Локализованная сортировка текста (например, e->è->f)" + +#: application/helper/ApplicationUtils.php:287 +msgid "Better retrieval of bookmark metadata and thumbnail" +msgstr "Лучшее получение метаданных закладок и миниатюр" + +#: application/helper/ApplicationUtils.php:288 +msgid "Use the translation system in gettext mode" +msgstr "Используйте систему перевода в режиме gettext" + +#: application/helper/ApplicationUtils.php:289 +msgid "Login using LDAP server" +msgstr "Вход через LDAP сервер" + +#: application/helper/DailyPageHelper.php:172 +msgid "Week" +msgstr "Неделя" + +#: application/helper/DailyPageHelper.php:176 +msgid "Today" +msgstr "Сегодня" + +#: application/helper/DailyPageHelper.php:178 +msgid "Yesterday" +msgstr "Вчера" + +#: application/helper/FileUtils.php:100 +msgid "Provided path is not a directory." +msgstr "Указанный путь не является папкой." + +#: application/helper/FileUtils.php:104 +msgid "Trying to delete a folder outside of Shaarli path." +msgstr "Попытка удалить папку за пределами пути Shaarli." + +#: application/legacy/LegacyLinkDB.php:131 +msgid "You are not authorized to add a link." +msgstr "Вы не авторизованы для изменения ссылки." + +#: application/legacy/LegacyLinkDB.php:134 +msgid "Internal Error: A link should always have an id and URL." +msgstr "Внутренняя ошибка: ссылка всегда должна иметь идентификатор и URL." + +#: application/legacy/LegacyLinkDB.php:137 +msgid "You must specify an integer as a key." +msgstr "В качестве ключа необходимо указать целое число." + +#: application/legacy/LegacyLinkDB.php:140 +msgid "Array offset and link ID must be equal." +msgstr "Смещение массива и идентификатор ссылки должны быть одинаковыми." + +#: application/legacy/LegacyLinkDB.php:249 +msgid "" +"Welcome to Shaarli! This is your first public bookmark. To edit or delete " +"me, you must first login.\n" +"\n" +"To learn how to use Shaarli, consult the link \"Documentation\" at the " +"bottom of this page.\n" +"\n" +"You use the community supported version of the original Shaarli project, by " +"Sebastien Sauvage." +msgstr "" +"Добро пожаловать в Shaarli! Это ваша первая общедоступная закладка. Чтобы " +"отредактировать или удалить меня, вы должны сначала авторизоваться.\n" +"\n" +"Чтобы узнать, как использовать Shaarli, перейдите по ссылке \"Документация\" " +"внизу этой страницы.\n" +"\n" +"Вы используете поддерживаемую сообществом версию оригинального проекта " +"Shaarli от Себастьяна Соваж." + +#: application/legacy/LegacyLinkDB.php:266 +msgid "My secret stuff... - Pastebin.com" +msgstr "Мой секрет... - Pastebin.com" + +#: application/legacy/LegacyLinkDB.php:268 +msgid "Shhhh! I'm a private link only YOU can see. You can delete me too." +msgstr "" +"Тссс! Это личная ссылка, которую видите только ВЫ. Вы тоже можете удалить " +"меня." + +#: application/legacy/LegacyUpdater.php:104 +msgid "Couldn't retrieve updater class methods." +msgstr "Не удалось получить методы класса средства обновления." + +#: application/legacy/LegacyUpdater.php:540 +msgid "" +msgstr "" + +#: application/netscape/NetscapeBookmarkUtils.php:63 +msgid "Invalid export selection:" +msgstr "Неверный выбор экспорта:" + +#: application/netscape/NetscapeBookmarkUtils.php:215 +#, php-format +msgid "File %s (%d bytes) " +msgstr "Файл %s (%d байт) " + +#: application/netscape/NetscapeBookmarkUtils.php:217 +msgid "has an unknown file format. Nothing was imported." +msgstr "имеет неизвестный формат файла. Ничего не импортировано." + +#: application/netscape/NetscapeBookmarkUtils.php:221 +#, php-format +msgid "" +"was successfully processed in %d seconds: %d bookmarks imported, %d " +"bookmarks overwritten, %d bookmarks skipped." +msgstr "" +"успешно обработано за %d секунд: %d закладок импортировано, %d закладок " +"перезаписаны, %d закладок пропущено." + +#: application/plugin/PluginManager.php:125 +msgid " [plugin incompatibility]: " +msgstr " [несовместимость плагинов]: " + +#: application/plugin/exception/PluginFileNotFoundException.php:22 +#, php-format +msgid "Plugin \"%s\" files not found." +msgstr "Файл плагина \"%s\" не найден." + +#: application/render/PageCacheManager.php:32 +#, php-format +msgid "Cannot purge %s: no directory" +msgstr "Невозможно очистить%s: нет папки" + +#: application/updater/exception/UpdaterException.php:51 +msgid "An error occurred while running the update " +msgstr "Произошла ошибка при запуске обновления " + +#: index.php:81 +msgid "Shared bookmarks on " +msgstr "Общие закладки на " + +#: plugins/addlink_toolbar/addlink_toolbar.php:31 +msgid "URI" +msgstr "URI" + +#: plugins/addlink_toolbar/addlink_toolbar.php:35 +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20 +msgid "Add link" +msgstr "Добавить ссылку" + +#: plugins/addlink_toolbar/addlink_toolbar.php:52 +msgid "Adds the addlink input on the linklist page." +msgstr "" +"Добавляет на страницу списка ссылок поле для добавления новой закладки." + +#: plugins/archiveorg/archiveorg.php:29 +msgid "View on archive.org" +msgstr "Посмотреть на archive.org" + +#: plugins/archiveorg/archiveorg.php:42 +msgid "For each link, add an Archive.org icon." +msgstr "Для каждой ссылки добавить значок с Archive.org." + +#: plugins/default_colors/default_colors.php:38 +msgid "" +"Default colors plugin error: This plugin is active and no custom color is " +"configured." +msgstr "" +"Ошибка плагина цветов по умолчанию: этот плагин активен, и пользовательский " +"цвет не настроен." + +#: plugins/default_colors/default_colors.php:113 +msgid "Override default theme colors. Use any CSS valid color." +msgstr "" +"Переопределить цвета темы по умолчанию. Используйте любой допустимый цвет " +"CSS." + +#: plugins/default_colors/default_colors.php:114 +msgid "Main color (navbar green)" +msgstr "Основной цвет (зеленый на панели навигации)" + +#: plugins/default_colors/default_colors.php:115 +msgid "Background color (light grey)" +msgstr "Цвет фона (светло-серый)" + +#: plugins/default_colors/default_colors.php:116 +msgid "Dark main color (e.g. visited links)" +msgstr "Темный основной цвет (например, посещенные ссылки)" + +#: plugins/demo_plugin/demo_plugin.php:478 +msgid "" +"A demo plugin covering all use cases for template designers and plugin " +"developers." +msgstr "" +"Демо плагин, охватывающий все варианты использования для дизайнеров шаблонов " +"и разработчиков плагинов." + +#: plugins/demo_plugin/demo_plugin.php:479 +msgid "This is a parameter dedicated to the demo plugin. It'll be suffixed." +msgstr "" +"Это параметр предназначен для демонстрационного плагина. Это будет суффикс." + +#: plugins/demo_plugin/demo_plugin.php:480 +msgid "Other demo parameter" +msgstr "Другой демонстрационный параметр" + +#: plugins/isso/isso.php:22 +msgid "" +"Isso plugin error: Please define the \"ISSO_SERVER\" setting in the plugin " +"administration page." +msgstr "" +"Ошибка плагина Isso: определите параметр \"ISSO_SERVER\" на странице " +"настройки плагина." + +#: plugins/isso/isso.php:92 +msgid "Let visitor comment your shaares on permalinks with Isso." +msgstr "" +"Позволить посетителю комментировать ваши закладки по постоянным ссылкам с " +"Isso." + +#: plugins/isso/isso.php:93 +msgid "Isso server URL (without 'http://')" +msgstr "URL сервера Isso (без 'http: //')" + +#: plugins/piwik/piwik.php:24 +msgid "" +"Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin " +"administration page." +msgstr "" +"Ошибка плагина Piwik: укажите PIWIK_URL и PIWIK_SITEID на странице настройки " +"плагина." + +#: plugins/piwik/piwik.php:73 +msgid "A plugin that adds Piwik tracking code to Shaarli pages." +msgstr "Плагин, который добавляет код отслеживания Piwik на страницы Shaarli." + +#: plugins/piwik/piwik.php:74 +msgid "Piwik URL" +msgstr "Piwik URL" + +#: plugins/piwik/piwik.php:75 +msgid "Piwik site ID" +msgstr "Piwik site ID" + +#: plugins/playvideos/playvideos.php:26 +msgid "Video player" +msgstr "Видео плеер" + +#: plugins/playvideos/playvideos.php:29 +msgid "Play Videos" +msgstr "Воспроизвести видео" + +#: plugins/playvideos/playvideos.php:60 +msgid "Add a button in the toolbar allowing to watch all videos." +msgstr "" +"Добавьте кнопку на панель инструментов, позволяющую смотреть все видео." + +#: plugins/playvideos/youtube_playlist.js:214 +msgid "plugins/playvideos/jquery-1.11.2.min.js" +msgstr "plugins/playvideos/jquery-1.11.2.min.js" + +#: plugins/pubsubhubbub/pubsubhubbub.php:72 +#, php-format +msgid "Could not publish to PubSubHubbub: %s" +msgstr "Не удалось опубликовать в PubSubHubbub: %s" + +#: plugins/pubsubhubbub/pubsubhubbub.php:99 +#, php-format +msgid "Could not post to %s" +msgstr "Не удалось отправить сообщение в %s" + +#: plugins/pubsubhubbub/pubsubhubbub.php:103 +#, php-format +msgid "Bad response from the hub %s" +msgstr "Плохой ответ от хаба %s" + +#: plugins/pubsubhubbub/pubsubhubbub.php:114 +msgid "Enable PubSubHubbub feed publishing." +msgstr "Включить публикацию канала PubSubHubbub." + +#: plugins/qrcode/qrcode.php:74 plugins/wallabag/wallabag.php:72 +msgid "For each link, add a QRCode icon." +msgstr "Для каждой ссылки добавить значок QR кода." + +#: plugins/wallabag/wallabag.php:22 +msgid "" +"Wallabag plugin error: Please define the \"WALLABAG_URL\" setting in the " +"plugin administration page." +msgstr "" +"Ошибка плагина Wallabag: определите параметр \"WALLABAG_URL\" на странице " +"настройки плагина." + +#: plugins/wallabag/wallabag.php:49 +msgid "Save to wallabag" +msgstr "Сохранить в wallabag" + +#: plugins/wallabag/wallabag.php:73 +msgid "Wallabag API URL" +msgstr "Wallabag API URL" + +#: plugins/wallabag/wallabag.php:74 +msgid "Wallabag API version (1 or 2)" +msgstr "Wallabag версия API (1 или 2)" + +#: tmp/404.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12 +msgid "Sorry, nothing to see here." +msgstr "Извините, тут ничего нет." + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "URL or leave empty to post a note" +msgstr "URL или оставьте пустым, чтобы опубликовать заметку" + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 +msgid "BULK CREATION" +msgstr "МАССОВОЕ СОЗДАНИЕ" + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 +msgid "Metadata asynchronous retrieval is disabled." +msgstr "Асинхронное получение метаданных отключено." + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 +msgid "" +"We recommend that you enable the setting general > " +"enable_async_metadata in your configuration file to use bulk link " +"creation." +msgstr "" +"Мы рекомендуем включить параметр general > enable_async_metadata в " +"вашем файле конфигурации, чтобы использовать массовое создание ссылок." + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56 +msgid "Shaare multiple new links" +msgstr "Поделиться несколькими новыми ссылками" + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59 +msgid "Add one URL per line to create multiple bookmarks." +msgstr "Добавьте по одному URL в строке, чтобы создать несколько закладок." + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67 +msgid "Tags" +msgstr "Теги" + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 +msgid "Private" +msgstr "Личный" + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78 +msgid "Add links" +msgstr "Добавить ссылки" + +#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "Current password" +msgstr "Текущий пароль" + +#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 +msgid "New password" +msgstr "Новый пароль" + +#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 +msgid "Change" +msgstr "Изменить" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 +msgid "Tag" +msgstr "Тег" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 +msgid "New name" +msgstr "Новое имя" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31 +msgid "Case sensitive" +msgstr "С учетом регистра" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 +#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68 +msgid "Rename tag" +msgstr "Переименовать тег" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 +msgid "Delete tag" +msgstr "Удалить тег" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 +msgid "You can also edit tags in the" +msgstr "Вы также можете редактировать теги в" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 +msgid "tag list" +msgstr "список тегов" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 +msgid "Change tags separator" +msgstr "Изменить разделитель тегов" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50 +msgid "Your current tag separator is" +msgstr "Текущий разделитель тегов" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53 +msgid "New separator" +msgstr "Новый разделитель" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:355 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 +msgid "Save" +msgstr "Сохранить" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:61 +msgid "Note that hashtags won't fully work with a non-whitespace separator." +msgstr "" +"Обратите внимание, что хэштеги не будут полностью работать с разделителем, " +"отличным от пробелов." + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 +msgid "title" +msgstr "заголовок" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 +msgid "Home link" +msgstr "Домашняя ссылка" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 +msgid "Default value" +msgstr "Значение по умолчанию" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 +msgid "Theme" +msgstr "Тема" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:85 +msgid "Description formatter" +msgstr "Средство форматирования описания" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 +msgid "Language" +msgstr "Язык" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:143 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101 +msgid "Timezone" +msgstr "Часовой пояс" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 +msgid "Continent" +msgstr "Континент" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 +msgid "City" +msgstr "Город" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:191 +msgid "Disable session cookie hijacking protection" +msgstr "Отключить защиту от перехвата файлов сеанса cookie" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:193 +msgid "Check this if you get disconnected or if your IP address changes often" +msgstr "Проверьте это, если вы отключаетесь или ваш IP адрес часто меняется" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:210 +msgid "Private links by default" +msgstr "Приватные ссылки по умолчанию" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:211 +msgid "All new links are private by default" +msgstr "Все новые ссылки по умолчанию являются приватными" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:226 +msgid "RSS direct links" +msgstr "RSS прямые ссылки" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:227 +msgid "Check this to use direct URL instead of permalink in feeds" +msgstr "" +"Установите этот флажок, чтобы использовать прямой URL вместо постоянной " +"ссылки в фидах" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:242 +msgid "Hide public links" +msgstr "Скрыть общедоступные ссылки" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:243 +msgid "Do not show any links if the user is not logged in" +msgstr "Не показывать ссылки, если пользователь не авторизован" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:258 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:149 +msgid "Check updates" +msgstr "Проверить обновления" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:259 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:151 +msgid "Notify me when a new release is ready" +msgstr "Оповестить, когда будет готов новый выпуск" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:274 +msgid "Automatically retrieve description for new bookmarks" +msgstr "Автоматически получать описание для новых закладок" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:275 +msgid "Shaarli will try to retrieve the description from meta HTML headers" +msgstr "Shaarli попытается получить описание из мета заголовков HTML" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:290 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168 +msgid "Enable REST API" +msgstr "Включить REST API" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:291 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 +msgid "Allow third party software to use Shaarli such as mobile application" +msgstr "" +"Разрешить стороннему программному обеспечению использовать Shaarli, например " +"мобильное приложение" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:306 +msgid "API secret" +msgstr "API ключ" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:320 +msgid "Enable thumbnails" +msgstr "Включить миниатюры" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:324 +msgid "You need to enable the extension php-gd to use thumbnails." +msgstr "" +"Вам необходимо включить расширение php-gd для использования " +"миниатюр." + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328 +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122 +msgid "Synchronize thumbnails" +msgstr "Синхронизировать миниатюры" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:339 +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 +msgid "All" +msgstr "Все" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:343 +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106 +msgid "Only common media hosts" +msgstr "Только обычные медиа хосты" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:347 +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110 +msgid "None" +msgstr "Ничего" + +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 +msgid "1 RSS entry per :type" +msgid_plural "" +msgstr[0] "1 RSS запись для каждого :type" +msgstr[1] "1 RSS запись для каждого :type" +msgstr[2] "1 RSS запись для каждого :type" + +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 +msgid "Previous :type" +msgid_plural "" +msgstr[0] "Предыдущий :type" +msgstr[1] "Предыдущих :type" +msgstr[2] "Предыдущих :type" + +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56 +#: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7 +msgid "All links of one :type in a single page." +msgid_plural "" +msgstr[0] "Все ссылки одного :type на одной странице." +msgstr[1] "Все ссылки одного :type на одной странице." +msgstr[2] "Все ссылки одного :type на одной странице." + +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63 +msgid "Next :type" +msgid_plural "" +msgstr[0] "Следующий :type" +msgstr[1] "Следующие :type" +msgstr[2] "Следующие :type" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 +msgid "Edit Shaare" +msgstr "Изменить закладку" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 +msgid "New Shaare" +msgstr "Новая закладка" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38 +msgid "Created:" +msgstr "Создано:" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 +msgid "URL" +msgstr "URL" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 +msgid "Title" +msgstr "Заголовок" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:124 +msgid "Description" +msgstr "Описание" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:89 +msgid "Description will be rendered with" +msgstr "Описание будет отображаться с" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91 +msgid "Markdown syntax documentation" +msgstr "Документация по синтаксису Markdown" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92 +msgid "Markdown syntax" +msgstr "Синтаксис Markdown" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:115 +msgid "Cancel" +msgstr "Отменить" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121 +msgid "Apply Changes" +msgstr "Применить изменения" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:126 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147 +#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67 +msgid "Delete" +msgstr "Удалить" + +#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21 +#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32 +msgid "Save all" +msgstr "Сохранить все" + +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "Export Database" +msgstr "Экспорт базы данных" + +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 +msgid "Selection" +msgstr "Выбор" + +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 +msgid "Public" +msgstr "Общедоступно" + +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51 +msgid "Prepend note permalinks with this Shaarli instance's URL" +msgstr "" +"Добавить постоянные ссылки на заметку с URL адресом этого экземпляра Shaarli" + +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52 +msgid "Useful to import bookmarks in a web browser" +msgstr "Useful to import bookmarks in a web browser" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "Import Database" +msgstr "Импорт базы данных" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 +msgid "Maximum size allowed:" +msgstr "Максимально допустимый размер:" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 +msgid "Visibility" +msgstr "Видимость" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 +msgid "Use values from the imported file, default to public" +msgstr "" +"Использовать значения из импортированного файла, по умолчанию общедоступные" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 +msgid "Import all bookmarks as private" +msgstr "Импортировать все закладки как личные" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 +msgid "Import all bookmarks as public" +msgstr "Импортировать все закладки как общедоступные" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:57 +msgid "Overwrite existing bookmarks" +msgstr "Заменить существующие закладки" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 +msgid "Duplicates based on URL" +msgstr "Дубликаты на основе URL" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72 +msgid "Add default tags" +msgstr "Добавить теги по умолчанию" + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25 +msgid "It looks like it's the first time you run Shaarli. Please configure it." +msgstr "Похоже, вы впервые запускаете Shaarli. Пожалуйста, настройте его." + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32 +#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:167 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:167 +msgid "Username" +msgstr "Имя пользователя" + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 +#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:168 +msgid "Password" +msgstr "Пароль" + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:62 +msgid "Shaarli title" +msgstr "Заголовок Shaarli" + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68 +msgid "My links" +msgstr "Мои ссылки" + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181 +msgid "Install" +msgstr "Установка" + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:190 +msgid "Server requirements" +msgstr "Системные требования" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79 +msgid "shaare" +msgid_plural "shaares" +msgstr[0] "закладка" +msgstr[1] "закладки" +msgstr[2] "закладок" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 +msgid "private link" +msgid_plural "private links" +msgstr[0] "личная ссылка" +msgstr[1] "личные ссылки" +msgstr[2] "личных ссылок" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:123 +msgid "Search text" +msgstr "Поиск текста" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:130 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:130 +#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 +#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65 +#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 +#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 +msgid "Filter by tag" +msgstr "Фильтровать по тегу" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:87 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:139 +#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 +#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45 +msgid "Search" +msgstr "Поиск" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110 +msgid "Nothing found." +msgstr "Ничего не найдено." + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:118 +#, php-format +msgid "%s result" +msgid_plural "%s results" +msgstr[0] "%s результат" +msgstr[1] "%s результатов" +msgstr[2] "%s результатов" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122 +msgid "for" +msgstr "для" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:129 +msgid "tagged" +msgstr "отмечено" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:133 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134 +msgid "Remove tag" +msgstr "Удалить тег" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144 +msgid "with status" +msgstr "со статусом" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155 +msgid "without any tag" +msgstr "без тега" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:41 +msgid "Fold" +msgstr "Сложить" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:177 +msgid "Edited: " +msgstr "Отредактировано: " + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181 +msgid "permalink" +msgstr "постоянная ссылка" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183 +msgid "Add tag" +msgstr "Добавить тег" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:185 +msgid "Toggle sticky" +msgstr "Закрепить / Открепить" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:187 +msgid "Sticky" +msgstr "Закреплено" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:189 +msgid "Share a private link" +msgstr "Поделиться личной ссылкой" + +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:5 +msgid "Filters" +msgstr "Фильтры" + +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:10 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:10 +msgid "Only display private links" +msgstr "Отображать только личные ссылки" + +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:13 +msgid "Only display public links" +msgstr "Отображать только общедоступные ссылки" + +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:18 +msgid "Filter untagged links" +msgstr "Фильтровать неотмеченные ссылки" + +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:24 +msgid "Select all" +msgstr "Выбрать все" + +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:89 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:29 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:89 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:42 +msgid "Fold all" +msgstr "Сложить все" + +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:76 +msgid "Links per page" +msgstr "Ссылок на страницу" + +#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:171 +msgid "Remember me" +msgstr "Запомнить меня" + +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48 +msgid "by the Shaarli community" +msgstr "сообществом Shaarli" + +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:16 +msgid "Documentation" +msgstr "Документация" + +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43 +msgid "Expand" +msgstr "Развернуть" + +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44 +msgid "Expand all" +msgstr "Развернуть все" + +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:45 +msgid "Are you sure you want to delete this link?" +msgstr "Вы уверены, что хотите удалить эту ссылку?" + +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:46 +msgid "Are you sure you want to delete this tag?" +msgstr "Вы уверены, что хотите удалить этот тег?" + +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:11 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:11 +msgid "Menu" +msgstr "Меню" + +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:38 +#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 +msgid "Tag cloud" +msgstr "Облако тегов" + +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:67 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:92 +msgid "RSS Feed" +msgstr "RSS канал" + +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:108 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:72 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:108 +msgid "Logout" +msgstr "Выйти" + +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:152 +msgid "Set public" +msgstr "Сделать общедоступным" + +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:157 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:157 +msgid "Set private" +msgstr "Сделать личным" + +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:189 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:189 +msgid "is available" +msgstr "доступно" + +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:196 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:196 +msgid "Error" +msgstr "Ошибка" + +#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 +msgid "There is no cached thumbnail." +msgstr "Нет кэшированных миниатюр." + +#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17 +msgid "Try to synchronize them." +msgstr "Попробуйте синхронизировать их." + +#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +msgid "Picture Wall" +msgstr "Галерея" + +#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +msgid "pics" +msgstr "изображений" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 +msgid "You need to enable Javascript to change plugin loading order." +msgstr "" +"Вам необходимо включить Javascript, чтобы изменить порядок загрузки плагинов." + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 +msgid "Plugin administration" +msgstr "Управление плагинами" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 +msgid "Enabled Plugins" +msgstr "Включенные плагины" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155 +msgid "No plugin enabled." +msgstr "Нет включенных плагинов." + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73 +msgid "Disable" +msgstr "Отключить" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:98 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123 +msgid "Name" +msgstr "Имя" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76 +msgid "Order" +msgstr "Порядок" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86 +msgid "Disabled Plugins" +msgstr "Отключенные плагины" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91 +msgid "No plugin disabled." +msgstr "Нет отключенных плагинов." + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:97 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122 +msgid "Enable" +msgstr "Включить" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134 +msgid "More plugins available" +msgstr "Доступны другие плагины" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:136 +msgid "in the documentation" +msgstr "в документации" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150 +msgid "Plugin configuration" +msgstr "Настройка плагинов" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:195 +msgid "No parameter available." +msgstr "Нет доступных параметров." + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "General" +msgstr "Общее" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20 +msgid "Index URL" +msgstr "Индексный URL" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +msgid "Base path" +msgstr "Базовый путь" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 +msgid "Client IP" +msgstr "IP клиента" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 +msgid "Trusted reverse proxies" +msgstr "Надежные обратные прокси" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 +msgid "N/A" +msgstr "Нет данных" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84 +msgid "Visit releases page on Github" +msgstr "Посетить страницу релизов на Github" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121 +msgid "Synchronize all link thumbnails" +msgstr "Синхронизировать все миниатюры ссылок" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:2 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:2 +msgid "Permissions" +msgstr "Разрешения" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:8 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:8 +msgid "There are permissions that need to be fixed." +msgstr "Есть разрешения, которые нужно исправить." + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:23 +msgid "All read/write permissions are properly set." +msgstr "Все разрешения на чтение и запись установлены правильно." + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:32 +msgid "Running PHP" +msgstr "Запуск PHP" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:36 +msgid "End of life: " +msgstr "Конец жизни: " + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:48 +msgid "Extension" +msgstr "Расширение" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:49 +msgid "Usage" +msgstr "Применение" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:50 +msgid "Status" +msgstr "Статус" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51 +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:51 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:66 +msgid "Loaded" +msgstr "Загружено" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60 +msgid "Required" +msgstr "Обязательно" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60 +msgid "Optional" +msgstr "Необязательно" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:70 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:70 +msgid "Not loaded" +msgstr "Не загружено" + +#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 +#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 +msgid "tags" +msgstr "теги" + +#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 +#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 +msgid "List all links with those tags" +msgstr "Список всех ссылок с этими тегами" + +#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 +msgid "Tag list" +msgstr "Список тегов" + +#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:3 +#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3 +msgid "Sort by:" +msgstr "Сортировать по:" + +#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5 +#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:5 +msgid "Cloud" +msgstr "Облако" + +#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:6 +#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:6 +msgid "Most used" +msgstr "Наиболее используемое" + +#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7 +#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:7 +msgid "Alphabetical" +msgstr "Алфавит" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 +msgid "Settings" +msgstr "Настройки" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "Change Shaarli settings: title, timezone, etc." +msgstr "Измените настройки Shaarli: заголовок, часовой пояс и т.д." + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17 +msgid "Configure your Shaarli" +msgstr "Настройка Shaarli" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21 +msgid "Enable, disable and configure plugins" +msgstr "Включить, отключить и настроить плагины" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:27 +msgid "Check instance's server configuration" +msgstr "Проверка конфигурации экземпляра сервера" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 +msgid "Change your password" +msgstr "Изменить пароль" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 +msgid "Rename or delete a tag in all links" +msgstr "Переименовать или удалить тег во всех ссылках" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 +msgid "" +"Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, " +"delicious...)" +msgstr "" +"Импорт закладок Netscape HTML (экспортированные из Firefox, Chrome, Opera, " +"delicious...)" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 +msgid "Import links" +msgstr "Импорт ссылок" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53 +msgid "" +"Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, " +"Opera, delicious...)" +msgstr "" +"Экспорт закладок Netscape HTML (которые могут быть импортированы в Firefox, " +"Chrome, Opera, delicious...)" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:54 +msgid "Export database" +msgstr "Экспорт базы данных" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 +msgid "" +"Drag one of these button to your bookmarks toolbar or right-click it and " +"\"Bookmark This Link\"" +msgstr "" +"Перетащите одну из этих кнопок на панель закладок или щелкните по ней правой " +"кнопкой мыши и выберите \"Добавить ссылку в закладки\"" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78 +msgid "then click on the bookmarklet in any page you want to share." +msgstr "" +"затем щелкните букмарклет на любой странице, которой хотите поделиться." + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106 +msgid "" +"Drag this link to your bookmarks toolbar or right-click it and Bookmark This " +"Link" +msgstr "" +"Перетащите эту ссылку на панель закладок или щелкните по ней правой кнопкой " +"мыши и добавьте эту ссылку в закладки" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 +msgid "then click ✚Shaare link button in any page you want to share" +msgstr "" +"затем нажмите кнопку ✚Поделиться ссылкой на любой странице, которой хотите " +"поделиться" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114 +msgid "The selected text is too long, it will be truncated." +msgstr "Выделенный текст слишком длинный, он будет обрезан." + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 +msgid "Shaare link" +msgstr "Поделиться ссылкой" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:107 +msgid "" +"Then click ✚Add Note button anytime to start composing a private Note (text " +"post) to your Shaarli" +msgstr "" +"Затем в любое время нажмите кнопку ✚Добавить заметку, чтобы начать создавать " +"личную заметку (текстовое сообщение) в своем Shaarli" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123 +msgid "Add Note" +msgstr "Добавить заметку" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:132 +msgid "3rd party" +msgstr "Третья сторона" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:135 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:140 +msgid "plugin" +msgstr "плагин" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:165 +msgid "" +"Drag this link to your bookmarks toolbar, or right-click it and choose " +"Bookmark This Link" +msgstr "" +"Перетащите эту ссылку на панель закладок или щелкните по ней правой кнопкой " +"мыши и выберите \"Добавить ссылку в закладки\"" From 2883c6d0a71db174ee8df7548178a8fbee486e25 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sun, 15 Nov 2020 12:05:08 +0100 Subject: [PATCH 006/155] Daily RSS - Remove relative description (today, yesterday) It is not useful for the RSS feed, as every new entry will be 'yesterday', and it requires an update the next day. --- .../controller/visitor/DailyController.php | 2 +- application/helper/DailyPageHelper.php | 18 ++++++++----- tests/helper/DailyPageHelperTest.php | 27 +++++++++++++++++++ 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php index 846cfe22a..5ae892992 100644 --- a/application/front/controller/visitor/DailyController.php +++ b/application/front/controller/visitor/DailyController.php @@ -131,7 +131,7 @@ public function rss(Request $request, Response $response): Response $dataPerDay[$day] = [ 'date' => $endDateTime, 'date_rss' => $endDateTime->format(DateTime::RSS), - 'date_human' => DailyPageHelper::getDescriptionByType($type, $dayDateTime), + 'date_human' => DailyPageHelper::getDescriptionByType($type, $dayDateTime, false), 'absolute_url' => $indexUrl . 'daily?' . $type . '=' . $day, 'links' => [], ]; diff --git a/application/helper/DailyPageHelper.php b/application/helper/DailyPageHelper.php index 5fabc9078..9bdb7ba50 100644 --- a/application/helper/DailyPageHelper.php +++ b/application/helper/DailyPageHelper.php @@ -154,16 +154,20 @@ public static function getEndDateTimeByType(string $type, \DateTimeImmutable $re * Get localized description of the time period depending on given datetime and type. * Example: for a month period, it returns `October, 2020`. * - * @param string $type month/week/day - * @param \DateTimeImmutable $requested DateTime extracted from request input - * (should come from extractRequestedDateTime) + * @param string $type month/week/day + * @param \DateTimeImmutable $requested DateTime extracted from request input + * (should come from extractRequestedDateTime) + * @param bool $includeRelative Include relative date description (today, yesterday, etc.) * * @return string Localized time period description * * @throws \Exception Type not supported. */ - public static function getDescriptionByType(string $type, \DateTimeImmutable $requested): string - { + public static function getDescriptionByType( + string $type, + \DateTimeImmutable $requested, + bool $includeRelative = true + ): string { switch ($type) { case static::MONTH: return $requested->format('F') . ', ' . $requested->format('Y'); @@ -172,9 +176,9 @@ public static function getDescriptionByType(string $type, \DateTimeImmutable $re return t('Week') . ' ' . $requested->format('W') . ' (' . format_date($requested, false) . ')'; case static::DAY: $out = ''; - if ($requested->format('Ymd') === date('Ymd')) { + if ($includeRelative && $requested->format('Ymd') === date('Ymd')) { $out = t('Today') . ' - '; - } elseif ($requested->format('Ymd') === date('Ymd', strtotime('-1 days'))) { + } elseif ($includeRelative && $requested->format('Ymd') === date('Ymd', strtotime('-1 days'))) { $out = t('Yesterday') . ' - '; } return $out . format_date($requested, false); diff --git a/tests/helper/DailyPageHelperTest.php b/tests/helper/DailyPageHelperTest.php index 5255b7b16..6238e6481 100644 --- a/tests/helper/DailyPageHelperTest.php +++ b/tests/helper/DailyPageHelperTest.php @@ -121,6 +121,19 @@ public function testGeDescriptionsByType( static::assertEquals($expectedDescription, $description); } + /** + * @dataProvider getDescriptionsByTypeNotIncludeRelative + */ + public function testGeDescriptionsByTypeNotIncludeRelative( + string $type, + \DateTimeImmutable $dateTime, + string $expectedDescription + ): void { + $description = DailyPageHelper::getDescriptionByType($type, $dateTime, false); + + static::assertEquals($expectedDescription, $description); + } + public function getDescriptionByTypeExceptionUnknownType(): void { $this->expectException(\Exception::class); @@ -248,6 +261,20 @@ public function getDescriptionsByType(): array ]; } + /** + * Data provider for testGeDescriptionsByTypeNotIncludeRelative() test method. + */ + public function getDescriptionsByTypeNotIncludeRelative(): array + { + return [ + [DailyPageHelper::DAY, $date = new \DateTimeImmutable(), $date->format('F j, Y')], + [DailyPageHelper::DAY, $date = new \DateTimeImmutable('-1 day'), $date->format('F j, Y')], + [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October 9, 2020'], + [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), 'Week 41 (October 5, 2020)'], + [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October, 2020'], + ]; + } + /** * Data provider for testGetDescriptionsByType() test method. */ From a6e9c08499f9f79dad88cb3ae9eacda0e0c34c96 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 27 Oct 2020 19:23:45 +0100 Subject: [PATCH 007/155] Plugin system: allow plugins to provide custom routes - each route will be prefixed by `/plugin/` - add a new template for plugins rendering - add a live example in the demo_plugin Check out the "Plugin System" documentation for more detail. Related to #143 --- application/container/ContainerBuilder.php | 17 ++--- application/plugin/PluginManager.php | 64 +++++++++++++++++++ .../exception/PluginInvalidRouteException.php | 26 ++++++++ doc/md/dev/Plugin-system.md | 25 ++++++++ index.php | 22 ++++++- phpcs.xml | 1 + plugins/demo_plugin/DemoPluginController.php | 24 +++++++ plugins/demo_plugin/demo_plugin.php | 19 +++++- tests/PluginManagerTest.php | 39 +++++++++++ tests/container/ContainerBuilderTest.php | 5 ++ tests/plugins/test/test.php | 16 +++++ .../test_route_invalid/test_route_invalid.php | 12 ++++ tpl/default/pluginscontent.html | 13 ++++ 13 files changed, 270 insertions(+), 13 deletions(-) create mode 100644 application/plugin/exception/PluginInvalidRouteException.php create mode 100644 plugins/demo_plugin/DemoPluginController.php create mode 100644 tests/plugins/test_route_invalid/test_route_invalid.php create mode 100644 tpl/default/pluginscontent.html diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php index f0234eca2..6d69a880f 100644 --- a/application/container/ContainerBuilder.php +++ b/application/container/ContainerBuilder.php @@ -50,6 +50,9 @@ class ContainerBuilder /** @var LoginManager */ protected $login; + /** @var PluginManager */ + protected $pluginManager; + /** @var LoggerInterface */ protected $logger; @@ -61,12 +64,14 @@ public function __construct( SessionManager $session, CookieManager $cookieManager, LoginManager $login, + PluginManager $pluginManager, LoggerInterface $logger ) { $this->conf = $conf; $this->session = $session; $this->login = $login; $this->cookieManager = $cookieManager; + $this->pluginManager = $pluginManager; $this->logger = $logger; } @@ -78,12 +83,10 @@ public function build(): ShaarliContainer $container['sessionManager'] = $this->session; $container['cookieManager'] = $this->cookieManager; $container['loginManager'] = $this->login; + $container['pluginManager'] = $this->pluginManager; $container['logger'] = $this->logger; $container['basePath'] = $this->basePath; - $container['plugins'] = function (ShaarliContainer $container): PluginManager { - return new PluginManager($container->conf); - }; $container['history'] = function (ShaarliContainer $container): History { return new History($container->conf->get('resource.history')); @@ -113,14 +116,6 @@ public function build(): ShaarliContainer ); }; - $container['pluginManager'] = function (ShaarliContainer $container): PluginManager { - $pluginManager = new PluginManager($container->conf); - - $pluginManager->load($container->conf->get('general.enabled_plugins')); - - return $pluginManager; - }; - $container['formatterFactory'] = function (ShaarliContainer $container): FormatterFactory { return new FormatterFactory( $container->conf, diff --git a/application/plugin/PluginManager.php b/application/plugin/PluginManager.php index 3ea55728c..7fc0cb047 100644 --- a/application/plugin/PluginManager.php +++ b/application/plugin/PluginManager.php @@ -4,6 +4,7 @@ use Shaarli\Config\ConfigManager; use Shaarli\Plugin\Exception\PluginFileNotFoundException; +use Shaarli\Plugin\Exception\PluginInvalidRouteException; /** * Class PluginManager @@ -26,6 +27,14 @@ class PluginManager */ private $loadedPlugins = []; + /** @var array List of registered routes. Contains keys: + * - `method`: HTTP method, GET/POST/PUT/PATCH/DELETE + * - `route` (path): without prefix, e.g. `/up/{variable}` + * It will be later prefixed by `/plugin//`. + * - `callable` string, function name or FQN class's method, e.g. `demo_plugin_custom_controller`. + */ + protected $registeredRoutes = []; + /** * @var ConfigManager Configuration Manager instance. */ @@ -86,6 +95,9 @@ public function load($authorizedPlugins) $this->loadPlugin($dirs[$index], $plugin); } catch (PluginFileNotFoundException $e) { error_log($e->getMessage()); + } catch (\Throwable $e) { + $error = $plugin . t(' [plugin incompatibility]: ') . $e->getMessage(); + $this->errors = array_unique(array_merge($this->errors, [$error])); } } } @@ -166,6 +178,22 @@ private function loadPlugin($dir, $pluginName) } } + $registerRouteFunction = $pluginName . '_register_routes'; + $routes = null; + if (function_exists($registerRouteFunction)) { + $routes = call_user_func($registerRouteFunction); + } + + if ($routes !== null) { + foreach ($routes as $route) { + if (static::validateRouteRegistration($route)) { + $this->registeredRoutes[$pluginName][] = $route; + } else { + throw new PluginInvalidRouteException($pluginName); + } + } + } + $this->loadedPlugins[] = $pluginName; } @@ -237,6 +265,14 @@ public function getPluginsMeta() return $metaData; } + /** + * @return array List of registered custom routes by plugins. + */ + public function getRegisteredRoutes(): array + { + return $this->registeredRoutes; + } + /** * Return the list of encountered errors. * @@ -246,4 +282,32 @@ public function getErrors() { return $this->errors; } + + /** + * Checks whether provided input is valid to register a new route. + * It must contain keys `method`, `route`, `callable` (all strings). + * + * @param string[] $input + * + * @return bool + */ + protected static function validateRouteRegistration(array $input): bool + { + if ( + !array_key_exists('method', $input) + || !in_array(strtoupper($input['method']), ['GET', 'PUT', 'PATCH', 'POST', 'DELETE']) + ) { + return false; + } + + if (!array_key_exists('route', $input) || !preg_match('#^[a-z\d/\.\-_]+$#', $input['route'])) { + return false; + } + + if (!array_key_exists('callable', $input)) { + return false; + } + + return true; + } } diff --git a/application/plugin/exception/PluginInvalidRouteException.php b/application/plugin/exception/PluginInvalidRouteException.php new file mode 100644 index 000000000..6ba9bc436 --- /dev/null +++ b/application/plugin/exception/PluginInvalidRouteException.php @@ -0,0 +1,26 @@ +message = 'trying to register invalid route.'; + } +} diff --git a/doc/md/dev/Plugin-system.md b/doc/md/dev/Plugin-system.md index f09fadc29..79654011b 100644 --- a/doc/md/dev/Plugin-system.md +++ b/doc/md/dev/Plugin-system.md @@ -139,6 +139,31 @@ Each file contain two keys: > Note: In PHP, `parse_ini_file()` seems to want strings to be between by quotes `"` in the ini file. +### Register plugin's routes + +Shaarli lets you register custom Slim routes for your plugin. + +To register a route, the plugin must include a function called `function _register_routes(): array`. + +This method must return an array of routes, each entry must contain the following keys: + + - `method`: HTTP method, `GET/POST/PUT/PATCH/DELETE` + - `route` (path): without prefix, e.g. `/up/{variable}` + It will be later prefixed by `/plugin//`. + - `callable` string, function name or FQN class's method to execute, e.g. `demo_plugin_custom_controller`. + +Callable functions or methods must have `Slim\Http\Request` and `Slim\Http\Response` parameters +and return a `Slim\Http\Response`. We recommend creating a dedicated class and extend either +`ShaarliVisitorController` or `ShaarliAdminController` to use helper functions they provide. + +A dedicated plugin template is available for rendering content: `pluginscontent.html` using `content` placeholder. + +> **Warning**: plugins are not able to use RainTPL template engine for their content due to technical restrictions. +> RainTPL does not allow to register multiple template folders, so all HTML rendering must be done within plugin +> custom controller. + +Check out the `demo_plugin` for a live example: `GET /plugin/demo_plugin/custom`. + ### Understanding relative paths Because Shaarli is a self-hosted tool, an instance can either be installed at the root directory, or under a subfolder. diff --git a/index.php b/index.php index 1eb7659af..862c53efa 100644 --- a/index.php +++ b/index.php @@ -31,6 +31,7 @@ use Shaarli\Config\ConfigManager; use Shaarli\Container\ContainerBuilder; use Shaarli\Languages; +use Shaarli\Plugin\PluginManager; use Shaarli\Security\BanManager; use Shaarli\Security\CookieManager; use Shaarli\Security\LoginManager; @@ -87,7 +88,17 @@ $loginManager->checkLoginState(client_ip_id($_SERVER)); -$containerBuilder = new ContainerBuilder($conf, $sessionManager, $cookieManager, $loginManager, $logger); +$pluginManager = new PluginManager($conf); +$pluginManager->load($conf->get('general.enabled_plugins', [])); + +$containerBuilder = new ContainerBuilder( + $conf, + $sessionManager, + $cookieManager, + $loginManager, + $pluginManager, + $logger +); $container = $containerBuilder->build(); $app = new App($container); @@ -154,6 +165,15 @@ $this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility'); })->add('\Shaarli\Front\ShaarliAdminMiddleware'); +$app->group('/plugin', function () use ($pluginManager) { + foreach ($pluginManager->getRegisteredRoutes() as $pluginName => $routes) { + $this->group('/' . $pluginName, function () use ($routes) { + foreach ($routes as $route) { + $this->{strtolower($route['method'])}('/' . ltrim($route['route'], '/'), $route['callable']); + } + }); + } +})->add('\Shaarli\Front\ShaarliMiddleware'); // REST API routes $app->group('/api/v1', function () { diff --git a/phpcs.xml b/phpcs.xml index c559e35da..9bdc87209 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -18,5 +18,6 @@ index.php + plugins/* diff --git a/plugins/demo_plugin/DemoPluginController.php b/plugins/demo_plugin/DemoPluginController.php new file mode 100644 index 000000000..b8ace9c80 --- /dev/null +++ b/plugins/demo_plugin/DemoPluginController.php @@ -0,0 +1,24 @@ +assignView( + 'content', + '
' . + 'This is a demo page. I have access to Shaarli container, so I\'m free to do whatever I want here.' . + '
' + ); + + return $response->write($this->render('pluginscontent')); + } +} diff --git a/plugins/demo_plugin/demo_plugin.php b/plugins/demo_plugin/demo_plugin.php index 22d27b682..15cfc2c51 100644 --- a/plugins/demo_plugin/demo_plugin.php +++ b/plugins/demo_plugin/demo_plugin.php @@ -7,6 +7,8 @@ * Can be used by plugin developers to make their own plugin. */ +require_once __DIR__ . '/DemoPluginController.php'; + /* * RENDER HEADER, INCLUDES, FOOTER * @@ -60,6 +62,17 @@ function demo_plugin_init($conf) return $errors; } +function demo_plugin_register_routes(): array +{ + return [ + [ + 'method' => 'GET', + 'route' => '/custom', + 'callable' => 'Shaarli\DemoPlugin\DemoPluginController:index', + ], + ]; +} + /** * Hook render_header. * Executed on every page render. @@ -304,7 +317,11 @@ function hook_demo_plugin_render_editlink($data) function hook_demo_plugin_render_tools($data) { // field_plugin - $data['tools_plugin'][] = 'tools_plugin'; + $data['tools_plugin'][] = '
'; return $data; } diff --git a/tests/PluginManagerTest.php b/tests/PluginManagerTest.php index efef5e874..8947f6791 100644 --- a/tests/PluginManagerTest.php +++ b/tests/PluginManagerTest.php @@ -120,4 +120,43 @@ public function testGetPluginsMeta(): void $this->assertEquals('test plugin', $meta[self::$pluginName]['description']); $this->assertEquals($expectedParameters, $meta[self::$pluginName]['parameters']); } + + /** + * Test plugin custom routes - note that there is no check on callable functions + */ + public function testRegisteredRoutes(): void + { + PluginManager::$PLUGINS_PATH = self::$pluginPath; + $this->pluginManager->load([self::$pluginName]); + + $expectedParameters = [ + [ + 'method' => 'GET', + 'route' => '/test', + 'callable' => 'getFunction', + ], + [ + 'method' => 'POST', + 'route' => '/custom', + 'callable' => 'postFunction', + ], + ]; + $meta = $this->pluginManager->getRegisteredRoutes(); + static::assertSame($expectedParameters, $meta[self::$pluginName]); + } + + /** + * Test plugin custom routes with invalid route + */ + public function testRegisteredRoutesInvalid(): void + { + $plugin = 'test_route_invalid'; + $this->pluginManager->load([$plugin]); + + $meta = $this->pluginManager->getRegisteredRoutes(); + static::assertSame([], $meta); + + $errors = $this->pluginManager->getErrors(); + static::assertSame(['test_route_invalid [plugin incompatibility]: trying to register invalid route.'], $errors); + } } diff --git a/tests/container/ContainerBuilderTest.php b/tests/container/ContainerBuilderTest.php index 3d43c3447..04d4ef014 100644 --- a/tests/container/ContainerBuilderTest.php +++ b/tests/container/ContainerBuilderTest.php @@ -43,11 +43,15 @@ class ContainerBuilderTest extends TestCase /** @var CookieManager */ protected $cookieManager; + /** @var PluginManager */ + protected $pluginManager; + public function setUp(): void { $this->conf = new ConfigManager('tests/utils/config/configJson'); $this->sessionManager = $this->createMock(SessionManager::class); $this->cookieManager = $this->createMock(CookieManager::class); + $this->pluginManager = $this->createMock(PluginManager::class); $this->loginManager = $this->createMock(LoginManager::class); $this->loginManager->method('isLoggedIn')->willReturn(true); @@ -57,6 +61,7 @@ public function setUp(): void $this->sessionManager, $this->cookieManager, $this->loginManager, + $this->pluginManager, $this->createMock(LoggerInterface::class) ); } diff --git a/tests/plugins/test/test.php b/tests/plugins/test/test.php index 03be4f4e8..34cd339e1 100644 --- a/tests/plugins/test/test.php +++ b/tests/plugins/test/test.php @@ -27,3 +27,19 @@ function hook_test_error() { new Unknown(); } + +function test_register_routes(): array +{ + return [ + [ + 'method' => 'GET', + 'route' => '/test', + 'callable' => 'getFunction', + ], + [ + 'method' => 'POST', + 'route' => '/custom', + 'callable' => 'postFunction', + ], + ]; +} diff --git a/tests/plugins/test_route_invalid/test_route_invalid.php b/tests/plugins/test_route_invalid/test_route_invalid.php new file mode 100644 index 000000000..0c5a51012 --- /dev/null +++ b/tests/plugins/test_route_invalid/test_route_invalid.php @@ -0,0 +1,12 @@ + 'GET', + 'route' => 'not a route', + 'callable' => 'getFunction', + ], + ]; +} diff --git a/tpl/default/pluginscontent.html b/tpl/default/pluginscontent.html new file mode 100644 index 000000000..1e4f6b807 --- /dev/null +++ b/tpl/default/pluginscontent.html @@ -0,0 +1,13 @@ + + + + {include="includes"} + + + {include="page.header"} + + {$content} + + {include="page.footer"} + + From 70507b86037450450a5ac74597304f2ee1cb4c0d Mon Sep 17 00:00:00 2001 From: nodiscc Date: Sun, 22 Nov 2020 11:06:14 +0000 Subject: [PATCH 008/155] ConfigureControllerTest.php: update expected languages number to 6 Following the addition of russian translations in #1642 Fixes https://github.com/shaarli/Shaarli/issues/1647 --- tests/front/controller/admin/ConfigureControllerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/front/controller/admin/ConfigureControllerTest.php b/tests/front/controller/admin/ConfigureControllerTest.php index d82db0a7c..13644df99 100644 --- a/tests/front/controller/admin/ConfigureControllerTest.php +++ b/tests/front/controller/admin/ConfigureControllerTest.php @@ -62,7 +62,7 @@ public function testIndex(): void static::assertSame('privacy.hide_public_links', $assignedVariables['hide_public_links']); static::assertSame('api.enabled', $assignedVariables['api_enabled']); static::assertSame('api.secret', $assignedVariables['api_secret']); - static::assertCount(5, $assignedVariables['languages']); + static::assertCount(6, $assignedVariables['languages']); static::assertArrayHasKey('gd_enabled', $assignedVariables); static::assertSame('thumbnails.mode', $assignedVariables['thumbnails_mode']); } From 05c616f7a08936108d6feee4460dca9407e83d9f Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 24 Nov 2020 13:35:37 +0100 Subject: [PATCH 009/155] chmod -x russian translation file --- inc/languages/ru/LC_MESSAGES/shaarli.po | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 inc/languages/ru/LC_MESSAGES/shaarli.po diff --git a/inc/languages/ru/LC_MESSAGES/shaarli.po b/inc/languages/ru/LC_MESSAGES/shaarli.po old mode 100755 new mode 100644 From 8a6b7e96b7176e03238bbb1bcaa4c8b0c25e6358 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 24 Nov 2020 13:28:17 +0100 Subject: [PATCH 010/155] Fix: soft fail if the mutex is not working And display the error in server admin page Fixes #1650 --- application/bookmark/BookmarkIO.php | 22 ++- .../controller/admin/ServerController.php | 7 +- .../controller/visitor/InstallController.php | 7 +- application/helper/ApplicationUtils.php | 16 ++ inc/languages/fr/LC_MESSAGES/shaarli.po | 186 +++++++++--------- 5 files changed, 145 insertions(+), 93 deletions(-) diff --git a/application/bookmark/BookmarkIO.php b/application/bookmark/BookmarkIO.php index c78dbe41f..8439d470d 100644 --- a/application/bookmark/BookmarkIO.php +++ b/application/bookmark/BookmarkIO.php @@ -4,6 +4,7 @@ namespace Shaarli\Bookmark; +use malkusch\lock\exception\LockAcquireException; use malkusch\lock\mutex\Mutex; use malkusch\lock\mutex\NoMutex; use Shaarli\Bookmark\Exception\DatastoreNotInitializedException; @@ -80,7 +81,7 @@ public function read() } $content = null; - $this->mutex->synchronized(function () use (&$content) { + $this->synchronized(function () use (&$content) { $content = file_get_contents($this->datastore); }); @@ -119,11 +120,28 @@ public function write($links) $data = self::$phpPrefix . base64_encode(gzdeflate(serialize($links))) . self::$phpSuffix; - $this->mutex->synchronized(function () use ($data) { + $this->synchronized(function () use ($data) { file_put_contents( $this->datastore, $data ); }); } + + /** + * Wrapper applying mutex to provided function. + * If the lock can't be acquired (e.g. some shared hosting provider), we execute the function without mutex. + * + * @see https://github.com/shaarli/Shaarli/issues/1650 + * + * @param callable $function + */ + protected function synchronized(callable $function): void + { + try { + $this->mutex->synchronized($function); + } catch (LockAcquireException $exception) { + $function(); + } + } } diff --git a/application/front/controller/admin/ServerController.php b/application/front/controller/admin/ServerController.php index fabeaf2f2..4b74f4a94 100644 --- a/application/front/controller/admin/ServerController.php +++ b/application/front/controller/admin/ServerController.php @@ -39,11 +39,16 @@ public function index(Request $request, Response $response): Response $currentVersion = $currentVersion === 'dev' ? $currentVersion : 'v' . $currentVersion; $phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION)); + $permissions = array_merge( + ApplicationUtils::checkResourcePermissions($this->container->conf), + ApplicationUtils::checkDatastoreMutex() + ); + $this->assignView('php_version', PHP_VERSION); $this->assignView('php_eol', format_date($phpEol, false)); $this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable()); $this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement()); - $this->assignView('permissions', ApplicationUtils::checkResourcePermissions($this->container->conf)); + $this->assignView('permissions', $permissions); $this->assignView('release_url', $releaseUrl); $this->assignView('latest_version', $latestVersion); $this->assignView('current_version', $currentVersion); diff --git a/application/front/controller/visitor/InstallController.php b/application/front/controller/visitor/InstallController.php index bf9659294..418d4a49c 100644 --- a/application/front/controller/visitor/InstallController.php +++ b/application/front/controller/visitor/InstallController.php @@ -56,11 +56,16 @@ public function index(Request $request, Response $response): Response $phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION)); + $permissions = array_merge( + ApplicationUtils::checkResourcePermissions($this->container->conf), + ApplicationUtils::checkDatastoreMutex() + ); + $this->assignView('php_version', PHP_VERSION); $this->assignView('php_eol', format_date($phpEol, false)); $this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable()); $this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement()); - $this->assignView('permissions', ApplicationUtils::checkResourcePermissions($this->container->conf)); + $this->assignView('permissions', $permissions); $this->assignView('pagetitle', t('Install Shaarli')); diff --git a/application/helper/ApplicationUtils.php b/application/helper/ApplicationUtils.php index 212dd8e2d..a6c03aaea 100644 --- a/application/helper/ApplicationUtils.php +++ b/application/helper/ApplicationUtils.php @@ -3,6 +3,8 @@ namespace Shaarli\Helper; use Exception; +use malkusch\lock\exception\LockAcquireException; +use malkusch\lock\mutex\FlockMutex; use Shaarli\Config\ConfigManager; /** @@ -252,6 +254,20 @@ public static function checkResourcePermissions(ConfigManager $conf, bool $minim return $errors; } + public static function checkDatastoreMutex(): array + { + $mutex = new FlockMutex(fopen(SHAARLI_MUTEX_FILE, 'r'), 2); + try { + $mutex->synchronized(function () { + return true; + }); + } catch (LockAcquireException $e) { + $errors[] = t('Lock can not be acquired on the datastore. You might encounter concurrent access issues.'); + } + + return $errors ?? []; + } + /** * Returns a salted hash representing the current Shaarli version. * diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.po b/inc/languages/fr/LC_MESSAGES/shaarli.po index 26dede4e2..01492af46 100644 --- a/inc/languages/fr/LC_MESSAGES/shaarli.po +++ b/inc/languages/fr/LC_MESSAGES/shaarli.po @@ -1,8 +1,8 @@ msgid "" msgstr "" "Project-Id-Version: Shaarli\n" -"POT-Creation-Date: 2020-11-09 14:39+0100\n" -"PO-Revision-Date: 2020-11-09 14:42+0100\n" +"POT-Creation-Date: 2020-11-24 13:13+0100\n" +"PO-Revision-Date: 2020-11-24 13:14+0100\n" "Last-Translator: \n" "Language-Team: Shaarli\n" "Language: fr_FR\n" @@ -20,31 +20,31 @@ msgstr "" "X-Poedit-SearchPath-3: init.php\n" "X-Poedit-SearchPath-4: plugins\n" -#: application/History.php:180 +#: application/History.php:181 msgid "History file isn't readable or writable" msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture" -#: application/History.php:191 +#: application/History.php:192 msgid "Could not parse history file" msgstr "Format incorrect pour le fichier d'historique" -#: application/Languages.php:181 +#: application/Languages.php:184 msgid "Automatic" msgstr "Automatique" -#: application/Languages.php:182 +#: application/Languages.php:185 msgid "German" msgstr "Allemand" -#: application/Languages.php:183 +#: application/Languages.php:186 msgid "English" msgstr "Anglais" -#: application/Languages.php:184 +#: application/Languages.php:187 msgid "French" msgstr "Français" -#: application/Languages.php:185 +#: application/Languages.php:188 msgid "Japanese" msgstr "Japonais" @@ -56,46 +56,46 @@ msgstr "" "l'extension php-gd doit être chargée pour utiliser les miniatures. Les " "miniatures sont désormais désactivées. Rechargez la page." -#: application/Utils.php:402 +#: application/Utils.php:405 msgid "Setting not set" msgstr "Paramètre non défini" -#: application/Utils.php:409 +#: application/Utils.php:412 msgid "Unlimited" msgstr "Illimité" -#: application/Utils.php:412 +#: application/Utils.php:415 msgid "B" msgstr "o" -#: application/Utils.php:412 +#: application/Utils.php:415 msgid "kiB" msgstr "ko" -#: application/Utils.php:412 +#: application/Utils.php:415 msgid "MiB" msgstr "Mo" -#: application/Utils.php:412 +#: application/Utils.php:415 msgid "GiB" msgstr "Go" -#: application/bookmark/BookmarkFileService.php:183 -#: application/bookmark/BookmarkFileService.php:205 -#: application/bookmark/BookmarkFileService.php:227 -#: application/bookmark/BookmarkFileService.php:241 +#: application/bookmark/BookmarkFileService.php:185 +#: application/bookmark/BookmarkFileService.php:207 +#: application/bookmark/BookmarkFileService.php:229 +#: application/bookmark/BookmarkFileService.php:243 msgid "You're not authorized to alter the datastore" msgstr "Vous n'êtes pas autorisé à modifier les données" -#: application/bookmark/BookmarkFileService.php:208 +#: application/bookmark/BookmarkFileService.php:210 msgid "This bookmarks already exists" msgstr "Ce marque-page existe déjà" -#: application/bookmark/BookmarkInitializer.php:39 +#: application/bookmark/BookmarkInitializer.php:42 msgid "(private bookmark with thumbnail demo)" msgstr "(marque page privé avec une miniature)" -#: application/bookmark/BookmarkInitializer.php:42 +#: application/bookmark/BookmarkInitializer.php:45 msgid "" "Shaarli will automatically pick up the thumbnail for links to a variety of " "websites.\n" @@ -118,11 +118,11 @@ msgstr "" "\n" "Maintenant, vous pouvez modifier ou supprimer les shaares créés par défaut.\n" -#: application/bookmark/BookmarkInitializer.php:55 +#: application/bookmark/BookmarkInitializer.php:58 msgid "Note: Shaare descriptions" msgstr "Note : Description des Shaares" -#: application/bookmark/BookmarkInitializer.php:57 +#: application/bookmark/BookmarkInitializer.php:60 msgid "" "Adding a shaare without entering a URL creates a text-only \"note\" post " "such as this one.\n" @@ -186,7 +186,7 @@ msgstr "" "| Citron | Fruit | Jaune | 30 |\n" "| Carotte | Légume | Orange | 14 |\n" -#: application/bookmark/BookmarkInitializer.php:91 +#: application/bookmark/BookmarkInitializer.php:94 #: application/legacy/LegacyLinkDB.php:246 #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 @@ -198,7 +198,7 @@ msgstr "" "Le gestionnaire de marque-pages personnel, minimaliste, et sans base de " "données" -#: application/bookmark/BookmarkInitializer.php:94 +#: application/bookmark/BookmarkInitializer.php:97 msgid "" "Welcome to Shaarli!\n" "\n" @@ -247,11 +247,11 @@ msgstr "" "issues) si vous avez une suggestion ou si vous rencontrez un problème.\n" " \n" -#: application/bookmark/exception/BookmarkNotFoundException.php:13 +#: application/bookmark/exception/BookmarkNotFoundException.php:14 msgid "The link you are trying to reach does not exist or has been deleted." msgstr "Le lien que vous essayez de consulter n'existe pas ou a été supprimé." -#: application/config/ConfigJson.php:52 application/config/ConfigPhp.php:129 +#: application/config/ConfigJson.php:52 application/config/ConfigPhp.php:131 msgid "" "Shaarli could not create the config file. Please make sure Shaarli has the " "right to write in the folder is it installed in." @@ -259,12 +259,12 @@ msgstr "" "Shaarli n'a pas pu créer le fichier de configuration. Merci de vérifier que " "Shaarli a les droits d'écriture dans le dossier dans lequel il est installé." -#: application/config/ConfigManager.php:136 -#: application/config/ConfigManager.php:163 +#: application/config/ConfigManager.php:137 +#: application/config/ConfigManager.php:164 msgid "Invalid setting key parameter. String expected, got: " msgstr "Clé de paramétrage invalide. Chaîne de caractères obtenue, attendu : " -#: application/config/exception/MissingFieldConfigException.php:21 +#: application/config/exception/MissingFieldConfigException.php:20 #, php-format msgid "Configuration value is required for %s" msgstr "Le paramètre %s est obligatoire" @@ -274,48 +274,48 @@ msgid "An error occurred while trying to save plugins loading order." msgstr "" "Une erreur s'est produite lors de la sauvegarde de l'ordre des extensions." -#: application/config/exception/UnauthorizedConfigException.php:16 +#: application/config/exception/UnauthorizedConfigException.php:15 msgid "You are not authorized to alter config." msgstr "Vous n'êtes pas autorisé à modifier la configuration." -#: application/exceptions/IOException.php:22 +#: application/exceptions/IOException.php:23 msgid "Error accessing" msgstr "Une erreur s'est produite en accédant à" -#: application/feed/FeedBuilder.php:179 +#: application/feed/FeedBuilder.php:180 msgid "Direct link" msgstr "Liens directs" -#: application/feed/FeedBuilder.php:181 +#: application/feed/FeedBuilder.php:182 #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103 #: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179 msgid "Permalink" msgstr "Permalien" -#: application/front/controller/admin/ConfigureController.php:54 +#: application/front/controller/admin/ConfigureController.php:56 #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 msgid "Configure" msgstr "Configurer" -#: application/front/controller/admin/ConfigureController.php:102 -#: application/legacy/LegacyUpdater.php:537 +#: application/front/controller/admin/ConfigureController.php:106 +#: application/legacy/LegacyUpdater.php:539 msgid "You have enabled or changed thumbnails mode." msgstr "Vous avez activé ou changé le mode de miniatures." -#: application/front/controller/admin/ConfigureController.php:103 -#: application/front/controller/admin/ServerController.php:75 -#: application/legacy/LegacyUpdater.php:538 +#: application/front/controller/admin/ConfigureController.php:108 +#: application/front/controller/admin/ServerController.php:76 +#: application/legacy/LegacyUpdater.php:540 msgid "Please synchronize them." msgstr "Merci de les synchroniser." -#: application/front/controller/admin/ConfigureController.php:113 -#: application/front/controller/visitor/InstallController.php:146 +#: application/front/controller/admin/ConfigureController.php:119 +#: application/front/controller/visitor/InstallController.php:149 msgid "Error while writing config file after configuration update." msgstr "" "Une erreur s'est produite lors de la sauvegarde du fichier de configuration." -#: application/front/controller/admin/ConfigureController.php:122 +#: application/front/controller/admin/ConfigureController.php:128 msgid "Configuration was saved." msgstr "La configuration a été sauvegardée." @@ -433,7 +433,7 @@ msgstr "Administration serveur" msgid "Thumbnails cache has been cleared." msgstr "Le cache des miniatures a été vidé." -#: application/front/controller/admin/ServerController.php:83 +#: application/front/controller/admin/ServerController.php:85 msgid "Shaarli's cache folder has been cleared!" msgstr "Le dossier de cache de Shaarli a été vidé !" @@ -459,18 +459,18 @@ msgstr "Le lien avec l'identifiant %s n'a pas pu être trouvé." msgid "Invalid visibility provided." msgstr "Visibilité du lien non valide." -#: application/front/controller/admin/ShaarePublishController.php:171 +#: application/front/controller/admin/ShaarePublishController.php:173 #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171 msgid "Edit" msgstr "Modifier" -#: application/front/controller/admin/ShaarePublishController.php:174 +#: application/front/controller/admin/ShaarePublishController.php:176 #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28 msgid "Shaare" msgstr "Shaare" -#: application/front/controller/admin/ShaarePublishController.php:205 +#: application/front/controller/admin/ShaarePublishController.php:208 msgid "Note: " msgstr "Note : " @@ -485,7 +485,7 @@ msgstr "Mise à jour des miniatures" msgid "Tools" msgstr "Outils" -#: application/front/controller/visitor/BookmarkListController.php:120 +#: application/front/controller/visitor/BookmarkListController.php:121 msgid "Search: " msgstr "Recherche : " @@ -535,12 +535,12 @@ msgstr "Une erreur inattendue s'est produite." msgid "Requested page could not be found." msgstr "La page demandée n'a pas pu être trouvée." -#: application/front/controller/visitor/InstallController.php:64 +#: application/front/controller/visitor/InstallController.php:65 #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 msgid "Install Shaarli" msgstr "Installation de Shaarli" -#: application/front/controller/visitor/InstallController.php:83 +#: application/front/controller/visitor/InstallController.php:85 #, php-format msgid "" "
Sessions do not seem to work correctly on your server.
Make sure the " @@ -559,14 +559,14 @@ msgstr "" "des cookies. Nous vous recommandons d'accéder à votre serveur depuis son " "adresse IP ou un Fully Qualified Domain Name.
" -#: application/front/controller/visitor/InstallController.php:154 +#: application/front/controller/visitor/InstallController.php:157 msgid "" "Shaarli is now configured. Please login and start shaaring your bookmarks!" msgstr "" "Shaarli est maintenant configuré. Vous pouvez vous connecter et commencez à " "shaare vos liens !" -#: application/front/controller/visitor/InstallController.php:168 +#: application/front/controller/visitor/InstallController.php:171 msgid "Insufficient permissions:" msgstr "Permissions insuffisantes :" @@ -580,7 +580,7 @@ msgstr "Permissions insuffisantes :" msgid "Login" msgstr "Connexion" -#: application/front/controller/visitor/LoginController.php:77 +#: application/front/controller/visitor/LoginController.php:78 msgid "Wrong login/password." msgstr "Nom d'utilisateur ou mot de passe incorrect(s)." @@ -620,7 +620,7 @@ msgstr "" msgid "Wrong token." msgstr "Jeton invalide." -#: application/helper/ApplicationUtils.php:162 +#: application/helper/ApplicationUtils.php:165 #, php-format msgid "" "Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus " @@ -631,52 +631,60 @@ msgstr "" "peut donc pas fonctionner. Votre version de PHP a des failles de sécurités " "connues et devrait être mise à jour au plus tôt." -#: application/helper/ApplicationUtils.php:195 -#: application/helper/ApplicationUtils.php:215 +#: application/helper/ApplicationUtils.php:200 +#: application/helper/ApplicationUtils.php:220 msgid "directory is not readable" msgstr "le répertoire n'est pas accessible en lecture" -#: application/helper/ApplicationUtils.php:218 +#: application/helper/ApplicationUtils.php:223 msgid "directory is not writable" msgstr "le répertoire n'est pas accessible en écriture" -#: application/helper/ApplicationUtils.php:240 +#: application/helper/ApplicationUtils.php:247 msgid "file is not readable" msgstr "le fichier n'est pas accessible en lecture" -#: application/helper/ApplicationUtils.php:243 +#: application/helper/ApplicationUtils.php:250 msgid "file is not writable" msgstr "le fichier n'est pas accessible en écriture" -#: application/helper/ApplicationUtils.php:277 +#: application/helper/ApplicationUtils.php:260 +msgid "" +"Lock can not be acquired on the datastore. You might encounter concurrent " +"access issues." +msgstr "" +"Le fichier datastore ne peut pas être verrouillé. Vous pourriez rencontrer " +"des problèmes d'accès concurrents." + +#: application/helper/ApplicationUtils.php:293 msgid "Configuration parsing" msgstr "Chargement de la configuration" -#: application/helper/ApplicationUtils.php:278 +#: application/helper/ApplicationUtils.php:294 msgid "Slim Framework (routing, etc.)" msgstr "Slim Framwork (routage, etc.)" -#: application/helper/ApplicationUtils.php:279 +#: application/helper/ApplicationUtils.php:295 msgid "Multibyte (Unicode) string support" msgstr "Support des chaînes de caractère multibytes (Unicode)" -#: application/helper/ApplicationUtils.php:280 +#: application/helper/ApplicationUtils.php:296 msgid "Required to use thumbnails" msgstr "Obligatoire pour utiliser les miniatures" -#: application/helper/ApplicationUtils.php:281 +#: application/helper/ApplicationUtils.php:297 msgid "Localized text sorting (e.g. e->è->f)" msgstr "Tri des textes traduits (ex : e->è->f)" -#: application/helper/ApplicationUtils.php:282 +#: application/helper/ApplicationUtils.php:298 msgid "Better retrieval of bookmark metadata and thumbnail" msgstr "Meilleure récupération des meta-données des marque-pages et minatures" -#: application/helper/ApplicationUtils.php:283 +#: application/helper/ApplicationUtils.php:299 msgid "Use the translation system in gettext mode" msgstr "Utiliser le système de traduction en mode gettext" -#: application/helper/ApplicationUtils.php:284 +#: application/helper/ApplicationUtils.php:300 msgid "Login using LDAP server" msgstr "Authentification via un serveur LDAP" @@ -750,7 +758,7 @@ msgstr "" msgid "Couldn't retrieve updater class methods." msgstr "Impossible de récupérer les méthodes de la classe Updater." -#: application/legacy/LegacyUpdater.php:538 +#: application/legacy/LegacyUpdater.php:540 msgid "" msgstr "" @@ -776,11 +784,11 @@ msgstr "" "a été importé avec succès en %d secondes : %d liens importés, %d liens " "écrasés, %d liens ignorés." -#: application/plugin/PluginManager.php:124 +#: application/plugin/PluginManager.php:125 msgid " [plugin incompatibility]: " msgstr " [incompatibilité de l'extension] : " -#: application/plugin/exception/PluginFileNotFoundException.php:21 +#: application/plugin/exception/PluginFileNotFoundException.php:22 #, php-format msgid "Plugin \"%s\" files not found." msgstr "Les fichiers de l'extension \"%s\" sont introuvables." @@ -794,7 +802,7 @@ msgstr "Impossible de purger %s : le répertoire n'existe pas" msgid "An error occurred while running the update " msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour " -#: index.php:80 +#: index.php:81 msgid "Shared bookmarks on " msgstr "Liens partagés sur " @@ -811,11 +819,11 @@ msgstr "Shaare" msgid "Adds the addlink input on the linklist page." msgstr "Ajoute le formulaire d'ajout de liens sur la page principale." -#: plugins/archiveorg/archiveorg.php:28 +#: plugins/archiveorg/archiveorg.php:29 msgid "View on archive.org" msgstr "Voir sur archive.org" -#: plugins/archiveorg/archiveorg.php:41 +#: plugins/archiveorg/archiveorg.php:42 msgid "For each link, add an Archive.org icon." msgstr "Pour chaque lien, ajoute une icône pour Archive.org." @@ -845,7 +853,7 @@ msgstr "Couleur de fond (gris léger)" msgid "Dark main color (e.g. visited links)" msgstr "Couleur principale sombre (ex : les liens visités)" -#: plugins/demo_plugin/demo_plugin.php:477 +#: plugins/demo_plugin/demo_plugin.php:478 msgid "" "A demo plugin covering all use cases for template designers and plugin " "developers." @@ -853,11 +861,11 @@ msgstr "" "Une extension de démonstration couvrant tous les cas d'utilisation pour les " "designers de thèmes et les développeurs d'extensions." -#: plugins/demo_plugin/demo_plugin.php:478 +#: plugins/demo_plugin/demo_plugin.php:479 msgid "This is a parameter dedicated to the demo plugin. It'll be suffixed." msgstr "Ceci est un paramètre dédié au plugin de démo. Il sera suffixé." -#: plugins/demo_plugin/demo_plugin.php:479 +#: plugins/demo_plugin/demo_plugin.php:480 msgid "Other demo parameter" msgstr "Un autre paramètre de démo" @@ -879,7 +887,7 @@ msgstr "" msgid "Isso server URL (without 'http://')" msgstr "URL du serveur Isso (sans 'http://')" -#: plugins/piwik/piwik.php:23 +#: plugins/piwik/piwik.php:24 msgid "" "Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin " "administration page." @@ -887,27 +895,27 @@ msgstr "" "Erreur de l'extension Piwik : Merci de définir les paramètres PIWIK_URL et " "PIWIK_SITEID dans la page d'administration des extensions." -#: plugins/piwik/piwik.php:72 +#: plugins/piwik/piwik.php:73 msgid "A plugin that adds Piwik tracking code to Shaarli pages." msgstr "Ajoute le code de traçage de Piwik sur les pages de Shaarli." -#: plugins/piwik/piwik.php:73 +#: plugins/piwik/piwik.php:74 msgid "Piwik URL" msgstr "URL de Piwik" -#: plugins/piwik/piwik.php:74 +#: plugins/piwik/piwik.php:75 msgid "Piwik site ID" msgstr "Site ID de Piwik" -#: plugins/playvideos/playvideos.php:25 +#: plugins/playvideos/playvideos.php:26 msgid "Video player" msgstr "Lecteur vidéo" -#: plugins/playvideos/playvideos.php:28 +#: plugins/playvideos/playvideos.php:29 msgid "Play Videos" msgstr "Jouer les vidéos" -#: plugins/playvideos/playvideos.php:59 +#: plugins/playvideos/playvideos.php:60 msgid "Add a button in the toolbar allowing to watch all videos." msgstr "" "Ajoute un bouton dans la barre de menu pour regarder toutes les vidéos." @@ -935,11 +943,11 @@ msgstr "Mauvaise réponse du hub %s" msgid "Enable PubSubHubbub feed publishing." msgstr "Active la publication de flux vers PubSubHubbub." -#: plugins/qrcode/qrcode.php:73 plugins/wallabag/wallabag.php:71 +#: plugins/qrcode/qrcode.php:74 plugins/wallabag/wallabag.php:72 msgid "For each link, add a QRCode icon." msgstr "Pour chaque lien, ajouter une icône de QRCode." -#: plugins/wallabag/wallabag.php:21 +#: plugins/wallabag/wallabag.php:22 msgid "" "Wallabag plugin error: Please define the \"WALLABAG_URL\" setting in the " "plugin administration page." @@ -947,15 +955,15 @@ msgstr "" "Erreur de l'extension Wallabag : Merci de définir le paramètre « " "WALLABAG_URL » dans la page d'administration des extensions." -#: plugins/wallabag/wallabag.php:48 +#: plugins/wallabag/wallabag.php:49 msgid "Save to wallabag" msgstr "Sauvegarder dans Wallabag" -#: plugins/wallabag/wallabag.php:72 +#: plugins/wallabag/wallabag.php:73 msgid "Wallabag API URL" msgstr "URL de l'API Wallabag" -#: plugins/wallabag/wallabag.php:73 +#: plugins/wallabag/wallabag.php:74 msgid "Wallabag API version (1 or 2)" msgstr "Version de l'API Wallabag (1 ou 2)" From 495545f2f07f00ead911908d8bee9a572889eced Mon Sep 17 00:00:00 2001 From: Doug Breaux <25640850+dougbreaux@users.noreply.github.com> Date: Fri, 4 Dec 2020 16:12:39 -0600 Subject: [PATCH 011/155] newer alpine (for newer PHP) and apk upgrade #1655 --- Dockerfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index f6120b71f..bedc9496c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,7 +26,7 @@ RUN cd shaarli \ # Stage 4: # - Shaarli image -FROM alpine:3.8 +FROM alpine:3.12 LABEL maintainer="Shaarli Community" RUN apk --update --no-cache add \ @@ -46,7 +46,8 @@ RUN apk --update --no-cache add \ php7-xml \ php7-simplexml \ php7-zlib \ - s6 + s6 \ + && apk -U upgrade COPY .docker/nginx.conf /etc/nginx/nginx.conf COPY .docker/php-fpm.conf /etc/php7/php-fpm.conf From ee07df357bce7bd407ba93fa6b10677b355f631f Mon Sep 17 00:00:00 2001 From: Emilien Klein Date: Sun, 6 Dec 2020 21:02:39 +0100 Subject: [PATCH 012/155] Upgrade alpine from 3.8 to 3.10 in armhf Dockerfile The Docker for armhf doesn't build anymore on Alpine 3.8, upgrading to 3.10. Building works fine on a Rapsberry Pi 4 running Raspbian GNU/Linux 10 (buster) This is the error with 3.8: ``` [2/4] Fetching packages... error css-loader@4.3.0: The engine "node" is incompatible with this module. Expected version ">= 10.13.0". error Found incompatible module info Visit https://yarnpkg.com/en/docs/cli/install for documentation about this command. The command '/bin/sh -c apk --update --no-cache add yarn nodejs-current python2 build-base && cd /shaarli && yarn install && yarn run build && rm -rf node_modules' returned a non-zero code: 1 ``` Not upgrading to 3.11, due to this error: ``` 2/4] Fetching packages... error browserslist@4.14.3: The engine "node" is incompatible with this module. Expected version "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7". Got "13.1.0" error Found incompatible module. info Visit https://yarnpkg.com/en/docs/cli/install for documentation about this command. The command '/bin/sh -c apk --update --no-cache add yarn nodejs-current python2 build-base && cd /shaarli && yarn install && yarn run build && rm -rf node_modules' returned a non-zero code: 1 ``` Not upgrading to 3.12 either, due to this error: ``` ERROR: unsatisfiable constraints: py2-pip (missing): required by: world[py2-pip] The command '/bin/sh -c apk --update --no-cache add py2-pip && cd /usr/src/app/shaarli && pip install --no-cache-dir mkdocs && mkdocs build --clean' returned a non-zero code: 1 ``` There's probably a way to have Python2 pip installed on 3.12, but I suppose other issues would arise (such as the one happening with 3.11), so only proposing to upgrade to 3.10 now. This would probably be looked at more in detail when merging the amd64 and arm/v7 Docker builds, see #1496. --- Dockerfile.armhf | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 5bbf66804..471f23974 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -1,7 +1,7 @@ # Stage 1: # - Copy Shaarli sources # - Build documentation -FROM arm32v6/alpine:3.8 as docs +FROM arm32v6/alpine:3.10 as docs ADD . /usr/src/app/shaarli RUN apk --update --no-cache add py2-pip \ && cd /usr/src/app/shaarli \ @@ -10,7 +10,7 @@ RUN apk --update --no-cache add py2-pip \ # Stage 2: # - Resolve PHP dependencies with Composer -FROM arm32v6/alpine:3.8 as composer +FROM arm32v6/alpine:3.10 as composer COPY --from=docs /usr/src/app/shaarli /app/shaarli RUN apk --update --no-cache add php7-curl php7-mbstring php7-simplexml composer \ && cd /app/shaarli \ @@ -18,7 +18,7 @@ RUN apk --update --no-cache add php7-curl php7-mbstring php7-simplexml composer # Stage 3: # - Frontend dependencies -FROM arm32v6/alpine:3.8 as node +FROM arm32v6/alpine:3.10 as node COPY --from=composer /app/shaarli /shaarli RUN apk --update --no-cache add yarn nodejs-current python2 build-base \ && cd /shaarli \ @@ -28,7 +28,7 @@ RUN apk --update --no-cache add yarn nodejs-current python2 build-base \ # Stage 4: # - Shaarli image -FROM arm32v6/alpine:3.8 +FROM arm32v6/alpine:3.10 LABEL maintainer="Shaarli Community" RUN apk --update --no-cache add \ From 2a20e67f760fb0587c8ea5c8219ec4546dde55bd Mon Sep 17 00:00:00 2001 From: Doug Breaux <25640850+dougbreaux@users.noreply.github.com> Date: Sun, 6 Dec 2020 22:02:34 -0600 Subject: [PATCH 013/155] remove apk upgrade #1655 --- Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index bedc9496c..79d331309 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,8 +46,7 @@ RUN apk --update --no-cache add \ php7-xml \ php7-simplexml \ php7-zlib \ - s6 \ - && apk -U upgrade + s6 COPY .docker/nginx.conf /etc/nginx/nginx.conf COPY .docker/php-fpm.conf /etc/php7/php-fpm.conf From 5b74c67461dbfb3fe87486646b64afd14aec4860 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Dec 2020 03:39:18 +0000 Subject: [PATCH 014/155] Bump ini from 1.3.5 to 1.3.7 Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7. - [Release notes](https://github.com/isaacs/ini/releases) - [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7) Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 55bd98278..97fb0fad1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3052,9 +3052,9 @@ inherits@2.0.3: integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= ini@^1.3.4, ini@^1.3.5: - version "1.3.5" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" - integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + version "1.3.7" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84" + integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ== interpret@^1.4.0: version "1.4.0" From 6a3a78d023aa320138bb88505b58347db268264a Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Wed, 16 Dec 2020 14:04:32 +0100 Subject: [PATCH 015/155] Fix: synchronous metadata retrieval is failing in strict mode Metadata can now only be string or null. Fixes #1653 --- .../front/controller/admin/ShaarePublishController.php | 2 +- application/http/MetadataRetriever.php | 9 +++++++-- tests/http/MetadataRetrieverTest.php | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php index 4cbfcdc50..fb9cacc22 100644 --- a/application/front/controller/admin/ShaarePublishController.php +++ b/application/front/controller/admin/ShaarePublishController.php @@ -227,7 +227,7 @@ protected function buildLinkDataFromUrl(Request $request, string $url): array protected function buildFormData(array $link, bool $isNew, Request $request): array { - $link['tags'] = strlen($link['tags']) > 0 + $link['tags'] = $link['tags'] !== null && strlen($link['tags']) > 0 ? $link['tags'] . $this->container->conf->get('general.tags_separator', ' ') : $link['tags'] ; diff --git a/application/http/MetadataRetriever.php b/application/http/MetadataRetriever.php index 2e1401eca..cfc72583e 100644 --- a/application/http/MetadataRetriever.php +++ b/application/http/MetadataRetriever.php @@ -60,10 +60,15 @@ public function retrieve(string $url): array $title = mb_convert_encoding($title, 'utf-8', $charset); } - return [ + return array_map([$this, 'cleanMetadata'], [ 'title' => $title, 'description' => $description, 'tags' => $tags, - ]; + ]); + } + + protected function cleanMetadata($data): ?string + { + return !is_string($data) || empty(trim($data)) ? null : trim($data); } } diff --git a/tests/http/MetadataRetrieverTest.php b/tests/http/MetadataRetrieverTest.php index 3c9eaa0e7..cae65091f 100644 --- a/tests/http/MetadataRetrieverTest.php +++ b/tests/http/MetadataRetrieverTest.php @@ -41,7 +41,7 @@ public function testFullRetrieval(): void $remoteCharset = 'utf-8'; $expectedResult = [ - 'title' => $remoteTitle, + 'title' => trim($remoteTitle), 'description' => $remoteDesc, 'tags' => $remoteTags, ]; From 88a8e284b2825cbd77c75dbbbf3655a961d9eb09 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 17 Dec 2020 13:56:24 +0100 Subject: [PATCH 016/155] Fix metadata extract regex (2) Reference: https://stackoverflow.com/questions/8055727/negating-a-backreference-in-regular-expressions Fixes #1656 --- application/bookmark/LinkUtils.php | 6 ++++-- tests/bookmark/LinkUtilsTest.php | 10 ++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php index d65e97ed4..0ab2d2138 100644 --- a/application/bookmark/LinkUtils.php +++ b/application/bookmark/LinkUtils.php @@ -68,11 +68,13 @@ function html_extract_tag($tag, $html) $properties = implode('|', $propertiesKey); // We need a OR here to accept either 'property=og:noquote' or 'property="og:unrelated og:my-tag"' $orCondition = '["\']?(?:og:)?' . $tag . '["\']?|["\'][^\'"]*?(?:og:)?' . $tag . '[^\'"]*?[\'"]'; + // Support quotes in double quoted content, and the other way around + $content = 'content=(["\'])((?:(?!\1).)*)\1'; // Try to retrieve OpenGraph tag. - $ogRegex = '#]+(?:' . $properties . ')=(?:' . $orCondition . ')[^>]*content=(["\'])([^\1]*?)\1.*?>#'; + $ogRegex = '#]+(?:' . $properties . ')=(?:' . $orCondition . ')[^>]*' . $content . '.*?>#'; // If the attributes are not in the order property => content (e.g. Github) // New regex to keep this readable... more or less. - $ogRegexReverse = '#]+content=(["\'])([^\1]*?)\1[^>]+(?:' . $properties . ')=(?:' . $orCondition . ').*?>#'; + $ogRegexReverse = '#]+' . $content . '[^>]+(?:' . $properties . ')=(?:' . $orCondition . ').*?>#'; if ( preg_match($ogRegex, $html, $matches) > 0 diff --git a/tests/bookmark/LinkUtilsTest.php b/tests/bookmark/LinkUtilsTest.php index ddab4e3ca..46a7f1fe7 100644 --- a/tests/bookmark/LinkUtilsTest.php +++ b/tests/bookmark/LinkUtilsTest.php @@ -245,6 +245,16 @@ public function testHtmlExtractNonExistentOgTag() $this->assertFalse(html_extract_tag('description', $html)); } + public function testHtmlExtractDescriptionFromGoogleRealCase(): void + { + $html = 'id="gsr">'. + ''. + 'assertSame('Bonnes fêtes de fin d\'année ! #GoogleDoodle', html_extract_tag('description', $html)); + } + /** * Test the header callback with valid value */ From f00600a283617286c813dc902fe3a2d66938b5fc Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 17 Dec 2020 15:43:33 +0100 Subject: [PATCH 017/155] Daily RSS Cache: invalidate cache base on the date Currently the cache is only invalidated when the datastore changes, while it should rely on selected period of time. Fixes #1659 --- application/feed/CachedPage.php | 45 ++++++--- .../controller/visitor/DailyController.php | 5 +- application/helper/DailyPageHelper.php | 66 ++++++++----- application/render/PageCacheManager.php | 14 ++- tests/feed/CachedPageTest.php | 57 +++++++++-- tests/helper/DailyPageHelperTest.php | 94 ++++++++++++++----- 6 files changed, 213 insertions(+), 68 deletions(-) diff --git a/application/feed/CachedPage.php b/application/feed/CachedPage.php index d809bdd96..c23c200f3 100644 --- a/application/feed/CachedPage.php +++ b/application/feed/CachedPage.php @@ -1,34 +1,43 @@ cacheDir = $cacheDir; $this->filename = $this->cacheDir . '/' . sha1($url) . '.cache'; $this->shouldBeCached = $shouldBeCached; + $this->validityPeriod = $validityPeriod; } /** @@ -41,10 +50,20 @@ public function cachedVersion() if (!$this->shouldBeCached) { return null; } - if (is_file($this->filename)) { - return file_get_contents($this->filename); + if (!is_file($this->filename)) { + return null; + } + if ($this->validityPeriod !== null) { + $cacheDate = \DateTime::createFromFormat('U', (string) filemtime($this->filename)); + if ( + $cacheDate < $this->validityPeriod->getStartDate() + || $cacheDate > $this->validityPeriod->getEndDate() + ) { + return null; + } } - return null; + + return file_get_contents($this->filename); } /** diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php index 5ae892992..29492a5f3 100644 --- a/application/front/controller/visitor/DailyController.php +++ b/application/front/controller/visitor/DailyController.php @@ -86,9 +86,11 @@ public function index(Request $request, Response $response): Response public function rss(Request $request, Response $response): Response { $response = $response->withHeader('Content-Type', 'application/rss+xml; charset=utf-8'); + $type = DailyPageHelper::extractRequestedType($request); + $cacheDuration = DailyPageHelper::getCacheDatePeriodByType($type); $pageUrl = page_url($this->container->environment); - $cache = $this->container->pageCacheManager->getCachePage($pageUrl); + $cache = $this->container->pageCacheManager->getCachePage($pageUrl, $cacheDuration); $cached = $cache->cachedVersion(); if (!empty($cached)) { @@ -96,7 +98,6 @@ public function rss(Request $request, Response $response): Response } $days = []; - $type = DailyPageHelper::extractRequestedType($request); $format = DailyPageHelper::getFormatByType($type); $length = DailyPageHelper::getRssLengthByType($type); foreach ($this->container->bookmarkService->search() as $bookmark) { diff --git a/application/helper/DailyPageHelper.php b/application/helper/DailyPageHelper.php index 9bdb7ba50..05f95812a 100644 --- a/application/helper/DailyPageHelper.php +++ b/application/helper/DailyPageHelper.php @@ -4,6 +4,9 @@ namespace Shaarli\Helper; +use DatePeriod; +use DateTimeImmutable; +use Exception; use Shaarli\Bookmark\Bookmark; use Slim\Http\Request; @@ -40,31 +43,31 @@ public static function extractRequestedType(Request $request): string * @param string|null $requestedDate Input string extracted from the request * @param Bookmark|null $latestBookmark Latest bookmark found in the datastore (by date) * - * @return \DateTimeImmutable from input or latest bookmark. + * @return DateTimeImmutable from input or latest bookmark. * - * @throws \Exception Type not supported. + * @throws Exception Type not supported. */ public static function extractRequestedDateTime( string $type, ?string $requestedDate, Bookmark $latestBookmark = null - ): \DateTimeImmutable { + ): DateTimeImmutable { $format = static::getFormatByType($type); if (empty($requestedDate)) { return $latestBookmark instanceof Bookmark - ? new \DateTimeImmutable($latestBookmark->getCreated()->format(\DateTime::ATOM)) - : new \DateTimeImmutable() + ? new DateTimeImmutable($latestBookmark->getCreated()->format(\DateTime::ATOM)) + : new DateTimeImmutable() ; } // W is not supported by createFromFormat... if ($type === static::WEEK) { - return (new \DateTimeImmutable()) + return (new DateTimeImmutable()) ->setISODate((int) substr($requestedDate, 0, 4), (int) substr($requestedDate, 4, 2)) ; } - return \DateTimeImmutable::createFromFormat($format, $requestedDate); + return DateTimeImmutable::createFromFormat($format, $requestedDate); } /** @@ -80,7 +83,7 @@ public static function extractRequestedDateTime( * * @see https://www.php.net/manual/en/datetime.format.php * - * @throws \Exception Type not supported. + * @throws Exception Type not supported. */ public static function getFormatByType(string $type): string { @@ -92,7 +95,7 @@ public static function getFormatByType(string $type): string case static::DAY: return 'Ymd'; default: - throw new \Exception('Unsupported daily format type'); + throw new Exception('Unsupported daily format type'); } } @@ -102,14 +105,14 @@ public static function getFormatByType(string $type): string * and we don't want to alter original datetime. * * @param string $type month/week/day - * @param \DateTimeImmutable $requested DateTime extracted from request input + * @param DateTimeImmutable $requested DateTime extracted from request input * (should come from extractRequestedDateTime) * * @return \DateTimeInterface First DateTime of the time period * - * @throws \Exception Type not supported. + * @throws Exception Type not supported. */ - public static function getStartDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface + public static function getStartDateTimeByType(string $type, DateTimeImmutable $requested): \DateTimeInterface { switch ($type) { case static::MONTH: @@ -119,7 +122,7 @@ public static function getStartDateTimeByType(string $type, \DateTimeImmutable $ case static::DAY: return $requested->modify('Today midnight'); default: - throw new \Exception('Unsupported daily format type'); + throw new Exception('Unsupported daily format type'); } } @@ -129,14 +132,14 @@ public static function getStartDateTimeByType(string $type, \DateTimeImmutable $ * and we don't want to alter original datetime. * * @param string $type month/week/day - * @param \DateTimeImmutable $requested DateTime extracted from request input + * @param DateTimeImmutable $requested DateTime extracted from request input * (should come from extractRequestedDateTime) * * @return \DateTimeInterface Last DateTime of the time period * - * @throws \Exception Type not supported. + * @throws Exception Type not supported. */ - public static function getEndDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface + public static function getEndDateTimeByType(string $type, DateTimeImmutable $requested): \DateTimeInterface { switch ($type) { case static::MONTH: @@ -146,7 +149,7 @@ public static function getEndDateTimeByType(string $type, \DateTimeImmutable $re case static::DAY: return $requested->modify('Today 23:59:59'); default: - throw new \Exception('Unsupported daily format type'); + throw new Exception('Unsupported daily format type'); } } @@ -161,7 +164,7 @@ public static function getEndDateTimeByType(string $type, \DateTimeImmutable $re * * @return string Localized time period description * - * @throws \Exception Type not supported. + * @throws Exception Type not supported. */ public static function getDescriptionByType( string $type, @@ -183,7 +186,7 @@ public static function getDescriptionByType( } return $out . format_date($requested, false); default: - throw new \Exception('Unsupported daily format type'); + throw new Exception('Unsupported daily format type'); } } @@ -194,7 +197,7 @@ public static function getDescriptionByType( * * @return int number of elements * - * @throws \Exception Type not supported. + * @throws Exception Type not supported. */ public static function getRssLengthByType(string $type): int { @@ -206,7 +209,28 @@ public static function getRssLengthByType(string $type): int case static::DAY: return 30; // ~1 month default: - throw new \Exception('Unsupported daily format type'); + throw new Exception('Unsupported daily format type'); } } + + /** + * Get the number of items to display in the RSS feed depending on the given type. + * + * @param string $type month/week/day + * @param ?DateTimeImmutable $requested Currently only used for UT + * + * @return DatePeriod number of elements + * + * @throws Exception Type not supported. + */ + public static function getCacheDatePeriodByType(string $type, DateTimeImmutable $requested = null): DatePeriod + { + $requested = $requested ?? new DateTimeImmutable(); + + return new DatePeriod( + static::getStartDateTimeByType($type, $requested), + new \DateInterval('P1D'), + static::getEndDateTimeByType($type, $requested) + ); + } } diff --git a/application/render/PageCacheManager.php b/application/render/PageCacheManager.php index 97805c352..fe74bf271 100644 --- a/application/render/PageCacheManager.php +++ b/application/render/PageCacheManager.php @@ -2,6 +2,7 @@ namespace Shaarli\Render; +use DatePeriod; use Shaarli\Feed\CachedPage; /** @@ -49,12 +50,21 @@ public function invalidateCaches(): void $this->purgeCachedPages(); } - public function getCachePage(string $pageUrl): CachedPage + /** + * Get CachedPage instance for provided URL. + * + * @param string $pageUrl + * @param ?DatePeriod $validityPeriod Optionally specify a time limit on requested cache + * + * @return CachedPage + */ + public function getCachePage(string $pageUrl, DatePeriod $validityPeriod = null): CachedPage { return new CachedPage( $this->pageCacheDir, $pageUrl, - false === $this->isLoggedIn + false === $this->isLoggedIn, + $validityPeriod ); } } diff --git a/tests/feed/CachedPageTest.php b/tests/feed/CachedPageTest.php index 904db9dc2..1decfaf3b 100644 --- a/tests/feed/CachedPageTest.php +++ b/tests/feed/CachedPageTest.php @@ -40,10 +40,10 @@ protected function setUp(): void */ public function testConstruct() { - new CachedPage(self::$testCacheDir, '', true); - new CachedPage(self::$testCacheDir, '', false); - new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/rss', true); - new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/atom', false); + new CachedPage(self::$testCacheDir, '', true, null); + new CachedPage(self::$testCacheDir, '', false, null); + new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/rss', true, null); + new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/atom', false, null); $this->addToAssertionCount(1); } @@ -52,7 +52,7 @@ public function testConstruct() */ public function testCache() { - $page = new CachedPage(self::$testCacheDir, self::$url, true); + $page = new CachedPage(self::$testCacheDir, self::$url, true, null); $this->assertFileNotExists(self::$filename); $page->cache('

Some content

'); @@ -68,7 +68,7 @@ public function testCache() */ public function testShouldNotCache() { - $page = new CachedPage(self::$testCacheDir, self::$url, false); + $page = new CachedPage(self::$testCacheDir, self::$url, false, null); $this->assertFileNotExists(self::$filename); $page->cache('

Some content

'); @@ -80,7 +80,7 @@ public function testShouldNotCache() */ public function testCachedVersion() { - $page = new CachedPage(self::$testCacheDir, self::$url, true); + $page = new CachedPage(self::$testCacheDir, self::$url, true, null); $this->assertFileNotExists(self::$filename); $page->cache('

Some content

'); @@ -96,7 +96,7 @@ public function testCachedVersion() */ public function testCachedVersionNoFile() { - $page = new CachedPage(self::$testCacheDir, self::$url, true); + $page = new CachedPage(self::$testCacheDir, self::$url, true, null); $this->assertFileNotExists(self::$filename); $this->assertEquals( @@ -110,7 +110,7 @@ public function testCachedVersionNoFile() */ public function testNoCachedVersion() { - $page = new CachedPage(self::$testCacheDir, self::$url, false); + $page = new CachedPage(self::$testCacheDir, self::$url, false, null); $this->assertFileNotExists(self::$filename); $this->assertEquals( @@ -118,4 +118,43 @@ public function testNoCachedVersion() $page->cachedVersion() ); } + + /** + * Return a page's cached content within date period + */ + public function testCachedVersionInDatePeriod() + { + $period = new \DatePeriod( + new \DateTime('yesterday'), + new \DateInterval('P1D'), + new \DateTime('tomorrow') + ); + $page = new CachedPage(self::$testCacheDir, self::$url, true, $period); + + $this->assertFileNotExists(self::$filename); + $page->cache('

Some content

'); + $this->assertFileExists(self::$filename); + $this->assertEquals( + '

Some content

', + $page->cachedVersion() + ); + } + + /** + * Return a page's cached content outside of date period + */ + public function testCachedVersionNotInDatePeriod() + { + $period = new \DatePeriod( + new \DateTime('yesterday noon'), + new \DateInterval('P1D'), + new \DateTime('yesterday midnight') + ); + $page = new CachedPage(self::$testCacheDir, self::$url, true, $period); + + $this->assertFileNotExists(self::$filename); + $page->cache('

Some content

'); + $this->assertFileExists(self::$filename); + $this->assertNull($page->cachedVersion()); + } } diff --git a/tests/helper/DailyPageHelperTest.php b/tests/helper/DailyPageHelperTest.php index 6238e6481..2d7458004 100644 --- a/tests/helper/DailyPageHelperTest.php +++ b/tests/helper/DailyPageHelperTest.php @@ -4,6 +4,8 @@ namespace Shaarli\Helper; +use DateTimeImmutable; +use DateTimeInterface; use Shaarli\Bookmark\Bookmark; use Shaarli\TestCase; use Slim\Http\Request; @@ -32,7 +34,7 @@ public function testExtractRequestedDateTime( string $type, string $input, ?Bookmark $bookmark, - \DateTimeInterface $expectedDateTime, + DateTimeInterface $expectedDateTime, string $compareFormat = 'Ymd' ): void { $dateTime = DailyPageHelper::extractRequestedDateTime($type, $input, $bookmark); @@ -71,8 +73,8 @@ public function testGetFormatByTypeExceptionUnknownType(): void */ public function testGetStartDatesByType( string $type, - \DateTimeImmutable $dateTime, - \DateTimeInterface $expectedDateTime + DateTimeImmutable $dateTime, + DateTimeInterface $expectedDateTime ): void { $startDateTime = DailyPageHelper::getStartDateTimeByType($type, $dateTime); @@ -84,7 +86,7 @@ public function testGetStartDatesByTypeExceptionUnknownType(): void $this->expectException(\Exception::class); $this->expectExceptionMessage('Unsupported daily format type'); - DailyPageHelper::getStartDateTimeByType('nope', new \DateTimeImmutable()); + DailyPageHelper::getStartDateTimeByType('nope', new DateTimeImmutable()); } /** @@ -92,8 +94,8 @@ public function testGetStartDatesByTypeExceptionUnknownType(): void */ public function testGetEndDatesByType( string $type, - \DateTimeImmutable $dateTime, - \DateTimeInterface $expectedDateTime + DateTimeImmutable $dateTime, + DateTimeInterface $expectedDateTime ): void { $endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dateTime); @@ -105,7 +107,7 @@ public function testGetEndDatesByTypeExceptionUnknownType(): void $this->expectException(\Exception::class); $this->expectExceptionMessage('Unsupported daily format type'); - DailyPageHelper::getEndDateTimeByType('nope', new \DateTimeImmutable()); + DailyPageHelper::getEndDateTimeByType('nope', new DateTimeImmutable()); } /** @@ -113,7 +115,7 @@ public function testGetEndDatesByTypeExceptionUnknownType(): void */ public function testGeDescriptionsByType( string $type, - \DateTimeImmutable $dateTime, + DateTimeImmutable $dateTime, string $expectedDescription ): void { $description = DailyPageHelper::getDescriptionByType($type, $dateTime); @@ -139,7 +141,7 @@ public function getDescriptionByTypeExceptionUnknownType(): void $this->expectException(\Exception::class); $this->expectExceptionMessage('Unsupported daily format type'); - DailyPageHelper::getDescriptionByType('nope', new \DateTimeImmutable()); + DailyPageHelper::getDescriptionByType('nope', new DateTimeImmutable()); } /** @@ -159,6 +161,29 @@ public function testGeRssLengthsByTypeExceptionUnknownType(): void DailyPageHelper::getRssLengthByType('nope'); } + /** + * @dataProvider getCacheDatePeriodByType + */ + public function testGetCacheDatePeriodByType( + string $type, + DateTimeImmutable $requested, + DateTimeInterface $start, + DateTimeInterface $end + ): void { + $period = DailyPageHelper::getCacheDatePeriodByType($type, $requested); + + static::assertEquals($start, $period->getStartDate()); + static::assertEquals($end, $period->getEndDate()); + } + + public function testGetCacheDatePeriodByTypeExceptionUnknownType(): void + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Unsupported daily format type'); + + DailyPageHelper::getCacheDatePeriodByType('nope'); + } + /** * Data provider for testExtractRequestedType() test method. */ @@ -229,9 +254,9 @@ public function getFormatsByType(): array public function getStartDatesByType(): array { return [ - [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 00:00:00')], - [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-05 00:00:00')], - [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-01 00:00:00')], + [DailyPageHelper::DAY, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 00:00:00')], + [DailyPageHelper::WEEK, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-05 00:00:00')], + [DailyPageHelper::MONTH, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-01 00:00:00')], ]; } @@ -241,9 +266,9 @@ public function getStartDatesByType(): array public function getEndDatesByType(): array { return [ - [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 23:59:59')], - [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-11 23:59:59')], - [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-31 23:59:59')], + [DailyPageHelper::DAY, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 23:59:59')], + [DailyPageHelper::WEEK, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-11 23:59:59')], + [DailyPageHelper::MONTH, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-31 23:59:59')], ]; } @@ -253,11 +278,11 @@ public function getEndDatesByType(): array public function getDescriptionsByType(): array { return [ - [DailyPageHelper::DAY, $date = new \DateTimeImmutable(), 'Today - ' . $date->format('F j, Y')], - [DailyPageHelper::DAY, $date = new \DateTimeImmutable('-1 day'), 'Yesterday - ' . $date->format('F j, Y')], - [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October 9, 2020'], - [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), 'Week 41 (October 5, 2020)'], - [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October, 2020'], + [DailyPageHelper::DAY, $date = new DateTimeImmutable(), 'Today - ' . $date->format('F j, Y')], + [DailyPageHelper::DAY, $date = new DateTimeImmutable('-1 day'), 'Yesterday - ' . $date->format('F j, Y')], + [DailyPageHelper::DAY, new DateTimeImmutable('2020-10-09 04:05:06'), 'October 9, 2020'], + [DailyPageHelper::WEEK, new DateTimeImmutable('2020-10-09 04:05:06'), 'Week 41 (October 5, 2020)'], + [DailyPageHelper::MONTH, new DateTimeImmutable('2020-10-09 04:05:06'), 'October, 2020'], ]; } @@ -276,7 +301,7 @@ public function getDescriptionsByTypeNotIncludeRelative(): array } /** - * Data provider for testGetDescriptionsByType() test method. + * Data provider for testGetRssLengthsByType() test method. */ public function getRssLengthsByType(): array { @@ -286,4 +311,31 @@ public function getRssLengthsByType(): array [DailyPageHelper::MONTH], ]; } + + /** + * Data provider for testGetCacheDatePeriodByType() test method. + */ + public function getCacheDatePeriodByType(): array + { + return [ + [ + DailyPageHelper::DAY, + new DateTimeImmutable('2020-10-09 04:05:06'), + new \DateTime('2020-10-09 00:00:00'), + new \DateTime('2020-10-09 23:59:59'), + ], + [ + DailyPageHelper::WEEK, + new DateTimeImmutable('2020-10-09 04:05:06'), + new \DateTime('2020-10-05 00:00:00'), + new \DateTime('2020-10-11 23:59:59'), + ], + [ + DailyPageHelper::MONTH, + new DateTimeImmutable('2020-10-09 04:05:06'), + new \DateTime('2020-10-01 00:00:00'), + new \DateTime('2020-10-31 23:59:59'), + ], + ]; + } } From 151fa1e450740b08ee783e819dd42cf2c48c335c Mon Sep 17 00:00:00 2001 From: leyrer Date: Sat, 26 Dec 2020 13:45:01 +0100 Subject: [PATCH 018/155] Typo fix line 76 'Authentication' -> Authorization --- doc/md/REST-API.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/md/REST-API.md b/doc/md/REST-API.md index 01071d8e5..2a36ea29d 100644 --- a/doc/md/REST-API.md +++ b/doc/md/REST-API.md @@ -73,7 +73,7 @@ var_dump(getInfo($baseUrl, $secret)); ### Authentication - All requests to Shaarli's API must include a **JWT token** to verify their authenticity. -- This token must be included as an HTTP header called `Authentication: Bearer `. +- This token must be included as an HTTP header called `Authorization: Bearer `. - JWT tokens are composed by three parts, separated by a dot `.` and encoded in base64: ``` From 035a002edc7376a961e5621dc1039b082637fbc6 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 29 Dec 2020 11:59:14 +0100 Subject: [PATCH 019/155] Fix default_colors plugin: update CSS file on color change Last update of this plugin remove the save_plugin_parameters hook. Fixes #1657 --- plugins/default_colors/default_colors.php | 14 ++++++++++++++ tests/plugins/PluginDefaultColorsTest.php | 23 +++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/plugins/default_colors/default_colors.php b/plugins/default_colors/default_colors.php index 574a0bd4d..d3e1fa761 100644 --- a/plugins/default_colors/default_colors.php +++ b/plugins/default_colors/default_colors.php @@ -46,6 +46,20 @@ function default_colors_init($conf) } } +/** + * When plugin parameters are saved, we regenerate the custom CSS file with provided settings. + * + * @param array $data $_POST array + * + * @return array Updated $_POST array + */ +function hook_default_colors_save_plugin_parameters($data) +{ + default_colors_generate_css_file($data); + + return $data; +} + /** * When linklist is displayed, include default_colors CSS file. * diff --git a/tests/plugins/PluginDefaultColorsTest.php b/tests/plugins/PluginDefaultColorsTest.php index cc844c60f..54e976125 100644 --- a/tests/plugins/PluginDefaultColorsTest.php +++ b/tests/plugins/PluginDefaultColorsTest.php @@ -193,4 +193,27 @@ public function testFormatCssRuleInvalid() $result = default_colors_format_css_rule($data, ''); $this->assertEmpty($result); } + + /** + * Make sure that a new CSS file is generated when save_plugin_parameters hook is triggered. + */ + public function testHookSavePluginParameters(): void + { + $params = [ + 'other1' => true, + 'DEFAULT_COLORS_BACKGROUND' => 'pink', + 'other2' => ['yep'], + 'DEFAULT_COLORS_DARK_MAIN' => '', + ]; + + hook_default_colors_save_plugin_parameters($params); + $this->assertFileExists($file = 'sandbox/default_colors/default_colors.css'); + $content = file_get_contents($file); + $expected = ':root { + --background-color: pink; + +} +'; + $this->assertEquals($expected, $content); + } } From 0640c1a6db6d9a13e5d0079f0bf42497010edbc7 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 29 Dec 2020 12:50:23 +0100 Subject: [PATCH 020/155] API: POST/PUT Link - properly parse tags string Even though the documentation specify that tags should be passed as an array, tags string is actually allowed. So this adds a proper parsing with configured separator. Related to #1651 --- application/api/ApiUtils.php | 21 +++++++-- application/api/controllers/Links.php | 12 ++++- tests/api/controllers/links/PostLinkTest.php | 48 ++++++++++++++++++++ tests/api/controllers/links/PutLinkTest.php | 48 ++++++++++++++++++++ 4 files changed, 123 insertions(+), 6 deletions(-) diff --git a/application/api/ApiUtils.php b/application/api/ApiUtils.php index 05a2840a6..9228bb2da 100644 --- a/application/api/ApiUtils.php +++ b/application/api/ApiUtils.php @@ -91,13 +91,17 @@ public static function formatLink($bookmark, $indexUrl) * If no URL is provided, it will generate a local note URL. * If no title is provided, it will use the URL as title. * - * @param array|null $input Request Link. - * @param bool $defaultPrivate Setting defined if a bookmark is private by default. + * @param array|null $input Request Link. + * @param bool $defaultPrivate Setting defined if a bookmark is private by default. + * @param string $tagsSeparator Tags separator loaded from the config file. * * @return Bookmark instance. */ - public static function buildBookmarkFromRequest(?array $input, bool $defaultPrivate): Bookmark - { + public static function buildBookmarkFromRequest( + ?array $input, + bool $defaultPrivate, + string $tagsSeparator + ): Bookmark { $bookmark = new Bookmark(); $url = ! empty($input['url']) ? cleanup_url($input['url']) : ''; if (isset($input['private'])) { @@ -109,6 +113,15 @@ public static function buildBookmarkFromRequest(?array $input, bool $defaultPriv $bookmark->setTitle(! empty($input['title']) ? $input['title'] : ''); $bookmark->setUrl($url); $bookmark->setDescription(! empty($input['description']) ? $input['description'] : ''); + + // Be permissive with provided tags format + if (is_string($input['tags'] ?? null)) { + $input['tags'] = tags_str2array($input['tags'], $tagsSeparator); + } + if (is_array($input['tags'] ?? null) && count($input['tags']) === 1 && is_string($input['tags'][0])) { + $input['tags'] = tags_str2array($input['tags'][0], $tagsSeparator); + } + $bookmark->setTags(! empty($input['tags']) ? $input['tags'] : []); $bookmark->setPrivate($private); diff --git a/application/api/controllers/Links.php b/application/api/controllers/Links.php index c379b9622..b83b2260f 100644 --- a/application/api/controllers/Links.php +++ b/application/api/controllers/Links.php @@ -117,7 +117,11 @@ public function getLink($request, $response, $args) public function postLink($request, $response) { $data = (array) ($request->getParsedBody() ?? []); - $bookmark = ApiUtils::buildBookmarkFromRequest($data, $this->conf->get('privacy.default_private_links')); + $bookmark = ApiUtils::buildBookmarkFromRequest( + $data, + $this->conf->get('privacy.default_private_links'), + $this->conf->get('general.tags_separator', ' ') + ); // duplicate by URL, return 409 Conflict if ( ! empty($bookmark->getUrl()) @@ -158,7 +162,11 @@ public function putLink($request, $response, $args) $index = index_url($this->ci['environment']); $data = $request->getParsedBody(); - $requestBookmark = ApiUtils::buildBookmarkFromRequest($data, $this->conf->get('privacy.default_private_links')); + $requestBookmark = ApiUtils::buildBookmarkFromRequest( + $data, + $this->conf->get('privacy.default_private_links'), + $this->conf->get('general.tags_separator', ' ') + ); // duplicate URL on a different link, return 409 Conflict if ( ! empty($requestBookmark->getUrl()) diff --git a/tests/api/controllers/links/PostLinkTest.php b/tests/api/controllers/links/PostLinkTest.php index e12f803be..f755e2d2b 100644 --- a/tests/api/controllers/links/PostLinkTest.php +++ b/tests/api/controllers/links/PostLinkTest.php @@ -229,4 +229,52 @@ public function testPostLinkDuplicate() \DateTime::createFromFormat(\DateTime::ATOM, $data['updated']) ); } + + /** + * Test link creation with a tag string provided + */ + public function testPostLinkWithTagString(): void + { + $link = [ + 'tags' => 'one two', + ]; + $env = Environment::mock([ + 'REQUEST_METHOD' => 'POST', + 'CONTENT_TYPE' => 'application/json' + ]); + + $request = Request::createFromEnvironment($env); + $request = $request->withParsedBody($link); + $response = $this->controller->postLink($request, new Response()); + + $this->assertEquals(201, $response->getStatusCode()); + $this->assertEquals('/api/v1/bookmarks/1', $response->getHeader('Location')[0]); + $data = json_decode((string) $response->getBody(), true); + $this->assertEquals(self::NB_FIELDS_LINK, count($data)); + $this->assertEquals(['one', 'two'], $data['tags']); + } + + /** + * Test link creation with a tag string provided + */ + public function testPostLinkWithTagString2(): void + { + $link = [ + 'tags' => ['one two'], + ]; + $env = Environment::mock([ + 'REQUEST_METHOD' => 'POST', + 'CONTENT_TYPE' => 'application/json' + ]); + + $request = Request::createFromEnvironment($env); + $request = $request->withParsedBody($link); + $response = $this->controller->postLink($request, new Response()); + + $this->assertEquals(201, $response->getStatusCode()); + $this->assertEquals('/api/v1/bookmarks/1', $response->getHeader('Location')[0]); + $data = json_decode((string) $response->getBody(), true); + $this->assertEquals(self::NB_FIELDS_LINK, count($data)); + $this->assertEquals(['one', 'two'], $data['tags']); + } } diff --git a/tests/api/controllers/links/PutLinkTest.php b/tests/api/controllers/links/PutLinkTest.php index 240ee323a..fe24f2eb9 100644 --- a/tests/api/controllers/links/PutLinkTest.php +++ b/tests/api/controllers/links/PutLinkTest.php @@ -233,4 +233,52 @@ public function testGetLink404() $this->controller->putLink($request, new Response(), ['id' => -1]); } + + /** + * Test link creation with a tag string provided + */ + public function testPutLinkWithTagString(): void + { + $link = [ + 'tags' => 'one two', + ]; + $id = '41'; + $env = Environment::mock([ + 'REQUEST_METHOD' => 'PUT', + 'CONTENT_TYPE' => 'application/json' + ]); + + $request = Request::createFromEnvironment($env); + $request = $request->withParsedBody($link); + $response = $this->controller->putLink($request, new Response(), ['id' => $id]); + + $this->assertEquals(200, $response->getStatusCode()); + $data = json_decode((string) $response->getBody(), true); + $this->assertEquals(self::NB_FIELDS_LINK, count($data)); + $this->assertEquals(['one', 'two'], $data['tags']); + } + + /** + * Test link creation with a tag string provided + */ + public function testPutLinkWithTagString2(): void + { + $link = [ + 'tags' => ['one two'], + ]; + $id = '41'; + $env = Environment::mock([ + 'REQUEST_METHOD' => 'PUT', + 'CONTENT_TYPE' => 'application/json' + ]); + + $request = Request::createFromEnvironment($env); + $request = $request->withParsedBody($link); + $response = $this->controller->putLink($request, new Response(), ['id' => $id]); + + $this->assertEquals(200, $response->getStatusCode()); + $data = json_decode((string) $response->getBody(), true); + $this->assertEquals(self::NB_FIELDS_LINK, count($data)); + $this->assertEquals(['one', 'two'], $data['tags']); + } } From bf02f8ba8e2864ff0cd12ac098b6e04e497cead9 Mon Sep 17 00:00:00 2001 From: yudete Date: Mon, 4 Jan 2021 18:55:03 +0900 Subject: [PATCH 021/155] Update Japanese translations --- inc/languages/jp/LC_MESSAGES/shaarli.po | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/inc/languages/jp/LC_MESSAGES/shaarli.po b/inc/languages/jp/LC_MESSAGES/shaarli.po index 57f42fc2a..d5a833419 100644 --- a/inc/languages/jp/LC_MESSAGES/shaarli.po +++ b/inc/languages/jp/LC_MESSAGES/shaarli.po @@ -3,14 +3,14 @@ msgstr "" "Project-Id-Version: Shaarli\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-10-19 10:19+0900\n" -"PO-Revision-Date: 2020-10-19 10:25+0900\n" +"PO-Revision-Date: 2021-01-04 18:54+0900\n" "Last-Translator: yude \n" "Language-Team: Shaarli\n" "Language: ja\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Generator: Poedit 2.2.3\n" +"X-Generator: Poedit 2.4.2\n" "X-Poedit-Basepath: ../../../..\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Poedit-SourceCharset: UTF-8\n" @@ -79,8 +79,8 @@ msgid "" "php-gd extension must be loaded to use thumbnails. Thumbnails are now " "disabled. Please reload the page." msgstr "" -"サムネイルを使用するには、php-gd エクステンションが読み込まれている必要があり" -"ます。サムネイルは無効化されました。ページを再読込してください。" +"サムネイルを使用するには、php-gd 拡張機能が読み込まれている必要があります。サ" +"ムネイルは無効化されました。ページを再読込してください。" #: application/Utils.php:383 tests/UtilsTest.php:343 msgid "Setting not set" @@ -118,7 +118,7 @@ msgstr "設定を変更する権限がありません" #: application/bookmark/BookmarkFileService.php:205 msgid "This bookmarks already exists" -msgstr "このブックマークは既に存在します。" +msgstr "このブックマークは既に存在します" #: application/bookmark/BookmarkInitializer.php:39 msgid "(private bookmark with thumbnail demo)" @@ -594,8 +594,6 @@ msgstr "" "す。" #: application/legacy/LegacyUpdater.php:104 -#, fuzzy -#| msgid "Couldn't retrieve Updater class methods." msgid "Couldn't retrieve updater class methods." msgstr "アップデーターのクラスメゾットを受信できませんでした。" @@ -617,10 +615,7 @@ msgid "has an unknown file format. Nothing was imported." msgstr "は不明なファイル形式です。インポートは中止されました。" #: application/netscape/NetscapeBookmarkUtils.php:221 -#, fuzzy, php-format -#| msgid "" -#| "was successfully processed in %d seconds: %d links imported, %d links " -#| "overwritten, %d links skipped." +#, php-format msgid "" "was successfully processed in %d seconds: %d bookmarks imported, %d " "bookmarks overwritten, %d bookmarks skipped." @@ -630,7 +625,7 @@ msgstr "" #: application/plugin/PluginManager.php:124 msgid " [plugin incompatibility]: " -msgstr "[非対応のプラグイン]: " +msgstr " [非対応のプラグイン]: " #: application/plugin/exception/PluginFileNotFoundException.php:21 #, php-format From ccd1862d5f6f2c0548473466aaff7ee99f9d67d2 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 19 Jan 2021 10:34:11 +0100 Subject: [PATCH 022/155] Inject current template name in templates Use either legacy key _PAGE_ or new 'template' one. Related to https://github.com/kalvn/Shaarli-Material/issues/118 --- .../front/controller/visitor/ShaarliVisitorController.php | 4 ++++ .../front/controller/visitor/ShaarliVisitorControllerTest.php | 3 +++ 2 files changed, 7 insertions(+) diff --git a/application/front/controller/visitor/ShaarliVisitorController.php b/application/front/controller/visitor/ShaarliVisitorController.php index ae946c592..d3f28f2f7 100644 --- a/application/front/controller/visitor/ShaarliVisitorController.php +++ b/application/front/controller/visitor/ShaarliVisitorController.php @@ -56,6 +56,10 @@ protected function assignAllView(array $data): self protected function render(string $template): string { + // Legacy key that used to be injected by PluginManager + $this->assignView('_PAGE_', $template); + $this->assignView('template', $template); + $this->assignView('linkcount', $this->container->bookmarkService->count(BookmarkFilter::$ALL)); $this->assignView('privateLinkcount', $this->container->bookmarkService->count(BookmarkFilter::$PRIVATE)); diff --git a/tests/front/controller/visitor/ShaarliVisitorControllerTest.php b/tests/front/controller/visitor/ShaarliVisitorControllerTest.php index 935ec24ef..7676f14d5 100644 --- a/tests/front/controller/visitor/ShaarliVisitorControllerTest.php +++ b/tests/front/controller/visitor/ShaarliVisitorControllerTest.php @@ -93,6 +93,9 @@ public function testRender(): void static::assertSame('templateName', $render); + static::assertSame('templateName', $this->assignedValues['_PAGE_']); + static::assertSame('templateName', $this->assignedValues['template']); + static::assertSame(10, $this->assignedValues['linkcount']); static::assertSame(5, $this->assignedValues['privateLinkcount']); static::assertSame(['error'], $this->assignedValues['plugin_errors']); From 9e55beebfdbc7c8db8590792479689322b92a235 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 19 Jan 2021 11:18:56 +0100 Subject: [PATCH 023/155] Fix: error when using bulk shaare with a single URL Make sure that header metadata associated with permalink is only used in linklist template. Fixes #1686 --- tpl/default/includes.html | 2 +- tpl/vintage/includes.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tpl/default/includes.html b/tpl/default/includes.html index 3e3fb6640..6be2a2ea7 100644 --- a/tpl/default/includes.html +++ b/tpl/default/includes.html @@ -19,7 +19,7 @@ {/if} -{if="! empty($links) && count($links) === 1"} +{if="$template === 'linklist' && ! empty($links) && count($links) === 1"} {$link=reset($links)} diff --git a/tpl/vintage/includes.html b/tpl/vintage/includes.html index 2ce9da423..4fd78467e 100644 --- a/tpl/vintage/includes.html +++ b/tpl/vintage/includes.html @@ -16,7 +16,7 @@ {if="is_file('data/user.css')"}{/if} -{if="! empty($links) && count($links) === 1"} +{if="$template === 'linklist' && ! empty($links) && count($links) === 1"} {$link=reset($links)} From 8fbc29de0252c462884605d4f691b6746e524fc9 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 19 Jan 2021 11:35:42 +0100 Subject: [PATCH 024/155] Fix: bulk add - use unique HTML ID Use links loop ID to make ID unique and fix browser labels behaviour. Fixes #1685 --- tpl/default/editlink.batch.html | 1 + tpl/default/editlink.html | 23 ++++++++++++----------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/tpl/default/editlink.batch.html b/tpl/default/editlink.batch.html index b1f8e5bd9..973a5ccc6 100644 --- a/tpl/default/editlink.batch.html +++ b/tpl/default/editlink.batch.html @@ -20,6 +20,7 @@ {loop="$links"} + {$batchId=$key} {include="editlink"} {/loop} diff --git a/tpl/default/editlink.html b/tpl/default/editlink.html index 83e541fdf..a5828c756 100644 --- a/tpl/default/editlink.html +++ b/tpl/default/editlink.html @@ -1,3 +1,4 @@ +{$batchId=isset($batchId) ? $batchId : ''} {if="empty($batch_mode)"} @@ -10,7 +11,7 @@ {ignore}Lil hack: when included in a loop in batch mode, `$value` is assigned by RainTPL with template vars.{/ignore} {function="extract($value) ? '' : ''"} {/if} -